mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-06-30 23:49:42 +02:00
Compare commits
239 Commits
stable
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7146973b1b | ||
|
|
fc57b5c4d9 | ||
|
|
e0d8f1bdb1 | ||
|
|
b40e379cc6 | ||
|
|
6c79dd92db | ||
|
|
c3f9ad5bce | ||
|
|
797e062167 | ||
|
|
3f291f38f5 | ||
|
|
4b19f7be3e | ||
|
|
62f85f0f02 | ||
|
|
91f5dccfe6 | ||
|
|
e4b8403a8b | ||
|
|
196c94d0fd | ||
|
|
60c778bcec | ||
|
|
38189ae123 | ||
|
|
cc70171613 | ||
|
|
59513894c5 | ||
|
|
0516550367 | ||
|
|
60e7a6be07 | ||
|
|
7359924123 | ||
|
|
16096506f3 | ||
|
|
04f795099c | ||
|
|
814c17ce6d | ||
|
|
22c2232409 | ||
|
|
50c8bda0f7 | ||
|
|
768a7084f4 | ||
|
|
5214aeb267 | ||
|
|
18caa0478c | ||
|
|
90fe3c3d8d | ||
|
|
cdf4096da6 | ||
|
|
304d4f13ac | ||
|
|
255a8d14c0 | ||
|
|
0595eaf789 | ||
|
|
409d02c606 | ||
|
|
82ddf6d148 | ||
|
|
672b0213a7 | ||
|
|
faa4bf7b50 | ||
|
|
8d705866c1 | ||
|
|
b326300537 | ||
|
|
88ed8cc9bc | ||
|
|
f47e8b5dd8 | ||
|
|
c7edda29d8 | ||
|
|
d7c0b0b9d1 | ||
|
|
8c541c9117 | ||
|
|
504f451860 | ||
|
|
1a4e971092 | ||
|
|
fcf4b0727b | ||
|
|
d7d72fe2f4 | ||
|
|
fd31b286ac | ||
|
|
1fb7261544 | ||
|
|
a6df9aca56 | ||
|
|
2e43f8c034 | ||
|
|
dcbd17989d | ||
|
|
c9b2fa1d2d | ||
|
|
b1b14fb0b0 | ||
|
|
35bbddb929 | ||
|
|
f726f186d4 | ||
|
|
2876d2a496 | ||
|
|
27ec533bc0 | ||
|
|
01851e80b3 | ||
|
|
64872a5467 | ||
|
|
50e702ad9b | ||
|
|
6438793daa | ||
|
|
6c1f3d436b | ||
|
|
965520ffe6 | ||
|
|
84706830e7 | ||
|
|
c203335558 | ||
|
|
673db83022 | ||
|
|
a0a1f275b4 | ||
|
|
340bc7fe4b | ||
|
|
58850be91d | ||
|
|
311d05538b | ||
|
|
f531dd4de0 | ||
|
|
cc87469ada | ||
|
|
7159b8c17b | ||
|
|
ea354b6a8b | ||
|
|
f087c4c239 | ||
|
|
8b07c05887 | ||
|
|
b023a6bf96 | ||
|
|
bcb9a2a205 | ||
|
|
14c10b192a | ||
|
|
5da332fc9b | ||
|
|
a567cd6b19 | ||
|
|
f0179f2455 | ||
|
|
7d9b7582f9 | ||
|
|
637d6d7e96 | ||
|
|
c98689e385 | ||
|
|
a53dd0f040 | ||
|
|
5a328c429b | ||
|
|
41f14c4bf4 | ||
|
|
2913477f07 | ||
|
|
0cc5c08ea4 | ||
|
|
2494bf84b3 | ||
|
|
7f217609f9 | ||
|
|
a0c8b70697 | ||
|
|
7833526186 | ||
|
|
b56e4ea560 | ||
|
|
80e734587b | ||
|
|
daf586b86d | ||
|
|
95247aa6d5 | ||
|
|
947457d20c | ||
|
|
5d4a971bcf | ||
|
|
0cacfa3cc9 | ||
|
|
40f3425a1b | ||
|
|
c32f67bc5a | ||
|
|
ba2ad4b317 | ||
|
|
d46bb66c11 | ||
|
|
ea0af2bb9c | ||
|
|
806d9eebe6 | ||
|
|
e24cf24835 | ||
|
|
30ba1e7aca | ||
|
|
2996d4b9d5 | ||
|
|
efec6cfc03 | ||
|
|
d5017fffe4 | ||
|
|
1bffcfb6fa | ||
|
|
5d55c5c120 | ||
|
|
90df6f15ae | ||
|
|
aa6acf8145 | ||
|
|
3bae9a57e7 | ||
|
|
e0972996ff | ||
|
|
90afe7e9f5 | ||
|
|
884d1ec6d6 | ||
|
|
0b38be2c01 | ||
|
|
5d7211cf85 | ||
|
|
a5e93da8ad | ||
|
|
17aa472af5 | ||
|
|
96581fd24c | ||
|
|
41d12017b6 | ||
|
|
6e7add6feb | ||
|
|
d55bad1457 | ||
|
|
e02d384051 | ||
|
|
2293b76f7b | ||
|
|
b7ea22e017 | ||
|
|
d83dc94841 | ||
|
|
031f29418e | ||
|
|
fe533cd350 | ||
|
|
d4802fec40 | ||
|
|
12af2770e6 | ||
|
|
d64ccf686f | ||
|
|
17b840c583 | ||
|
|
421df9f1e0 | ||
|
|
806d383272 | ||
|
|
bffce430cc | ||
|
|
b310df6e2e | ||
|
|
1e1f0118d9 | ||
|
|
037062e754 | ||
|
|
19d77a4d02 | ||
|
|
72c0922af7 | ||
|
|
329a6434f8 | ||
|
|
b0250fc0f4 | ||
|
|
029c894ee4 | ||
|
|
f624625344 | ||
|
|
2c61ec1a8c | ||
|
|
3e4e3dd6f1 | ||
|
|
edcce595df | ||
|
|
ef0efcbe2f | ||
|
|
4de34eca96 | ||
|
|
f886d52484 | ||
|
|
017966e3db | ||
|
|
e499f90ee5 | ||
|
|
c0fe992e37 | ||
|
|
bd316d4d34 | ||
|
|
f03eb96b9c | ||
|
|
bbd15a3ae8 | ||
|
|
777a301666 | ||
|
|
f9e3ab4852 | ||
|
|
f311a65708 | ||
|
|
b9040d82c3 | ||
|
|
1d8b45f824 | ||
|
|
221cf083bc | ||
|
|
ccac1e1ac9 | ||
|
|
fb428b2d61 | ||
|
|
acb933643a | ||
|
|
f63785d80d | ||
|
|
87c1a73ecc | ||
|
|
44b0b9ac67 | ||
|
|
7629c6fbfa | ||
|
|
b8c024ac07 | ||
|
|
640c1a8388 | ||
|
|
78b2b23764 | ||
|
|
46d26041b9 | ||
|
|
08454f8b18 | ||
|
|
b7a65ab609 | ||
|
|
08d3435a0d | ||
|
|
46b8eea695 | ||
|
|
5b255011c7 | ||
|
|
6782829cdd | ||
|
|
6ed8d73b50 | ||
|
|
38dfee0234 | ||
|
|
d547a6f613 | ||
|
|
58bea1c813 | ||
|
|
5ad2bdf6c2 | ||
|
|
44f739a289 | ||
|
|
f3d9fc2342 | ||
|
|
90d4ca060e | ||
|
|
6554a4aaaa | ||
|
|
cac0048ca7 | ||
|
|
ddb28a8606 | ||
|
|
a7206863bc | ||
|
|
96def3b79a | ||
|
|
5231543ed2 | ||
|
|
2462da68bc | ||
|
|
bbfa6c6ccb | ||
|
|
f0ea908ee6 | ||
|
|
6e11230fed | ||
|
|
6c26e86e9a | ||
|
|
1d19705568 | ||
|
|
e5e20eca9c | ||
|
|
ef0639602f | ||
|
|
fdd4416049 | ||
|
|
0dab46e58f | ||
|
|
86d1061a25 | ||
|
|
e0197dd7a5 | ||
|
|
64ea63b77d | ||
|
|
bc6b2af03c | ||
|
|
c1af5fdc57 | ||
|
|
5be208520e | ||
|
|
5aaf0e010a | ||
|
|
48eee1b0d9 | ||
|
|
1447a825ee | ||
|
|
76f7dd3b09 | ||
|
|
ee174ddd1d | ||
|
|
35c4f8fdaa | ||
|
|
2ec7ae664e | ||
|
|
1b8ddaa849 | ||
|
|
d6bca1d38e | ||
|
|
b1d7626ab7 | ||
|
|
91598c091e | ||
|
|
fd3e73ee7e | ||
|
|
06a664a53a | ||
|
|
87d2509380 | ||
|
|
c1dc487f2c | ||
|
|
e0dd7ad44a | ||
|
|
aaa68fa351 | ||
|
|
d9e4133b5a | ||
|
|
821b99c4e0 | ||
|
|
8b5a2e9537 | ||
|
|
2e49835b4d | ||
|
|
ef106f6811 |
12
.github/actions/spell-check/allow/code.txt
vendored
12
.github/actions/spell-check/allow/code.txt
vendored
@@ -315,6 +315,7 @@ xef
|
||||
xes
|
||||
PACKAGEVERSIONNUMBER
|
||||
APPXMANIFESTVERSION
|
||||
PROGMAN
|
||||
|
||||
# MRU lists
|
||||
CACHEWRITE
|
||||
@@ -325,6 +326,14 @@ REGSTR
|
||||
# Misc Win32 APIs and PInvokes
|
||||
INVOKEIDLIST
|
||||
MEMORYSTATUSEX
|
||||
ABE
|
||||
HTCAPTION
|
||||
POSCHANGED
|
||||
QUERYPOS
|
||||
SETAUTOHIDEBAR
|
||||
WINDOWPOS
|
||||
WINEVENTPROC
|
||||
WORKERW
|
||||
|
||||
# PowerRename metadata pattern abbreviations (used in tests and regex patterns)
|
||||
DDDD
|
||||
@@ -349,3 +358,6 @@ nostdin
|
||||
# Performance counter keys
|
||||
engtype
|
||||
Nonpaged
|
||||
|
||||
# XAML
|
||||
Untargeted
|
||||
|
||||
@@ -26,6 +26,11 @@
|
||||
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
|
||||
"name": "Update template project",
|
||||
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
|
||||
},
|
||||
{
|
||||
"input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1",
|
||||
"name": "Build SDK",
|
||||
"description": "Builds the SDK nuget package with the specified version."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.CmdPal.Common;
|
||||
|
||||
public static class CoreLogger
|
||||
@@ -15,6 +13,8 @@ public static class CoreLogger
|
||||
|
||||
private static ILogger? _logger;
|
||||
|
||||
public static ILogger? Instance => _logger;
|
||||
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public partial class PinnedDockItem : WrappedDockItem
|
||||
{
|
||||
public override string Title => $"{base.Title} ({Properties.Resources.PinnedItemSuffix})";
|
||||
|
||||
public PinnedDockItem(ICommand command)
|
||||
: base(command, command.Name)
|
||||
{
|
||||
}
|
||||
|
||||
public PinnedDockItem(IListItem item, string id)
|
||||
: base([item], id, item.Title)
|
||||
{
|
||||
Icon = item.Icon;
|
||||
}
|
||||
}
|
||||
@@ -72,5 +72,14 @@ namespace Microsoft.CmdPal.Common.Properties {
|
||||
return ResourceManager.GetString("ErrorReport_Global_Preamble", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pinned.
|
||||
/// </summary>
|
||||
internal static string PinnedItemSuffix {
|
||||
get {
|
||||
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
@@ -117,6 +117,10 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
<data name="ErrorReport_Global_Preamble" xml:space="preserve">
|
||||
<value>This is an error report generated by Windows Command Palette.
|
||||
If you are seeing this, it means something went a little sideways in the app.
|
||||
@@ -124,4 +128,4 @@ You can help us fix it by filing a report at https://aka.ms/powerToysReportBug.
|
||||
|
||||
(While you’re at it, give the details below a quick skim — just to make sure there’s nothing personal you’d prefer not to share. It’s rare, but sometimes little surprises sneak in.)</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@@ -166,5 +166,5 @@ public interface IAppHostService
|
||||
|
||||
AppExtensionHost GetHostForCommand(object? context, AppExtensionHost? currentHost);
|
||||
|
||||
CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext);
|
||||
ICommandProviderContext GetProviderContextForCommand(object? command, ICommandProviderContext? currentContext);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
|
||||
internal static readonly Color DefaultTintColor = Color.FromArgb(255, 0, 120, 212);
|
||||
|
||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
internal static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
|
||||
// row 0
|
||||
Color.FromArgb(255, 255, 185, 0), // #ffb900
|
||||
|
||||
@@ -96,9 +96,10 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||
|
||||
ShouldShowContextMenu = SelectedItem.MoreCommands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.Count() > 1;
|
||||
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
|
||||
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
|
||||
|
||||
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
|
||||
@@ -22,7 +22,7 @@ public partial class CommandContextItemViewModel : CommandItemViewModel, IContex
|
||||
public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord);
|
||||
|
||||
public CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context)
|
||||
: base(new(contextItem), context)
|
||||
: base(new(contextItem), context, contextMenuFactory: null)
|
||||
{
|
||||
Model = new(contextItem);
|
||||
IsContextMenuItem = true;
|
||||
|
||||
@@ -51,7 +51,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
private string _itemTitle = string.Empty;
|
||||
|
||||
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
protected string ItemTitle => _itemTitle;
|
||||
|
||||
public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
|
||||
public string Subtitle { get; private set; } = string.Empty;
|
||||
|
||||
@@ -73,10 +75,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => this;
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
|
||||
public CommandItemViewModel? SecondaryCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasMoreCommands)
|
||||
{
|
||||
if (MoreCommands[0] is CommandContextItemViewModel command)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
public virtual bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
@@ -103,7 +125,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
public CommandItemViewModel(
|
||||
ExtensionObject<ICommandItem> item,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
IContextMenuFactory? contextMenuFactory = null)
|
||||
IContextMenuFactory? contextMenuFactory)
|
||||
: base(errorContext)
|
||||
{
|
||||
_commandItemModel = item;
|
||||
@@ -331,11 +353,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(Name));
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Icon));
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Title):
|
||||
_itemTitle = model.Title;
|
||||
_titleCache.Invalidate();
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Subtitle):
|
||||
@@ -343,6 +367,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
this.Subtitle = modelSubtitle;
|
||||
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
||||
_subtitleCache.Invalidate();
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Icon):
|
||||
@@ -401,11 +426,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDefaultContextItemIcon()
|
||||
{
|
||||
private void UpdateDefaultContextItemIcon() =>
|
||||
|
||||
// Command icon takes precedence over our icon on the primary command
|
||||
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
|
||||
}
|
||||
|
||||
private void UpdateTitle(string? title)
|
||||
{
|
||||
@@ -464,6 +488,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
.ForEach(c => c.SafeCleanup());
|
||||
}
|
||||
|
||||
public void RefreshMoreCommands()
|
||||
{
|
||||
Task.Run(RefreshMoreCommandsSynchronous);
|
||||
}
|
||||
|
||||
private void RefreshMoreCommandsSynchronous()
|
||||
{
|
||||
try
|
||||
{
|
||||
BuildAndInitMoreCommands();
|
||||
UpdateProperty(nameof(MoreCommands));
|
||||
UpdateProperty(nameof(AllCommands));
|
||||
UpdateProperty(nameof(SecondaryCommand));
|
||||
UpdateProperty(nameof(SecondaryCommandName));
|
||||
UpdateProperty(nameof(HasMoreCommands));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any exceptions that might occur during the refresh process
|
||||
CoreLogger.LogError("Error refreshing MoreCommands in CommandItemViewModel", ex);
|
||||
ShowException(ex, _commandItemModel?.Unsafe?.Title);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandPaletteContentPageViewModel : ContentPageViewModel
|
||||
{
|
||||
public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext)
|
||||
public CommandPaletteContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext)
|
||||
: base(model, scheduler, host, providerContext)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ public class CommandPalettePageViewModelFactory
|
||||
: IPageViewModelFactoryService
|
||||
{
|
||||
private readonly TaskScheduler _scheduler;
|
||||
private readonly IContextMenuFactory? _contextMenuFactory;
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory? contextMenuFactory)
|
||||
public CommandPalettePageViewModelFactory(TaskScheduler scheduler, IContextMenuFactory contextMenuFactory)
|
||||
{
|
||||
_scheduler = scheduler;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
}
|
||||
|
||||
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext)
|
||||
public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, ICommandProviderContext providerContext)
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsNested = nested },
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host, providerContext, _contextMenuFactory) { IsRootPage = !nested },
|
||||
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -4,9 +4,21 @@
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class CommandProviderContext
|
||||
public static class CommandProviderContext
|
||||
{
|
||||
public required string ProviderId { get; init; }
|
||||
public static ICommandProviderContext Empty { get; } = new EmptyCommandProviderContext();
|
||||
|
||||
public static CommandProviderContext Empty { get; } = new() { ProviderId = "<EMPTY>" };
|
||||
private sealed class EmptyCommandProviderContext : ICommandProviderContext
|
||||
{
|
||||
public string ProviderId => "<EMPTY>";
|
||||
|
||||
public bool SupportsPinning => false;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICommandProviderContext
|
||||
{
|
||||
string ProviderId { get; }
|
||||
|
||||
bool SupportsPinning { get; }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -13,7 +14,7 @@ using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class CommandProviderWrapper
|
||||
public sealed class CommandProviderWrapper : ICommandProviderContext
|
||||
{
|
||||
public bool IsExtension => Extension is not null;
|
||||
|
||||
@@ -29,6 +30,8 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
public TopLevelViewModel[] FallbackItems { get; private set; } = [];
|
||||
|
||||
public TopLevelViewModel[] DockBandItems { get; private set; } = [];
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
public IExtensionWrapper? Extension { get; }
|
||||
@@ -47,12 +50,17 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
public string ProviderId => string.IsNullOrEmpty(Extension?.ExtensionUniqueId) ? Id : Extension.ExtensionUniqueId;
|
||||
|
||||
public bool SupportsPinning { get; private set; }
|
||||
|
||||
public TopLevelItemPageContext TopLevelPageContext { get; }
|
||||
|
||||
public CommandProviderWrapper(ICommandProvider provider, TaskScheduler mainThread)
|
||||
{
|
||||
// This ctor is only used for in-proc builtin commands. So the Unsafe!
|
||||
// calls are pretty dang safe actually.
|
||||
_commandProvider = new(provider);
|
||||
_taskScheduler = mainThread;
|
||||
TopLevelPageContext = new(this, _taskScheduler);
|
||||
|
||||
// Hook the extension back into us
|
||||
ExtensionHost = new CommandPaletteHost(provider);
|
||||
@@ -77,6 +85,7 @@ public sealed class CommandProviderWrapper
|
||||
{
|
||||
_taskScheduler = mainThread;
|
||||
_commandProviderCache = commandProviderCache;
|
||||
TopLevelPageContext = new(this, _taskScheduler);
|
||||
|
||||
Extension = extension;
|
||||
ExtensionHost = new CommandPaletteHost(extension);
|
||||
@@ -121,7 +130,7 @@ public sealed class CommandProviderWrapper
|
||||
return settings.GetProviderSettings(this);
|
||||
}
|
||||
|
||||
public async Task LoadTopLevelCommands(IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
|
||||
public async Task LoadTopLevelCommands(IServiceProvider serviceProvider)
|
||||
{
|
||||
if (!isValid)
|
||||
{
|
||||
@@ -140,25 +149,47 @@ public sealed class CommandProviderWrapper
|
||||
return;
|
||||
}
|
||||
|
||||
ICommandItem[]? commands = null;
|
||||
IFallbackCommandItem[]? fallbacks = null;
|
||||
ICommandItem[] dockBands = []; // do not initialize me to null
|
||||
var displayInfoInitialized = false;
|
||||
|
||||
try
|
||||
{
|
||||
var model = _commandProvider.Unsafe!;
|
||||
|
||||
Task<ICommandItem[]> loadTopLevelCommandsTask = new(model.TopLevelCommands);
|
||||
loadTopLevelCommandsTask.Start();
|
||||
var commands = await loadTopLevelCommandsTask.ConfigureAwait(false);
|
||||
commands = await loadTopLevelCommandsTask.ConfigureAwait(false);
|
||||
|
||||
// On a BG thread here
|
||||
var fallbacks = model.FallbackCommands();
|
||||
fallbacks = model.FallbackCommands();
|
||||
|
||||
if (model is ICommandProvider2 two)
|
||||
{
|
||||
UnsafePreCacheApiAdditions(two);
|
||||
}
|
||||
|
||||
// Load pinned commands from saved settings
|
||||
var pinnedCommands = LoadPinnedCommands(model, providerSettings);
|
||||
if (model is ICommandProvider3 supportsDockBands)
|
||||
{
|
||||
var bands = supportsDockBands.GetDockBands();
|
||||
if (bands is not null)
|
||||
{
|
||||
Logger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
|
||||
dockBands = bands;
|
||||
}
|
||||
}
|
||||
|
||||
ICommandItem[] pinnedCommands = [];
|
||||
ICommandProvider4? four = null;
|
||||
if (model is ICommandProvider4 defintelyFour)
|
||||
{
|
||||
four = defintelyFour; // stash this away so we don't need to QI again
|
||||
SupportsPinning = true;
|
||||
|
||||
// Load pinned commands from saved settings
|
||||
pinnedCommands = LoadPinnedCommands(four, providerSettings);
|
||||
}
|
||||
|
||||
Id = model.Id;
|
||||
DisplayName = model.DisplayName;
|
||||
@@ -177,7 +208,8 @@ public sealed class CommandProviderWrapper
|
||||
Settings = new(model.Settings, this, _taskScheduler);
|
||||
|
||||
// We do need to explicitly initialize commands though
|
||||
InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider, pageContext);
|
||||
var objects = new TopLevelObjects(commands, fallbacks, pinnedCommands, dockBands);
|
||||
InitializeCommands(objects, serviceProvider, four);
|
||||
|
||||
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
|
||||
}
|
||||
@@ -208,15 +240,27 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, ICommandItem[] pinnedCommands, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
|
||||
private record TopLevelObjects(
|
||||
ICommandItem[]? Commands,
|
||||
IFallbackCommandItem[]? Fallbacks,
|
||||
ICommandItem[]? PinnedCommands,
|
||||
ICommandItem[]? DockBands);
|
||||
|
||||
private void InitializeCommands(
|
||||
TopLevelObjects objects,
|
||||
IServiceProvider serviceProvider,
|
||||
ICommandProvider4? four)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!;
|
||||
var state = serviceProvider.GetService<AppStateModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
var ourContext = GetProviderContext();
|
||||
var makeAndAdd = (ICommandItem? i, bool fallback) =>
|
||||
WeakReference<IPageContext> pageContext = new(this.TopLevelPageContext);
|
||||
var make = (ICommandItem? i, TopLevelType t) =>
|
||||
{
|
||||
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i);
|
||||
CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i);
|
||||
topLevelViewModel.InitializeProperties();
|
||||
|
||||
return topLevelViewModel;
|
||||
@@ -224,47 +268,123 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
var topLevelList = new List<TopLevelViewModel>();
|
||||
|
||||
if (commands is not null)
|
||||
if (objects.Commands is not null)
|
||||
{
|
||||
topLevelList.AddRange(commands.Select(c => makeAndAdd(c, false)));
|
||||
topLevelList.AddRange(objects.Commands.Select(c => make(c, TopLevelType.Normal)));
|
||||
}
|
||||
|
||||
if (pinnedCommands is not null)
|
||||
if (objects.PinnedCommands is not null)
|
||||
{
|
||||
topLevelList.AddRange(pinnedCommands.Select(c => makeAndAdd(c, false)));
|
||||
topLevelList.AddRange(objects.PinnedCommands.Select(c => make(c, TopLevelType.Normal)));
|
||||
}
|
||||
|
||||
TopLevelItems = topLevelList.ToArray();
|
||||
|
||||
if (fallbacks is not null)
|
||||
if (objects.Fallbacks is not null)
|
||||
{
|
||||
FallbackItems = fallbacks
|
||||
.Select(c => makeAndAdd(c, true))
|
||||
FallbackItems = objects.Fallbacks
|
||||
.Select(c => make(c, TopLevelType.Fallback))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private ICommandItem[] LoadPinnedCommands(ICommandProvider model, ProviderSettings providerSettings)
|
||||
{
|
||||
var pinnedItems = new List<ICommandItem>();
|
||||
|
||||
if (model is ICommandProvider4 provider4)
|
||||
List<TopLevelViewModel> bands = new();
|
||||
if (objects.DockBands is not null)
|
||||
{
|
||||
foreach (var pinnedId in providerSettings.PinnedCommandIds)
|
||||
// Start by adding TopLevelViewModels for all the dock bands which
|
||||
// are explicitly provided by the provider through the GetDockBands
|
||||
// API.
|
||||
foreach (var b in objects.DockBands)
|
||||
{
|
||||
var bandVm = make(b, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
}
|
||||
}
|
||||
|
||||
var dockSettings = settings.DockSettings;
|
||||
var allPinnedCommands = dockSettings.AllPinnedCommands;
|
||||
var pinnedBandsForThisProvider = allPinnedCommands.Where(c => c.ProviderId == ProviderId);
|
||||
foreach (var (providerId, commandId) in pinnedBandsForThisProvider)
|
||||
{
|
||||
Logger.LogDebug($"Looking for pinned dock band command {commandId} for provider {providerId}");
|
||||
|
||||
// First, try to lookup the command as one of this provider's
|
||||
// top-level commands. If it's there, then we can skip a lot of
|
||||
// work and just clone it as a band.
|
||||
if (LookupTopLevelCommand(commandId) is TopLevelViewModel topLevelCommand)
|
||||
{
|
||||
Logger.LogDebug($"Found pinned dock band command {commandId} for provider {providerId} as a top-level command");
|
||||
var bandModel = topLevelCommand.ToPinnedDockBandItem();
|
||||
var bandVm = make(bandModel, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we didn't find it as a top-level command, then we need to
|
||||
// try to get it directly from the provider and hope it supports
|
||||
// being a dock band. This is the fallback for providers that
|
||||
// don't explicitly support dock bands through GetDockBands, but
|
||||
// do support pinning commands (ICommandProvider4)
|
||||
if (four is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commandItem = provider4.GetCommandItem(pinnedId);
|
||||
var commandItem = four.GetCommandItem(commandId);
|
||||
if (commandItem is not null)
|
||||
{
|
||||
pinnedItems.Add(commandItem);
|
||||
Logger.LogDebug($"Found pinned dock band command {commandId} for provider {providerId} through ICommandProvider4 API");
|
||||
var bandVm = make(commandItem, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Couldn't find pinned dock band command {commandId} for provider {providerId} through ICommandProvider4 API. This command won't be shown as a dock band.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}");
|
||||
Logger.LogError($"Failed to load pinned dock band command {commandId} for provider {providerId}: {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Couldn't find pinned dock band command {commandId} for provider {providerId} as a top-level command, and provider doesn't support ICommandProvider4 API to get it directly. This command won't be shown as a dock band.");
|
||||
}
|
||||
}
|
||||
|
||||
DockBandItems = bands.ToArray();
|
||||
}
|
||||
|
||||
private TopLevelViewModel? LookupTopLevelCommand(string commandId)
|
||||
{
|
||||
foreach (var c in TopLevelItems)
|
||||
{
|
||||
if (c.Id == commandId)
|
||||
{
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ICommandItem[] LoadPinnedCommands(ICommandProvider4 model, ProviderSettings providerSettings)
|
||||
{
|
||||
var pinnedItems = new List<ICommandItem>();
|
||||
|
||||
foreach (var pinnedId in providerSettings.PinnedCommandIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commandItem = model.GetCommandItem(pinnedId);
|
||||
if (commandItem is not null)
|
||||
{
|
||||
pinnedItems.Add(commandItem);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError($"Failed to load pinned command {pinnedId}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return pinnedItems.ToArray();
|
||||
@@ -280,6 +400,10 @@ public sealed class CommandProviderWrapper
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
|
||||
}
|
||||
else if (a is ICommandItem[] commands)
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,18 +415,57 @@ public sealed class CommandProviderWrapper
|
||||
if (!providerSettings.PinnedCommandIds.Contains(commandId))
|
||||
{
|
||||
providerSettings.PinnedCommandIds.Add(commandId);
|
||||
SettingsModel.SaveSettings(settings);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
}
|
||||
|
||||
public CommandProviderContext GetProviderContext()
|
||||
public void UnpinCommand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
return new() { ProviderId = ProviderId };
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
|
||||
if (providerSettings.PinnedCommandIds.Remove(commandId))
|
||||
{
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void PinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var bandSettings = new DockBandSettings
|
||||
{
|
||||
CommandId = commandId,
|
||||
ProviderId = this.ProviderId,
|
||||
};
|
||||
settings.DockSettings.StartBands.Add(bandSettings);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
|
||||
public void UnpinDockBand(string commandId, IServiceProvider serviceProvider)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
settings.DockSettings.StartBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
settings.DockSettings.CenterBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
settings.DockSettings.EndBands.RemoveAll(b => b.CommandId == commandId && b.ProviderId == ProviderId);
|
||||
|
||||
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
|
||||
SettingsModel.SaveSettings(settings, false);
|
||||
}
|
||||
|
||||
public ICommandProviderContext GetProviderContext() => this;
|
||||
|
||||
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
|
||||
|
||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||
@@ -316,4 +479,14 @@ public sealed class CommandProviderWrapper
|
||||
// In handling this, a call will be made to `LoadTopLevelCommands` to
|
||||
// retrieve the new items.
|
||||
this.CommandsChanged?.Invoke(this, args);
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
Logger.LogDebug($"CommandProviderWrapper.PinDockBand: {ProviderId} - {bandVm.Id}");
|
||||
|
||||
var bands = this.DockBandItems.ToList();
|
||||
bands.Add(bandVm);
|
||||
this.DockBandItems = bands.ToArray();
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -18,6 +18,8 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
private readonly FallbackLogItem _fallbackLogItem = new();
|
||||
private readonly NewExtensionPage _newExtension = new();
|
||||
|
||||
private readonly IRootPageService _rootPageService;
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { },
|
||||
@@ -37,11 +39,22 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
_fallbackLogItem,
|
||||
];
|
||||
|
||||
public BuiltInsCommandProvider()
|
||||
public BuiltInsCommandProvider(IRootPageService rootPageService)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.builtin.core";
|
||||
DisplayName = Properties.Resources.builtin_display_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
|
||||
|
||||
_rootPageService = rootPageService;
|
||||
}
|
||||
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
var rootPage = _rootPageService.GetRootPage();
|
||||
List<ICommandItem> bandItems = new();
|
||||
bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
|
||||
|
||||
return bandItems.ToArray();
|
||||
}
|
||||
|
||||
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -11,7 +11,6 @@ using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
using Microsoft.CmdPal.Ext.Apps;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
||||
@@ -36,6 +35,11 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
|
||||
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
||||
|
||||
// Stable separator instances so that the VM cache and InPlaceUpdateList
|
||||
// recognise them across successive GetItems() calls
|
||||
private readonly Separator _resultsSeparator = new(Resources.results);
|
||||
private readonly Separator _fallbacksSeparator = new(Resources.fallbacks);
|
||||
|
||||
private RoScored<IListItem>[]? _filteredItems;
|
||||
private RoScored<IListItem>[]? _filteredApps;
|
||||
|
||||
@@ -61,6 +65,7 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
AppStateModel appStateModel,
|
||||
IFuzzyMatcherProvider fuzzyMatcherProvider)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.home";
|
||||
Title = Resources.builtin_home_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
||||
@@ -171,9 +176,40 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
// filtered results.
|
||||
if (string.IsNullOrWhiteSpace(SearchText))
|
||||
{
|
||||
return _tlcManager.TopLevelCommands
|
||||
.Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title))
|
||||
.ToArray();
|
||||
var allCommands = _tlcManager.TopLevelCommands;
|
||||
|
||||
// First pass: count eligible commands
|
||||
var eligibleCount = 0;
|
||||
for (var i = 0; i < allCommands.Count; i++)
|
||||
{
|
||||
var cmd = allCommands[i];
|
||||
if (!cmd.IsFallback && !string.IsNullOrEmpty(cmd.Title))
|
||||
{
|
||||
eligibleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (eligibleCount == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// +1 for the separator
|
||||
var result = new IListItem[eligibleCount + 1];
|
||||
result[0] = _resultsSeparator;
|
||||
|
||||
// Second pass: populate
|
||||
var writeIndex = 1;
|
||||
for (var i = 0; i < allCommands.Count; i++)
|
||||
{
|
||||
var cmd = allCommands[i];
|
||||
if (!cmd.IsFallback && !string.IsNullOrEmpty(cmd.Title))
|
||||
{
|
||||
result[writeIndex++] = cmd;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -190,6 +226,8 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
validScoredFallbacks,
|
||||
_filteredApps,
|
||||
validFallbacks,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
AppResultLimit);
|
||||
}
|
||||
}
|
||||
@@ -371,11 +409,13 @@ public sealed partial class MainListPage : DynamicListPage,
|
||||
var allNewApps = AllAppsCommandProvider.Page.GetItems().Cast<AppListItem>().ToList();
|
||||
|
||||
// We need to remove pinned apps from allNewApps so they don't show twice.
|
||||
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
|
||||
// Pinned app command IDs are stored in ProviderSettings.PinnedCommandIds.
|
||||
_settings.ProviderSettings.TryGetValue(AllAppsCommandProvider.WellKnownId, out var providerSettings);
|
||||
var pinnedCommandIds = providerSettings?.PinnedCommandIds;
|
||||
|
||||
if (pinnedApps.Length > 0)
|
||||
if (pinnedCommandIds is not null && pinnedCommandIds.Count > 0)
|
||||
{
|
||||
newApps = allNewApps.Where(w => pinnedApps.IndexOf(w.AppIdentifier) < 0);
|
||||
newApps = allNewApps.Where(li => li.Command != null && !pinnedCommandIds.Contains(li.Command.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -21,6 +21,8 @@ internal static class MainListPageResultFactory
|
||||
IList<RoScored<IListItem>>? scoredFallbackItems,
|
||||
IList<RoScored<IListItem>>? filteredApps,
|
||||
IList<RoScored<IListItem>>? fallbackItems,
|
||||
IListItem resultsSeparator,
|
||||
IListItem fallbacksSeparator,
|
||||
int appResultLimit)
|
||||
{
|
||||
if (appResultLimit < 0)
|
||||
@@ -40,8 +42,13 @@ internal static class MainListPageResultFactory
|
||||
int nonEmptyFallbackCount = fallbackItems?.Count ?? 0;
|
||||
|
||||
// Allocate the exact size of the result array.
|
||||
// We'll add an extra slot for the fallbacks section header if needed.
|
||||
int totalCount = len1 + len2 + len3 + nonEmptyFallbackCount + (nonEmptyFallbackCount > 0 ? 1 : 0);
|
||||
// We'll add an extra slot for the fallbacks section header if needed,
|
||||
// and another for the "Results" section header when merged results exist.
|
||||
int mergedCount = len1 + len2 + len3;
|
||||
bool needsResultsHeader = mergedCount > 0;
|
||||
int totalCount = mergedCount + nonEmptyFallbackCount
|
||||
+ (needsResultsHeader ? 1 : 0)
|
||||
+ (nonEmptyFallbackCount > 0 ? 1 : 0);
|
||||
|
||||
var result = new IListItem[totalCount];
|
||||
|
||||
@@ -49,6 +56,12 @@ internal static class MainListPageResultFactory
|
||||
int idx1 = 0, idx2 = 0, idx3 = 0;
|
||||
int writePos = 0;
|
||||
|
||||
// Add "Results" section header when merged results will precede the fallbacks.
|
||||
if (needsResultsHeader)
|
||||
{
|
||||
result[writePos++] = resultsSeparator;
|
||||
}
|
||||
|
||||
// Merge while all three lists have items. To maintain a stable sort, the
|
||||
// priority is: list1 > list2 > list3 when scores are equal.
|
||||
while (idx1 < len1 && idx2 < len2 && idx3 < len3)
|
||||
@@ -132,7 +145,7 @@ internal static class MainListPageResultFactory
|
||||
// Create the fallbacks section header
|
||||
if (fallbackItems.Count > 0)
|
||||
{
|
||||
result[writePos++] = new Separator(Properties.Resources.fallbacks);
|
||||
result[writePos++] = fallbacksSeparator;
|
||||
}
|
||||
|
||||
for (int i = 0; i < fallbackItems.Count; i++)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -70,6 +70,15 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
|
||||
RenderCard();
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void RenderCard()
|
||||
{
|
||||
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
|
||||
{
|
||||
Card = builtCard;
|
||||
@@ -93,8 +102,41 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
UpdateProperty(nameof(Card));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._formModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(DataJson):
|
||||
DataJson = model.DataJson;
|
||||
RenderCard();
|
||||
break;
|
||||
case nameof(TemplateJson):
|
||||
TemplateJson = model.TemplateJson;
|
||||
RenderCard();
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
|
||||
|
||||
@@ -47,7 +47,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
|
||||
// Remember - "observable" properties from the model (via PropChanged)
|
||||
// cannot be marked [ObservableProperty]
|
||||
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext)
|
||||
public ContentPageViewModel(IContentPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext)
|
||||
: base(model, scheduler, host, providerContext)
|
||||
{
|
||||
_model = new(model);
|
||||
|
||||
@@ -59,11 +59,8 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
{
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
if (SelectedItem.PrimaryCommand is not null || SelectedItem.HasMoreCommands)
|
||||
{
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public partial class DockBandSettingsViewModel : ObservableObject
|
||||
{
|
||||
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly DockBandSettings _dockSettingsModel;
|
||||
private readonly TopLevelViewModel _adapter;
|
||||
private readonly DockBandViewModel? _bandViewModel;
|
||||
|
||||
public string Title => _adapter.Title;
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
List<string> parts = [_adapter.ExtensionName];
|
||||
|
||||
// Add the number of items in the band
|
||||
var itemCount = NumItemsInBand();
|
||||
if (itemCount > 0)
|
||||
{
|
||||
var itemsString = itemCount == 1 ?
|
||||
Properties.Resources.dock_item_count_singular :
|
||||
string.Format(CultureInfo.CurrentCulture, PluralItemsFormatString, itemCount);
|
||||
parts.Add(itemsString);
|
||||
}
|
||||
|
||||
return string.Join(" - ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public string ProviderId => _adapter.CommandProviderId;
|
||||
|
||||
public IconInfoViewModel Icon => _adapter.IconViewModel;
|
||||
|
||||
private ShowLabelsOption _showLabels;
|
||||
|
||||
public ShowLabelsOption ShowLabels
|
||||
{
|
||||
get => _showLabels;
|
||||
set
|
||||
{
|
||||
if (value != _showLabels)
|
||||
{
|
||||
_showLabels = value;
|
||||
_dockSettingsModel.ShowLabels = value switch
|
||||
{
|
||||
ShowLabelsOption.Default => null,
|
||||
ShowLabelsOption.ShowLabels => true,
|
||||
ShowLabelsOption.HideLabels => false,
|
||||
_ => null,
|
||||
};
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ShowLabelsOption FetchShowLabels()
|
||||
{
|
||||
if (_dockSettingsModel.ShowLabels == null)
|
||||
{
|
||||
return ShowLabelsOption.Default;
|
||||
}
|
||||
|
||||
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
|
||||
}
|
||||
|
||||
// used to map to ComboBox selection
|
||||
public int ShowLabelsIndex
|
||||
{
|
||||
get => (int)ShowLabels;
|
||||
set => ShowLabels = (ShowLabelsOption)value;
|
||||
}
|
||||
|
||||
private DockPinSide PinSide
|
||||
{
|
||||
get => _pinSide;
|
||||
set
|
||||
{
|
||||
if (value != _pinSide)
|
||||
{
|
||||
UpdatePinSide(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DockPinSide _pinSide;
|
||||
|
||||
public int PinSideIndex
|
||||
{
|
||||
get => (int)PinSide;
|
||||
set => PinSide = (DockPinSide)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the band is pinned to the dock.
|
||||
/// When enabled, pins to Center. When disabled, removes from all sides.
|
||||
/// </summary>
|
||||
public bool IsPinned
|
||||
{
|
||||
get => PinSide != DockPinSide.None;
|
||||
set
|
||||
{
|
||||
if (value && PinSide == DockPinSide.None)
|
||||
{
|
||||
// Pin to Center by default when enabling
|
||||
PinSide = DockPinSide.Center;
|
||||
}
|
||||
else if (!value && PinSide != DockPinSide.None)
|
||||
{
|
||||
// Remove from dock when disabling
|
||||
PinSide = DockPinSide.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DockBandSettingsViewModel(
|
||||
DockBandSettings dockSettingsModel,
|
||||
TopLevelViewModel topLevelAdapter,
|
||||
DockBandViewModel? bandViewModel,
|
||||
SettingsModel settingsModel)
|
||||
{
|
||||
_dockSettingsModel = dockSettingsModel;
|
||||
_adapter = topLevelAdapter;
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsModel = settingsModel;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var inStart = dockSettings.StartBands.Any(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
if (inStart)
|
||||
{
|
||||
return DockPinSide.Start;
|
||||
}
|
||||
|
||||
var inCenter = dockSettings.CenterBands.Any(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
if (inCenter)
|
||||
{
|
||||
return DockPinSide.Center;
|
||||
}
|
||||
|
||||
var inEnd = dockSettings.EndBands.Any(b => b.CommandId == _dockSettingsModel.CommandId);
|
||||
if (inEnd)
|
||||
{
|
||||
return DockPinSide.End;
|
||||
}
|
||||
|
||||
return DockPinSide.None;
|
||||
}
|
||||
|
||||
private int NumItemsInBand()
|
||||
{
|
||||
var bandVm = _bandViewModel;
|
||||
if (bandVm is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _bandViewModel!.Items.Count;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
private void UpdatePinSide(DockPinSide value)
|
||||
{
|
||||
OnPinSideChanged(value);
|
||||
OnPropertyChanged(nameof(PinSideIndex));
|
||||
OnPropertyChanged(nameof(PinSide));
|
||||
OnPropertyChanged(nameof(IsPinned));
|
||||
}
|
||||
|
||||
public void SetBandPosition(DockPinSide side, int? index)
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from all sides first
|
||||
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 insertIndex = index ?? dockSettings.StartBands.Count;
|
||||
dockSettings.StartBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.CenterBands.Count;
|
||||
dockSettings.CenterBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.EndBands.Count;
|
||||
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.None:
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private void OnPinSideChanged(DockPinSide value)
|
||||
{
|
||||
SetBandPosition(value, null);
|
||||
_pinSide = value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DockPinSide
|
||||
{
|
||||
None,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
}
|
||||
|
||||
public enum ShowLabelsOption
|
||||
{
|
||||
Default,
|
||||
ShowLabels,
|
||||
HideLabels,
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly CommandItemViewModel _rootItem;
|
||||
private readonly DockBandSettings _bandSettings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly Action _saveSettings;
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
||||
|
||||
private bool _showTitles = true;
|
||||
private bool _showSubtitles = true;
|
||||
private bool? _showTitlesSnapshot;
|
||||
private bool? _showSubtitlesSnapshot;
|
||||
|
||||
public string Id => _rootItem.Command.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether titles are shown for items in this band.
|
||||
/// This is a preview value - call <see cref="SaveLabelSettings"/> to persist or
|
||||
/// <see cref="RestoreLabelSettings"/> to discard changes.
|
||||
/// </summary>
|
||||
public bool ShowTitles
|
||||
{
|
||||
get => _showTitles;
|
||||
set
|
||||
{
|
||||
if (_showTitles != value)
|
||||
{
|
||||
_showTitles = value;
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.ShowTitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether subtitles are shown for items in this band.
|
||||
/// This is a preview value - call <see cref="SaveLabelSettings"/> to persist or
|
||||
/// <see cref="RestoreLabelSettings"/> to discard changes.
|
||||
/// </summary>
|
||||
public bool ShowSubtitles
|
||||
{
|
||||
get => _showSubtitles;
|
||||
set
|
||||
{
|
||||
if (_showSubtitles != value)
|
||||
{
|
||||
_showSubtitles = value;
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.ShowSubtitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether labels (both titles and subtitles) are shown.
|
||||
/// Provided for backward compatibility - setting this sets both ShowTitles and ShowSubtitles.
|
||||
/// </summary>
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => _showTitles && _showSubtitles;
|
||||
set
|
||||
{
|
||||
ShowTitles = value;
|
||||
ShowSubtitles = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current label settings before editing.
|
||||
/// </summary>
|
||||
internal void SnapshotShowLabels()
|
||||
{
|
||||
_showTitlesSnapshot = _showTitles;
|
||||
_showSubtitlesSnapshot = _showSubtitles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current label settings to settings.
|
||||
/// </summary>
|
||||
internal void SaveShowLabels()
|
||||
{
|
||||
_bandSettings.ShowTitles = _showTitles;
|
||||
_bandSettings.ShowSubtitles = _showSubtitles;
|
||||
_showTitlesSnapshot = null;
|
||||
_showSubtitlesSnapshot = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the label settings from the snapshot.
|
||||
/// </summary>
|
||||
internal void RestoreShowLabels()
|
||||
{
|
||||
if (_showTitlesSnapshot.HasValue)
|
||||
{
|
||||
ShowTitles = _showTitlesSnapshot.Value;
|
||||
_showTitlesSnapshot = null;
|
||||
}
|
||||
|
||||
if (_showSubtitlesSnapshot.HasValue)
|
||||
{
|
||||
ShowSubtitles = _showSubtitlesSnapshot.Value;
|
||||
_showSubtitlesSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal DockBandViewModel(
|
||||
CommandItemViewModel commandItemViewModel,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
DockBandSettings settings,
|
||||
DockSettings dockSettings,
|
||||
Action saveSettings,
|
||||
IContextMenuFactory contextMenuFactory)
|
||||
: base(errorContext)
|
||||
{
|
||||
_rootItem = commandItemViewModel;
|
||||
_bandSettings = settings;
|
||||
_dockSettings = dockSettings;
|
||||
_saveSettings = saveSettings;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
|
||||
_showTitles = settings.ResolveShowTitles(dockSettings.ShowLabels);
|
||||
_showSubtitles = settings.ResolveShowSubtitles(dockSettings.ShowLabels);
|
||||
}
|
||||
|
||||
private void InitializeFromList(IListPage list)
|
||||
{
|
||||
var items = list.GetItems();
|
||||
var newViewModels = new List<DockItemViewModel>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var newItemVm = new DockItemViewModel(new(item), this.PageContext, _showTitles, _showSubtitles, _contextMenuFactory);
|
||||
newItemVm.SlowInitializeProperties();
|
||||
newViewModels.Add(newItemVm);
|
||||
}
|
||||
|
||||
List<DockItemViewModel> removed = new();
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removed);
|
||||
});
|
||||
|
||||
foreach (var removedItem in removed)
|
||||
{
|
||||
removedItem.SafeCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var command = _rootItem.Command;
|
||||
var list = command.Model.Unsafe as IListPage;
|
||||
if (list is not null)
|
||||
{
|
||||
InitializeFromList(list);
|
||||
list.ItemsChanged += HandleItemsChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
var dockItem = new DockItemViewModel(_rootItem, _showTitles, _showSubtitles, _contextMenuFactory);
|
||||
dockItem.SlowInitializeProperties();
|
||||
Items.Add(dockItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleItemsChanged(object sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
if (_rootItem.Command.Model.Unsafe is IListPage p)
|
||||
{
|
||||
InitializeFromList(p);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
{
|
||||
base.UnsafeCleanup();
|
||||
|
||||
var command = _rootItem.Command;
|
||||
if (command.Model.Unsafe is IListPage list)
|
||||
{
|
||||
list.ItemsChanged -= HandleItemsChanged;
|
||||
}
|
||||
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.SafeCleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DockItemViewModel : CommandItemViewModel
|
||||
{
|
||||
private bool _showTitle = true;
|
||||
private bool _showSubtitle = true;
|
||||
|
||||
public bool ShowTitle
|
||||
{
|
||||
get => _showTitle;
|
||||
internal set
|
||||
{
|
||||
if (_showTitle != value)
|
||||
{
|
||||
_showTitle = value;
|
||||
UpdateProperty(nameof(ShowTitle));
|
||||
UpdateProperty(nameof(ShowLabel));
|
||||
UpdateProperty(nameof(HasText));
|
||||
UpdateProperty(nameof(Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSubtitle
|
||||
{
|
||||
get => _showSubtitle;
|
||||
internal set
|
||||
{
|
||||
if (_showSubtitle != value)
|
||||
{
|
||||
_showSubtitle = value;
|
||||
UpdateProperty(nameof(ShowSubtitle));
|
||||
UpdateProperty(nameof(ShowLabel));
|
||||
UpdateProperty(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether labels are shown (either titles or subtitles).
|
||||
/// Setting this sets both ShowTitle and ShowSubtitle.
|
||||
/// </summary>
|
||||
public bool ShowLabel
|
||||
{
|
||||
get => _showTitle || _showSubtitle;
|
||||
internal set
|
||||
{
|
||||
ShowTitle = value;
|
||||
ShowSubtitle = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string Title => _showTitle ? ItemTitle : string.Empty;
|
||||
|
||||
public new string Subtitle => _showSubtitle ? base.Subtitle : string.Empty;
|
||||
|
||||
public override bool HasText => (_showTitle && !string.IsNullOrEmpty(ItemTitle)) || (_showSubtitle && !string.IsNullOrEmpty(base.Subtitle));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tooltip for the dock item, which includes the title and
|
||||
/// subtitle. If it doesn't have one part, it just returns the other.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Trickery: in the case one is empty, we can just concatenate, and it will
|
||||
/// always only be the one that's non-empty
|
||||
/// </remarks>
|
||||
public string Tooltip =>
|
||||
!string.IsNullOrEmpty(ItemTitle) && !string.IsNullOrEmpty(base.Subtitle) ?
|
||||
$"{ItemTitle}\n{base.Subtitle}" :
|
||||
ItemTitle + base.Subtitle;
|
||||
|
||||
public DockItemViewModel(CommandItemViewModel root, bool showTitle, bool showSubtitle, IContextMenuFactory contextMenuFactory)
|
||||
: this(root.Model, root.PageContext, showTitle, showSubtitle, contextMenuFactory)
|
||||
{
|
||||
}
|
||||
|
||||
public DockItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext, bool showTitle, bool showSubtitle, IContextMenuFactory contextMenuFactory)
|
||||
: base(item, errorContext, contextMenuFactory)
|
||||
{
|
||||
_showTitle = showTitle;
|
||||
_showSubtitle = showSubtitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,625 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public sealed partial class DockViewModel
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly DockPageContext _pageContext; // only to be used for our own context menu - not for dock bands themselves
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
private DockSettings _settings;
|
||||
|
||||
public TaskScheduler Scheduler { get; }
|
||||
|
||||
public ObservableCollection<DockBandViewModel> StartItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> CenterItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
|
||||
|
||||
public DockViewModel(
|
||||
TopLevelCommandManager tlcManager,
|
||||
IContextMenuFactory contextMenuFactory,
|
||||
SettingsModel settings,
|
||||
TaskScheduler scheduler)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
_settingsModel = settings;
|
||||
_settings = settings.DockSettings;
|
||||
Scheduler = scheduler;
|
||||
_pageContext = new(this);
|
||||
|
||||
_topLevelCommandManager.DockBands.CollectionChanged += DockBands_CollectionChanged;
|
||||
}
|
||||
|
||||
private void DockBands_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
Logger.LogDebug("Starting DockBands_CollectionChanged");
|
||||
SetupBands();
|
||||
Logger.LogDebug("Ended DockBands_CollectionChanged");
|
||||
}
|
||||
|
||||
public void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
Logger.LogDebug($"DockViewModel.UpdateSettings");
|
||||
_settings = settings;
|
||||
SetupBands();
|
||||
}
|
||||
|
||||
private void SetupBands()
|
||||
{
|
||||
Logger.LogDebug($"Setting up dock bands");
|
||||
SetupBands(_settings.StartBands, StartItems);
|
||||
SetupBands(_settings.CenterBands, CenterItems);
|
||||
SetupBands(_settings.EndBands, EndItems);
|
||||
}
|
||||
|
||||
private void SetupBands(
|
||||
List<DockBandSettings> bands,
|
||||
ObservableCollection<DockBandViewModel> target)
|
||||
{
|
||||
List<DockBandViewModel> newBands = new();
|
||||
foreach (var band in bands)
|
||||
{
|
||||
var commandId = band.CommandId;
|
||||
var topLevelCommand = _topLevelCommandManager.LookupDockBand(commandId);
|
||||
|
||||
if (topLevelCommand is null)
|
||||
{
|
||||
Logger.LogWarning($"Failed to find band {commandId}");
|
||||
}
|
||||
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
// note: CreateBandItem doesn't actually initialize the band, it
|
||||
// just creates the VM. Callers need to make sure to call
|
||||
// InitializeProperties() on a BG thread elsewhere
|
||||
var bandVm = CreateBandItem(band, topLevelCommand.ItemViewModel);
|
||||
newBands.Add(bandVm);
|
||||
}
|
||||
}
|
||||
|
||||
var beforeCount = target.Count;
|
||||
var afterCount = newBands.Count;
|
||||
|
||||
List<DockBandViewModel> removed = new();
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(target, newBands, out removed);
|
||||
var isStartBand = target == StartItems;
|
||||
var label = isStartBand ? "Start bands:" : "End bands:";
|
||||
Logger.LogDebug($"{label} ({beforeCount}) -> ({afterCount}), Removed {removed?.Count ?? 0} items");
|
||||
});
|
||||
|
||||
// Initialize properties on BG thread
|
||||
Task.Run(() =>
|
||||
{
|
||||
foreach (var band in newBands)
|
||||
{
|
||||
band.SafeInitializePropertiesSynchronous();
|
||||
}
|
||||
});
|
||||
foreach (var removedItem in removed)
|
||||
{
|
||||
removedItem.SafeCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiate a new band view model for this CommandItem, given the
|
||||
/// settings. The DockBandViewModel will _not_ be initialized - callers
|
||||
/// will need to make sure to initialize it somewhere else (off the UI
|
||||
/// thread)
|
||||
/// </summary>
|
||||
private DockBandViewModel CreateBandItem(
|
||||
DockBandSettings bandSettings,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
DockBandViewModel band = new(commandItem, commandItem.PageContext, bandSettings, _settings, SaveSettings, _contextMenuFactory);
|
||||
|
||||
// the band is NOT initialized here!
|
||||
return band;
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||
{
|
||||
var id = tlc.Id;
|
||||
return FindBandById(id);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandById(string id)
|
||||
{
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Syncs the band position in settings after a same-list reorder.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.CommandId == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all settings lists
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
||||
|
||||
// Add to target settings list at the correct index
|
||||
var targetSettings = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => dockSettings.StartBands,
|
||||
DockPinSide.Center => dockSettings.CenterBands,
|
||||
DockPinSide.End => dockSettings.EndBands,
|
||||
_ => dockSettings.StartBands,
|
||||
};
|
||||
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
|
||||
targetSettings.Insert(insertIndex, bandSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a dock band to a new position (cross-list drop).
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.CommandId == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.CommandId == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
Logger.LogWarning($"Could not find band settings for band {bandId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all sides (settings and UI)
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
// Add to the target side at the specified index
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
|
||||
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, StartItems.Count);
|
||||
StartItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.CenterBands.Count);
|
||||
dockSettings.CenterBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
|
||||
CenterItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
|
||||
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, EndItems.Count);
|
||||
EndItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current band order and label settings to settings.
|
||||
/// Call this when exiting edit mode.
|
||||
/// </summary>
|
||||
public void SaveBandOrder()
|
||||
{
|
||||
// Save ShowLabels for all bands
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
band.SaveShowLabels();
|
||||
}
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
Logger.LogDebug("Saved band order to settings");
|
||||
}
|
||||
|
||||
private List<DockBandSettings>? _snapshotStartBands;
|
||||
private List<DockBandSettings>? _snapshotCenterBands;
|
||||
private List<DockBandSettings>? _snapshotEndBands;
|
||||
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current band order and label settings before editing.
|
||||
/// Call this when entering edit mode.
|
||||
/// </summary>
|
||||
public void SnapshotBandOrder()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
_snapshotStartBands = dockSettings.StartBands.Select(b => b.Clone()).ToList();
|
||||
_snapshotCenterBands = dockSettings.CenterBands.Select(b => b.Clone()).ToList();
|
||||
_snapshotEndBands = dockSettings.EndBands.Select(b => b.Clone()).ToList();
|
||||
|
||||
// Snapshot band ViewModels so we can restore unpinned bands
|
||||
// Use a dictionary but handle potential duplicates gracefully
|
||||
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
_snapshotBandViewModels.TryAdd(band.Id, band);
|
||||
}
|
||||
|
||||
// Snapshot ShowLabels for all bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.SnapshotShowLabels();
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotCenterBands.Count} center bands, {_snapshotEndBands.Count} end bands");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the band order and label settings from the snapshot taken when entering edit mode.
|
||||
/// Call this when discarding edit mode changes.
|
||||
/// </summary>
|
||||
public void RestoreBandOrder()
|
||||
{
|
||||
if (_snapshotStartBands == null ||
|
||||
_snapshotCenterBands == null ||
|
||||
_snapshotEndBands == null || _snapshotBandViewModels == null)
|
||||
{
|
||||
Logger.LogWarning("No snapshot to restore from");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore ShowLabels for all snapshotted bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.RestoreShowLabels();
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// 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
|
||||
RebuildUICollectionsFromSnapshot();
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
Logger.LogDebug("Restored band order from snapshot");
|
||||
}
|
||||
|
||||
private void RebuildUICollectionsFromSnapshot()
|
||||
{
|
||||
if (_snapshotBandViewModels == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.CommandId, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.CommandId, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.CommandId, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildUICollections()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Create a lookup of all current band ViewModels
|
||||
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.CommandId, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.CommandId, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.CommandId, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of dock bands that are not currently pinned to any section.
|
||||
/// </summary>
|
||||
public IEnumerable<TopLevelViewModel> GetAvailableBandsToAdd()
|
||||
{
|
||||
// Get IDs of all bands currently in the dock
|
||||
var pinnedBandIds = new HashSet<string>();
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
// Return all dock bands that are not already pinned
|
||||
return AllItems.Where(tlc => !pinnedBandIds.Contains(tlc.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a band to the specified dock section.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void AddBandToSection(TopLevelViewModel topLevel, DockPinSide targetSide)
|
||||
{
|
||||
var bandId = topLevel.Id;
|
||||
|
||||
// Check if already in the dock
|
||||
if (FindBandById(bandId) != null)
|
||||
{
|
||||
Logger.LogWarning($"Band {bandId} is already in the dock");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { ProviderId = topLevel.CommandProviderId, CommandId = bandId, ShowLabels = null };
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Create the band view model
|
||||
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
||||
|
||||
// Add to the appropriate section
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
StartItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.Center:
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
CenterItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.End:
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
EndItems.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
|
||||
// Snapshot the new band so it can be removed on discard
|
||||
bandVm.SnapshotShowLabels();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
bandVm.SafeInitializePropertiesSynchronous();
|
||||
});
|
||||
|
||||
Logger.LogDebug($"Added band {bandId} to {targetSide} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpins a band from the dock, removing it from whichever section it's in.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void UnpinBand(DockBandViewModel band)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from settings
|
||||
dockSettings.StartBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.CommandId == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.CommandId == bandId);
|
||||
|
||||
// Remove from UI collections
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
Logger.LogDebug($"Unpinned band {bandId} (not saved yet)");
|
||||
}
|
||||
|
||||
private void DoOnUiThread(Action action)
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
Scheduler);
|
||||
}
|
||||
|
||||
public CommandItemViewModel GetContextMenuForDock()
|
||||
{
|
||||
var model = new DockContextMenuItem();
|
||||
var vm = new CommandItemViewModel(new(model), new(_pageContext), contextMenuFactory: null);
|
||||
vm.SlowInitializeProperties();
|
||||
return vm;
|
||||
}
|
||||
|
||||
private sealed partial class DockContextMenuItem : CommandItem
|
||||
{
|
||||
public DockContextMenuItem()
|
||||
{
|
||||
var editDockCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new EnterDockEditModeMessage());
|
||||
})
|
||||
{
|
||||
Name = Properties.Resources.dock_edit_dock_name,
|
||||
Icon = Icons.EditIcon,
|
||||
};
|
||||
|
||||
var openSettingsCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
|
||||
})
|
||||
{
|
||||
Name = Properties.Resources.dock_settings_name,
|
||||
Icon = Icons.SettingsIcon,
|
||||
};
|
||||
|
||||
MoreCommands = new CommandContextItem[]
|
||||
{
|
||||
new CommandContextItem(editDockCommand),
|
||||
new CommandContextItem(openSettingsCommand),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an empty page context, for the dock's own context menu. We're
|
||||
/// building the context menu for the dock using literally our own cmdpal
|
||||
/// types, but that means we need a page context for the VM we will
|
||||
/// generate.
|
||||
/// </summary>
|
||||
private sealed partial class DockPageContext(DockViewModel dockViewModel) : IPageContext
|
||||
{
|
||||
public TaskScheduler Scheduler => dockViewModel.Scheduler;
|
||||
|
||||
public ICommandProviderContext ProviderContext => CommandProviderContext.Empty;
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint)
|
||||
{
|
||||
var extensionText = extensionHint ?? "<unknown>";
|
||||
Logger.LogError($"Error in dock context {extensionText}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public partial class DockWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ImageSource? BackgroundImageSource { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageOpacity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundImageTint { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageTintIntensity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int BackgroundImageBlurAmount { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageBrightness { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowBackgroundImage { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowColorizationOverlay { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color ColorizationColor { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double ColorizationOpacity { get; private set; }
|
||||
|
||||
public DockWindowViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
UpdateFromThemeSnapshot();
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(UpdateFromThemeSnapshot);
|
||||
}
|
||||
|
||||
private void UpdateFromThemeSnapshot()
|
||||
{
|
||||
var snapshot = _themeService.CurrentDockTheme;
|
||||
|
||||
BackgroundImageSource = snapshot.BackgroundImageSource;
|
||||
BackgroundImageStretch = snapshot.BackgroundImageStretch;
|
||||
BackgroundImageOpacity = snapshot.BackgroundImageOpacity;
|
||||
|
||||
BackgroundImageBrightness = snapshot.BackgroundBrightness;
|
||||
BackgroundImageTint = snapshot.Tint;
|
||||
BackgroundImageTintIntensity = snapshot.TintIntensity;
|
||||
BackgroundImageBlurAmount = snapshot.BlurAmount;
|
||||
|
||||
ShowBackgroundImage = BackgroundImageSource != null;
|
||||
|
||||
// Colorization overlay for transparent backdrop
|
||||
ShowColorizationOverlay = snapshot.Backdrop == DockBackdrop.Transparent && snapshot.TintIntensity > 0;
|
||||
ColorizationColor = snapshot.Tint;
|
||||
ColorizationOpacity = snapshot.TintIntensity;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for dock appearance settings, controlling theme, backdrop, colorization,
|
||||
/// and background image settings for the dock.
|
||||
/// </summary>
|
||||
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private ElementTheme? _elementThemeOverride;
|
||||
private Color _currentSystemAccentColor;
|
||||
|
||||
public ObservableCollection<Color> Swatches => AppearanceSettingsViewModel.WindowsColorSwatches;
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_dockSettings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _dockSettings.Theme;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Theme != value)
|
||||
{
|
||||
_dockSettings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackdropIndex
|
||||
{
|
||||
get => (int)_dockSettings.Backdrop;
|
||||
set => Backdrop = (DockBackdrop)value;
|
||||
}
|
||||
|
||||
public DockBackdrop Backdrop
|
||||
{
|
||||
get => _dockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Backdrop != value)
|
||||
{
|
||||
_dockSettings.Backdrop = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackdropIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _dockSettings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.ColorizationMode != value)
|
||||
{
|
||||
_dockSettings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||
|
||||
if (value == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
|
||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_dockSettings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _dockSettings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.CustomThemeColor != value)
|
||||
{
|
||||
_dockSettings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
if (ColorIntensity == 0)
|
||||
{
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _dockSettings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_dockSettings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _dockSettings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImagePath != value)
|
||||
{
|
||||
_dockSettings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
{
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _dockSettings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _dockSettings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageFit != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageFitIndex
|
||||
{
|
||||
get => BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => 1,
|
||||
_ => 0,
|
||||
};
|
||||
set => BackgroundImageFit = value switch
|
||||
{
|
||||
1 => BackgroundImageFit.Fill,
|
||||
_ => BackgroundImageFit.UniformToFill,
|
||||
};
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor => ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
|
||||
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
|
||||
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
|
||||
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_dockSettings = settings.DockSettings;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
UpdateAccentColor(_uiSettings);
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _dockSettings.ColorizationMode != ColorizationMode.None;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
|
||||
private void UpdateAccentColor(UISettings sender)
|
||||
{
|
||||
_currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
|
||||
if (ColorizationMode == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Reapply()
|
||||
{
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
||||
OnPropertyChanged(nameof(EffectiveThemeColor));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
|
||||
|
||||
// LOAD BEARING:
|
||||
// We need to cycle through the EffectiveTheme property to force reload of resources.
|
||||
_elementThemeOverride = ElementTheme.Light;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = ElementTheme.Dark;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = null;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetBackgroundImageProperties()
|
||||
{
|
||||
BackgroundImageBrightness = 0;
|
||||
BackgroundImageBlurAmount = 0;
|
||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||
BackgroundImageOpacity = 100;
|
||||
ColorIntensity = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject, IBatc
|
||||
LogIfDefaultScheduler();
|
||||
}
|
||||
|
||||
private protected ExtensionObjectViewModel(WeakReference<IPageContext> contextRef)
|
||||
protected ExtensionObjectViewModel(WeakReference<IPageContext> contextRef)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(contextRef);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -8,6 +8,8 @@ public class GlobalLogPageContext : IPageContext
|
||||
{
|
||||
public TaskScheduler Scheduler { get; private init; }
|
||||
|
||||
ICommandProviderContext IPageContext.ProviderContext => CommandProviderContext.Empty;
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint)
|
||||
{ /*do nothing*/
|
||||
}
|
||||
|
||||
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class Icons
|
||||
{
|
||||
public static IconInfo PinIcon => new("\uE718"); // Pin icon
|
||||
|
||||
public static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon
|
||||
|
||||
public static IconInfo SettingsIcon => new("\uE713"); // Settings icon
|
||||
|
||||
public static IconInfo EditIcon => new("\uE70F"); // Edit icon
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public sealed partial class ItemsUpdatedEventArgs : EventArgs
|
||||
{
|
||||
public bool ForceFirstItem { get; }
|
||||
|
||||
public ItemsUpdatedEventArgs(bool forceFirstItem)
|
||||
{
|
||||
ForceFirstItem = forceFirstItem;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
}
|
||||
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context, IContextMenuFactory? contextMenuFactory = null)
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context, IContextMenuFactory contextMenuFactory)
|
||||
: base(new(model), context, contextMenuFactory)
|
||||
{
|
||||
Model = new ExtensionObject<IListItem>(model);
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -19,6 +22,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
// private readonly HashSet<ListItemViewModel> _itemCache = [];
|
||||
private readonly TaskFactory filterTaskFactory = new(new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
|
||||
|
||||
private readonly Dictionary<IListItem, ListItemViewModel> _vmCache = new(new ProxyReferenceEqualityComparer());
|
||||
|
||||
// TODO: Do we want a base "ItemsPageViewModel" for anything that's going to have items?
|
||||
|
||||
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
|
||||
@@ -32,12 +37,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
private readonly ExtensionObject<IListPage> _model;
|
||||
|
||||
private readonly Lock _listLock = new();
|
||||
private readonly IContextMenuFactory? _contextMenuFactory;
|
||||
private readonly IContextMenuFactory _contextMenuFactory;
|
||||
|
||||
private InterlockedBoolean _isLoading;
|
||||
private bool _isFetching;
|
||||
|
||||
public event TypedEventHandler<ListViewModel, object>? ItemsUpdated;
|
||||
public event TypedEventHandler<ListViewModel, ItemsUpdatedEventArgs>? ItemsUpdated;
|
||||
|
||||
public bool ShowEmptyContent =>
|
||||
IsInitialized &&
|
||||
@@ -80,6 +85,9 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
private ListItemViewModel? _lastSelectedItem;
|
||||
|
||||
// For cancelling a deferred SafeSlowInit when the user navigates rapidly
|
||||
private CancellationTokenSource? _selectedItemCts;
|
||||
|
||||
public override bool IsInitialized
|
||||
{
|
||||
get => base.IsInitialized; protected set
|
||||
@@ -89,12 +97,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, CommandProviderContext providerContext, IContextMenuFactory? contextMenuFactory)
|
||||
public ListViewModel(IListPage model, TaskScheduler scheduler, AppExtensionHost host, ICommandProviderContext providerContext, IContextMenuFactory contextMenuFactory)
|
||||
: base(model, scheduler, host, providerContext)
|
||||
{
|
||||
_model = new(model);
|
||||
_contextMenuFactory = contextMenuFactory;
|
||||
EmptyContent = new(new(null), PageContext, _contextMenuFactory);
|
||||
EmptyContent = new(new(null), PageContext, contextMenuFactory: null);
|
||||
}
|
||||
|
||||
private void FiltersPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@@ -113,10 +121,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
protected override void OnSearchTextBoxUpdated(string searchTextBox)
|
||||
{
|
||||
//// TODO: Just temp testing, need to think about where we want to filter, as AdvancedCollectionView in View could be done, but then grouping need CollectionViewSource, maybe we do grouping in view
|
||||
//// and manage filtering below, but we should be smarter about this and understand caching and other requirements...
|
||||
//// Investigate if we re-use src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\ListHelpers.cs InPlaceUpdateList and FilterList?
|
||||
|
||||
// Dynamic pages will handler their own filtering. They will tell us if
|
||||
// something needs to change, by raising ItemsChanged.
|
||||
if (_isDynamic)
|
||||
@@ -132,24 +136,24 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
// concurrently.
|
||||
_ = filterTaskFactory.StartNew(
|
||||
() =>
|
||||
{
|
||||
filterCancellationTokenSource.Token.ThrowIfCancellationRequested();
|
||||
{
|
||||
filterCancellationTokenSource.Token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
if (_model.Unsafe is IDynamicListPage dynamic)
|
||||
try
|
||||
{
|
||||
dynamic.SearchText = searchTextBox;
|
||||
if (_model.Unsafe is IDynamicListPage dynamic)
|
||||
{
|
||||
dynamic.SearchText = searchTextBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex, _model?.Unsafe?.Name);
|
||||
}
|
||||
},
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex, _model?.Unsafe?.Name);
|
||||
}
|
||||
},
|
||||
filterCancellationTokenSource.Token,
|
||||
TaskCreationOptions.None,
|
||||
filterTaskFactory.Scheduler!);
|
||||
@@ -162,7 +166,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
ApplyFilterUnderLock();
|
||||
}
|
||||
|
||||
ItemsUpdated?.Invoke(this, EventArgs.Empty);
|
||||
ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(true));
|
||||
UpdateEmptyContent();
|
||||
_isLoading.Clear();
|
||||
}
|
||||
@@ -198,12 +202,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
var cancellationToken = _fetchItemsCancellationTokenSource.Token;
|
||||
|
||||
// TEMPORARY: just plop all the items into a single group
|
||||
// see 9806fe5d8 for the last commit that had this with sections
|
||||
_isFetching = true;
|
||||
|
||||
// Collect all the items into new viewmodels
|
||||
Collection<ListItemViewModel> newViewModels = [];
|
||||
List<ListItemViewModel> newViewModels = [];
|
||||
|
||||
try
|
||||
{
|
||||
@@ -221,11 +223,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO we can probably further optimize this by also keeping a
|
||||
// HashSet of every ExtensionObject we currently have, and only
|
||||
// building new viewmodels for the ones we haven't already built.
|
||||
var showsTitle = GridProperties?.ShowTitle ?? true;
|
||||
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
|
||||
var created = 0;
|
||||
var reused = 0;
|
||||
foreach (var item in newItems)
|
||||
{
|
||||
// Check for cancellation during item processing
|
||||
@@ -234,17 +235,33 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
ListItemViewModel viewModel = new(item, new(this));
|
||||
if (_vmCache.TryGetValue(item, out var existing))
|
||||
{
|
||||
existing.LayoutShowsTitle = showsTitle;
|
||||
existing.LayoutShowsSubtitle = showsSubtitle;
|
||||
newViewModels.Add(existing);
|
||||
reused++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var viewModel = new ListItemViewModel(item, new(this), _contextMenuFactory);
|
||||
|
||||
// If an item fails to load, silently ignore it.
|
||||
if (viewModel.SafeFastInit())
|
||||
{
|
||||
viewModel.LayoutShowsTitle = showsTitle;
|
||||
viewModel.LayoutShowsSubtitle = showsSubtitle;
|
||||
|
||||
_vmCache[item] = viewModel;
|
||||
newViewModels.Add(viewModel);
|
||||
created++;
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
CoreLogger.LogInfo($"[ListViewModel] FetchItems: {created} created, {reused} reused, {_vmCache.Count} cached");
|
||||
#endif
|
||||
|
||||
// Check for cancellation before initializing first twenty items
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -271,13 +288,22 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
List<ListItemViewModel> removedItems = [];
|
||||
List<ListItemViewModel> removedItems;
|
||||
lock (_listLock)
|
||||
{
|
||||
// Now that we have new ViewModels for everything from the
|
||||
// extension, smartly update our list of VMs
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems);
|
||||
|
||||
_vmCache.Clear();
|
||||
foreach (var vm in newViewModels)
|
||||
{
|
||||
if (vm.Model.Unsafe is { } li)
|
||||
{
|
||||
_vmCache[li] = vm;
|
||||
}
|
||||
}
|
||||
|
||||
// DO NOT ThrowIfCancellationRequested AFTER THIS! If you do,
|
||||
// you'll clean up list items that we've now transferred into
|
||||
// .Items
|
||||
@@ -288,9 +314,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
removedItem.SafeCleanup();
|
||||
}
|
||||
|
||||
// TODO: Iterate over everything in Items, and prune items from the
|
||||
// cache if we don't need them anymore
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -342,13 +365,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
{
|
||||
// A dynamic list? Even better! Just stick everything into
|
||||
// FilteredItems. The extension already did any filtering it cared about.
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, Items.Where(i => !i.IsInErrorState));
|
||||
var snapshot = Items.Where(i => !i.IsInErrorState).ToList();
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, snapshot);
|
||||
}
|
||||
|
||||
UpdateEmptyContent();
|
||||
}
|
||||
|
||||
ItemsUpdated?.Invoke(this, EventArgs.Empty);
|
||||
ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(IsRootPage));
|
||||
_isLoading.Clear();
|
||||
});
|
||||
}
|
||||
@@ -485,40 +509,57 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
private void SetSelectedItem(ListItemViewModel item)
|
||||
{
|
||||
if (!item.SafeSlowInit())
|
||||
{
|
||||
// Even if initialization fails, we need to hide any previously shown details
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// GH #322:
|
||||
// For inexplicable reasons, if you try updating the command bar and
|
||||
// the details on the same UI thread tick as updating the list, we'll
|
||||
// explode
|
||||
DoOnUiThread(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
|
||||
|
||||
if (ShowDetails && item.HasDetails)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
}
|
||||
|
||||
TextToSuggest = item.TextToSuggest;
|
||||
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(item.TextToSuggest));
|
||||
});
|
||||
|
||||
_lastSelectedItem = item;
|
||||
_lastSelectedItem.PropertyChanged += SelectedItemPropertyChanged;
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
|
||||
TextToSuggest = item.TextToSuggest;
|
||||
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(item.TextToSuggest));
|
||||
|
||||
// Cancel any in-flight slow init from a previous selection and defer
|
||||
// the expensive work (extension IPC for MoreCommands, details) so
|
||||
// rapid arrow-key navigation skips intermediate items entirely.
|
||||
_selectedItemCts?.Cancel();
|
||||
var cts = _selectedItemCts = new CancellationTokenSource();
|
||||
var ct = cts.Token;
|
||||
|
||||
_ = Task.Run(
|
||||
() =>
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!item.SafeSlowInit())
|
||||
{
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// SafeSlowInit completed on a background thread — details
|
||||
// messages will be marshalled to the UI thread by the receiver.
|
||||
if (ShowDetails && item.HasDetails)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
}
|
||||
},
|
||||
ct);
|
||||
}
|
||||
|
||||
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
@@ -557,21 +598,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
private void ClearSelectedItem()
|
||||
{
|
||||
// GH #322:
|
||||
// For inexplicable reasons, if you try updating the command bar and
|
||||
// the details on the same UI thread tick as updating the list, we'll
|
||||
// explode
|
||||
DoOnUiThread(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
_selectedItemCts?.Cancel();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(string.Empty));
|
||||
|
||||
TextToSuggest = string.Empty;
|
||||
});
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
WeakReferenceMessenger.Default.Send<UpdateSuggestionMessage>(new(string.Empty));
|
||||
TextToSuggest = string.Empty;
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
@@ -604,7 +636,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
UpdateProperty(nameof(SearchText));
|
||||
UpdateProperty(nameof(InitialSearchText));
|
||||
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext);
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext, _contextMenuFactory);
|
||||
EmptyContent.SlowInitializeProperties();
|
||||
|
||||
Filters?.PropertyChanged -= FiltersPropertyChanged;
|
||||
@@ -700,7 +732,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
SearchText = model.SearchText;
|
||||
break;
|
||||
case nameof(EmptyContent):
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext);
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext, contextMenuFactory: null);
|
||||
EmptyContent.SlowInitializeProperties();
|
||||
break;
|
||||
case nameof(Filters):
|
||||
@@ -763,6 +795,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
_fetchItemsCancellationTokenSource?.Cancel();
|
||||
_fetchItemsCancellationTokenSource?.Dispose();
|
||||
_fetchItemsCancellationTokenSource = null;
|
||||
|
||||
_selectedItemCts?.Cancel();
|
||||
_selectedItemCts?.Dispose();
|
||||
_selectedItemCts = null;
|
||||
}
|
||||
|
||||
protected override void UnsafeCleanup()
|
||||
@@ -770,11 +806,12 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
base.UnsafeCleanup();
|
||||
|
||||
EmptyContent?.SafeCleanup();
|
||||
EmptyContent = new(new(null), PageContext); // necessary?
|
||||
EmptyContent = new(new(null), PageContext, contextMenuFactory: null); // necessary?
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
filterCancellationTokenSource?.Cancel();
|
||||
_fetchItemsCancellationTokenSource?.Cancel();
|
||||
_selectedItemCts?.Cancel();
|
||||
|
||||
lock (_listLock)
|
||||
{
|
||||
@@ -801,4 +838,11 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
model.ItemsChanged -= Model_ItemsChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ProxyReferenceEqualityComparer : IEqualityComparer<IListItem>
|
||||
{
|
||||
public bool Equals(IListItem? x, IListItem? y) => ReferenceEquals(x, y);
|
||||
|
||||
public int GetHashCode(IListItem obj) => RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public partial class LoadingPageViewModel : PageViewModel
|
||||
: base(model, scheduler, host, CommandProviderContext.Empty)
|
||||
{
|
||||
ModelIsLoading = true;
|
||||
HasBackButton = false;
|
||||
IsInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record EnterDockEditModeMessage();
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken, bool TransientPage = false);
|
||||
|
||||
@@ -18,6 +18,8 @@ public record PerformCommandMessage
|
||||
|
||||
public bool WithAnimation { get; set; } = true;
|
||||
|
||||
public bool TransientPage { get; set; }
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command)
|
||||
{
|
||||
Command = command;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record PinToDockMessage(string ProviderId, string CommandId, bool Pin);
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record ShowHideDockMessage(bool ShowDock);
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record UnpinCommandItemMessage(string ProviderId, string CommandId);
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.ViewModels.Messages;
|
||||
|
||||
public sealed record WindowHiddenMessage();
|
||||
@@ -4,5 +4,11 @@
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
|
||||
: PageViewModel(null, scheduler, extensionHost, CommandProviderContext.Empty);
|
||||
internal sealed partial class NullPageViewModel : PageViewModel
|
||||
{
|
||||
internal NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
|
||||
: base(null, scheduler, extensionHost, CommandProviderContext.Empty)
|
||||
{
|
||||
HasBackButton = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
public partial string ErrorMessage { get; protected set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsNested { get; set; } = true;
|
||||
public partial bool IsRootPage { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool HasBackButton { get; set; } = true;
|
||||
|
||||
// This is set from the SearchBar
|
||||
[ObservableProperty]
|
||||
@@ -76,9 +79,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
|
||||
public IconInfoViewModel Icon { get; protected set; }
|
||||
|
||||
public CommandProviderContext ProviderContext { get; protected set; }
|
||||
public ICommandProviderContext ProviderContext { get; protected set; }
|
||||
|
||||
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost, CommandProviderContext providerContext)
|
||||
public PageViewModel(IPage? model, TaskScheduler scheduler, AppExtensionHost extensionHost, ICommandProviderContext providerContext)
|
||||
: base(scheduler)
|
||||
{
|
||||
InitializeSelfAsPageContext();
|
||||
@@ -267,6 +270,8 @@ public interface IPageContext
|
||||
void ShowException(Exception ex, string? extensionHint = null);
|
||||
|
||||
TaskScheduler Scheduler { get; }
|
||||
|
||||
ICommandProviderContext ProviderContext { get; }
|
||||
}
|
||||
|
||||
public interface IPageViewModelFactoryService
|
||||
@@ -278,5 +283,5 @@ public interface IPageViewModelFactoryService
|
||||
/// <param name="nested">Indicates whether the page is not the top-level page.</param>
|
||||
/// <param name="host">The command palette host that will host the page (for status messages)</param>
|
||||
/// <returns>A new instance of the page view model.</returns>
|
||||
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, CommandProviderContext providerContext);
|
||||
PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host, ICommandProviderContext providerContext);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open Command Palette.
|
||||
/// </summary>
|
||||
public static string builtin_command_palette_title {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_command_palette_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create another.
|
||||
/// </summary>
|
||||
@@ -294,6 +303,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Built-in.
|
||||
/// </summary>
|
||||
public static string builtin_extension_name_fallback {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_extension_name_fallback", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0}, {1} commands.
|
||||
/// </summary>
|
||||
@@ -465,6 +483,42 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit dock.
|
||||
/// </summary>
|
||||
public static string dock_edit_dock_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_edit_dock_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} items.
|
||||
/// </summary>
|
||||
public static string dock_item_count_plural {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_plural", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 1 item.
|
||||
/// </summary>
|
||||
public static string dock_item_count_singular {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_singular", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dock settings.
|
||||
/// </summary>
|
||||
public static string dock_settings_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_settings_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fallbacks.
|
||||
/// </summary>
|
||||
@@ -474,6 +528,23 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pinned.
|
||||
/// </summary>
|
||||
public static string PinnedItemSuffix {
|
||||
get {
|
||||
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up a localized string similar to Results.
|
||||
/// </summary>
|
||||
public static string results {
|
||||
get {
|
||||
return ResourceManager.GetString("results", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show details.
|
||||
/// </summary>
|
||||
|
||||
@@ -254,14 +254,46 @@
|
||||
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
|
||||
<value>{0} extensions installed</value>
|
||||
</data>
|
||||
<data name="builtin_extension_name_fallback" xml:space="preserve">
|
||||
<value>Built-in</value>
|
||||
<comment>Fallback name for built-in extensions</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_singular" xml:space="preserve">
|
||||
<value>1 item</value>
|
||||
<comment>Singular form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_plural" xml:space="preserve">
|
||||
<value>{0} items</value>
|
||||
<comment>Plural form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="builtin_command_palette_title" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
<comment>Title for the command to open the command palette</comment>
|
||||
</data>
|
||||
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
|
||||
<value>Pick background image</value>
|
||||
</data>
|
||||
<data name="fallbacks" xml:space="preserve">
|
||||
<value>Fallbacks</value>
|
||||
</data>
|
||||
<data name="dock_edit_dock_name" xml:space="preserve">
|
||||
<value>Edit dock</value>
|
||||
<comment>Command name for editing the dock</comment>
|
||||
</data>
|
||||
<data name="dock_settings_name" xml:space="preserve">
|
||||
<value>Dock settings</value>
|
||||
<comment>Command name for opening dock settings</comment>
|
||||
</data>
|
||||
<data name="ShowDetailsCommand" xml:space="preserve">
|
||||
<value>Show details</value>
|
||||
<comment>Name for the command that shows details of an item</comment>
|
||||
</data>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
<data name="results" xml:space="preserve">
|
||||
<value>Results</value>
|
||||
<comment>Section title for list of all search results that doesn't fall into any other category</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a snapshot of dock theme-related visual settings, including accent color, theme preference,
|
||||
/// backdrop, and background image configuration, for use in rendering the Dock UI.
|
||||
/// </summary>
|
||||
public sealed class DockThemeSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Dock visuals.
|
||||
/// </summary>
|
||||
public required Color Tint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the intensity of the accent tint color (0-1 range).
|
||||
/// </summary>
|
||||
public required float TintIntensity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured application theme preference for the Dock.
|
||||
/// </summary>
|
||||
public required ElementTheme Theme { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backdrop type for the Dock.
|
||||
/// </summary>
|
||||
public required DockBackdrop Backdrop { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image source to render as the background, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <see langword="null"/> when no background image is configured.
|
||||
/// </remarks>
|
||||
public required ImageSource? BackgroundImageSource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stretch mode used to lay out the background image.
|
||||
/// </summary>
|
||||
public required Stretch BackgroundImageStretch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the opacity applied to the background image.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
|
||||
/// </value>
|
||||
public required double BackgroundImageOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
|
||||
/// </summary>
|
||||
public required BackdropParameters BackdropParameters { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blur amount for the background image.
|
||||
/// </summary>
|
||||
public required int BlurAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brightness adjustment for the background (0-1 range).
|
||||
/// </summary>
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,4 +38,9 @@ public interface IThemeService
|
||||
/// Gets the current theme settings.
|
||||
/// </summary>
|
||||
ThemeSnapshot Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current dock theme settings.
|
||||
/// </summary>
|
||||
DockThemeSnapshot CurrentDockTheme { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.UI;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
||||
/// settings are in <see cref="DockBandSettings"/>.
|
||||
/// </summary>
|
||||
public class DockSettings
|
||||
{
|
||||
public DockSide Side { get; set; } = DockSide.Top;
|
||||
|
||||
public DockSize DockSize { get; set; } = DockSize.Small;
|
||||
|
||||
public DockSize DockIconsSize { get; set; } = DockSize.Small;
|
||||
|
||||
// <Theme settings>
|
||||
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
|
||||
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
|
||||
public int BackgroundImageBrightness { get; set; }
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
// </Theme settings>
|
||||
// public List<string> PinnedCommands { get; set; } = [];
|
||||
public List<DockBandSettings> StartBands { get; set; } = [];
|
||||
|
||||
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",
|
||||
CommandId = "com.microsoft.cmdpal.home",
|
||||
});
|
||||
StartBands.Add(new DockBandSettings
|
||||
{
|
||||
ProviderId = "WinGet",
|
||||
CommandId = "com.microsoft.cmdpal.winget",
|
||||
ShowLabels = false,
|
||||
});
|
||||
|
||||
EndBands.Add(new DockBandSettings
|
||||
{
|
||||
ProviderId = "PerformanceMonitor",
|
||||
CommandId = "com.microsoft.cmdpal.performanceWidget",
|
||||
});
|
||||
EndBands.Add(new DockBandSettings
|
||||
{
|
||||
ProviderId = "com.microsoft.cmdpal.builtin.datetime",
|
||||
CommandId = "com.microsoft.cmdpal.timedate.dockBand",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for a specific dock band. These are per-band settings stored
|
||||
/// within the overall <see cref="DockSettings"/>.
|
||||
/// </summary>
|
||||
public class DockBandSettings
|
||||
{
|
||||
public required string ProviderId { get; set; }
|
||||
|
||||
public required string CommandId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether titles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowTitles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether subtitles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowSubtitles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool? ShowLabels
|
||||
{
|
||||
get => ShowTitles;
|
||||
set => ShowTitles = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowTitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
public bool ResolveShowTitles(bool defaultValue) => ShowTitles ?? defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowSubtitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
Left = 0,
|
||||
Top = 1,
|
||||
Right = 2,
|
||||
Bottom = 3,
|
||||
}
|
||||
|
||||
public enum DockSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
public enum DockBackdrop
|
||||
{
|
||||
Transparent,
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -68,6 +68,11 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public bool EnableDock { get; set; }
|
||||
|
||||
public DockSettings DockSettings { get; set; } = new();
|
||||
|
||||
// Theme settings
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
@@ -92,6 +97,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public int BackdropOpacity { get; set; } = 100;
|
||||
|
||||
// </Theme settings>
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -230,7 +237,7 @@ public partial class SettingsModel : ObservableObject
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void SaveSettings(SettingsModel model)
|
||||
public static void SaveSettings(SettingsModel model, bool hotReload = true)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
{
|
||||
@@ -265,7 +272,10 @@ public partial class SettingsModel : ObservableObject
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
// have a file change watcher on the settings file, and
|
||||
// reload the settings then
|
||||
model.SettingsChanged?.Invoke(model, null);
|
||||
if (hotReload)
|
||||
{
|
||||
model.SettingsChanged?.Invoke(model, null);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -311,6 +321,7 @@ public partial class SettingsModel : ObservableObject
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(Color))]
|
||||
[JsonSerializable(typeof(HistoryItem))]
|
||||
[JsonSerializable(typeof(SettingsModel))]
|
||||
[JsonSerializable(typeof(WindowPosition))]
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -32,6 +34,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public AppearanceSettingsViewModel Appearance { get; }
|
||||
|
||||
public DockAppearanceSettingsViewModel DockAppearance { get; }
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
@@ -183,6 +187,58 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide Dock_Side
|
||||
{
|
||||
get => _settings.DockSettings.Side;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Side = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockSize Dock_DockSize
|
||||
{
|
||||
get => _settings.DockSettings.DockSize;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.DockSize = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockBackdrop Dock_Backdrop
|
||||
{
|
||||
get => _settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Backdrop = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_ShowLabels
|
||||
{
|
||||
get => _settings.DockSettings.ShowLabels;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.ShowLabels = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableDock
|
||||
{
|
||||
get => _settings.EnableDock;
|
||||
set
|
||||
{
|
||||
_settings.EnableDock = value;
|
||||
Save();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = new();
|
||||
|
||||
public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();
|
||||
@@ -195,6 +251,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
|
||||
@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -16,7 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IDisposable,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<HandleCommandResultMessage>
|
||||
IRecipient<HandleCommandResultMessage>,
|
||||
IRecipient<WindowHiddenMessage>
|
||||
{
|
||||
private readonly IRootPageService _rootPageService;
|
||||
private readonly IAppHostService _appHostService;
|
||||
@@ -79,8 +81,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
private IPage? _rootPage;
|
||||
|
||||
private bool _isNested;
|
||||
private bool _currentlyTransient;
|
||||
|
||||
public bool IsNested => _isNested;
|
||||
public bool IsNested => _isNested && !_currentlyTransient;
|
||||
|
||||
public PageViewModel NullPage { get; private set; }
|
||||
|
||||
@@ -101,6 +104,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
// Register to receive messages
|
||||
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<WindowHiddenMessage>(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -260,7 +264,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
|
||||
var providerContext = _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext);
|
||||
|
||||
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
|
||||
_rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -270,6 +274,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
var isMainPage = command == _rootPage;
|
||||
_isNested = !isMainPage;
|
||||
_currentlyTransient = message.TransientPage;
|
||||
|
||||
// Telemetry: Track extension page navigation for session metrics
|
||||
if (host is not null)
|
||||
@@ -289,6 +294,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
pageViewModel.IsRootPage = isMainPage;
|
||||
pageViewModel.HasBackButton = IsNested;
|
||||
|
||||
// Clear command bar, ViewModel initialization can already set new commands if it wants to
|
||||
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
|
||||
|
||||
@@ -308,7 +316,8 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_scheduler);
|
||||
|
||||
// While we're loading in the background, immediately move to the next page.
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
|
||||
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
@@ -479,6 +488,19 @@ public partial class ShellViewModel : ObservableObject,
|
||||
UnsafeHandleCommandResult(message.Result.Unsafe);
|
||||
}
|
||||
|
||||
public void Receive(WindowHiddenMessage message)
|
||||
{
|
||||
// If the window was hidden while we had a transient page, we need to reset that state.
|
||||
if (_currentlyTransient)
|
||||
{
|
||||
_currentlyTransient = false;
|
||||
|
||||
// navigate back to the main page without animation
|
||||
GoHome(withAnimation: false, focusSearch: false);
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUIThread(Action action)
|
||||
{
|
||||
_ = Task.Factory.StartNew(
|
||||
|
||||
@@ -22,7 +22,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class TopLevelCommandManager : ObservableObject,
|
||||
IRecipient<ReloadCommandsMessage>,
|
||||
IRecipient<PinCommandItemMessage>,
|
||||
IPageContext,
|
||||
IRecipient<UnpinCommandItemMessage>,
|
||||
IRecipient<PinToDockMessage>,
|
||||
IDisposable
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
@@ -32,10 +33,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
private readonly List<CommandProviderWrapper> _builtInCommands = [];
|
||||
private readonly List<CommandProviderWrapper> _extensionCommandProviders = [];
|
||||
private readonly Lock _commandProvidersLock = new();
|
||||
private readonly Lock _dockBandsLock = new();
|
||||
private readonly SupersedingAsyncGate _reloadCommandsGate;
|
||||
|
||||
TaskScheduler IPageContext.Scheduler => _taskScheduler;
|
||||
|
||||
public TopLevelCommandManager(IServiceProvider serviceProvider, ICommandProviderCache commandProviderCache)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
@@ -43,11 +43,15 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_taskScheduler = _serviceProvider.GetService<TaskScheduler>()!;
|
||||
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UnpinCommandItemMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<PinToDockMessage>(this);
|
||||
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
|
||||
}
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; private set; } = true;
|
||||
|
||||
@@ -83,12 +87,26 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_builtInCommands.Add(wrapper);
|
||||
}
|
||||
|
||||
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
var objects = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
if (objects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
if (objects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,16 +119,15 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
|
||||
// May be called from a background thread
|
||||
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
|
||||
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||
await commandProvider.LoadTopLevelCommands(_serviceProvider);
|
||||
|
||||
var commands = await Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
List<TopLevelViewModel> commands = [];
|
||||
List<TopLevelViewModel> bands = [];
|
||||
foreach (var item in commandProvider.TopLevelItems)
|
||||
{
|
||||
commands.Add(item);
|
||||
@@ -124,7 +141,15 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
foreach (var item in commandProvider.DockBandItems)
|
||||
{
|
||||
bands.Add(item);
|
||||
}
|
||||
|
||||
var commandsCount = commands.Count;
|
||||
var bandsCount = bands.Count;
|
||||
Logger.LogDebug($"{commandProvider.ProviderId}: Loaded {commandsCount} commands, {bandsCount} bands");
|
||||
return new TopLevelObjectSets(commands, bands);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
@@ -152,8 +177,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
/// <returns>an awaitable task</returns>
|
||||
private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
await sender.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||
await sender.LoadTopLevelCommands(_serviceProvider);
|
||||
|
||||
List<TopLevelViewModel> newItems = [.. sender.TopLevelItems];
|
||||
foreach (var i in sender.FallbackItems)
|
||||
@@ -164,6 +188,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
List<TopLevelViewModel> newBands = [.. sender.DockBandItems];
|
||||
|
||||
// modify the TopLevelCommands under shared lock; event if we clone it, we don't want
|
||||
// TopLevelCommands to get modified while we're working on it. Otherwise, we might
|
||||
// out clone would be stale at the end of this method.
|
||||
@@ -182,6 +208,16 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||
}
|
||||
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
// same idea for DockBands
|
||||
List<TopLevelViewModel> dockClone = [.. DockBands];
|
||||
var dockStartIndex = FindIndexForFirstProviderItem(dockClone, sender.ProviderId);
|
||||
dockClone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||
dockClone.InsertRange(dockStartIndex, newBands);
|
||||
ListHelpers.InPlaceUpdateList(DockBands, dockClone);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
static int FindIndexForFirstProviderItem(List<TopLevelViewModel> topLevelItems, string providerId)
|
||||
@@ -228,6 +264,11 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
TopLevelCommands.Clear();
|
||||
}
|
||||
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
DockBands.Clear();
|
||||
}
|
||||
|
||||
await LoadBuiltinsAsync();
|
||||
_ = Task.Run(LoadExtensionsAsync);
|
||||
}
|
||||
@@ -302,13 +343,31 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results is not null).Select(r => r!).ToList();
|
||||
|
||||
lock (TopLevelCommands)
|
||||
foreach (var providerObjects in commandSets)
|
||||
{
|
||||
foreach (var commands in commandSets)
|
||||
var commandsCount = providerObjects.Commands?.Count() ?? 0;
|
||||
var bandsCount = providerObjects.DockBands?.Count() ?? 0;
|
||||
Logger.LogDebug($"(some provider) Loaded {commandsCount} commands and {bandsCount} bands");
|
||||
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
if (providerObjects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
if (providerObjects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -332,7 +391,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
private record TopLevelObjectSets(IEnumerable<TopLevelViewModel>? Commands, IEnumerable<TopLevelViewModel>? DockBands);
|
||||
|
||||
private async Task<TopLevelObjectSets?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -358,6 +419,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
{
|
||||
// Then find all the top-level commands that belonged to that extension
|
||||
List<TopLevelViewModel> commandsToRemove = [];
|
||||
List<TopLevelViewModel> bandsToRemove = [];
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var extension in extensions)
|
||||
@@ -370,6 +432,15 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
commandsToRemove.Add(command);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in DockBands)
|
||||
{
|
||||
var host = band.ExtensionHost;
|
||||
if (host?.Extension == extension)
|
||||
{
|
||||
bandsToRemove.Add(band);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,6 +460,17 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
if (bandsToRemove.Count != 0)
|
||||
{
|
||||
foreach (var deleted in bandsToRemove)
|
||||
{
|
||||
DockBands.Remove(deleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
@@ -412,6 +494,22 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
return null;
|
||||
}
|
||||
|
||||
public TopLevelViewModel? LookupDockBand(string id)
|
||||
{
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
foreach (var command in DockBands)
|
||||
{
|
||||
if (command.Id == id)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Receive(ReloadCommandsMessage message) =>
|
||||
ReloadAllCommandsAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -421,7 +519,28 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
wrapper?.PinCommand(message.CommandId, _serviceProvider);
|
||||
}
|
||||
|
||||
private CommandProviderWrapper? LookupProvider(string providerId)
|
||||
public void Receive(UnpinCommandItemMessage message)
|
||||
{
|
||||
var wrapper = LookupProvider(message.ProviderId);
|
||||
wrapper?.UnpinCommand(message.CommandId, _serviceProvider);
|
||||
}
|
||||
|
||||
public void Receive(PinToDockMessage message)
|
||||
{
|
||||
if (LookupProvider(message.ProviderId) is CommandProviderWrapper wrapper)
|
||||
{
|
||||
if (message.Pin)
|
||||
{
|
||||
wrapper?.PinDockBand(message.CommandId, _serviceProvider);
|
||||
}
|
||||
else
|
||||
{
|
||||
wrapper?.UnpinDockBand(message.CommandId, _serviceProvider);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CommandProviderWrapper? LookupProvider(string providerId)
|
||||
{
|
||||
lock (_commandProvidersLock)
|
||||
{
|
||||
@@ -430,12 +549,6 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
void IPageContext.ShowException(Exception ex, string? extensionHint)
|
||||
{
|
||||
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager");
|
||||
CommandPaletteHost.Instance.Log(message);
|
||||
}
|
||||
|
||||
internal bool IsProviderActive(string id)
|
||||
{
|
||||
lock (_commandProvidersLock)
|
||||
@@ -445,6 +558,49 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
lock (_dockBandsLock)
|
||||
{
|
||||
foreach (var existing in DockBands)
|
||||
{
|
||||
if (existing.Id == bandVm.Id)
|
||||
{
|
||||
// already pinned
|
||||
Logger.LogDebug($"Dock band '{bandVm.Id}' is already pinned.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Attempting to pin dock band '{bandVm.Id}' from provider '{bandVm.CommandProviderId}'.");
|
||||
var providerId = bandVm.CommandProviderId;
|
||||
var foundProvider = false;
|
||||
foreach (var provider in CommandProviders)
|
||||
{
|
||||
if (provider.Id == providerId)
|
||||
{
|
||||
Logger.LogDebug($"Found provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
provider.PinDockBand(bandVm);
|
||||
foundProvider = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundProvider)
|
||||
{
|
||||
Logger.LogWarning($"Could not find provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the band to DockBands if not already present
|
||||
if (!DockBands.Any(b => b.Id == bandVm.Id))
|
||||
{
|
||||
DockBands.Add(bandVm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_reloadCommandsGate.Dispose();
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Used as the PageContext for top-level items. Top level items are displayed
|
||||
/// on the MainListPage, which _we_ own. We need to have a placeholder page
|
||||
/// context for each provider that still connects those top-level items to the
|
||||
/// CommandProvider they came from.
|
||||
/// </summary>
|
||||
public partial class TopLevelItemPageContext : IPageContext
|
||||
{
|
||||
public TaskScheduler Scheduler { get; private set; }
|
||||
|
||||
public ICommandProviderContext ProviderContext { get; private set; }
|
||||
|
||||
TaskScheduler IPageContext.Scheduler => Scheduler;
|
||||
|
||||
ICommandProviderContext IPageContext.ProviderContext => ProviderContext;
|
||||
|
||||
internal TopLevelItemPageContext(CommandProviderWrapper provider, TaskScheduler scheduler)
|
||||
{
|
||||
ProviderContext = provider.GetProviderContext();
|
||||
Scheduler = scheduler;
|
||||
}
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint = null)
|
||||
{
|
||||
var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? $"TopLevelItemPageContext({ProviderContext.ProviderId})");
|
||||
CommandPaletteHost.Instance.Log(message);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Text;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -25,8 +26,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly CommandItemViewModel _commandItemViewModel;
|
||||
private readonly DockViewModel? _dockViewModel;
|
||||
|
||||
public CommandProviderContext ProviderContext { get; private set; }
|
||||
public ICommandProviderContext ProviderContext { get; private set; }
|
||||
|
||||
private string IdFromModel => IsFallback && !string.IsNullOrWhiteSpace(_fallbackId) ? _fallbackId : _commandItemViewModel.Command.Id;
|
||||
|
||||
@@ -52,39 +54,28 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
|
||||
public CommandPaletteHost ExtensionHost { get; private set; }
|
||||
|
||||
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
|
||||
|
||||
public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
|
||||
|
||||
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
|
||||
|
||||
public string CommandProviderId => ProviderContext.ProviderId;
|
||||
|
||||
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
|
||||
|
||||
////// ICommandItem
|
||||
public string Title => _commandItemViewModel.Title;
|
||||
|
||||
public string Subtitle => _commandItemViewModel.Subtitle;
|
||||
|
||||
public IIconInfo Icon => _commandItemViewModel.Icon;
|
||||
public IIconInfo Icon => (IIconInfo)IconViewModel;
|
||||
|
||||
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
|
||||
|
||||
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
|
||||
|
||||
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
|
||||
.Select(item =>
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
return item as IContextItem;
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
return commandItem.Model.Unsafe;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).ToArray();
|
||||
IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();
|
||||
|
||||
////// IListItem
|
||||
ITag[] IListItem.Tags => Tags.ToArray();
|
||||
@@ -183,13 +174,40 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
}
|
||||
}
|
||||
|
||||
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
|
||||
// Dock properties
|
||||
public bool IsDockBand { get; private set; }
|
||||
|
||||
public DockBandSettings? DockBandSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsDockBand)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bandSettings = _settings.DockSettings.StartBands
|
||||
.Concat(_settings.DockSettings.EndBands)
|
||||
.FirstOrDefault(band => band.CommandId == this.Id);
|
||||
if (bandSettings is null)
|
||||
{
|
||||
return new DockBandSettings()
|
||||
{
|
||||
ProviderId = this.CommandProviderId,
|
||||
CommandId = this.Id,
|
||||
ShowLabels = true,
|
||||
};
|
||||
}
|
||||
|
||||
return bandSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public TopLevelViewModel(
|
||||
CommandItemViewModel item,
|
||||
bool isFallback,
|
||||
TopLevelType topLevelType,
|
||||
CommandPaletteHost extensionHost,
|
||||
CommandProviderContext commandProviderContext,
|
||||
ICommandProviderContext commandProviderContext,
|
||||
SettingsModel settings,
|
||||
ProviderSettings providerSettings,
|
||||
IServiceProvider serviceProvider,
|
||||
@@ -201,23 +219,23 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
ProviderContext = commandProviderContext;
|
||||
_commandItemViewModel = item;
|
||||
|
||||
IsFallback = isFallback;
|
||||
IsFallback = topLevelType == TopLevelType.Fallback;
|
||||
IsDockBand = topLevelType == TopLevelType.DockBand;
|
||||
ExtensionHost = extensionHost;
|
||||
if (isFallback && commandItem is FallbackCommandItem fallback)
|
||||
if (IsFallback && commandItem is FallbackCommandItem fallback)
|
||||
{
|
||||
_fallbackId = fallback.Id;
|
||||
}
|
||||
|
||||
item.PropertyChangedBackground += Item_PropertyChanged;
|
||||
|
||||
// UpdateAlias();
|
||||
// UpdateHotkey();
|
||||
// UpdateTags();
|
||||
_dockViewModel = serviceProvider.GetService<DockViewModel>();
|
||||
}
|
||||
|
||||
internal void InitializeProperties()
|
||||
{
|
||||
ItemViewModel.SlowInitializeProperties();
|
||||
GenerateId();
|
||||
|
||||
if (IsFallback)
|
||||
{
|
||||
@@ -278,7 +296,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
return;
|
||||
}
|
||||
|
||||
_initialIcon = _commandItemViewModel.Icon;
|
||||
_initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
|
||||
|
||||
if (raiseNotification)
|
||||
{
|
||||
@@ -452,4 +470,41 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to convert our context menu viewmodels back into the API
|
||||
/// interfaces that ICommandItem expects.
|
||||
/// </summary>
|
||||
private IContextItem?[] BuildContextMenu()
|
||||
{
|
||||
List<IContextItem?> contextItems = new();
|
||||
|
||||
foreach (var item in _commandItemViewModel.MoreCommands)
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
contextItems.Add(item as IContextItem);
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
contextItems.Add(commandItem.Model.Unsafe);
|
||||
}
|
||||
}
|
||||
|
||||
return contextItems.ToArray();
|
||||
}
|
||||
|
||||
internal ICommandItem ToPinnedDockBandItem()
|
||||
{
|
||||
var item = new PinnedDockItem(item: this, id: Id);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
public enum TopLevelType
|
||||
{
|
||||
Normal,
|
||||
Fallback,
|
||||
DockBand,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
xmlns:services="using:Microsoft.CmdPal.UI.Services">
|
||||
@@ -12,7 +13,7 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TeachingTip.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
|
||||
@@ -21,6 +22,7 @@
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/KeyVisual/KeyCharPresenter.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Dock/DockItemControl.xaml" />
|
||||
<!-- Default theme dictionary -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
|
||||
<services:MutableOverridesDictionary />
|
||||
@@ -29,6 +31,12 @@
|
||||
|
||||
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
|
||||
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="ptcontrols:CheckBoxWithDescriptionControl" />
|
||||
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -28,6 +28,7 @@ using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -220,6 +221,7 @@ public partial class App : Application, IDisposable
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<DockViewModel>();
|
||||
services.AddSingleton<IContextMenuFactory, CommandPaletteContextMenuFactory>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
@@ -24,8 +30,217 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
|
||||
{
|
||||
var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem);
|
||||
|
||||
// TODO: #45201 Here, we'll want to add pin/unpin commands for pinning
|
||||
// items to the top-level or to the dock.
|
||||
List<IContextItem> moreCommands = [];
|
||||
var itemId = commandItem.Command.Id;
|
||||
|
||||
if (commandItem.PageContext.TryGetTarget(out var page) &&
|
||||
page.ProviderContext.SupportsPinning &&
|
||||
!string.IsNullOrEmpty(itemId))
|
||||
{
|
||||
// Add pin/unpin commands for pinning items to the top-level or to
|
||||
// the dock.
|
||||
var providerId = page.ProviderContext.ProviderId;
|
||||
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
|
||||
{
|
||||
var providerSettings = _settingsModel.GetProviderSettings(provider);
|
||||
|
||||
var alreadyPinnedToTopLevel = providerSettings.PinnedCommandIds.Contains(itemId);
|
||||
|
||||
// Don't add pin/unpin commands for items displayed as
|
||||
// TopLevelViewModels that aren't already pinned.
|
||||
//
|
||||
// We can't look up if this command item is in the top level
|
||||
// items in the manager, because we are being called _before_ we
|
||||
// get added to the manager's list of commands.
|
||||
var isTopLevelItem = page is TopLevelItemPageContext;
|
||||
|
||||
if (!isTopLevelItem || alreadyPinnedToTopLevel)
|
||||
{
|
||||
var pinToTopLevelCommand = new PinToCommand(
|
||||
commandId: itemId,
|
||||
providerId: providerId,
|
||||
pin: !alreadyPinnedToTopLevel,
|
||||
PinLocation.TopLevel,
|
||||
_settingsModel,
|
||||
_topLevelCommandManager);
|
||||
|
||||
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
|
||||
moreCommands.Add(contextItem);
|
||||
}
|
||||
|
||||
TryAddPinToDockCommand(providerSettings, itemId, providerId, moreCommands, commandItem);
|
||||
}
|
||||
}
|
||||
|
||||
if (moreCommands.Count > 0)
|
||||
{
|
||||
moreCommands.Insert(0, new Separator());
|
||||
var moreResults = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(moreCommands.ToArray(), commandItem);
|
||||
results.AddRange(moreResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void TryAddPinToDockCommand(
|
||||
ProviderSettings providerSettings,
|
||||
string itemId,
|
||||
string providerId,
|
||||
List<IContextItem> moreCommands,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
if (!_settingsModel.EnableDock)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var inStartBands = _settingsModel.DockSettings.StartBands.Any(band => MatchesBand(band, itemId, providerId));
|
||||
var inCenterBands = _settingsModel.DockSettings.CenterBands.Any(band => MatchesBand(band, itemId, providerId));
|
||||
var inEndBands = _settingsModel.DockSettings.EndBands.Any(band => MatchesBand(band, itemId, providerId));
|
||||
var alreadyPinned = inStartBands || inCenterBands || inEndBands; /** &&
|
||||
_settingsModel.DockSettings.PinnedCommands.Contains(this.Id)**/
|
||||
var pinToTopLevelCommand = new PinToCommand(
|
||||
commandId: itemId,
|
||||
providerId: providerId,
|
||||
pin: !alreadyPinned,
|
||||
PinLocation.Dock,
|
||||
_settingsModel,
|
||||
_topLevelCommandManager);
|
||||
|
||||
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
|
||||
moreCommands.Add(contextItem);
|
||||
}
|
||||
|
||||
internal static bool MatchesBand(DockBandSettings bandSettings, string commandId, string providerId)
|
||||
{
|
||||
return bandSettings.CommandId == commandId &&
|
||||
bandSettings.ProviderId == providerId;
|
||||
}
|
||||
|
||||
internal enum PinLocation
|
||||
{
|
||||
TopLevel,
|
||||
Dock,
|
||||
}
|
||||
|
||||
private sealed partial class PinToContextItem : CommandContextItem, IDisposable
|
||||
{
|
||||
private readonly PinToCommand _command;
|
||||
private readonly CommandItemViewModel _commandItem;
|
||||
|
||||
public PinToContextItem(PinToCommand command, CommandItemViewModel commandItem)
|
||||
: base(command)
|
||||
{
|
||||
_command = command;
|
||||
_commandItem = commandItem;
|
||||
command.PinStateChanged += this.OnPinStateChanged;
|
||||
}
|
||||
|
||||
private void OnPinStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// update our MoreCommands
|
||||
_commandItem.RefreshMoreCommands();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
_command.PinStateChanged -= this.OnPinStateChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed partial class PinToCommand : InvokableCommand
|
||||
{
|
||||
private readonly string _commandId;
|
||||
private readonly string _providerId;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly bool _pin;
|
||||
private readonly PinLocation _pinLocation;
|
||||
|
||||
private bool IsPinToDock => _pinLocation == PinLocation.Dock;
|
||||
|
||||
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
|
||||
|
||||
public override string Name => _pin ?
|
||||
(IsPinToDock ? RS_.GetString("dock_pin_command_name") : RS_.GetString("top_level_pin_command_name")) :
|
||||
(IsPinToDock ? RS_.GetString("dock_unpin_command_name") : RS_.GetString("top_level_unpin_command_name"));
|
||||
|
||||
internal event EventHandler? PinStateChanged;
|
||||
|
||||
public PinToCommand(
|
||||
string commandId,
|
||||
string providerId,
|
||||
bool pin,
|
||||
PinLocation pinLocation,
|
||||
SettingsModel settings,
|
||||
TopLevelCommandManager topLevelCommandManager)
|
||||
{
|
||||
_commandId = commandId;
|
||||
_providerId = providerId;
|
||||
_pinLocation = pinLocation;
|
||||
_settings = settings;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
_pin = pin;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
CoreLogger.LogDebug($"PinTo{_pinLocation}Command.Invoke({_pin}): {_providerId}/{_commandId}");
|
||||
if (_pin)
|
||||
{
|
||||
switch (_pinLocation)
|
||||
{
|
||||
case PinLocation.TopLevel:
|
||||
PinToTopLevel();
|
||||
break;
|
||||
|
||||
case PinLocation.Dock:
|
||||
PinToDock();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (_pinLocation)
|
||||
{
|
||||
case PinLocation.TopLevel:
|
||||
UnpinFromTopLevel();
|
||||
break;
|
||||
|
||||
case PinLocation.Dock:
|
||||
UnpinFromDock();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
PinStateChanged?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
private void PinToTopLevel()
|
||||
{
|
||||
PinCommandItemMessage message = new(_providerId, _commandId);
|
||||
WeakReferenceMessenger.Default.Send(message);
|
||||
}
|
||||
|
||||
private void UnpinFromTopLevel()
|
||||
{
|
||||
UnpinCommandItemMessage message = new(_providerId, _commandId);
|
||||
WeakReferenceMessenger.Default.Send(message);
|
||||
}
|
||||
|
||||
private void PinToDock()
|
||||
{
|
||||
PinToDockMessage message = new(_providerId, _commandId, true);
|
||||
WeakReferenceMessenger.Default.Send(message);
|
||||
}
|
||||
|
||||
private void UnpinFromDock()
|
||||
{
|
||||
PinToDockMessage message = new(_providerId, _commandId, false);
|
||||
WeakReferenceMessenger.Default.Send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<Grid
|
||||
x:Name="IconRoot"
|
||||
Margin="3,0,-5,0"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<Button
|
||||
x:Name="StatusMessagesButton"
|
||||
x:Uid="StatusMessagesButton"
|
||||
@@ -134,7 +134,7 @@
|
||||
x:Uid="SettingsButton"
|
||||
Click="SettingsIcon_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
@@ -153,7 +153,7 @@
|
||||
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Padding="0,0,4,0"
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ScrollContainer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="ScrollButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{ThemeResource FlipViewNextPreviousButtonBackground}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
Background="{x:Bind Background, Mode=OneWay}"
|
||||
BorderBrush="{x:Bind BorderBrush, Mode=OneWay}"
|
||||
BorderThickness="{x:Bind BorderThickness, Mode=OneWay}"
|
||||
CornerRadius="{x:Bind CornerRadius, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition x:Name="Row2" Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Action button - position controlled by visual states -->
|
||||
<ContentPresenter
|
||||
x:Name="ActionButtonPresenter"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="2"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Content="{x:Bind ActionButton, Mode=OneWay}"
|
||||
Visibility="{x:Bind ActionButtonVisibility, Mode=OneWay}" />
|
||||
|
||||
<Grid
|
||||
x:Name="ScrollerContainer"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1">
|
||||
<ScrollViewer
|
||||
x:Name="scroller"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
HorizontalScrollMode="Enabled"
|
||||
SizeChanged="Scroller_SizeChanged"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
VerticalScrollMode="Disabled"
|
||||
ViewChanging="Scroller_ViewChanging">
|
||||
<Grid x:Name="ContentGrid">
|
||||
<ContentPresenter Content="{x:Bind Source, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<Button
|
||||
x:Name="ScrollBackBtn"
|
||||
Margin="8,0,0,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll left"
|
||||
Click="ScrollBackBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll left"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
x:Name="ScrollBackIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="ScrollForwardBtn"
|
||||
Margin="0,0,8,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll right"
|
||||
Click="ScrollForwardBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll right">
|
||||
<FontIcon
|
||||
x:Name="ScrollForwardIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="OrientationStates">
|
||||
<VisualState x:Name="HorizontalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Enabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Disabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="8,0,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,8,0" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="VerticalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Disabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Enabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="0,8,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,0,8" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="LayoutStates">
|
||||
<!-- Horizontal + Start: button on right -->
|
||||
<VisualState x:Name="HorizontalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="4,0,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Horizontal + End: button on left -->
|
||||
<VisualState x:Name="HorizontalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,4,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + Start: button on bottom -->
|
||||
<VisualState x:Name="VerticalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,4,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + End: button on top -->
|
||||
<VisualState x:Name="VerticalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,0,4" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ScrollContainer : UserControl
|
||||
{
|
||||
public enum ScrollContentAlignment
|
||||
{
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
public ScrollContainer()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += ScrollContainer_Loaded;
|
||||
}
|
||||
|
||||
private void ScrollContainer_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateOrientationState();
|
||||
UpdateLayoutState();
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => (object)GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SourceProperty =
|
||||
DependencyProperty.Register(nameof(Source), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(ScrollContainer), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
|
||||
|
||||
public ScrollContentAlignment ContentAlignment
|
||||
{
|
||||
get => (ScrollContentAlignment)GetValue(ContentAlignmentProperty);
|
||||
set => SetValue(ContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentAlignmentProperty =
|
||||
DependencyProperty.Register(nameof(ContentAlignment), typeof(ScrollContentAlignment), typeof(ScrollContainer), new PropertyMetadata(ScrollContentAlignment.Start, OnContentAlignmentChanged));
|
||||
|
||||
public object ActionButton
|
||||
{
|
||||
get => (object)GetValue(ActionButtonProperty);
|
||||
set => SetValue(ActionButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonProperty =
|
||||
DependencyProperty.Register(nameof(ActionButton), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Visibility ActionButtonVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(ActionButtonVisibilityProperty);
|
||||
set => SetValue(ActionButtonVisibilityProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonVisibilityProperty =
|
||||
DependencyProperty.Register(nameof(ActionButtonVisibility), typeof(Visibility), typeof(ScrollContainer), new PropertyMetadata(Visibility.Collapsed));
|
||||
|
||||
private static void OnContentAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToAlignment()
|
||||
{
|
||||
// Reset button visibility
|
||||
ScrollBackBtn.Visibility = Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (ContentAlignment == ScrollContentAlignment.End)
|
||||
{
|
||||
// Scroll to the end
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.ScrollableWidth, null, null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.ScrollableHeight, null, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scroll to the beginning
|
||||
scroller.ChangeView(0, 0, null, true);
|
||||
}
|
||||
|
||||
// Defer visibility update until after layout
|
||||
void OnLayoutUpdated(object? sender, object args)
|
||||
{
|
||||
scroller.LayoutUpdated -= OnLayoutUpdated;
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
scroller.LayoutUpdated += OnLayoutUpdated;
|
||||
}
|
||||
|
||||
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateOrientationState();
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOrientationState()
|
||||
{
|
||||
var stateName = Orientation == Orientation.Horizontal ? "HorizontalState" : "VerticalState";
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void UpdateLayoutState()
|
||||
{
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var isStart = ContentAlignment == ScrollContentAlignment.Start;
|
||||
|
||||
var stateName = (isHorizontal, isStart) switch
|
||||
{
|
||||
(true, true) => "HorizontalStartState",
|
||||
(true, false) => "HorizontalEndState",
|
||||
(false, true) => "VerticalStartState",
|
||||
(false, false) => "VerticalEndState",
|
||||
};
|
||||
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void Scroller_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility(e.FinalView.HorizontalOffset, e.FinalView.VerticalOffset);
|
||||
}
|
||||
|
||||
private void ScrollBackBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset - scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset - scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollForwardBtn since this button disappears after scrolling to the end.
|
||||
ScrollForwardBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void ScrollForwardBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset + scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset + scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollBackBtn since this button disappears after scrolling to the end.
|
||||
ScrollBackBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void Scroller_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
private void UpdateScrollButtonsVisibility(double? horizontalOffset = null, double? verticalOffset = null)
|
||||
{
|
||||
var hOffset = horizontalOffset ?? scroller.HorizontalOffset;
|
||||
var vOffset = verticalOffset ?? scroller.VerticalOffset;
|
||||
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
ScrollBackBtn.Visibility = hOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableWidth > 0 && hOffset < scroller.ScrollableWidth - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrollBackBtn.Visibility = vOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableHeight > 0 && vOffset < scroller.ScrollableHeight - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
449
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal file
449
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal file
@@ -0,0 +1,449 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreVm="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<StackLayout
|
||||
x:Key="ItemsOrientationLayout"
|
||||
Orientation="{x:Bind ItemsOrientation, Mode=OneWay}"
|
||||
Spacing="4" />
|
||||
<ItemsPanelTemplate x:Key="HorizontalItemsPanel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="VerticalItemsPanel">
|
||||
<StackPanel Orientation="Vertical" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
<DataTemplate x:Key="DockBandTemplate" x:DataType="dockVm:DockBandViewModel">
|
||||
<ItemsRepeater ItemsSource="{x:Bind Items, Mode=OneWay}" Layout="{StaticResource ItemsOrientationLayout}">
|
||||
<ItemsRepeater.Transitions>
|
||||
<TransitionCollection />
|
||||
</ItemsRepeater.Transitions>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="dockVm:DockItemViewModel">
|
||||
<local:DockItemControl
|
||||
Title="{x:Bind Title, Mode=OneWay}"
|
||||
RightTapped="BandItem_RightTapped"
|
||||
Subtitle="{x:Bind Subtitle, Mode=OneWay}"
|
||||
Tag="{x:Bind}"
|
||||
Tapped="BandItem_Tapped"
|
||||
ToolTip="{x:Bind Tooltip, Mode=OneWay}">
|
||||
<local:DockItemControl.Icon>
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="16"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested16}" />
|
||||
</local:DockItemControl.Icon>
|
||||
</local:DockItemControl>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</DataTemplate>
|
||||
|
||||
<Style x:Key="DockBandListViewStyle" TargetType="ListView">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="IsItemClickEnabled" Value="False" />
|
||||
<Setter Property="SelectionMode" Value="None" />
|
||||
<!-- Drag properties controlled by code-behind based on IsEditMode -->
|
||||
<Setter Property="CanDragItems" Value="False" />
|
||||
<Setter Property="CanReorderItems" Value="False" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DockBandListViewItemStyle" TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0,0,4,0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Name="ContextMenuFlyoutStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||
</Flyout>
|
||||
|
||||
<!-- Edit mode context menu for dock bands -->
|
||||
<MenuFlyout x:Name="EditModeContextMenu" ShouldConstrainToRootBounds="False">
|
||||
<MenuFlyoutSubItem x:Name="LabelsSubMenu" Text="Labels">
|
||||
<MenuFlyoutSubItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutSubItem.Icon>
|
||||
<ToggleMenuFlyoutItem
|
||||
x:Name="ShowTitlesMenuItem"
|
||||
Click="ShowTitlesMenuItem_Click"
|
||||
Text="Show titles" />
|
||||
<ToggleMenuFlyoutItem
|
||||
x:Name="ShowSubtitlesMenuItem"
|
||||
Click="ShowSubtitlesMenuItem_Click"
|
||||
Text="Show subtitles" />
|
||||
</MenuFlyoutSubItem>
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Name="UnpinBandMenuItem"
|
||||
Click="UnpinBandMenuItem_Click"
|
||||
Text="Unpin">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
|
||||
<!-- Add band flyout - used in edit mode to add bands to dock sections -->
|
||||
<Flyout
|
||||
x:Name="AddBandFlyout"
|
||||
Placement="Bottom"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<StackPanel Width="320">
|
||||
<TextBlock
|
||||
x:Name="NoAvailableBandsText"
|
||||
Padding="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="No commands available to pin"
|
||||
TextAlignment="Center"
|
||||
Visibility="Collapsed" />
|
||||
<ListView
|
||||
x:Name="AddBandListView"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="AddBandListView_ItemClick"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TopLevelViewModel">
|
||||
<Grid Padding="4" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<cpcontrols:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
BorderThickness="0,0,0,1"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<!-- Edit Mode Overlay - shown when in edit mode -->
|
||||
<Grid
|
||||
x:Name="ContentGrid"
|
||||
Margin="4"
|
||||
Padding="4,0,4,0"
|
||||
Background="Transparent"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="StartScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="StartAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="Start"
|
||||
ToolTipService.ToolTip="Add band to Start">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="StartListView"
|
||||
HorizontalAlignment="Stretch"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="StartListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}" />
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="CenterScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1"
|
||||
MinWidth="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Stretch">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="CenterAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="Center"
|
||||
ToolTipService.ToolTip="Add band to Center">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="CenterListView"
|
||||
HorizontalAlignment="Stretch"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="CenterListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.CenterItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}" />
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="EndScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
ContentAlignment="End">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="EndAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="End"
|
||||
ToolTipService.ToolTip="Add band to End">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="EndListView"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="EndListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
<TeachingTip
|
||||
x:Name="EditButtonsTeachingTip"
|
||||
MinWidth="0"
|
||||
PreferredPlacement="Bottom"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
Style="{StaticResource TeachingTipWithoutCloseButtonStyle}"
|
||||
Target="{x:Bind ContentGrid}">
|
||||
|
||||
<TeachingTip.Content>
|
||||
<StackPanel
|
||||
x:Name="EditButtonsPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="DoneEditingButton_Click"
|
||||
Content="Save"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="DiscardEditingButton_Click"
|
||||
Content="Discard" />
|
||||
</StackPanel>
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="DockOrientation">
|
||||
<VisualState x:Name="DockOnTop">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
|
||||
</VisualState.StateTriggers>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnBottom">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Bottom" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,1,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnLeft">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Left" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="EndScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
|
||||
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,0,1,0" />
|
||||
|
||||
<Setter Target="StartListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="CenterListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="EndListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnRight">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Right" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="EndScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
|
||||
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="1,0,0,0" />
|
||||
|
||||
<Setter Target="StartListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="CenterListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="EndListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
|
||||
<!-- Edit Mode Visual States -->
|
||||
<VisualStateGroup x:Name="EditModeStates">
|
||||
<VisualState x:Name="EditModeOff" />
|
||||
<VisualState x:Name="EditModeOn">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="StartScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="StartScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="StartScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="CenterScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="CenterScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="CenterScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="CenterScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="EndScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="EndScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="EndScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="EndScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="StartScroller.ActionButtonVisibility" Value="Visible" />
|
||||
<Setter Target="CenterScroller.ActionButtonVisibility" Value="Visible" />
|
||||
<Setter Target="EndScroller.ActionButtonVisibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
510
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal file
510
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal file
@@ -0,0 +1,510 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
public sealed partial class DockControl : UserControl, IRecipient<CloseContextMenuMessage>, IRecipient<EnterDockEditModeMessage>
|
||||
{
|
||||
private DockViewModel _viewModel;
|
||||
|
||||
internal DockViewModel ViewModel => _viewModel;
|
||||
|
||||
public static readonly DependencyProperty ItemsOrientationProperty =
|
||||
DependencyProperty.Register(nameof(ItemsOrientation), typeof(Orientation), typeof(DockControl), new PropertyMetadata(Orientation.Horizontal));
|
||||
|
||||
public Orientation ItemsOrientation
|
||||
{
|
||||
get => (Orientation)GetValue(ItemsOrientationProperty);
|
||||
set => SetValue(ItemsOrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DockSideProperty =
|
||||
DependencyProperty.Register(nameof(DockSide), typeof(DockSide), typeof(DockControl), new PropertyMetadata(DockSide.Top));
|
||||
|
||||
public DockSide DockSide
|
||||
{
|
||||
get => (DockSide)GetValue(DockSideProperty);
|
||||
set => SetValue(DockSideProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsEditModeProperty =
|
||||
DependencyProperty.Register(nameof(IsEditMode), typeof(bool), typeof(DockControl), new PropertyMetadata(false, OnIsEditModeChanged));
|
||||
|
||||
public bool IsEditMode
|
||||
{
|
||||
get => (bool)GetValue(IsEditModeProperty);
|
||||
set => SetValue(IsEditModeProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIsEditModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockControl control && e.NewValue is bool isEditMode)
|
||||
{
|
||||
control.UpdateEditMode(isEditMode);
|
||||
}
|
||||
}
|
||||
|
||||
internal DockControl(DockViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
|
||||
|
||||
// Start with edit mode disabled - normal click behavior
|
||||
UpdateEditMode(false);
|
||||
}
|
||||
|
||||
public void Receive(EnterDockEditModeMessage message)
|
||||
{
|
||||
// Message may arrive from a background thread, dispatch to UI thread
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
EnterEditMode();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateEditMode(bool isEditMode)
|
||||
{
|
||||
// Enable/disable drag-and-drop based on edit mode
|
||||
StartListView.CanDragItems = isEditMode;
|
||||
StartListView.CanReorderItems = isEditMode;
|
||||
StartListView.AllowDrop = isEditMode;
|
||||
|
||||
CenterListView.CanDragItems = isEditMode;
|
||||
CenterListView.CanReorderItems = isEditMode;
|
||||
CenterListView.AllowDrop = isEditMode;
|
||||
|
||||
EndListView.CanDragItems = isEditMode;
|
||||
EndListView.CanReorderItems = isEditMode;
|
||||
EndListView.AllowDrop = isEditMode;
|
||||
|
||||
if (isEditMode)
|
||||
{
|
||||
EditButtonsTeachingTip.PreferredPlacement = DockSide switch
|
||||
{
|
||||
DockSide.Left => TeachingTipPlacementMode.Right,
|
||||
DockSide.Right => TeachingTipPlacementMode.Left,
|
||||
DockSide.Top => TeachingTipPlacementMode.Bottom,
|
||||
DockSide.Bottom => TeachingTipPlacementMode.Top,
|
||||
_ => TeachingTipPlacementMode.Auto,
|
||||
};
|
||||
}
|
||||
|
||||
EditButtonsTeachingTip.IsOpen = isEditMode;
|
||||
|
||||
// Update visual state
|
||||
VisualStateManager.GoToState(this, isEditMode ? "EditModeOn" : "EditModeOff", true);
|
||||
}
|
||||
|
||||
internal void EnterEditMode()
|
||||
{
|
||||
// Snapshot current state so we can restore on discard
|
||||
ViewModel.SnapshotBandOrder();
|
||||
IsEditMode = true;
|
||||
}
|
||||
|
||||
internal void ExitEditMode()
|
||||
{
|
||||
IsEditMode = false;
|
||||
|
||||
// Save all changes when exiting edit mode
|
||||
ViewModel.SaveBandOrder();
|
||||
}
|
||||
|
||||
internal void DiscardEditMode()
|
||||
{
|
||||
IsEditMode = false;
|
||||
|
||||
// Restore the original band order from snapshot
|
||||
ViewModel.RestoreBandOrder();
|
||||
}
|
||||
|
||||
private void DoneEditingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitEditMode();
|
||||
}
|
||||
|
||||
private void DiscardEditingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DiscardEditMode();
|
||||
}
|
||||
|
||||
internal void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
DockSide = settings.Side;
|
||||
|
||||
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
|
||||
|
||||
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
|
||||
|
||||
if (settings.Backdrop == DockBackdrop.Transparent)
|
||||
{
|
||||
RootGrid.BorderBrush = new SolidColorBrush(Colors.Transparent);
|
||||
}
|
||||
}
|
||||
|
||||
private void BandItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
|
||||
{
|
||||
// Ignore clicks when in edit mode - allow drag behavior instead
|
||||
if (IsEditMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockBandViewModel band && dockItem.Tag is DockItemViewModel item)
|
||||
{
|
||||
// Use the center of the border as the point to open at
|
||||
var borderPos = dockItem.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var borderCenter = new Point(
|
||||
borderPos.X + (dockItem.ActualWidth / 2),
|
||||
borderPos.Y + (dockItem.ActualHeight / 2));
|
||||
|
||||
InvokeItem(item, borderCenter);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Stores the band that was right-clicked for edit mode context menu
|
||||
private DockBandViewModel? _editModeContextBand;
|
||||
|
||||
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockBandViewModel band && dockItem.Tag is DockItemViewModel item)
|
||||
{
|
||||
// In edit mode, show the edit mode context menu (show/hide labels)
|
||||
if (IsEditMode)
|
||||
{
|
||||
// Find the parent DockBandViewModel for this item
|
||||
_editModeContextBand = band;
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
// Update toggle menu item checked state based on current settings
|
||||
ShowTitlesMenuItem.IsChecked = _editModeContextBand.ShowTitles;
|
||||
ShowSubtitlesMenuItem.IsChecked = _editModeContextBand.ShowSubtitles;
|
||||
|
||||
EditModeContextMenu.ShowAt(
|
||||
dockItem,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal mode - show the command context menu
|
||||
if (item.HasMoreCommands)
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextMenuFlyout.ShowAt(
|
||||
dockItem,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTitlesMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
_editModeContextBand.ShowTitles = ShowTitlesMenuItem.IsChecked;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowSubtitlesMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
_editModeContextBand.ShowSubtitles = ShowSubtitlesMenuItem.IsChecked;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnpinBandMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
ViewModel.UnpinBand(_editModeContextBand);
|
||||
_editModeContextBand = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeItem(DockItemViewModel item, Point pos)
|
||||
{
|
||||
var command = item.Command;
|
||||
try
|
||||
{
|
||||
PerformCommandMessage m = new(command.Model);
|
||||
m.WithAnimation = false;
|
||||
m.TransientPage = true;
|
||||
WeakReferenceMessenger.Default.Send(m);
|
||||
|
||||
var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
|
||||
if (isPage)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos));
|
||||
}
|
||||
}
|
||||
catch (COMException e)
|
||||
{
|
||||
Logger.LogError("Error invoking dock command", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
// We need to wait until our flyout is opened to try and toss focus
|
||||
// at its search box. The control isn't in the UI tree before that
|
||||
ContextControl.FocusSearchBox();
|
||||
}
|
||||
|
||||
public void Receive(CloseContextMenuMessage message)
|
||||
{
|
||||
if (ContextMenuFlyout.IsOpen)
|
||||
{
|
||||
ContextMenuFlyout.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void RootGrid_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(null);
|
||||
var item = this.ViewModel.GetContextMenuForDock();
|
||||
if (item.HasMoreCommands)
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextMenuFlyout.ShowAt(
|
||||
this.RootGrid,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
Position = pos,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private DockBandViewModel? _draggedBand;
|
||||
|
||||
private void BandListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
|
||||
{
|
||||
if (e.Items.Count > 0 && e.Items[0] is DockBandViewModel band)
|
||||
{
|
||||
_draggedBand = band;
|
||||
e.Data.RequestedOperation = DataPackageOperation.Move;
|
||||
}
|
||||
}
|
||||
|
||||
private void BandListView_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
if (_draggedBand != null)
|
||||
{
|
||||
e.AcceptedOperation = DataPackageOperation.Move;
|
||||
}
|
||||
}
|
||||
|
||||
private void BandListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
// Reordering within the same list is handled automatically by ListView
|
||||
// We just need to sync the ViewModel order without saving
|
||||
if (args.DropResult == DataPackageOperation.Move && _draggedBand != null)
|
||||
{
|
||||
DockPinSide targetSide;
|
||||
ObservableCollection<DockBandViewModel> targetCollection;
|
||||
|
||||
if (sender == StartListView)
|
||||
{
|
||||
targetSide = DockPinSide.Start;
|
||||
targetCollection = ViewModel.StartItems;
|
||||
}
|
||||
else if (sender == CenterListView)
|
||||
{
|
||||
targetSide = DockPinSide.Center;
|
||||
targetCollection = ViewModel.CenterItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetSide = DockPinSide.End;
|
||||
targetCollection = ViewModel.EndItems;
|
||||
}
|
||||
|
||||
// Find the new index and sync ViewModel (without saving)
|
||||
var newIndex = targetCollection.IndexOf(_draggedBand);
|
||||
if (newIndex >= 0)
|
||||
{
|
||||
ViewModel.SyncBandPosition(_draggedBand, targetSide, newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
_draggedBand = null;
|
||||
}
|
||||
|
||||
private void StartListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.Start, e);
|
||||
}
|
||||
|
||||
private void CenterListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.Center, e);
|
||||
}
|
||||
|
||||
private void EndListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.End, e);
|
||||
}
|
||||
|
||||
private void HandleCrossListDrop(DockPinSide targetSide, DragEventArgs e)
|
||||
{
|
||||
if (_draggedBand == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check which list the band is currently in
|
||||
var isInStart = ViewModel.StartItems.Contains(_draggedBand);
|
||||
var isInCenter = ViewModel.CenterItems.Contains(_draggedBand);
|
||||
var isInEnd = ViewModel.EndItems.Contains(_draggedBand);
|
||||
|
||||
DockPinSide sourceSide;
|
||||
if (isInStart)
|
||||
{
|
||||
sourceSide = DockPinSide.Start;
|
||||
}
|
||||
else if (isInCenter)
|
||||
{
|
||||
sourceSide = DockPinSide.Center;
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceSide = DockPinSide.End;
|
||||
}
|
||||
|
||||
// Only handle cross-list drops here; same-list reorders are handled in DragItemsCompleted
|
||||
if (sourceSide != targetSide)
|
||||
{
|
||||
// Calculate drop index based on drop position
|
||||
var targetListView = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => StartListView,
|
||||
DockPinSide.Center => CenterListView,
|
||||
_ => EndListView,
|
||||
};
|
||||
var targetCollection = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => ViewModel.StartItems,
|
||||
DockPinSide.Center => ViewModel.CenterItems,
|
||||
_ => ViewModel.EndItems,
|
||||
};
|
||||
|
||||
var dropIndex = GetDropIndex(targetListView, e, targetCollection.Count);
|
||||
|
||||
// Move the band to the new side (without saving - save happens on Done)
|
||||
ViewModel.MoveBandWithoutSaving(_draggedBand, targetSide, dropIndex);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetDropIndex(ListView listView, DragEventArgs e, int itemCount)
|
||||
{
|
||||
var position = e.GetPosition(listView);
|
||||
|
||||
// Find the item at the drop position
|
||||
for (var i = 0; i < itemCount; i++)
|
||||
{
|
||||
if (listView.ContainerFromIndex(i) is ListViewItem container)
|
||||
{
|
||||
var itemBounds = container.TransformToVisual(listView).TransformBounds(
|
||||
new Rect(0, 0, container.ActualWidth, container.ActualHeight));
|
||||
|
||||
if (ItemsOrientation == Orientation.Horizontal)
|
||||
{
|
||||
// For horizontal layout, check X position
|
||||
if (position.X < itemBounds.X + (itemBounds.Width / 2))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For vertical layout, check Y position
|
||||
if (position.Y < itemBounds.Y + (itemBounds.Height / 2))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're past all items, insert at the end
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
// Tracks which section (Start/Center/End) the add button was clicked for
|
||||
private DockPinSide _addBandTargetSide;
|
||||
|
||||
private void AddBandButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.Tag is string sideTag)
|
||||
{
|
||||
_addBandTargetSide = sideTag switch
|
||||
{
|
||||
"Start" => DockPinSide.Start,
|
||||
"Center" => DockPinSide.Center,
|
||||
"End" => DockPinSide.End,
|
||||
_ => DockPinSide.Center,
|
||||
};
|
||||
|
||||
// Populate the list with available bands (not already in the dock)
|
||||
var availableBands = ViewModel.GetAvailableBandsToAdd().ToList();
|
||||
AddBandListView.ItemsSource = availableBands;
|
||||
|
||||
// Show/hide empty state text based on whether there are bands to add
|
||||
var hasAvailableBands = availableBands.Count > 0;
|
||||
NoAvailableBandsText.Visibility = hasAvailableBands ? Visibility.Collapsed : Visibility.Visible;
|
||||
AddBandListView.Visibility = hasAvailableBands ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// Show the flyout
|
||||
AddBandFlyout.ShowAt(button);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBandListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is TopLevelViewModel topLevel)
|
||||
{
|
||||
// Add the band to the target section
|
||||
ViewModel.AddBandToSection(topLevel, _addBandTargetSide);
|
||||
|
||||
// Close the flyout
|
||||
AddBandFlyout.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml
Normal file
178
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml
Normal file
@@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock">
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#0FFFFFFF" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#0BFFFFFF" />
|
||||
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#0FFFFFFF" />
|
||||
<GradientStop Offset="1.0" Color="#19FFFFFF" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#0BFFFFFF" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#80FFFFFF" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#4DFFFFFF" />
|
||||
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#08000000" />
|
||||
<GradientStop Offset="1.0" Color="#17000000" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#05000000" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPointerOver" Color="{StaticResource SystemColorHighlightColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<CornerRadius x:Key="DockItemCornerRadius">4</CornerRadius>
|
||||
<Thickness x:Key="DockItemPadding">4,0,4,0</Thickness>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultDockItemControlStyle}" TargetType="local:DockItemControl" />
|
||||
|
||||
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
|
||||
<Style.Setters>
|
||||
<Setter Property="Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DockItemBorderBrush}" />
|
||||
<Setter Property="Padding" Value="{StaticResource DockItemPadding}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource DockItemCornerRadius}" />
|
||||
<Setter Property="TextVisibility" Value="Visible" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:DockItemControl">
|
||||
<Grid
|
||||
x:Name="PART_RootGrid"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
ToolTipService.ToolTip="{TemplateBinding ToolTip}">
|
||||
<Grid
|
||||
x:Name="ContentGrid"
|
||||
AutomationProperties.Name="{TemplateBinding Title}"
|
||||
Background="Transparent"
|
||||
ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Icon -->
|
||||
<ContentPresenter
|
||||
x:Name="IconPresenter"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Icon}" />
|
||||
|
||||
<!-- Text (Title + Subtitle) -->
|
||||
<StackPanel
|
||||
x:Name="TextPanel"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{TemplateBinding TextVisibility}">
|
||||
<TextBlock
|
||||
x:Name="TitleText"
|
||||
MinWidth="24"
|
||||
MaxWidth="100"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="12"
|
||||
Text="{TemplateBinding Title}"
|
||||
TextAlignment="Left"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
x:Name="SubtitleText"
|
||||
MaxWidth="100"
|
||||
Margin="0,-4,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{TemplateBinding Subtitle}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="TitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="SubtitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="TextVisibilityStates">
|
||||
<VisualState x:Name="TextVisible" />
|
||||
<VisualState x:Name="TitleOnly">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SubtitleText.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="SubtitleOnly">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitleText.Visibility" Value="Collapsed" />
|
||||
<Setter Target="SubtitleText.Margin" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TextHidden">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
|
||||
<Setter Target="TextPanel.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="IconVisibilityStates">
|
||||
<VisualState x:Name="IconVisible" />
|
||||
<VisualState x:Name="IconHidden">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
[ContentProperty(Name = nameof(Icon))]
|
||||
public sealed partial class DockItemControl : Control
|
||||
{
|
||||
public DockItemControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(DockItemControl);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ToolTipProperty =
|
||||
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null));
|
||||
|
||||
public string ToolTip
|
||||
{
|
||||
get => (string)GetValue(ToolTipProperty);
|
||||
set => SetValue(ToolTipProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => (string)GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SubtitleProperty =
|
||||
DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public string Subtitle
|
||||
{
|
||||
get => (string)GetValue(SubtitleProperty);
|
||||
set => SetValue(SubtitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DockItemControl), new PropertyMetadata(null, OnIconPropertyChanged));
|
||||
|
||||
public object Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextVisibilityProperty =
|
||||
DependencyProperty.Register(nameof(TextVisibility), typeof(Visibility), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public Visibility TextVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(TextVisibilityProperty);
|
||||
set => SetValue(TextVisibilityProperty, value);
|
||||
}
|
||||
|
||||
private const string IconPresenterName = "IconPresenter";
|
||||
|
||||
private FrameworkElement? _iconPresenter;
|
||||
|
||||
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockItemControl control)
|
||||
{
|
||||
control.UpdateTextVisibility();
|
||||
control.UpdateAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnIconPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockItemControl control)
|
||||
{
|
||||
control.UpdateIconVisibility();
|
||||
control.UpdateAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
internal bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
internal bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
private void UpdateTextVisibility()
|
||||
{
|
||||
UpdateTextVisibilityState();
|
||||
}
|
||||
|
||||
private void UpdateTextVisibilityState()
|
||||
{
|
||||
// Determine which visual state to use based on title/subtitle presence
|
||||
var stateName = (HasTitle, HasSubtitle) switch
|
||||
{
|
||||
(true, true) => "TextVisible",
|
||||
(true, false) => "TitleOnly",
|
||||
(false, true) => "SubtitleOnly",
|
||||
(false, false) => "TextHidden",
|
||||
};
|
||||
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void UpdateIconVisibility()
|
||||
{
|
||||
if (Icon is IconBox icon)
|
||||
{
|
||||
var dt = icon.DataContext;
|
||||
var src = icon.Source;
|
||||
|
||||
if (_iconPresenter is not null)
|
||||
{
|
||||
// n.b. this might be wrong - I think we always have an Icon (an IconBox),
|
||||
// we need to check if the box has an icon
|
||||
_iconPresenter.Visibility = Icon is null ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
UpdateIconVisibilityState();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateIconVisibilityState()
|
||||
{
|
||||
var hasIcon = Icon is not null;
|
||||
VisualStateManager.GoToState(this, hasIcon ? "IconVisible" : "IconHidden", true);
|
||||
}
|
||||
|
||||
private void UpdateAlignment()
|
||||
{
|
||||
// If this item has both an icon and a label, left align so that the
|
||||
// icons don't wobble if the text changes.
|
||||
//
|
||||
// Otherwise, center align.
|
||||
var requestedTheme = ActualTheme;
|
||||
var isLight = requestedTheme == ElementTheme.Light;
|
||||
var showText = HasText;
|
||||
if (Icon is IconBox icoBox &&
|
||||
icoBox.DataContext is DockItemViewModel item &&
|
||||
item.Icon is IconInfoViewModel icon)
|
||||
{
|
||||
var showIcon = icon is not null && icon.HasIcon(isLight);
|
||||
if (showText && showIcon)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalAlignment = HorizontalAlignment.Center;
|
||||
}
|
||||
|
||||
private void UpdateAllVisibility()
|
||||
{
|
||||
UpdateTextVisibility();
|
||||
UpdateIconVisibility();
|
||||
UpdateAlignment();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
IsEnabledChanged -= OnIsEnabledChanged;
|
||||
|
||||
PointerEntered -= Control_PointerEntered;
|
||||
PointerExited -= Control_PointerExited;
|
||||
|
||||
PointerEntered += Control_PointerEntered;
|
||||
PointerExited += Control_PointerExited;
|
||||
|
||||
IsEnabledChanged += OnIsEnabledChanged;
|
||||
|
||||
// Get template children for visibility updates
|
||||
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
|
||||
|
||||
// Set initial visibility
|
||||
UpdateAllVisibility();
|
||||
}
|
||||
|
||||
private void Control_PointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "PointerOver", true);
|
||||
}
|
||||
|
||||
private void Control_PointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "Normal", true);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerRoutedEventArgs e)
|
||||
{
|
||||
if (IsEnabled)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
VisualStateManager.GoToState(this, "Pressed", true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Windows.Win32;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
internal static class DockSettingsToViews
|
||||
{
|
||||
public static double WidthForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 128,
|
||||
DockSize.Medium => 192,
|
||||
DockSize.Large => 256,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double HeightForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 32,
|
||||
DockSize.Medium => 54,
|
||||
DockSize.Large => 76,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double IconSizeForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 32 / 2,
|
||||
DockSize.Medium => 54 / 2,
|
||||
DockSize.Large => 76 / 2,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Microsoft.UI.Xaml.Media.SystemBackdrop? GetSystemBackdrop(DockBackdrop backdrop)
|
||||
{
|
||||
return backdrop switch
|
||||
{
|
||||
DockBackdrop.Transparent => new TransparentTintBackdrop(),
|
||||
DockBackdrop.Acrylic => null,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static uint GetAppBarEdge(DockSide side)
|
||||
{
|
||||
return side switch
|
||||
{
|
||||
DockSide.Left => PInvoke.ABE_LEFT,
|
||||
DockSide.Top => PInvoke.ABE_TOP,
|
||||
DockSide.Right => PInvoke.ABE_RIGHT,
|
||||
DockSide.Bottom => PInvoke.ABE_BOTTOM,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
49
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal file
49
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="PowerDock"
|
||||
Closed="DockWindow_Closed"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid
|
||||
x:Name="Root"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
|
||||
<!-- Colorization overlay for transparent backdrop -->
|
||||
<Border
|
||||
x:Name="ColorizationOverlay"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="{x:Bind WindowViewModel.ColorizationOpacity, Mode=OneWay}"
|
||||
Visibility="{x:Bind WindowViewModel.ShowColorizationOverlay, Mode=OneWay}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind WindowViewModel.ColorizationColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<cpcontrols:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind WindowViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind WindowViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind WindowViewModel.BackgroundImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind WindowViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind WindowViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
IsHoldingEnabled="False"
|
||||
TintColor="{x:Bind WindowViewModel.BackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind WindowViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind WindowViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
727
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal file
727
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal file
@@ -0,0 +1,727 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Accessibility;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockWindow : WindowEx,
|
||||
IRecipient<BringToTopMessage>,
|
||||
IRecipient<RequestShowPaletteAtMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IDisposable
|
||||
{
|
||||
#pragma warning disable SA1306 // Field names should begin with lower-case letter
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private readonly uint WM_TASKBAR_RESTART;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
#pragma warning restore SA1306 // Field names should begin with lower-case letter
|
||||
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DockWindowViewModel _windowViewModel;
|
||||
|
||||
private HWND _hwnd = HWND.Null;
|
||||
private APPBARDATA _appBarData;
|
||||
private uint _callbackMessageId;
|
||||
|
||||
private DockSettings _settings;
|
||||
private DockViewModel viewModel;
|
||||
private DockControl _dock;
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private DockSize _lastSize;
|
||||
|
||||
// Store the original WndProc
|
||||
private WNDPROC? _originalWndProc;
|
||||
private WNDPROC? _customWndProc;
|
||||
|
||||
// internal Settings CurrentSettings => _settings;
|
||||
public DockWindow()
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var mainSettings = serviceProvider.GetService<SettingsModel>()!;
|
||||
mainSettings.SettingsChanged += SettingsChangedHandler;
|
||||
_settings = mainSettings.DockSettings;
|
||||
_lastSize = _settings.DockSize;
|
||||
|
||||
viewModel = serviceProvider.GetService<DockViewModel>()!;
|
||||
_themeService = serviceProvider.GetRequiredService<IThemeService>();
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
_windowViewModel = new DockWindowViewModel(_themeService);
|
||||
_dock = new DockControl(viewModel);
|
||||
|
||||
InitializeComponent();
|
||||
Root.Children.Add(_dock);
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
AppWindow.IsShownInSwitchers = false;
|
||||
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
|
||||
{
|
||||
overlappedPresenter.SetBorderAndTitleBar(false, false);
|
||||
overlappedPresenter.IsResizable = false;
|
||||
}
|
||||
|
||||
this.Activated += DockWindow_Activated;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
|
||||
_hwnd = GetWindowHandle(this);
|
||||
|
||||
// Subclass the window to intercept messages
|
||||
//
|
||||
// Set up custom window procedure to listen for display changes
|
||||
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
|
||||
// member (and instead like, use a local), then the pointer we marshal
|
||||
// into the WindowLongPtr will be useless after we leave this function,
|
||||
// and our **WindProc will explode**.
|
||||
_customWndProc = CustomWndProc;
|
||||
|
||||
_callbackMessageId = PInvoke.RegisterWindowMessage($"CmdPal_ABM_{_hwnd}");
|
||||
|
||||
// TaskbarCreated is the message that's broadcast when explorer.exe
|
||||
// restarts. We need to know when that happens to be able to bring our
|
||||
// app bar back
|
||||
// And this apparently happens on lock screens / hibernates, too
|
||||
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
|
||||
|
||||
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_customWndProc);
|
||||
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
||||
|
||||
// Disable minimize and maximize box
|
||||
var style = (WINDOW_STYLE)PInvoke.GetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
|
||||
style &= ~WINDOW_STYLE.WS_MINIMIZEBOX; // Remove WS_MINIMIZEBOX
|
||||
style &= ~WINDOW_STYLE.WS_MAXIMIZEBOX; // Remove WS_MAXIMIZEBOX
|
||||
_ = PInvoke.SetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style);
|
||||
|
||||
ShowDesktop.AddHook(this);
|
||||
UpdateSettingsOnUiThread();
|
||||
}
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args)
|
||||
{
|
||||
_settings = sender.DockSettings;
|
||||
DispatcherQueue.TryEnqueue(UpdateSettingsOnUiThread);
|
||||
}
|
||||
|
||||
private void DockWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// These are used for removing the very subtle shadow/border that we get from Windows 11
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
unsafe
|
||||
{
|
||||
BOOL value = false;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
|
||||
}
|
||||
}
|
||||
|
||||
private HWND GetWindowHandle(Window window)
|
||||
{
|
||||
var hwnd = WindowNative.GetWindowHandle(window);
|
||||
return new HWND(hwnd);
|
||||
}
|
||||
|
||||
private void UpdateSettingsOnUiThread()
|
||||
{
|
||||
this.viewModel.UpdateSettings(_settings);
|
||||
|
||||
SystemBackdrop = DockSettingsToViews.GetSystemBackdrop(_settings.Backdrop);
|
||||
|
||||
// If the backdrop is acrylic, things are more complicated
|
||||
if (_settings.Backdrop == DockBackdrop.Acrylic)
|
||||
{
|
||||
SetAcrylic();
|
||||
}
|
||||
|
||||
_dock.UpdateSettings(_settings);
|
||||
var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);
|
||||
|
||||
if (_appBarData.hWnd != IntPtr.Zero)
|
||||
{
|
||||
var sameEdge = _appBarData.uEdge == side;
|
||||
var sameSize = _lastSize == _settings.DockSize;
|
||||
if (sameEdge && sameSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DestroyAppBar(_hwnd);
|
||||
}
|
||||
|
||||
CreateAppBar(_hwnd);
|
||||
}
|
||||
|
||||
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
|
||||
// other Shell surfaces are using, this cannot be set in XAML however.
|
||||
private void SetAcrylic()
|
||||
{
|
||||
if (DesktopAcrylicController.IsSupported())
|
||||
{
|
||||
// Hooking up the policy object.
|
||||
_configurationSource = new SystemBackdropConfiguration
|
||||
{
|
||||
// Initial configuration state.
|
||||
IsInputActive = true,
|
||||
};
|
||||
UpdateAcrylic();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAcrylic()
|
||||
{
|
||||
if (_acrylicController != null)
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
}
|
||||
|
||||
var backdrop = _themeService.CurrentDockTheme.BackdropParameters;
|
||||
_acrylicController = new DesktopAcrylicController
|
||||
{
|
||||
Kind = DesktopAcrylicKind.Thin,
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = backdrop.EffectiveOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.EffectiveLuminosityOpacity,
|
||||
};
|
||||
|
||||
// Enable the system backdrop.
|
||||
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void DisposeAcrylic()
|
||||
{
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null!;
|
||||
_configurationSource = null!;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
// We only need to handle acrylic here.
|
||||
// Transparent background is handled directly in XAML by binding to
|
||||
// the DockWindowViewModel's ColorizationColor properties.
|
||||
if (_settings.Backdrop == DockBackdrop.Acrylic)
|
||||
{
|
||||
UpdateAcrylic();
|
||||
}
|
||||
|
||||
// ActualTheme / RequestedTheme sync,
|
||||
// as pilfered from WindowThemeSynchronizer
|
||||
// LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces
|
||||
// a refresh of the theme.
|
||||
Root.RequestedTheme = ElementTheme.Dark;
|
||||
Root.RequestedTheme = ElementTheme.Light;
|
||||
Root.RequestedTheme = _themeService.CurrentDockTheme.Theme;
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateAppBar(HWND hwnd)
|
||||
{
|
||||
_appBarData = new APPBARDATA
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
|
||||
hWnd = hwnd,
|
||||
uCallbackMessage = _callbackMessageId,
|
||||
};
|
||||
|
||||
// Register this window as an app bar
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_NEW, ref _appBarData);
|
||||
|
||||
// Stash the last size we created the bar at, so we know when to hot-
|
||||
// reload it
|
||||
_lastSize = _settings.DockSize;
|
||||
|
||||
UpdateWindowPosition();
|
||||
}
|
||||
|
||||
private void DestroyAppBar(HWND hwnd)
|
||||
{
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_REMOVE, ref _appBarData);
|
||||
_appBarData = default;
|
||||
}
|
||||
|
||||
private void UpdateWindowPosition()
|
||||
{
|
||||
Logger.LogDebug("UpdateWindowPosition");
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
|
||||
// Get system border metrics
|
||||
var borderWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXBORDER);
|
||||
var edgeWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXEDGE);
|
||||
var frameWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXFRAME);
|
||||
|
||||
UpdateAppBarDataForEdge(_settings.Side, _settings.DockSize, dpi / 96.0);
|
||||
|
||||
// Query and set position
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_QUERYPOS, ref _appBarData);
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_SETPOS, ref _appBarData);
|
||||
|
||||
// TODO: investigate ABS_AUTOHIDE and auto hide bars.
|
||||
// I think it's something like this, but I don't totally know
|
||||
// _appBarData.lParam = ABS_ALWAYSONTOP;
|
||||
// _appBarData.lParam = (LPARAM)(int)PInvoke.ABS_AUTOHIDE;
|
||||
// PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
|
||||
// PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);
|
||||
|
||||
// Account for system borders when moving the window
|
||||
// Adjust position to account for window frame/border
|
||||
var adjustedLeft = _appBarData.rc.left - frameWidth;
|
||||
var adjustedTop = _appBarData.rc.top - frameWidth;
|
||||
var adjustedWidth = (_appBarData.rc.right - _appBarData.rc.left) + (2 * frameWidth);
|
||||
var adjustedHeight = (_appBarData.rc.bottom - _appBarData.rc.top) + (2 * frameWidth);
|
||||
|
||||
// Move the actual window
|
||||
PInvoke.MoveWindow(
|
||||
_hwnd,
|
||||
adjustedLeft,
|
||||
adjustedTop,
|
||||
adjustedWidth,
|
||||
adjustedHeight,
|
||||
true);
|
||||
}
|
||||
|
||||
private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
|
||||
{
|
||||
Logger.LogDebug("UpdateAppBarDataForEdge");
|
||||
var horizontalHeightDips = DockSettingsToViews.HeightForSize(size);
|
||||
var verticalWidthDips = DockSettingsToViews.WidthForSize(size);
|
||||
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
|
||||
if (side == DockSide.Top)
|
||||
{
|
||||
_appBarData.uEdge = PInvoke.ABE_TOP;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = (int)(horizontalHeightDips * scaleFactor);
|
||||
}
|
||||
else if (side == DockSide.Bottom)
|
||||
{
|
||||
var heightPixels = (int)(horizontalHeightDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_BOTTOM;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = screenHeight - heightPixels;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else if (side == DockSide.Left)
|
||||
{
|
||||
var widthPixels = (int)(verticalWidthDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_LEFT;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = widthPixels;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else if (side == DockSide.Right)
|
||||
{
|
||||
var widthPixels = (int)(verticalWidthDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_RIGHT;
|
||||
_appBarData.rc.left = screenWidth - widthPixels;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private LRESULT CustomWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// check settings changed
|
||||
if (msg == PInvoke.WM_SETTINGCHANGE)
|
||||
{
|
||||
var isFullscreen = IsWindowFullscreen();
|
||||
|
||||
Logger.LogDebug($"WM_SETTINGCHANGE ({isFullscreen})");
|
||||
|
||||
if (isFullscreen)
|
||||
{
|
||||
this.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Show();
|
||||
}
|
||||
|
||||
if (wParam == (uint)SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETWORKAREA)
|
||||
{
|
||||
Logger.LogDebug($"WM_SETTINGCHANGE(SPI_SETWORKAREA)");
|
||||
|
||||
// Use debounced call to throttle rapid successive calls
|
||||
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
|
||||
}
|
||||
}
|
||||
else if (msg == PInvoke.WM_DISPLAYCHANGE)
|
||||
{
|
||||
Logger.LogDebug("WM_DISPLAYCHANGE");
|
||||
|
||||
// Use dispatcher to ensure we're on the UI thread
|
||||
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
|
||||
}
|
||||
|
||||
// Intercept WM_SYSCOMMAND to prevent minimize and maximize
|
||||
else if (msg == PInvoke.WM_SYSCOMMAND)
|
||||
{
|
||||
var command = (int)(wParam.Value & 0xFFF0);
|
||||
if (command == PInvoke.SC_MINIMIZE || command == PInvoke.SC_MAXIMIZE)
|
||||
{
|
||||
// Block minimize and maximize commands
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop min/max on WM_WINDOWPOSCHANGING too
|
||||
else if (msg == PInvoke.WM_WINDOWPOSCHANGING)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var pWindowPos = (WINDOWPOS*)lParam.Value;
|
||||
|
||||
// Check if the window is being hidden (minimized) or if flags suggest minimize/maximize
|
||||
if ((pWindowPos->flags & SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW) != 0)
|
||||
{
|
||||
// Prevent hiding the window (minimize)
|
||||
pWindowPos->flags &= ~SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
|
||||
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW;
|
||||
}
|
||||
|
||||
// Additional check: if the window position suggests it's being minimized or maximized
|
||||
// by checking for dramatic size changes
|
||||
if (pWindowPos->cx <= 0 || pWindowPos->cy <= 0)
|
||||
{
|
||||
// Prevent zero or negative size changes (minimize)
|
||||
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_SIZE to prevent minimize/maximize state changes
|
||||
else if (msg == PInvoke.WM_SIZE)
|
||||
{
|
||||
var sizeType = (int)wParam.Value;
|
||||
if (sizeType == PInvoke.SIZE_MINIMIZED || sizeType == PInvoke.SIZE_MAXIMIZED)
|
||||
{
|
||||
// Block the size change by not calling the original window procedure
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_SHOWWINDOW to prevent hiding (minimize)
|
||||
else if (msg == PInvoke.WM_SHOWWINDOW)
|
||||
{
|
||||
var isBeingShown = wParam.Value != 0;
|
||||
if (!isBeingShown)
|
||||
{
|
||||
// Prevent hiding the window
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle double-click on title bar (non-client area)
|
||||
else if (msg == PInvoke.WM_NCLBUTTONDBLCLK)
|
||||
{
|
||||
var hitTest = (int)wParam.Value;
|
||||
if (hitTest == PInvoke.HTCAPTION)
|
||||
{
|
||||
// Block double-click on title bar to prevent maximize
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_GETMINMAXINFO to control window size limits
|
||||
else if (msg == PInvoke.WM_GETMINMAXINFO)
|
||||
{
|
||||
// We can modify the min/max tracking info here if needed
|
||||
// For now, let it pass through but we could restrict max size
|
||||
}
|
||||
|
||||
// Handle the AppBarMessage message
|
||||
// This is needed to update the position when the work area changes.
|
||||
// (notably, when the user toggles auto-hide taskbars)
|
||||
else if (msg == _callbackMessageId)
|
||||
{
|
||||
if (wParam.Value == PInvoke.ABN_POSCHANGED)
|
||||
{
|
||||
UpdateWindowPosition();
|
||||
}
|
||||
}
|
||||
else if (msg == WM_TASKBAR_RESTART)
|
||||
{
|
||||
Logger.LogDebug("WM_TASKBAR_RESTART");
|
||||
|
||||
DispatcherQueue.TryEnqueue(() => CreateAppBar(_hwnd));
|
||||
|
||||
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(false));
|
||||
}
|
||||
|
||||
// Call the original window procedure for all other messages
|
||||
return PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
void IRecipient<BringToTopMessage>.Receive(BringToTopMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
var onTop = message.OnTop ? HWND.HWND_TOPMOST : HWND.HWND_NOTOPMOST;
|
||||
PInvoke.SetWindowPos(_hwnd, onTop, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsWindowFullscreen()
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
||||
if (Marshal.GetExceptionForHR(PInvoke.SHQueryUserNotificationState(out var state)) is null)
|
||||
{
|
||||
if (state == QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN ||
|
||||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY ||
|
||||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_PRESENTATION_MODE)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Receive(QuitMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
DestroyAppBar(_hwnd);
|
||||
|
||||
this.Close();
|
||||
});
|
||||
}
|
||||
|
||||
void IRecipient<RequestShowPaletteAtMessage>.Receive(RequestShowPaletteAtMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => RequestShowPaletteOnUiThread(message.PosDips));
|
||||
}
|
||||
|
||||
private void RequestShowPaletteOnUiThread(Point posDips)
|
||||
{
|
||||
// pos is relative to our root. We need to convert to screen coords.
|
||||
var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var screenPosDips = new Point(rootPosDips.X + posDips.X, rootPosDips.Y + posDips.Y);
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
var scaleFactor = dpi / 96.0;
|
||||
var screenPosPixels = new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);
|
||||
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
|
||||
// Now we're going to find the best position for the palette.
|
||||
|
||||
// We want to anchor the palette on the dock side.
|
||||
// on the top:
|
||||
// - anchor to the top, left if we're on the left half of the screen
|
||||
// - anchor to the top, right if we're on the right half of the screen
|
||||
// On the left:
|
||||
// - anchor to the top, left if we're on the top half of the screen
|
||||
// - anchor to the bottom, left if we're on the bottom half of the screen
|
||||
// On the right:
|
||||
// - anchor to the top, right if we're on the top half of the screen
|
||||
// - anchor to the bottom, right if we're on the bottom half of the screen
|
||||
// On the bottom:
|
||||
// - anchor to the bottom, left if we're on the left half of the screen
|
||||
// - anchor to the bottom, right if we're on the right half of the screen
|
||||
var onTopHalf = screenPosPixels.Y < screenHeight / 2;
|
||||
var onLeftHalf = screenPosPixels.X < screenWidth / 2;
|
||||
var onRightHalf = !onLeftHalf;
|
||||
var onBottomHalf = !onTopHalf;
|
||||
|
||||
var anchorPoint = _settings.Side switch
|
||||
{
|
||||
DockSide.Top => onLeftHalf ? AnchorPoint.TopLeft : AnchorPoint.TopRight,
|
||||
DockSide.Bottom => onLeftHalf ? AnchorPoint.BottomLeft : AnchorPoint.BottomRight,
|
||||
DockSide.Left => onTopHalf ? AnchorPoint.TopLeft : AnchorPoint.BottomLeft,
|
||||
DockSide.Right => onTopHalf ? AnchorPoint.TopRight : AnchorPoint.BottomRight,
|
||||
_ => AnchorPoint.TopLeft,
|
||||
};
|
||||
|
||||
// we also need to slide the anchor point a bit away from the dock
|
||||
var paddingDips = 8;
|
||||
var paddingPixels = paddingDips * scaleFactor;
|
||||
PInvoke.GetWindowRect(_hwnd, out var ourRect);
|
||||
|
||||
// Depending on the side we're on, we need to offset differently
|
||||
switch (_settings.Side)
|
||||
{
|
||||
case DockSide.Top:
|
||||
screenPosPixels.Y = ourRect.bottom + paddingPixels;
|
||||
break;
|
||||
case DockSide.Bottom:
|
||||
screenPosPixels.Y = ourRect.top - paddingPixels;
|
||||
break;
|
||||
case DockSide.Left:
|
||||
screenPosPixels.X = ourRect.right + paddingPixels;
|
||||
break;
|
||||
case DockSide.Right:
|
||||
screenPosPixels.X = ourRect.left - paddingPixels;
|
||||
break;
|
||||
}
|
||||
|
||||
// Now that we know the anchor corner, and where to attempt to place it, we can
|
||||
// ask the palette to show itself there.
|
||||
WeakReferenceMessenger.Default.Send<ShowPaletteAtMessage>(new(screenPosPixels, anchorPoint));
|
||||
}
|
||||
|
||||
public DockWindowViewModel WindowViewModel => _windowViewModel;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAcrylic();
|
||||
_windowViewModel.Dispose();
|
||||
}
|
||||
|
||||
private void DockWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var settings = serviceProvider.GetService<SettingsModel>();
|
||||
settings?.SettingsChanged -= SettingsChangedHandler;
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
DisposeAcrylic();
|
||||
|
||||
// Remove our app bar registration
|
||||
DestroyAppBar(_hwnd);
|
||||
|
||||
// Unhook the window procedure
|
||||
ShowDesktop.RemoveHook();
|
||||
}
|
||||
}
|
||||
|
||||
// Thank you to https://stackoverflow.com/a/35422795/1481137
|
||||
internal static class ShowDesktop
|
||||
{
|
||||
private const string WORKERW = "WorkerW";
|
||||
private const string PROGMAN = "Progman";
|
||||
|
||||
private static WINEVENTPROC? _hookProc;
|
||||
private static IntPtr _hookHandle = IntPtr.Zero;
|
||||
|
||||
public static void AddHook(Window window)
|
||||
{
|
||||
if (IsHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsHooked = true;
|
||||
|
||||
_hookProc = (WINEVENTPROC)WinEventCallback;
|
||||
_hookHandle = PInvoke.SetWinEventHook(PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, HMODULE.Null, _hookProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT);
|
||||
}
|
||||
|
||||
public static void RemoveHook()
|
||||
{
|
||||
if (!IsHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsHooked = false;
|
||||
|
||||
PInvoke.UnhookWinEvent((HWINEVENTHOOK)_hookHandle);
|
||||
_hookProc = null;
|
||||
_hookHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static string GetWindowClass(HWND hwnd)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (char* c = new char[32])
|
||||
{
|
||||
_ = PInvoke.GetClassName(hwnd, (PWSTR)c, 32);
|
||||
return new string(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||
|
||||
private static void WinEventCallback(
|
||||
HWINEVENTHOOK hWinEventHook,
|
||||
uint eventType,
|
||||
HWND hwnd,
|
||||
int idObject,
|
||||
int idChild,
|
||||
uint dwEventThread,
|
||||
uint dwmsEventTime)
|
||||
{
|
||||
if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND)
|
||||
{
|
||||
var @class = GetWindowClass(hwnd);
|
||||
if (string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("ShowDesktop invoked. Bring us back");
|
||||
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsHooked { get; private set; }
|
||||
}
|
||||
|
||||
internal sealed record BringToTopMessage(bool OnTop);
|
||||
|
||||
internal sealed record RequestShowPaletteAtMessage(Point PosDips);
|
||||
|
||||
internal sealed record ShowPaletteAtMessage(Point PosPixels, AnchorPoint Anchor);
|
||||
|
||||
internal enum AnchorPoint
|
||||
{
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -201,12 +201,70 @@
|
||||
<Setter Property="cpcontrols:WrapPanel.IsFullLine" Value="True" />
|
||||
</Style>
|
||||
|
||||
<ControlTemplate x:Key="ListViewItemWithoutVisualIndicatorTemplate" TargetType="ListViewItem">
|
||||
<ListViewItemPresenter
|
||||
x:Name="Root"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
CheckBoxBorderBrush="{ThemeResource ListViewItemCheckBoxBorderBrush}"
|
||||
CheckBoxBrush="{ThemeResource ListViewItemCheckBoxBrush}"
|
||||
CheckBoxCornerRadius="{ThemeResource ListViewItemCheckBoxCornerRadius}"
|
||||
CheckBoxDisabledBorderBrush="{ThemeResource ListViewItemCheckBoxDisabledBorderBrush}"
|
||||
CheckBoxDisabledBrush="{ThemeResource ListViewItemCheckBoxDisabledBrush}"
|
||||
CheckBoxPointerOverBorderBrush="{ThemeResource ListViewItemCheckBoxPointerOverBorderBrush}"
|
||||
CheckBoxPointerOverBrush="{ThemeResource ListViewItemCheckBoxPointerOverBrush}"
|
||||
CheckBoxPressedBorderBrush="{ThemeResource ListViewItemCheckBoxPressedBorderBrush}"
|
||||
CheckBoxPressedBrush="{ThemeResource ListViewItemCheckBoxPressedBrush}"
|
||||
CheckBoxSelectedBrush="{ThemeResource ListViewItemCheckBoxSelectedBrush}"
|
||||
CheckBoxSelectedDisabledBrush="{ThemeResource ListViewItemCheckBoxSelectedDisabledBrush}"
|
||||
CheckBoxSelectedPointerOverBrush="{ThemeResource ListViewItemCheckBoxSelectedPointerOverBrush}"
|
||||
CheckBoxSelectedPressedBrush="{ThemeResource ListViewItemCheckBoxSelectedPressedBrush}"
|
||||
CheckBrush="{ThemeResource ListViewItemCheckBrush}"
|
||||
CheckDisabledBrush="{ThemeResource ListViewItemCheckDisabledBrush}"
|
||||
CheckMode="{ThemeResource ListViewItemCheckMode}"
|
||||
CheckPressedBrush="{ThemeResource ListViewItemCheckPressedBrush}"
|
||||
ContentMargin="{TemplateBinding Padding}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
Control.IsTemplateFocusTarget="True"
|
||||
CornerRadius="{ThemeResource ListViewItemCornerRadius}"
|
||||
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
|
||||
DragBackground="{ThemeResource ListViewItemDragBackground}"
|
||||
DragForeground="{ThemeResource ListViewItemDragForeground}"
|
||||
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
|
||||
FocusBorderBrush="{ThemeResource ListViewItemFocusBorderBrush}"
|
||||
FocusSecondaryBorderBrush="{ThemeResource ListViewItemFocusSecondaryBorderBrush}"
|
||||
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
|
||||
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
|
||||
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
|
||||
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
|
||||
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
|
||||
PlaceholderBackground="{ThemeResource ListViewItemPlaceholderBackground}"
|
||||
PointerOverBackground="{ThemeResource ListViewItemBackgroundPointerOver}"
|
||||
PointerOverForeground="{ThemeResource ListViewItemForegroundPointerOver}"
|
||||
PressedBackground="{ThemeResource ListViewItemBackgroundPressed}"
|
||||
ReorderHintOffset="{ThemeResource ListViewItemReorderHintThemeOffset}"
|
||||
SelectedBackground="{ThemeResource ListViewItemBackgroundSelected}"
|
||||
SelectedDisabledBackground="{ThemeResource ListViewItemBackgroundSelectedDisabled}"
|
||||
SelectedForeground="{ThemeResource ListViewItemForegroundSelected}"
|
||||
SelectedPointerOverBackground="{ThemeResource ListViewItemBackgroundSelectedPointerOver}"
|
||||
SelectedPressedBackground="{ThemeResource ListViewItemBackgroundSelectedPressed}"
|
||||
SelectionCheckMarkVisualEnabled="{ThemeResource ListViewItemSelectionCheckMarkVisualEnabled}"
|
||||
SelectionIndicatorBrush="{ThemeResource ListViewItemSelectionIndicatorBrush}"
|
||||
SelectionIndicatorCornerRadius="{ThemeResource ListViewItemSelectionIndicatorCornerRadius}"
|
||||
SelectionIndicatorDisabledBrush="{ThemeResource ListViewItemSelectionIndicatorDisabledBrush}"
|
||||
SelectionIndicatorPointerOverBrush="{ThemeResource ListViewItemSelectionIndicatorPointerOverBrush}"
|
||||
SelectionIndicatorPressedBrush="{ThemeResource ListViewItemSelectionIndicatorPressedBrush}"
|
||||
SelectionIndicatorVisualEnabled="False" />
|
||||
</ControlTemplate>
|
||||
|
||||
<Style
|
||||
x:Key="ListDefaultContainerStyle"
|
||||
x:Key="ListSingleRowItemContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="{StaticResource SingleRowListViewItemHeight}" />
|
||||
<Setter Property="Height" Value="{StaticResource SingleRowListViewItemHeight}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Template" Value="{StaticResource ListViewItemWithoutVisualIndicatorTemplate}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
@@ -246,14 +304,17 @@
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="viewModels:TagViewModel">
|
||||
<!--
|
||||
Tags are immutable, so we don't have to worry about binding mode.
|
||||
-->
|
||||
<cpcontrols:Tag
|
||||
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
|
||||
BackgroundColor="{x:Bind Background, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind Text}"
|
||||
BackgroundColor="{x:Bind Background}"
|
||||
FontSize="12"
|
||||
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
|
||||
Icon="{x:Bind Icon, Mode=OneWay}"
|
||||
Text="{x:Bind Text, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
ForegroundColor="{x:Bind Foreground}"
|
||||
Icon="{x:Bind Icon}"
|
||||
Text="{x:Bind Text}"
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip}" />
|
||||
</DataTemplate>
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
@@ -265,7 +326,7 @@
|
||||
|
||||
<cmdpalUI:ListItemContainerStyleSelector
|
||||
x:Key="ListItemContainerStyleSelector"
|
||||
Default="{StaticResource ListDefaultContainerStyle}"
|
||||
Default="{StaticResource ListSingleRowItemContainerStyle}"
|
||||
Section="{StaticResource ListSectionContainerStyle}"
|
||||
Separator="{StaticResource ListSeparatorContainerStyle}" />
|
||||
|
||||
@@ -310,10 +371,7 @@
|
||||
Title and subtitle are intentionally in a nested Grid instead in the outer container,
|
||||
to avoid pushing the following element (tags) out of bounds.
|
||||
-->
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
ColumnSpacing="12">
|
||||
<Grid Grid.Column="1" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -345,10 +403,11 @@
|
||||
</Grid>
|
||||
|
||||
<!--
|
||||
8px right margin is added to visually match the spacing between then icon,
|
||||
and the left margin of the list, as there's
|
||||
An 8px right margin is added to visually match the spacing between the icon
|
||||
and the left margin of the list.
|
||||
ItemRepeater is a lightweight control (compared to ItemsControl).
|
||||
-->
|
||||
<ItemsControl
|
||||
<ItemsRepeater
|
||||
Grid.Column="2"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"
|
||||
@@ -357,12 +416,10 @@
|
||||
ItemTemplate="{StaticResource TagTemplate}"
|
||||
ItemsSource="{x:Bind Tags, Mode=OneWay}"
|
||||
Visibility="{x:Bind HasTags, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsRepeater.Layout>
|
||||
</ItemsRepeater>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
@@ -12,6 +13,7 @@ using Microsoft.CmdPal.UI.ViewModels.Commands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
@@ -35,6 +37,14 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
private InputSource _lastInputSource;
|
||||
|
||||
private int _itemsUpdatedVersion;
|
||||
private bool _suppressSelectionChanged;
|
||||
|
||||
private bool _scrollOnNextSelectionChange;
|
||||
|
||||
private ListItemViewModel? _stickySelectedItem;
|
||||
private ListItemViewModel? _lastPushedToVm;
|
||||
|
||||
internal ListViewModel? ViewModel
|
||||
{
|
||||
get => (ListViewModel?)GetValue(ViewModelProperty);
|
||||
@@ -45,13 +55,7 @@ public sealed partial class ListPage : Page,
|
||||
public static readonly DependencyProperty ViewModelProperty =
|
||||
DependencyProperty.Register(nameof(ViewModel), typeof(ListViewModel), typeof(ListPage), new PropertyMetadata(null, OnViewModelChanged));
|
||||
|
||||
private ListViewBase ItemView
|
||||
{
|
||||
get
|
||||
{
|
||||
return ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;
|
||||
}
|
||||
}
|
||||
private ListViewBase ItemView => ViewModel?.IsGridView == true ? ItemsGrid : ItemsList;
|
||||
|
||||
public ListPage()
|
||||
{
|
||||
@@ -82,10 +86,17 @@ public sealed partial class ListPage : Page,
|
||||
// may return an incorrect index because item containers are not yet rendered.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
if (firstUsefulIndex != -1)
|
||||
// Only do this if we truly have no selection.
|
||||
if (ItemView.SelectedItem is null)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
using (SuppressSelectionChangedScope())
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -118,7 +129,6 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
if (ViewModel is not null)
|
||||
{
|
||||
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
|
||||
ViewModel.ItemsUpdated -= Page_ItemsUpdated;
|
||||
}
|
||||
|
||||
@@ -175,6 +185,9 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
else
|
||||
{
|
||||
// Click-driven selection should scroll into view (but only once).
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
@@ -196,51 +209,59 @@ public sealed partial class ListPage : Page,
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void Items_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressSelectionChanged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var vm = ViewModel;
|
||||
var li = ItemView.SelectedItem as ListItemViewModel;
|
||||
_ = Task.Run(() =>
|
||||
|
||||
// Transient null/separator selection can happen during in-place updates.
|
||||
// Do not push null into the VM; Page_ItemsUpdated will repair selection.
|
||||
if (li is null || IsSeparator(li))
|
||||
{
|
||||
vm?.UpdateSelectedItemCommand.Execute(li);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// There's mysterious behavior here, where the selection seemingly
|
||||
// changes to _nothing_ when we're backspacing to a single character.
|
||||
// And at that point, seemingly the item that's getting removed is not
|
||||
// a member of FilteredItems. Very bizarre.
|
||||
//
|
||||
// Might be able to fix in the future by stashing the removed item
|
||||
// here, then in Page_ItemsUpdated trying to select that cached item if
|
||||
// it's in the list (otherwise, clear the cache), but that seems
|
||||
// aggressively BODGY for something that mostly just works today.
|
||||
if (ItemView.SelectedItem is not null && !IsSeparator(ItemView.SelectedItem))
|
||||
_stickySelectedItem = li;
|
||||
|
||||
// Do not Task.Run (it reorders selection updates).
|
||||
vm?.UpdateSelectedItemCommand.Execute(li);
|
||||
|
||||
// Only scroll when explicitly requested by navigation/click handlers.
|
||||
if (_scrollOnNextSelectionChange)
|
||||
{
|
||||
var items = ItemView.Items;
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
var shouldScroll = false;
|
||||
_scrollOnNextSelectionChange = false;
|
||||
|
||||
if (e.RemovedItems.Count > 0)
|
||||
var scrollTarget = li;
|
||||
|
||||
// If the previous item is a separator, also scroll it into view to provide
|
||||
// better context for the user
|
||||
var index = ItemView.Items.IndexOf(li);
|
||||
if (index > 0)
|
||||
{
|
||||
shouldScroll = true;
|
||||
}
|
||||
else if (ItemView.SelectedIndex > firstUsefulIndex)
|
||||
{
|
||||
shouldScroll = true;
|
||||
var prevItem = ItemView.Items[index - 1] as ListItemViewModel;
|
||||
if (prevItem?.Type == ListItemType.SectionHeader)
|
||||
{
|
||||
scrollTarget = prevItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldScroll)
|
||||
if (scrollTarget is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
ItemView.ScrollIntoView(scrollTarget);
|
||||
}
|
||||
}
|
||||
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = Microsoft.UI.Xaml.Automation.Peers.ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
li.Title,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
// Automation notification for screen readers
|
||||
var listViewPeer = ListViewAutomationPeer.CreatePeerForElement(ItemView);
|
||||
if (listViewPeer is not null)
|
||||
{
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
li.Title,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +272,12 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
if (ItemView.SelectedItem != item)
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
using (SuppressSelectionChangedScope())
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
ViewModel?.UpdateSelectedItemCommand.Execute(item);
|
||||
@@ -264,7 +290,7 @@ public sealed partial class ListPage : Page,
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||
new OpenContextMenuMessage(
|
||||
element,
|
||||
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
pos,
|
||||
ContextMenuFilterLocation.Top));
|
||||
});
|
||||
@@ -274,7 +300,7 @@ public sealed partial class ListPage : Page,
|
||||
private void Items_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Find the ScrollViewer in the ItemView (ItemsList or ItemsGrid)
|
||||
var listViewScrollViewer = FindScrollViewer(this.ItemView);
|
||||
var listViewScrollViewer = FindScrollViewer(ItemView);
|
||||
|
||||
if (listViewScrollViewer is not null)
|
||||
{
|
||||
@@ -300,12 +326,31 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
}
|
||||
|
||||
// Message-driven navigation should count as keyboard.
|
||||
private void MarkKeyboardNavigation() => _lastInputSource = InputSource.Keyboard;
|
||||
|
||||
private void PushSelectionToVm()
|
||||
{
|
||||
if (ViewModel is null || ItemView.SelectedItem is not ListItemViewModel li || IsSeparator(li))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(_lastPushedToVm, li))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastPushedToVm = li;
|
||||
_stickySelectedItem = li;
|
||||
ViewModel.UpdateSelectedItemCommand.Execute(li);
|
||||
}
|
||||
|
||||
public void Receive(NavigateNextCommand message)
|
||||
{
|
||||
// Note: We may want to just have the notion of a 'SelectedCommand' in our VM
|
||||
// And then have these commands manipulate that state being bound to the UI instead
|
||||
// We may want to see how other non-list UIs need to behave to make this decision
|
||||
// At least it's decoupled from the SearchBox now :)
|
||||
MarkKeyboardNavigation();
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
// For grid views, use spatial navigation (down)
|
||||
@@ -316,10 +361,15 @@ public sealed partial class ListPage : Page,
|
||||
// For list views, use simple linear navigation
|
||||
NavigateDown();
|
||||
}
|
||||
|
||||
PushSelectionToVm();
|
||||
}
|
||||
|
||||
public void Receive(NavigatePreviousCommand message)
|
||||
{
|
||||
MarkKeyboardNavigation();
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
// For grid views, use spatial navigation (up)
|
||||
@@ -329,14 +379,20 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
NavigateUp();
|
||||
}
|
||||
|
||||
PushSelectionToVm();
|
||||
}
|
||||
|
||||
public void Receive(NavigateLeftCommand message)
|
||||
{
|
||||
MarkKeyboardNavigation();
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
// For grid views, use spatial navigation. For list views, just move up.
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
HandleGridArrowNavigation(VirtualKey.Left);
|
||||
PushSelectionToVm();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -347,10 +403,14 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
public void Receive(NavigateRightCommand message)
|
||||
{
|
||||
MarkKeyboardNavigation();
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
// For grid views, use spatial navigation. For list views, just move down.
|
||||
if (ViewModel?.IsGridView == true)
|
||||
{
|
||||
HandleGridArrowNavigation(VirtualKey.Right);
|
||||
PushSelectionToVm();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -385,6 +445,9 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
public void Receive(NavigatePageDownCommand message)
|
||||
{
|
||||
MarkKeyboardNavigation();
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
var indexes = CalculateTargetIndexPageUpDownScrollTo(true);
|
||||
if (indexes is null)
|
||||
{
|
||||
@@ -394,15 +457,16 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
PushSelectionToVm();
|
||||
}
|
||||
|
||||
public void Receive(NavigatePageUpCommand message)
|
||||
{
|
||||
MarkKeyboardNavigation();
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
var indexes = CalculateTargetIndexPageUpDownScrollTo(false);
|
||||
if (indexes is null)
|
||||
{
|
||||
@@ -412,11 +476,9 @@ public sealed partial class ListPage : Page,
|
||||
if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = indexes.Value.TargetIndex;
|
||||
if (ItemView.SelectedItem is not null)
|
||||
{
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
PushSelectionToVm();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -519,8 +581,8 @@ public sealed partial class ListPage : Page,
|
||||
}
|
||||
|
||||
var targetIndex = isPageDown
|
||||
? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
|
||||
: Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));
|
||||
? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage))
|
||||
: Math.Max(0, currentIndex - Math.Max(1, itemsPerPage));
|
||||
|
||||
return (currentIndex, targetIndex);
|
||||
}
|
||||
@@ -531,13 +593,11 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
if (e.OldValue is ListViewModel old)
|
||||
{
|
||||
old.PropertyChanged -= @this.ViewModel_PropertyChanged;
|
||||
old.ItemsUpdated -= @this.Page_ItemsUpdated;
|
||||
}
|
||||
|
||||
if (e.NewValue is ListViewModel page)
|
||||
{
|
||||
page.PropertyChanged += @this.ViewModel_PropertyChanged;
|
||||
page.ItemsUpdated += @this.Page_ItemsUpdated;
|
||||
}
|
||||
else if (e.NewValue is null)
|
||||
@@ -549,83 +609,139 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
// Called after we've finished updating the whole list for either a
|
||||
// GetItems or a change in the filter.
|
||||
private void Page_ItemsUpdated(ListViewModel sender, object args)
|
||||
private void Page_ItemsUpdated(ListViewModel sender, ItemsUpdatedEventArgs args)
|
||||
{
|
||||
// If for some reason, we don't have a selected item, fix that.
|
||||
//
|
||||
// It's important to do this here, because once there's no selection
|
||||
// (which can happen as the list updates) we won't get an
|
||||
// ItemView_SelectionChanged again to give us another chance to change
|
||||
// the selection from null -> something. Better to just update the
|
||||
// selection once, at the end of all the updating.
|
||||
// The selection logic must be deferred to the DispatcherQueue
|
||||
// to ensure the UI has processed the updated ItemsSource binding,
|
||||
// preventing ItemView.Items from appearing empty/null immediately after update.
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
var version = Interlocked.Increment(ref _itemsUpdatedVersion);
|
||||
var forceFirstItem = args.ForceFirstItem;
|
||||
|
||||
// Try to handle selection immediately — items should already be available
|
||||
// since FilteredItems is a direct ObservableCollection bound as ItemsSource.
|
||||
if (!TrySetSelectionAfterUpdate(sender, version, forceFirstItem))
|
||||
{
|
||||
var items = ItemView.Items;
|
||||
// Fallback: binding hasn't propagated yet, defer to next tick.
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
Microsoft.UI.Dispatching.DispatcherQueuePriority.Low,
|
||||
() =>
|
||||
{
|
||||
if (version != Volatile.Read(ref _itemsUpdatedVersion))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If the list is null or empty, clears the selection and return
|
||||
if (items is null || items.Count == 0)
|
||||
TrySetSelectionAfterUpdate(sender, version, forceFirstItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies selection after an items update. Returns false if ItemView.Items
|
||||
/// is not yet populated (caller should defer and retry).
|
||||
/// </summary>
|
||||
/// <param name="forceFirstItem">
|
||||
/// When true, always select the first selectable item and scroll to top
|
||||
/// (used for filter changes and top-level fetches).
|
||||
/// </param>
|
||||
private bool TrySetSelectionAfterUpdate(ListViewModel sender, long version, bool forceFirstItem)
|
||||
{
|
||||
if (version != Volatile.Read(ref _itemsUpdatedVersion))
|
||||
{
|
||||
return true; // superseded by a newer update, nothing to do
|
||||
}
|
||||
|
||||
var vm = ViewModel;
|
||||
if (vm is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use the stable source of truth, not ItemView.Items (which can be transiently empty)
|
||||
if (vm.FilteredItems.Count == 0)
|
||||
{
|
||||
using (SuppressSelectionChangedScope())
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
return;
|
||||
_stickySelectedItem = null;
|
||||
_lastPushedToVm = null;
|
||||
}
|
||||
|
||||
// Finds the first item that is not a separator
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there is only separators in the list, don't select anything.
|
||||
if (firstUsefulIndex == -1)
|
||||
// If ItemView.Items hasn't caught up with the ObservableCollection yet,
|
||||
// signal the caller to defer and retry.
|
||||
var items = ItemView.Items;
|
||||
if (items is null || items.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstUsefulIndex = GetFirstSelectableIndex();
|
||||
if (firstUsefulIndex == -1)
|
||||
{
|
||||
using (SuppressSelectionChangedScope())
|
||||
{
|
||||
ItemView.SelectedIndex = -1;
|
||||
|
||||
return;
|
||||
_stickySelectedItem = null;
|
||||
_lastPushedToVm = null;
|
||||
}
|
||||
|
||||
var shouldUpdateSelection = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's a top level list update we force the reset to the top useful item
|
||||
if (!sender.IsNested)
|
||||
var shouldUpdateSelection = forceFirstItem;
|
||||
|
||||
if (!shouldUpdateSelection)
|
||||
{
|
||||
// Check if selection needs repair (item gone, null, or separator).
|
||||
if (ItemView.SelectedItem is null)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// No current selection or current selection is null
|
||||
else if (ItemView.SelectedItem is null)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The current selected item is a separator
|
||||
else if (IsSeparator(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
// The selected item does not exist in the new list
|
||||
else if (!items.Contains(ItemView.SelectedItem))
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
if (shouldUpdateSelection)
|
||||
{
|
||||
if (firstUsefulIndex != -1)
|
||||
{
|
||||
ItemView.SelectedIndex = firstUsefulIndex;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
if (prop == nameof(ViewModel.FilteredItems))
|
||||
{
|
||||
Debug.WriteLine($"ViewModel.FilteredItems {ItemView.SelectedItem}");
|
||||
}
|
||||
|
||||
if (shouldUpdateSelection)
|
||||
{
|
||||
using (SuppressSelectionChangedScope())
|
||||
{
|
||||
if (!forceFirstItem &&
|
||||
_stickySelectedItem is not null &&
|
||||
items.Contains(_stickySelectedItem) &&
|
||||
!IsSeparator(_stickySelectedItem))
|
||||
{
|
||||
// Preserve sticky selection for nested dynamic updates.
|
||||
ItemView.SelectedItem = _stickySelectedItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Select the first interactive item.
|
||||
ItemView.SelectedItem = items[firstUsefulIndex];
|
||||
}
|
||||
|
||||
// Prevent any pending "scroll on selection" logic from fighting this.
|
||||
_scrollOnNextSelectionChange = false;
|
||||
|
||||
_ = DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
if (version != Volatile.Read(ref _itemsUpdatedVersion))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResetScrollToTop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PushSelectionToVm();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ScrollViewer? FindScrollViewer(DependencyObject parent)
|
||||
@@ -653,7 +769,6 @@ public sealed partial class ListPage : Page,
|
||||
{
|
||||
if (ItemView.Items.Count == 0)
|
||||
{
|
||||
// No items, goodbye.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -750,7 +865,6 @@ public sealed partial class ListPage : Page,
|
||||
if (bestIndex != currentIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = bestIndex;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -773,7 +887,6 @@ public sealed partial class ListPage : Page,
|
||||
if (fallback != currentIndex)
|
||||
{
|
||||
ItemView.SelectedIndex = fallback;
|
||||
ItemView.ScrollIntoView(ItemView.SelectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,7 +910,12 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
if (ItemView.SelectedItem != item)
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
_scrollOnNextSelectionChange = true;
|
||||
|
||||
using (SuppressSelectionChangedScope())
|
||||
{
|
||||
ItemView.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
|
||||
if (!e.TryGetPosition(element, out var pos))
|
||||
@@ -811,7 +929,7 @@ public sealed partial class ListPage : Page,
|
||||
WeakReferenceMessenger.Default.Send<OpenContextMenuMessage>(
|
||||
new OpenContextMenuMessage(
|
||||
element,
|
||||
Microsoft.UI.Xaml.Controls.Primitives.FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
FlyoutPlacementMode.BottomEdgeAlignedLeft,
|
||||
pos,
|
||||
ContextMenuFilterLocation.Top));
|
||||
});
|
||||
@@ -844,6 +962,7 @@ public sealed partial class ListPage : Page,
|
||||
case VirtualKey.Up:
|
||||
case VirtualKey.Down:
|
||||
_lastInputSource = InputSource.Keyboard;
|
||||
_scrollOnNextSelectionChange = true;
|
||||
HandleGridArrowNavigation(e.Key);
|
||||
e.Handled = true;
|
||||
break;
|
||||
@@ -1027,6 +1146,31 @@ public sealed partial class ListPage : Page,
|
||||
|
||||
private bool IsSeparator(object? item) => item is ListItemViewModel li && !li.IsInteractive;
|
||||
|
||||
private bool IsSectionHeader(object? item) => item is ListItemViewModel li && li.Type == ListItemType.SectionHeader;
|
||||
|
||||
private void ResetScrollToTop()
|
||||
{
|
||||
var scroll = FindScrollViewer(ItemView);
|
||||
if (scroll is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// disableAnimation: true prevents a visible jump animation
|
||||
scroll.ChangeView(horizontalOffset: null, verticalOffset: 0, zoomFactor: null, disableAnimation: true);
|
||||
}
|
||||
|
||||
private IDisposable SuppressSelectionChangedScope()
|
||||
{
|
||||
_suppressSelectionChanged = true;
|
||||
return new ActionOnDispose(() => _suppressSelectionChanged = false);
|
||||
}
|
||||
|
||||
private sealed partial class ActionOnDispose(Action action) : IDisposable
|
||||
{
|
||||
public void Dispose() => action();
|
||||
}
|
||||
|
||||
private enum InputSource
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -24,6 +24,7 @@ public static partial class IconCacheProvider
|
||||
| 256×256 | 256.0 KB | 64 | 16.0 MB | 1 MB | 64.0 MB | 2.3 MB | 144 MB |
|
||||
*/
|
||||
|
||||
private static IIconSourceProvider _provider16 = null!;
|
||||
private static IIconSourceProvider _provider20 = null!;
|
||||
private static IIconSourceProvider _provider32 = null!;
|
||||
private static IIconSourceProvider _provider64 = null!;
|
||||
@@ -31,6 +32,7 @@ public static partial class IconCacheProvider
|
||||
|
||||
public static void Initialize(IServiceProvider serviceProvider)
|
||||
{
|
||||
_provider16 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size16);
|
||||
_provider20 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size20);
|
||||
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
|
||||
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
|
||||
@@ -64,6 +66,9 @@ public static partial class IconCacheProvider
|
||||
}
|
||||
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
public static void SourceRequested16(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider16, args);
|
||||
|
||||
public static void SourceRequested20(IconBox sender, SourceRequestedEventArgs args)
|
||||
=> SourceRequestedCore(_provider20, args);
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ internal static class IconServiceRegistration
|
||||
services.AddSingleton<IIconLoaderService>(loader);
|
||||
|
||||
// Keyed providers by size
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size16,
|
||||
(_, _) => new IconSourceProvider(loader, 16));
|
||||
|
||||
services.AddKeyedSingleton<IIconSourceProvider>(
|
||||
WellKnownIconSize.Size20,
|
||||
(_, _) => new CachedIconSourceProvider(loader, 20, 1024));
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal enum WellKnownIconSize
|
||||
{
|
||||
Size16 = 16,
|
||||
Size20 = 20,
|
||||
Size32 = 32,
|
||||
Size64 = 64,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Services;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
@@ -18,6 +19,7 @@ using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Composition;
|
||||
@@ -45,6 +47,7 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<DismissMessage>,
|
||||
IRecipient<ShowWindowMessage>,
|
||||
IRecipient<ShowPaletteAtMessage>,
|
||||
IRecipient<HideWindowMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
@@ -145,6 +148,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
|
||||
@@ -206,7 +210,10 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
}
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(HotReloadSettings);
|
||||
}
|
||||
|
||||
private void RootElementLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -496,6 +503,78 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
{
|
||||
var positionWindowForTargetMonitor = (HWND hwnd) =>
|
||||
{
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
|
||||
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
|
||||
MoveAndResizeDpiAware(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
}
|
||||
};
|
||||
ShowHwnd(hwndValue, positionWindowForTargetMonitor);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, Point anchorInPixels, AnchorPoint anchorCorner)
|
||||
{
|
||||
var positionWindowForAnchor = (HWND hwnd) =>
|
||||
{
|
||||
PInvoke.GetWindowRect(hwnd, out var bounds);
|
||||
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER;
|
||||
switch (anchorCorner)
|
||||
{
|
||||
case AnchorPoint.TopLeft:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)anchorInPixels.X,
|
||||
(int)anchorInPixels.Y,
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.TopRight:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)(anchorInPixels.X - bounds.Width),
|
||||
(int)anchorInPixels.Y,
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.BottomLeft:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)anchorInPixels.X,
|
||||
(int)(anchorInPixels.Y - bounds.Height),
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.BottomRight:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)(anchorInPixels.X - bounds.Width),
|
||||
(int)(anchorInPixels.Y - bounds.Height),
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
}
|
||||
};
|
||||
ShowHwnd(hwndValue, positionWindowForAnchor);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, Action<HWND>? positionWindow)
|
||||
{
|
||||
StopAutoGoHome();
|
||||
|
||||
@@ -514,16 +593,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
||||
}
|
||||
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
if (positionWindow is not null)
|
||||
{
|
||||
var originalScreen = new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight);
|
||||
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
|
||||
MoveAndResizeDpiAware(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
positionWindow(hwnd);
|
||||
}
|
||||
|
||||
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
|
||||
@@ -605,6 +677,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
ShowHwnd(message.Hwnd, settings.SummonOn);
|
||||
}
|
||||
|
||||
internal void Receive(ShowPaletteAtMessage message)
|
||||
{
|
||||
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
|
||||
}
|
||||
|
||||
public void Receive(HideWindowMessage message)
|
||||
{
|
||||
// This might come in off the UI thread. Make sure to hop back.
|
||||
@@ -715,6 +792,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// Sure, it's not ideal, but at least it's not visible.
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
|
||||
|
||||
// Start auto-go-home timer
|
||||
RestartAutoGoHome();
|
||||
}
|
||||
@@ -1121,6 +1200,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
|
||||
Cloak();
|
||||
this.Hide();
|
||||
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1178,6 +1258,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
DisposeAcrylic();
|
||||
}
|
||||
|
||||
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
|
||||
|
||||
public void Receive(ToggleDevRibbonMessage message)
|
||||
{
|
||||
_devRibbon?.Visibility = _devRibbon.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
<Version>$(CmdPalVersion)</Version>
|
||||
|
||||
<!-- For MVVM Toolkit Partial Properties/AOT support -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
<!-- OutputPath is set in CmdPal.Branding.props -->
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
@@ -27,10 +30,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
|
||||
<!--<PropertyGroup>
|
||||
<!-- <PropertyGroup>
|
||||
<EnableCmdPalAOT>true</EnableCmdPalAOT>
|
||||
<GeneratePackageLocally>true</GeneratePackageLocally>
|
||||
</PropertyGroup>-->
|
||||
</PropertyGroup> -->
|
||||
|
||||
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -39,13 +42,13 @@
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
|
||||
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
|
||||
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- This lets us actually reference types from Microsoft.Terminal.UI and CmdPalKeyboardService -->
|
||||
<CsWinRTIncludes>Microsoft.Terminal.UI;CmdPalKeyboardService</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
@@ -73,6 +76,7 @@
|
||||
<None Remove="Controls\DevRibbon.xaml" />
|
||||
<None Remove="Controls\FallbackRankerDialog.xaml" />
|
||||
<None Remove="Controls\ScreenPreview.xaml" />
|
||||
<None Remove="Controls\ScrollContainer.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
<None Remove="LoadingPage.xaml" />
|
||||
@@ -83,8 +87,8 @@
|
||||
<None Remove="Settings\AppearancePage.xaml" />
|
||||
<None Remove="Settings\InternalPage.xaml" />
|
||||
<None Remove="ShellPage.xaml" />
|
||||
<None Remove="Styles\Colors.xaml" />
|
||||
<None Remove="Styles\Settings.xaml" />
|
||||
<None Remove="Styles\TeachingTip.xaml" />
|
||||
<None Remove="Styles\TextBox.xaml" />
|
||||
<None Remove="Styles\Theme.Normal.xaml" />
|
||||
</ItemGroup>
|
||||
@@ -214,6 +218,24 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\TeachingTip.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Dock\DockItemControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\ScrollContainer.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\CommandPalettePreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -59,15 +59,56 @@ GetModuleHandle
|
||||
GetWindowLong
|
||||
SetWindowLong
|
||||
WINDOW_EX_STYLE
|
||||
|
||||
CreateWindowEx
|
||||
WNDCLASSEXW
|
||||
RegisterClassEx
|
||||
GetStockObject
|
||||
GetModuleHandle
|
||||
|
||||
MoveWindow
|
||||
GetSystemMetrics
|
||||
SHAppBarMessage
|
||||
ABM_NEW
|
||||
ABM_QUERYPOS
|
||||
ABM_SETPOS
|
||||
ABM_REMOVE
|
||||
ABM_SETAUTOHIDEBAR
|
||||
ABS_AUTOHIDE
|
||||
ABN_POSCHANGED
|
||||
APPBARDATA
|
||||
ABE_TOP
|
||||
ABE_BOTTOM
|
||||
ABE_LEFT
|
||||
ABE_RIGHT
|
||||
SYSTEM_METRICS_INDEX
|
||||
GetDpiForWindow
|
||||
SHQueryUserNotificationState
|
||||
SYSTEM_PARAMETERS_INFO_ACTION
|
||||
WINDOWPOS
|
||||
WM_DISPLAYCHANGE
|
||||
WM_SYSCOMMAND
|
||||
WM_SETTINGCHANGE
|
||||
WM_WINDOWPOSCHANGING
|
||||
WM_SHOWWINDOW
|
||||
WM_SIZE
|
||||
WM_GETMINMAXINFO
|
||||
SetWinEventHook
|
||||
WINDOW_STYLE
|
||||
SC_MINIMIZE
|
||||
SC_MAXIMIZE
|
||||
SET_WINDOW_POS_FLAGS
|
||||
SIZE_MAXIMIZED
|
||||
SIZE_MINIMIZED
|
||||
HWND_NOTOPMOST
|
||||
HWND_TOP
|
||||
HTCAPTION
|
||||
GetClassName
|
||||
EVENT_SYSTEM_FOREGROUND
|
||||
WINEVENT_OUTOFCONTEXT
|
||||
GetWindowThreadProcessId
|
||||
AttachThreadInput
|
||||
|
||||
GetWindowPlacement
|
||||
WINDOWPLACEMENT
|
||||
WM_DPICHANGED
|
||||
WM_DPICHANGED
|
||||
|
||||
@@ -200,14 +200,19 @@
|
||||
|
||||
<!-- Back button -->
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!--
|
||||
This border is to hold a bit of padding we need when
|
||||
the back button is hidden
|
||||
-->
|
||||
<Border Margin="20,0,0,0" Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<Image
|
||||
Width="20"
|
||||
Margin="20,0,6,0"
|
||||
Margin="0,0,6,0"
|
||||
HorizontalAlignment="Center"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="ms-appx:///Assets/icon.svg"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
@@ -250,7 +255,7 @@
|
||||
FontSize=14}"
|
||||
FontSize="16"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
@@ -297,7 +302,7 @@
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Text;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
@@ -25,6 +26,7 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Windows.UI.Core;
|
||||
using WinUIEx;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
using VirtualKey = Windows.System.VirtualKey;
|
||||
|
||||
@@ -47,6 +49,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<ShowConfirmationMessage>,
|
||||
IRecipient<ShowToastMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
IRecipient<ShowHideDockMessage>,
|
||||
INotifyPropertyChanged,
|
||||
IDisposable
|
||||
{
|
||||
@@ -64,6 +67,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
private readonly CompositeFormat _pageNavigatedAnnouncement;
|
||||
|
||||
private SettingsWindow? _settingsWindow;
|
||||
private DockWindow? _dockWindow;
|
||||
|
||||
private CancellationTokenSource? _focusAfterLoadedCts;
|
||||
private WeakReference<Page>? _lastNavigatedPageRef;
|
||||
@@ -96,6 +100,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
|
||||
|
||||
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
|
||||
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
|
||||
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
|
||||
@@ -104,6 +110,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
|
||||
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
|
||||
|
||||
if (App.Current.Services.GetService<SettingsModel>()!.EnableDock)
|
||||
{
|
||||
_dockWindow = new DockWindow();
|
||||
_dockWindow.Show();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,7 +157,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
public void Receive(NavigateToPageMessage message)
|
||||
{
|
||||
// TODO GH #526 This needs more better locking too
|
||||
_ = _queue.TryEnqueue(() =>
|
||||
_ = _queue.TryEnqueue(DispatcherQueuePriority.High, () =>
|
||||
{
|
||||
// Also hide our details pane about here, if we had one
|
||||
HideDetails();
|
||||
@@ -250,10 +262,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
|
||||
{
|
||||
vm.SafeInitializePropertiesSynchronous();
|
||||
}
|
||||
private void InitializeConfirmationDialog(ConfirmResultViewModel vm) => vm.SafeInitializePropertiesSynchronous();
|
||||
|
||||
public void Receive(OpenSettingsMessage message)
|
||||
{
|
||||
@@ -277,49 +286,55 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public void Receive(ShowDetailsMessage message)
|
||||
{
|
||||
if (ViewModel is not null &&
|
||||
ViewModel.CurrentPage is not null)
|
||||
if (ViewModel is null || ViewModel.CurrentPage is null)
|
||||
{
|
||||
if (ViewModel.CurrentPage.PageContext.TryGetTarget(out var pageContext))
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
// TERRIBLE HACK TODO GH #245
|
||||
// There's weird wacky bugs with debounce currently.
|
||||
if (!ViewModel.IsDetailsVisible)
|
||||
{
|
||||
ViewModel.Details = message.Details;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage)));
|
||||
ViewModel.IsDetailsVisible = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// GH #322:
|
||||
// For inexplicable reasons, if you try to change the details too fast,
|
||||
// we'll explode. This seemingly only happens if you change the details
|
||||
// while we're also scrolling a new list view item into view.
|
||||
_debounceTimer.Debounce(
|
||||
() =>
|
||||
{
|
||||
ViewModel.Details = message.Details;
|
||||
var details = message.Details;
|
||||
var wasVisible = ViewModel.IsDetailsVisible;
|
||||
|
||||
// Trigger a re-evaluation of whether we have a hero image based on
|
||||
// the current theme
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage)));
|
||||
},
|
||||
interval: TimeSpan.FromMilliseconds(50),
|
||||
immediate: ViewModel.IsDetailsVisible == false);
|
||||
ViewModel.IsDetailsVisible = true;
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
pageContext.Scheduler);
|
||||
}
|
||||
var openDetails = () =>
|
||||
{
|
||||
ViewModel.Details = details;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasHeroImage)));
|
||||
ViewModel.IsDetailsVisible = true;
|
||||
};
|
||||
|
||||
if (!wasVisible)
|
||||
{
|
||||
// If the details pane wasn't already open, then we can just open it
|
||||
// immediately with no animation or debounce, and be done.
|
||||
DispatcherQueue.TryEnqueue(() => openDetails());
|
||||
}
|
||||
else
|
||||
{
|
||||
// GH #322:
|
||||
// For inexplicable reasons, if you try to change the details too fast,
|
||||
// we'll explode. This seemingly only happens if you change the details
|
||||
// while we're also scrolling a new list view item into view.
|
||||
//
|
||||
// Always debounce through the DispatcherQueue
|
||||
// timer so the UI settles between updates. Use immediate=true for
|
||||
// the first show so the panel appears without delay; subsequent
|
||||
// updates during rapid navigation are coalesced.
|
||||
_debounceTimer.Debounce(
|
||||
() => openDetails(),
|
||||
interval: TimeSpan.FromMilliseconds(100));
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(HideDetailsMessage message) => HideDetails();
|
||||
public void Receive(HideDetailsMessage message)
|
||||
{
|
||||
// Debounce the hide through the same timer used for show. If a
|
||||
// ShowDetailsMessage arrives before this fires, it cancels the
|
||||
// pending hide - preventing the panel from flickering closed and
|
||||
// reopened during rapid item navigation.
|
||||
_debounceTimer.Debounce(
|
||||
() => HideDetails(),
|
||||
interval: TimeSpan.FromMilliseconds(150),
|
||||
immediate: false);
|
||||
}
|
||||
|
||||
public void Receive(LaunchUriMessage message) => _ = global::Windows.System.Launcher.LaunchUriAsync(message.Uri);
|
||||
|
||||
@@ -331,10 +346,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
|
||||
|
||||
public void Receive(HotkeySummonMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
|
||||
}
|
||||
public void Receive(HotkeySummonMessage message) => _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
|
||||
|
||||
public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
|
||||
|
||||
@@ -403,10 +415,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
public void Receive(GoBackMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
||||
}
|
||||
public void Receive(GoBackMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
||||
|
||||
private void GoBack(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
@@ -447,10 +456,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(GoHomeMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
||||
}
|
||||
public void Receive(GoHomeMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
||||
|
||||
private void GoHome(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
@@ -468,6 +474,27 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ShowHideDockMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (message.ShowDock)
|
||||
{
|
||||
if (_dockWindow is null)
|
||||
{
|
||||
_dockWindow = new DockWindow();
|
||||
}
|
||||
|
||||
_dockWindow.Show();
|
||||
}
|
||||
else if (_dockWindow is not null)
|
||||
{
|
||||
_dockWindow.Close();
|
||||
_dockWindow = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
|
||||
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
||||
@@ -739,5 +766,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = null;
|
||||
|
||||
_dockWindow?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ internal sealed class PowerToysAppHostService : IAppHostService
|
||||
return topLevelHost ?? currentHost ?? CommandPaletteHost.Instance;
|
||||
}
|
||||
|
||||
public CommandProviderContext GetProviderContextForCommand(object? command, CommandProviderContext? currentContext)
|
||||
public ICommandProviderContext GetProviderContextForCommand(object? command, ICommandProviderContext? currentContext)
|
||||
{
|
||||
CommandProviderContext? topLevelId = null;
|
||||
ICommandProviderContext? topLevelId = null;
|
||||
if (command is TopLevelViewModel topLevelViewModel)
|
||||
{
|
||||
topLevelId = topLevelViewModel.ProviderContext;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -7,6 +7,7 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -36,11 +37,14 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
private bool _isInitialized;
|
||||
private bool _disposed;
|
||||
private InternalThemeState _currentState;
|
||||
private DockThemeSnapshot _currentDockState;
|
||||
|
||||
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||
|
||||
public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot;
|
||||
|
||||
public DockThemeSnapshot CurrentDockTheme => Volatile.Read(ref _currentDockState);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the theme service. Must be called after the application window is activated and on UI thread.
|
||||
/// </summary>
|
||||
@@ -144,6 +148,60 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
// Atomic swap
|
||||
Interlocked.Exchange(ref _currentState, newState);
|
||||
|
||||
// Compute DockThemeSnapshot from DockSettings
|
||||
var dockSettings = _settings.DockSettings;
|
||||
var dockIntensity = Math.Clamp(dockSettings.CustomThemeColorIntensity, 0, 100);
|
||||
IThemeProvider dockProvider = dockIntensity > 0 && dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
|
||||
? _colorfulThemeProvider
|
||||
: _normalThemeProvider;
|
||||
|
||||
var dockTint = dockSettings.ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.CustomColor => dockSettings.CustomThemeColor,
|
||||
ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent),
|
||||
ColorizationMode.Image => dockSettings.CustomThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
var dockEffectiveTheme = GetElementTheme((ElementTheme)dockSettings.Theme);
|
||||
var dockImageSource = dockSettings.ColorizationMode == ColorizationMode.Image
|
||||
? LoadImageSafe(dockSettings.BackgroundImagePath)
|
||||
: null;
|
||||
var dockStretch = dockSettings.BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => Stretch.Fill,
|
||||
_ => Stretch.UniformToFill,
|
||||
};
|
||||
var dockOpacity = Math.Clamp(dockSettings.BackgroundImageOpacity, 0, 100) / 100.0;
|
||||
|
||||
var dockContext = new ThemeContext
|
||||
{
|
||||
Tint = dockTint,
|
||||
ColorIntensity = dockIntensity,
|
||||
Theme = dockEffectiveTheme,
|
||||
BackgroundImageSource = dockImageSource,
|
||||
BackgroundImageStretch = dockStretch,
|
||||
BackgroundImageOpacity = dockOpacity,
|
||||
};
|
||||
var dockBackdrop = dockProvider.GetBackdropParameters(dockContext);
|
||||
var dockBlur = dockSettings.BackgroundImageBlurAmount;
|
||||
var dockBrightness = dockSettings.BackgroundImageBrightness;
|
||||
|
||||
var dockSnapshot = new DockThemeSnapshot
|
||||
{
|
||||
Tint = dockTint,
|
||||
TintIntensity = dockIntensity / 100f,
|
||||
Theme = dockEffectiveTheme,
|
||||
Backdrop = dockSettings.Backdrop,
|
||||
BackgroundImageSource = dockImageSource,
|
||||
BackgroundImageStretch = dockStretch,
|
||||
BackgroundImageOpacity = dockOpacity,
|
||||
BackdropParameters = dockBackdrop,
|
||||
BlurAmount = dockBlur,
|
||||
BackgroundBrightness = dockBrightness / 100f,
|
||||
};
|
||||
|
||||
Interlocked.Exchange(ref _currentDockState, dockSnapshot);
|
||||
|
||||
_resourceSwapper.TryActivateTheme(provider.ThemeKey);
|
||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
|
||||
}
|
||||
@@ -223,6 +281,20 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
},
|
||||
Provider = _normalThemeProvider,
|
||||
};
|
||||
|
||||
_currentDockState = new DockThemeSnapshot
|
||||
{
|
||||
Tint = Colors.Transparent,
|
||||
TintIntensity = 1.0f,
|
||||
Theme = ElementTheme.Light,
|
||||
Backdrop = DockBackdrop.Acrylic,
|
||||
BackdropParameters = new BackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
|
||||
BackgroundImageOpacity = 1,
|
||||
BackgroundImageSource = null,
|
||||
BackgroundImageStretch = Stretch.Fill,
|
||||
BlurAmount = 0,
|
||||
BackgroundBrightness = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private void RequestReload()
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.Settings.DockSettingsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:cpControls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
|
||||
xmlns:helpers="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Settings"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<Grid Padding="16">
|
||||
<StackPanel
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<!--
|
||||
I got these from the samples, but they break XAML hot-reloading,
|
||||
so I commented them out.
|
||||
-->
|
||||
|
||||
<!--<StackPanel.ChildrenTransitions>
|
||||
<EntranceThemeTransition FromVerticalOffset="50" />
|
||||
<RepositionThemeTransition IsStaggeringEnabled="False" />
|
||||
</StackPanel.ChildrenTransitions>-->
|
||||
|
||||
<!-- Enable Dock -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Appearance Section -->
|
||||
<TextBlock x:Uid="DockAppearanceSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<!-- Dock Position -->
|
||||
<controls:SettingsExpander x:Uid="DockAppearance_DockPosition_SettingsExpander" IsExpanded="True">
|
||||
<controls:SettingsExpander.HeaderIcon>
|
||||
<SymbolIcon Symbol="MoveToFolder" />
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
<ComboBox
|
||||
x:Name="DockPositionComboBox"
|
||||
MinWidth="120"
|
||||
SelectedIndex="{x:Bind SelectedSideIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="DockAppearance_DockPosition_Left" />
|
||||
<ComboBoxItem x:Uid="DockAppearance_DockPosition_Top" />
|
||||
<ComboBoxItem x:Uid="DockAppearance_DockPosition_Right" />
|
||||
<ComboBoxItem x:Uid="DockAppearance_DockPosition_Bottom" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
|
||||
<!-- Show Labels -->
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<ptcontrols:CheckBoxWithDescriptionControl x:Uid="DockAppearance_ShowLabels_CheckBox" IsChecked="{x:Bind ShowLabels, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- Theme Section -->
|
||||
<TextBlock x:Uid="DockAppearance_ThemeSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_System" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Light_Automation" Tag="Light">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Light" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark_Automation" Tag="Dark">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Backdrop Style -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_Backdrop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
x:Name="BackdropComboBox"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.DockAppearance.BackdropIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Transparent" />
|
||||
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Acrylic" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background / Colorization Section -->
|
||||
<controls:SettingsExpander
|
||||
x:Uid="DockAppearance_Background_SettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.DockAppearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
<!-- none -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="DockAppearance_NoBackground_SettingsCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
Visibility="{x:Bind ViewModel.DockAppearance.IsNoBackgroundVisible, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="DockAppearance_NoBackground_DescriptionTextBlock"
|
||||
Margin="24"
|
||||
HorizontalAlignment="Stretch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
HorizontalTextAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- system accent color -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_WindowsAccentColor_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsAccentColorControlsVisible, Mode=OneWay}">
|
||||
<controls:SettingsCard.Description>
|
||||
<TextBlock>
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1" />
|
||||
<Hyperlink
|
||||
Click="OpenWindowsColorsSettings_Click"
|
||||
TextDecorations="None"
|
||||
UnderlineStyle="None">
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_OpenWindowsColorsLinkText" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</controls:SettingsCard.Description>
|
||||
<controls:SettingsCard.Content>
|
||||
<Border
|
||||
MinWidth="32"
|
||||
MinHeight="32"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind ViewModel.DockAppearance.EffectiveThemeColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</controls:SettingsCard.Content>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- background image -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="DockAppearance_BackgroundImage_SettingsCard"
|
||||
Description="{x:Bind ViewModel.DockAppearance.BackgroundImagePath, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="-100"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBrightness, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="50"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background tint color and intensity -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTint_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintVisible, Mode=OneWay}">
|
||||
<cpControls:ColorPickerButton
|
||||
HasSelectedColor="True"
|
||||
IsAlphaEnabled="False"
|
||||
PaletteColors="{x:Bind ViewModel.DockAppearance.Swatches}"
|
||||
SelectedColor="{x:Bind ViewModel.DockAppearance.ThemeColor, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="1"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.ColorIntensity, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Reset background image properties -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- Bands Section -->
|
||||
<TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<ItemsRepeater ItemsSource="{x:Bind AllDockBandItems, Mode=OneWay}">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Spacing="{StaticResource SettingsCardSpacing}" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="dockVm:DockBandSettingsViewModel">
|
||||
<controls:SettingsCard
|
||||
Description="{x:Bind Description, Mode=OneWay}"
|
||||
Header="{x:Bind Title, Mode=OneWay}"
|
||||
IsClickEnabled="False">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<cpControls:ContentIcon>
|
||||
<cpControls:ContentIcon.Content>
|
||||
<cpControls:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested20}" />
|
||||
</cpControls:ContentIcon.Content>
|
||||
</cpControls:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<ToggleSwitch IsOn="{x:Bind IsPinned, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.Windows.Storage.Pickers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
public sealed partial class DockSettingsPage : Page
|
||||
{
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
internal SettingsViewModel ViewModel { get; }
|
||||
|
||||
public List<DockBandSettingsViewModel> AllDockBandItems => GetAllBandSettings();
|
||||
|
||||
public DockSettingsPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
ViewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
|
||||
|
||||
// Initialize UI state
|
||||
InitializeSettings();
|
||||
}
|
||||
|
||||
private void InitializeSettings()
|
||||
{
|
||||
// Initialize UI controls to match current settings
|
||||
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
|
||||
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
|
||||
}
|
||||
|
||||
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (XamlRoot?.ContentIslandEnvironment is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var windowId = XamlRoot?.ContentIslandEnvironment?.AppWindowId ?? new Microsoft.UI.WindowId(0);
|
||||
|
||||
var picker = new FileOpenPicker(windowId)
|
||||
{
|
||||
CommitButtonText = ViewModels.Properties.Resources.builtin_settings_appearance_pick_background_image_title!,
|
||||
SuggestedStartLocation = PickerLocationId.PicturesLibrary,
|
||||
ViewMode = PickerViewMode.Thumbnail,
|
||||
};
|
||||
|
||||
string[] extensions = [".png", ".bmp", ".jpg", ".jpeg", ".jfif", ".gif", ".tiff", ".tif", ".webp", ".jxr"];
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
picker.FileTypeFilter!.Add(ext);
|
||||
}
|
||||
|
||||
var file = await picker.PickSingleFileAsync()!;
|
||||
if (file != null)
|
||||
{
|
||||
ViewModel.DockAppearance.BackgroundImagePath = file.Path ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to pick background image file for dock", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenWindowsColorsSettings_Click(Hyperlink sender, HyperlinkClickEventArgs args)
|
||||
{
|
||||
// LOAD BEARING (or BEAR LOADING?): Process.Start with UseShellExecute inside a XAML input event can trigger WinUI reentrancy
|
||||
// and cause FailFast crashes. Task.Run moves the call off the UI thread to prevent hard process termination.
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = Process.Start(new ProcessStartInfo("ms-settings:colors") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open Windows Settings", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Property bindings for ComboBoxes
|
||||
public int SelectedDockSizeIndex
|
||||
{
|
||||
get => DockSizeToSelectedIndex(ViewModel.Dock_DockSize);
|
||||
set => ViewModel.Dock_DockSize = SelectedIndexToDockSize(value);
|
||||
}
|
||||
|
||||
public int SelectedSideIndex
|
||||
{
|
||||
get => SideToSelectedIndex(ViewModel.Dock_Side);
|
||||
set => ViewModel.Dock_Side = SelectedIndexToSide(value);
|
||||
}
|
||||
|
||||
public int SelectedBackdropIndex
|
||||
{
|
||||
get => BackdropToSelectedIndex(ViewModel.Dock_Backdrop);
|
||||
set => ViewModel.Dock_Backdrop = SelectedIndexToBackdrop(value);
|
||||
}
|
||||
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => ViewModel.Dock_ShowLabels;
|
||||
set => ViewModel.Dock_ShowLabels = value;
|
||||
}
|
||||
|
||||
// Conversion methods for ComboBox bindings
|
||||
private static int DockSizeToSelectedIndex(DockSize size) => size switch
|
||||
{
|
||||
DockSize.Small => 0,
|
||||
DockSize.Medium => 1,
|
||||
DockSize.Large => 2,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static DockSize SelectedIndexToDockSize(int index) => index switch
|
||||
{
|
||||
0 => DockSize.Small,
|
||||
1 => DockSize.Medium,
|
||||
2 => DockSize.Large,
|
||||
_ => DockSize.Small,
|
||||
};
|
||||
|
||||
private static int SideToSelectedIndex(DockSide side) => side switch
|
||||
{
|
||||
DockSide.Left => 0,
|
||||
DockSide.Top => 1,
|
||||
DockSide.Right => 2,
|
||||
DockSide.Bottom => 3,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
private static DockSide SelectedIndexToSide(int index) => index switch
|
||||
{
|
||||
0 => DockSide.Left,
|
||||
1 => DockSide.Top,
|
||||
2 => DockSide.Right,
|
||||
3 => DockSide.Bottom,
|
||||
_ => DockSide.Top,
|
||||
};
|
||||
|
||||
private static int BackdropToSelectedIndex(DockBackdrop backdrop) => backdrop switch
|
||||
{
|
||||
DockBackdrop.Transparent => 0,
|
||||
DockBackdrop.Acrylic => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static DockBackdrop SelectedIndexToBackdrop(int index) => index switch
|
||||
{
|
||||
0 => DockBackdrop.Transparent,
|
||||
1 => DockBackdrop.Acrylic,
|
||||
_ => DockBackdrop.Acrylic,
|
||||
};
|
||||
|
||||
private List<TopLevelViewModel> GetAllBands()
|
||||
{
|
||||
var allBands = new List<TopLevelViewModel>();
|
||||
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
foreach (var item in tlcManager.DockBands)
|
||||
{
|
||||
if (item.IsDockBand)
|
||||
{
|
||||
allBands.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return allBands;
|
||||
}
|
||||
|
||||
private List<DockBandSettingsViewModel> GetAllBandSettings()
|
||||
{
|
||||
var allSettings = new List<DockBandSettingsViewModel>();
|
||||
|
||||
// var allBands = GetAllBands();
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
var settingsModel = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var dockViewModel = App.Current.Services.GetService<DockViewModel>()!;
|
||||
var allBands = tlcManager.DockBands;
|
||||
foreach (var band in allBands)
|
||||
{
|
||||
var setting = band.DockBandSettings;
|
||||
if (setting is not null)
|
||||
{
|
||||
var bandVm = dockViewModel.FindBandByTopLevel(band);
|
||||
allSettings.Add(new(
|
||||
dockSettingsModel: setting,
|
||||
topLevelAdapter: band,
|
||||
bandViewModel: bandVm,
|
||||
settingsModel: settingsModel
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return allSettings;
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,12 @@
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Extensions" />
|
||||
<!-- xF596 is HolePunchLandscapeTop -->
|
||||
<NavigationViewItem
|
||||
x:Name="DockSettingsPageNavItem"
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Dock"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Dock" />
|
||||
<!-- "Internal Tools" page item is added dynamically from code -->
|
||||
</NavigationView.MenuItems>
|
||||
<Grid>
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
|
||||
|
||||
// Gets or sets optional action invoked after NavigationView is loaded.
|
||||
public Action NavigationViewLoaded { get; set; } = () => { };
|
||||
public Action? NavigationViewLoaded { get; set; }
|
||||
|
||||
public SettingsWindow()
|
||||
{
|
||||
@@ -125,6 +125,9 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
case "Extensions":
|
||||
pageType = typeof(ExtensionsPage);
|
||||
break;
|
||||
case "Dock":
|
||||
pageType = typeof(DockSettingsPage);
|
||||
break;
|
||||
case "Internal":
|
||||
pageType = typeof(InternalPage);
|
||||
break;
|
||||
@@ -139,9 +142,24 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
break;
|
||||
}
|
||||
|
||||
var actualPage = page ?? "General";
|
||||
if (pageType is not null)
|
||||
{
|
||||
// BreadCrumbs.Clear();
|
||||
// BreadCrumbs.Add(new(actualPage, actualPage));
|
||||
NavFrame.Navigate(pageType);
|
||||
|
||||
// Now, make sure to actually select the correct menu item too
|
||||
foreach (var obj in NavView.MenuItems)
|
||||
{
|
||||
if (obj is NavigationViewItem item)
|
||||
{
|
||||
if (item.Tag is string s && s == page)
|
||||
{
|
||||
NavView.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +310,12 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(DockSettingsPage))
|
||||
{
|
||||
NavView.SelectedItem = DockSettingsPageNavItem;
|
||||
var pageType = RS_.GetString("Settings_PageTitles_DockPage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm)
|
||||
{
|
||||
NavView.SelectedItem = ExtensionPageNavItem;
|
||||
|
||||
@@ -395,6 +395,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_NavigationViewItem_Extensions.Content" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_NavigationViewItem_Dock.Content" xml:space="preserve">
|
||||
<value>Dock (Preview)</value>
|
||||
</data>
|
||||
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open Command Palette settings</value>
|
||||
</data>
|
||||
@@ -404,6 +407,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Behavior</value>
|
||||
</data>
|
||||
<data name="DockAppearanceSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="DockBandsSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Bands</value>
|
||||
</data>
|
||||
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search commands...</value>
|
||||
</data>
|
||||
@@ -418,6 +427,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Disable animations when switching between pages</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Enable Dock</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Enable a toolbar with quick access to commands</value>
|
||||
</data>
|
||||
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
@@ -625,6 +640,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_DockPage" xml:space="preserve">
|
||||
<value>Dock</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack.Content" xml:space="preserve">
|
||||
<value>Clear search first, then go back</value>
|
||||
</data>
|
||||
@@ -739,6 +757,75 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
||||
<value>Version {0}</value>
|
||||
</data>
|
||||
<data name="Settings_NavigationViewItem_DockAppearance.Content" xml:space="preserve">
|
||||
<value>Dock Appearance</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_DockAppearancePage" xml:space="preserve">
|
||||
<value>Dock Appearance</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ThemeSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="DockAppearance_AppTheme_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Dock theme mode</value>
|
||||
</data>
|
||||
<data name="DockAppearance_AppTheme_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Select which theme to display for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Material</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Select the visual material used for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Mica.Content" xml:space="preserve">
|
||||
<value>Mica</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Transparent.Content" xml:space="preserve">
|
||||
<value>Transparent</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Acrylic.Content" xml:space="preserve">
|
||||
<value>Acrylic</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Background_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Background</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Background_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>Choose a custom background color or image for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ColorizationMode.Header" xml:space="preserve">
|
||||
<value>Colorization mode</value>
|
||||
</data>
|
||||
<data name="DockAppearance_NoBackground_SettingsCard.Header" xml:space="preserve">
|
||||
<value>No background</value>
|
||||
</data>
|
||||
<data name="DockAppearance_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
|
||||
<value>No settings</value>
|
||||
</data>
|
||||
<data name="DockAppearance_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
|
||||
<value>System accent color</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImage_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageBrightness_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image brightness</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageBlur_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image blur</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageFit_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image fit</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundTint_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color tint</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color intensity</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImage_ResetProperties_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Restore defaults</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_BackdropOpacity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Opacity</value>
|
||||
</data>
|
||||
@@ -801,10 +888,50 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>K</value>
|
||||
<comment>Keyboard key</comment>
|
||||
</data>
|
||||
<data name="ConfigureShortcut" xml:space="preserve">
|
||||
<data name="ConfigureShortcut" xml:space="preserve">
|
||||
<value>Configure shortcut</value>
|
||||
</data>
|
||||
<data name="ConfigureShortcutText.Text" xml:space="preserve">
|
||||
<value>Assign shortcut</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockPosition_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Dock position and appearance</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockPosition_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>Choose where the dock appears on your screen</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockPosition_Left.Content" xml:space="preserve">
|
||||
<value>Left</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockPosition_Top.Content" xml:space="preserve">
|
||||
<value>Top</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockPosition_Right.Content" xml:space="preserve">
|
||||
<value>Right</value>
|
||||
</data>
|
||||
<data name="DockAppearance_DockPosition_Bottom.Content" xml:space="preserve">
|
||||
<value>Bottom</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ShowLabels_CheckBox.Header" xml:space="preserve">
|
||||
<value>Show labels</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ShowLabels_CheckBox.Description" xml:space="preserve">
|
||||
<value>Show labels for dock items by default</value>
|
||||
</data>
|
||||
<data name="top_level_pin_command_name" xml:space="preserve">
|
||||
<value>Pin command</value>
|
||||
<comment>Command name for pinning an item to the top level list of commands</comment>
|
||||
</data>
|
||||
<data name="top_level_unpin_command_name" xml:space="preserve">
|
||||
<value>Unpin command</value>
|
||||
<comment>Command name for unpinning an item from the top level list of commands</comment>
|
||||
</data>
|
||||
<data name="dock_pin_command_name" xml:space="preserve">
|
||||
<value>Pin to dock</value>
|
||||
<comment>Command name for pinning an item to the dock</comment>
|
||||
</data>
|
||||
<data name="dock_unpin_command_name" xml:space="preserve">
|
||||
<value>Unpin from dock</value>
|
||||
<comment>Command name for unpinning an item from the dock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
442
src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TeachingTip.xaml
Normal file
442
src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TeachingTip.xaml
Normal file
@@ -0,0 +1,442 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- TeachingTip doesn't have a simple way to get rid of the close button. It requires a full custom style :( -->
|
||||
<Style x:Key="TeachingTipWithoutCloseButtonStyle" TargetType="TeachingTip">
|
||||
<Setter Property="Background" Value="{ThemeResource TeachingTipBackgroundBrush}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TeachingTipForegroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource TeachingTipBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource TeachingTipContentBorderThicknessUntargeted}" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource OverlayCornerRadius}" />
|
||||
<Setter Property="ActionButtonStyle" Value="{ThemeResource DefaultButtonStyle}" />
|
||||
<Setter Property="CloseButtonStyle" Value="{ThemeResource DefaultButtonStyle}" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TeachingTip">
|
||||
<Border
|
||||
x:Name="Container"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent">
|
||||
<Grid
|
||||
MinWidth="{ThemeResource TeachingTipMinWidth}"
|
||||
MinHeight="{ThemeResource TeachingTipMinHeight}"
|
||||
MaxWidth="{ThemeResource TeachingTipMaxWidth}"
|
||||
MaxHeight="{ThemeResource TeachingTipMaxHeight}"
|
||||
AutomationProperties.Name="{TemplateBinding AutomationProperties.Name}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
x:Name="TailOcclusionGrid"
|
||||
Grid.RowSpan="5"
|
||||
Grid.ColumnSpan="5"
|
||||
MinWidth="{ThemeResource TeachingTipMinWidth}"
|
||||
MinHeight="{ThemeResource TeachingTipMinHeight}"
|
||||
MaxWidth="{ThemeResource TeachingTipMaxWidth}"
|
||||
MaxHeight="{ThemeResource TeachingTipMaxHeight}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
x:Name="ContentRootGrid"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
AutomationProperties.LandmarkType="Custom"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
FlowDirection="{TemplateBinding FlowDirection}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
x:Name="HeroContentBorder"
|
||||
Grid.Row="0"
|
||||
Background="{TemplateBinding Background}"
|
||||
Child="{TemplateBinding HeroContent}" />
|
||||
<Grid x:Name="NonHeroContentRootGrid" Grid.Row="1">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="{StaticResource TeachingTipContentMargin}">
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter
|
||||
x:Name="IconPresenter"
|
||||
Grid.Column="0"
|
||||
Foreground="{TemplateBinding Foreground}">
|
||||
<Border Child="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.IconElement}" />
|
||||
</ContentPresenter>
|
||||
<StackPanel x:Name="TitlesStackPanel" Grid.Column="1">
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="0"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TeachingTipTitleForegroundBrush}"
|
||||
Text="{TemplateBinding Title}"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="Collapsed" />
|
||||
<TextBlock
|
||||
x:Name="SubtitleTextBlock"
|
||||
Grid.Row="1"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
Foreground="{ThemeResource TeachingTipSubtitleForegroundBrush}"
|
||||
Text="{TemplateBinding Subtitle}"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ContentPresenter
|
||||
x:Name="MainContentPresenter"
|
||||
Grid.Row="1"
|
||||
Background="{TemplateBinding Background}"
|
||||
Content="{TemplateBinding Content}"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontStretch="{TemplateBinding FontStretch}"
|
||||
FontStyle="{TemplateBinding FontStyle}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
x:Name="ActionButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{TemplateBinding ActionButtonCommand}"
|
||||
CommandParameter="{TemplateBinding ActionButtonCommandParameter}"
|
||||
Style="{TemplateBinding ActionButtonStyle}">
|
||||
<ContentPresenter Content="{TemplateBinding ActionButtonContent}" TextWrapping="WrapWholeWords" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="CloseButton"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{TemplateBinding CloseButtonCommand}"
|
||||
CommandParameter="{TemplateBinding CloseButtonCommandParameter}"
|
||||
Style="{TemplateBinding CloseButtonStyle}">
|
||||
<ContentPresenter Content="{TemplateBinding CloseButtonContent}" TextWrapping="WrapWholeWords" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Button
|
||||
x:Name="AlternateCloseButton"
|
||||
Style="{ThemeResource AlternateCloseButtonStyle}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Polygon
|
||||
x:Name="TailPolygon"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Fill="{TemplateBinding Background}"
|
||||
Stroke="{TemplateBinding BorderBrush}"
|
||||
StrokeThickness="{StaticResource TeachingTipBorderThickness}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="LightDismissStates">
|
||||
<VisualState x:Name="LightDismiss">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Fill" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
<Setter Target="ContentRootGrid.Background" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
<Setter Target="MainContentPresenter.Background" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
<Setter Target="HeroContentBorder.Background" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NormalDismiss" />
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="ButtonsStates">
|
||||
<VisualState x:Name="NoButtonsVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Collapsed" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="ActionButtonVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Collapsed" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Visible" />
|
||||
<Setter Target="ActionButton.(Grid.ColumnSpan)" Value="2" />
|
||||
<Setter Target="ActionButton.Margin" Value="{ThemeResource TeachingTipButtonPanelMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CloseButtonVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Visible" />
|
||||
<Setter Target="CloseButton.Margin" Value="{ThemeResource TeachingTipButtonPanelMargin}" />
|
||||
<Setter Target="CloseButton.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CloseButton.(Grid.ColumnSpan)" Value="2" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="BothButtonsVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Visible" />
|
||||
<Setter Target="CloseButton.Margin" Value="{ThemeResource TeachingTipRightButtonMargin}" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Visible" />
|
||||
<Setter Target="ActionButton.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButton.Margin" Value="{ThemeResource TeachingTipLeftButtonMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="ContentStates">
|
||||
<VisualState x:Name="Content">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="MainContentPresenter.Margin" Value="{StaticResource TeachingTipMainContentPresentMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NoContent">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="MainContentPresenter.Margin" Value="{StaticResource TeachingTipMainContentAbsentMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="CloseButtonLocations">
|
||||
<VisualState x:Name="HeaderCloseButton">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitlesStackPanel.Margin" Value="{StaticResource TeachingTipTitleStackPanelMarginWithHeaderCloseButton}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="FooterCloseButton">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitlesStackPanel.Margin" Value="{StaticResource TeachingTipTitleStackPanelMarginWithFooterCloseButton}" />
|
||||
<Setter Target="AlternateCloseButton.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="IconStates">
|
||||
<VisualState x:Name="Icon">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="IconPresenter.Margin" Value="{StaticResource TeachingTipIconPresenterMarginWithIcon}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NoIcon">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="IconPresenter.Margin" Value="{StaticResource TeachingTipIconPresenterMarginWithoutIcon}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="HeroContentPlacementStates">
|
||||
<VisualState x:Name="HeroContentTop">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="HeroContentBorder.(Grid.Row)" Value="0" />
|
||||
<Setter Target="HeroContentBorder.CornerRadius" Value="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopCornerRadiusFilterConverter}, FallbackValue=0}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="HeroContentBottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="HeroContentBorder.(Grid.Row)" Value="2" />
|
||||
<Setter Target="HeroContentBorder.CornerRadius" Value="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomCornerRadiusFilterConverter}, FallbackValue=0}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="PlacementStates">
|
||||
<VisualState x:Name="Top">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10, 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Bottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,10 10,0 20,10" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="0" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginBottom}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Left">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 0,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="4" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginLeft}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Right">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="10,0 0,10 10,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="0" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginRight}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TopRight">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TopLeft">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="BottomRight">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,10 10,0 20,10" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="0" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginBottom}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="BottomLeft">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,10 10,0 20,10" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="0" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginBottom}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="LeftTop">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 0,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="4" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginLeft}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="LeftBottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 0,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="4" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginLeft}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="RightTop">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="10,0 0,10 10,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="0" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginRight}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="RightBottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="10,0 0,10 10,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="0" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginRight}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Center">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10, 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Untargeted">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="TitleBlockStates">
|
||||
<VisualState x:Name="ShowTitleTextBlock">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitleTextBlock.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CollapseTitleTextBlock" />
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="SubtitleBlockStates">
|
||||
<VisualState x:Name="ShowSubtitleTextBlock">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SubtitleTextBlock.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CollapseSubtitleTextBlock" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -17,6 +17,9 @@ namespace Microsoft.CmdPal.UI.ViewModels.UnitTests;
|
||||
[TestClass]
|
||||
public partial class MainListPageResultFactoryTests
|
||||
{
|
||||
private static readonly Separator _resultsSeparator = new("Results");
|
||||
private static readonly Separator _fallbacksSeparator = new("Fallbacks");
|
||||
|
||||
private sealed partial class MockListItem : IListItem
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
@@ -82,18 +85,22 @@ public partial class MainListPageResultFactoryTests
|
||||
scoredFallback,
|
||||
apps,
|
||||
fallbacks,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
appResultLimit: 10);
|
||||
|
||||
// Expected order:
|
||||
// "Results" section header
|
||||
// 100: F1, SF1, A1
|
||||
// 60: SF2
|
||||
// 55: A2
|
||||
// 50: F2
|
||||
// "Fallbacks" section header
|
||||
// Then fallbacks in original order: FB1, FB2
|
||||
var titles = result.Select(r => r.Title).ToArray();
|
||||
#pragma warning disable CA1861 // Avoid constant arrays as arguments
|
||||
CollectionAssert.AreEqual(
|
||||
new[] { "F1", "SF1", "A1", "SF2", "A2", "F2", "Fallbacks", "FB1", "FB2" },
|
||||
new[] { "Results", "F1", "SF1", "A1", "SF2", "A2", "F2", "Fallbacks", "FB1", "FB2" },
|
||||
titles);
|
||||
#pragma warning restore CA1861 // Avoid constant arrays as arguments
|
||||
}
|
||||
@@ -113,11 +120,14 @@ public partial class MainListPageResultFactoryTests
|
||||
null,
|
||||
apps,
|
||||
null,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
2);
|
||||
|
||||
Assert.AreEqual(2, result.Length);
|
||||
Assert.AreEqual("A1", result[0].Title);
|
||||
Assert.AreEqual("A2", result[1].Title);
|
||||
Assert.AreEqual(3, result.Length);
|
||||
Assert.AreEqual("Results", result[0].Title);
|
||||
Assert.AreEqual("A1", result[1].Title);
|
||||
Assert.AreEqual("A2", result[2].Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -135,10 +145,13 @@ public partial class MainListPageResultFactoryTests
|
||||
null,
|
||||
apps,
|
||||
null,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
appResultLimit: 1);
|
||||
|
||||
Assert.AreEqual(1, result.Length);
|
||||
Assert.AreEqual("A1", result[0].Title);
|
||||
Assert.AreEqual(2, result.Length);
|
||||
Assert.AreEqual("Results", result[0].Title);
|
||||
Assert.AreEqual("A1", result[1].Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -155,6 +168,8 @@ public partial class MainListPageResultFactoryTests
|
||||
null,
|
||||
apps,
|
||||
null,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
appResultLimit: 0);
|
||||
|
||||
Assert.AreEqual(0, result.Length);
|
||||
@@ -181,12 +196,15 @@ public partial class MainListPageResultFactoryTests
|
||||
null,
|
||||
apps,
|
||||
null,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
appResultLimit: 1);
|
||||
|
||||
Assert.AreEqual(3, result.Length);
|
||||
Assert.AreEqual("F1", result[0].Title);
|
||||
Assert.AreEqual("A1", result[1].Title);
|
||||
Assert.AreEqual("F2", result[2].Title);
|
||||
Assert.AreEqual(4, result.Length);
|
||||
Assert.AreEqual("Results", result[0].Title);
|
||||
Assert.AreEqual("F1", result[1].Title);
|
||||
Assert.AreEqual("A1", result[2].Title);
|
||||
Assert.AreEqual("F2", result[3].Title);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -203,6 +221,8 @@ public partial class MainListPageResultFactoryTests
|
||||
null,
|
||||
null,
|
||||
fallbacks,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
appResultLimit: 10);
|
||||
|
||||
Assert.AreEqual(3, result.Length);
|
||||
@@ -219,6 +239,8 @@ public partial class MainListPageResultFactoryTests
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
_resultsSeparator,
|
||||
_fallbacksSeparator,
|
||||
appResultLimit: 10);
|
||||
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
@@ -0,0 +1,746 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class ListHelpersInPlaceUpdateTests
|
||||
{
|
||||
// Use a reference-type wrapper so tests work with the `where T : class` constraint
|
||||
// and we can verify identity (same instance) in removedItems tests.
|
||||
private sealed class Item(string name)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
public override bool Equals(object? obj) => obj is Item other && Name == other.Name;
|
||||
|
||||
public override int GetHashCode() => Name.GetHashCode();
|
||||
}
|
||||
|
||||
private static Item[] MakeItems(params string[] names) =>
|
||||
names.Select(n => new Item(n)).ToArray();
|
||||
|
||||
private static void AssertSequence(IList<Item> actual, params string[] expected)
|
||||
{
|
||||
var actualNames = actual.Select(i => i.Name).ToArray();
|
||||
CollectionAssert.AreEqual(expected, actualNames, $"Expected [{string.Join(", ", expected)}] but got [{string.Join(", ", actualNames)}]");
|
||||
}
|
||||
|
||||
private static void AssertRemovedContainsExactly(List<Item> removedItems, IList<Item> originalItems, IList<Item> newItems)
|
||||
{
|
||||
// removedItems should contain exactly the items from original that are not in newItems
|
||||
var newSet = new HashSet<Item>(newItems);
|
||||
var expectedRemoved = originalItems.Where(i => !newSet.Contains(i)).ToList();
|
||||
|
||||
// Same count
|
||||
Assert.AreEqual(expectedRemoved.Count, removedItems.Count, $"Expected {expectedRemoved.Count} removed items but got {removedItems.Count}");
|
||||
|
||||
// Same instances (by reference, since we're checking cleanup correctness)
|
||||
foreach (var expected in expectedRemoved)
|
||||
{
|
||||
Assert.IsTrue(removedItems.Contains(expected), $"Expected '{expected.Name}' in removedItems but it was missing");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IdenticalLists_NoChanges()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var newContents = items.ToList(); // same items, same order
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newContents, out var removed);
|
||||
|
||||
AssertSequence(original, "A", "B", "C");
|
||||
Assert.AreEqual(0, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyToNonEmpty_AddsAll()
|
||||
{
|
||||
var original = new ObservableCollection<Item>();
|
||||
var newItems = MakeItems("A", "B", "C");
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newItems, out var removed);
|
||||
|
||||
AssertSequence(original, "A", "B", "C");
|
||||
Assert.AreEqual(0, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NonEmptyToEmpty_RemovesAll()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, [], out var removed);
|
||||
|
||||
Assert.AreEqual(0, original.Count);
|
||||
Assert.AreEqual(3, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SingleItem_Replace()
|
||||
{
|
||||
var a = new Item("A");
|
||||
var b = new Item("B");
|
||||
var original = new ObservableCollection<Item> { a };
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, [b], out var removed);
|
||||
|
||||
AssertSequence(original, "B");
|
||||
Assert.AreEqual(1, removed.Count);
|
||||
Assert.AreSame(a, removed[0]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FilterDown_RemovesNonMatching()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var filtered = new[] { items[0], items[2], items[4] }; // A, C, E
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, filtered, out var removed);
|
||||
|
||||
AssertSequence(original, "A", "C", "E");
|
||||
Assert.AreEqual(2, removed.Count); // B, D removed
|
||||
AssertRemovedContainsExactly(removed, items, filtered);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FilterDown_EveryOtherItem()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E", "F", "G", "H");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var filtered = new[] { items[1], items[3], items[5], items[7] }; // B, D, F, H
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, filtered, out var removed);
|
||||
|
||||
AssertSequence(original, "B", "D", "F", "H");
|
||||
Assert.AreEqual(4, removed.Count);
|
||||
AssertRemovedContainsExactly(removed, items, filtered);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Expand_InsertsNewItems()
|
||||
{
|
||||
var items = MakeItems("A", "C", "E");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var b = new Item("B");
|
||||
var d = new Item("D");
|
||||
var expanded = new[] { items[0], b, items[1], d, items[2] }; // A, B, C, D, E
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, expanded, out var removed);
|
||||
|
||||
AssertSequence(original, "A", "B", "C", "D", "E");
|
||||
Assert.AreEqual(0, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Reversed_ReordersCorrectly()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var reversed = items.Reverse().ToArray();
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, reversed, out var removed);
|
||||
|
||||
AssertSequence(original, "E", "D", "C", "B", "A");
|
||||
Assert.AreEqual(0, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MoveFirstToLast()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var reordered = new[] { items[1], items[2], items[3], items[0] }; // B, C, D, A
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, reordered, out var removed);
|
||||
|
||||
AssertSequence(original, "B", "C", "D", "A");
|
||||
Assert.AreEqual(0, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MoveLastToFirst()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var reordered = new[] { items[3], items[0], items[1], items[2] }; // D, A, B, C
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, reordered, out var removed);
|
||||
|
||||
AssertSequence(original, "D", "A", "B", "C");
|
||||
Assert.AreEqual(0, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NoOverlap_ReplacesAll()
|
||||
{
|
||||
var oldItems = MakeItems("A", "B", "C");
|
||||
var newItems = MakeItems("X", "Y", "Z");
|
||||
var original = new ObservableCollection<Item>(oldItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newItems, out var removed);
|
||||
|
||||
AssertSequence(original, "X", "Y", "Z");
|
||||
Assert.AreEqual(3, removed.Count);
|
||||
AssertRemovedContainsExactly(removed, oldItems, newItems);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NoOverlap_DifferentSizes_OriginalLarger()
|
||||
{
|
||||
var oldItems = MakeItems("A", "B", "C", "D", "E");
|
||||
var newItems = MakeItems("X", "Y");
|
||||
var original = new ObservableCollection<Item>(oldItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newItems, out var removed);
|
||||
|
||||
AssertSequence(original, "X", "Y");
|
||||
Assert.AreEqual(5, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NoOverlap_DifferentSizes_NewLarger()
|
||||
{
|
||||
var oldItems = MakeItems("A", "B");
|
||||
var newItems = MakeItems("X", "Y", "Z", "W");
|
||||
var original = new ObservableCollection<Item>(oldItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newItems, out var removed);
|
||||
|
||||
AssertSequence(original, "X", "Y", "Z", "W");
|
||||
Assert.AreEqual(2, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MixedRemoveAndReorder()
|
||||
{
|
||||
var items = MakeItems("A", "X", "Y", "C", "B");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
// Keep A, B, C but reorder; remove X, Y
|
||||
var newList = new[] { items[0], items[4], items[3] }; // A, B, C
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newList, out var removed);
|
||||
|
||||
AssertSequence(original, "A", "B", "C");
|
||||
Assert.AreEqual(2, removed.Count);
|
||||
AssertRemovedContainsExactly(removed, items, newList);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MixedAddRemoveReorder()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var e = new Item("E");
|
||||
|
||||
// Remove B, D; add E; reorder to C, A, E
|
||||
var newList = new[] { items[2], items[0], e }; // C, A, E
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newList, out var removed);
|
||||
|
||||
AssertSequence(original, "C", "A", "E");
|
||||
Assert.AreEqual(2, removed.Count); // B, D
|
||||
AssertRemovedContainsExactly(removed, items, newList);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ItemsBetweenCurrentAndFoundAreInNewList_NotRemovedIncorrectly()
|
||||
{
|
||||
// This is the scenario that caused the icon bug:
|
||||
// Items between the current position and the found target
|
||||
// appear later in newList and must NOT be put in removedItems.
|
||||
var items = MakeItems("A", "B", "C", "D", "E");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
// Reverse: items B, C, D are between position 0 and E's position
|
||||
// but all appear in newList
|
||||
var reversed = new[] { items[4], items[3], items[2], items[1], items[0] };
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, reversed, out var removed);
|
||||
|
||||
AssertSequence(original, "E", "D", "C", "B", "A");
|
||||
Assert.AreEqual(0, removed.Count, "No items should be removed when all items are reused");
|
||||
|
||||
// Verify all original instances are still in the collection (not cleaned up)
|
||||
foreach (var item in items)
|
||||
{
|
||||
Assert.IsTrue(original.Contains(item), $"Item '{item.Name}' should still be in the collection (same instance)");
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RemovedItems_NeverContainsItemsStillInNewList()
|
||||
{
|
||||
// Simulate the exact FetchItems scenario: reuse ViewModel instances
|
||||
var a = new Item("A");
|
||||
var b = new Item("B");
|
||||
var c = new Item("C");
|
||||
var d = new Item("D");
|
||||
var e = new Item("E");
|
||||
var original = new ObservableCollection<Item> { a, b, c, d, e };
|
||||
|
||||
// New list reuses same instances but in different order, minus some
|
||||
var newList = new Item[] { e, c, a }; // reversed subset
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newList, out var removed);
|
||||
|
||||
AssertSequence(original, "E", "C", "A");
|
||||
|
||||
// Critical: removed should only contain b and d
|
||||
Assert.AreEqual(2, removed.Count);
|
||||
Assert.IsTrue(removed.Contains(b), "B should be in removedItems");
|
||||
Assert.IsTrue(removed.Contains(d), "D should be in removedItems");
|
||||
|
||||
// Critical: removed must NOT contain items still in the list
|
||||
Assert.IsFalse(removed.Contains(a), "A is still in use — must not be in removedItems");
|
||||
Assert.IsFalse(removed.Contains(c), "C is still in use — must not be in removedItems");
|
||||
Assert.IsFalse(removed.Contains(e), "E is still in use — must not be in removedItems");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WorksWithPlainList()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new List<Item>(items);
|
||||
var newList = new[] { items[2], items[0] }; // C, A
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newList, out var removed);
|
||||
|
||||
AssertSequence(original, "C", "A");
|
||||
Assert.AreEqual(2, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArgOverload_ProducesCorrectResult()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var newList = new[] { items[2], items[0] }; // C, A
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newList);
|
||||
|
||||
AssertSequence(original, "C", "A");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AcceptsLazyEnumerable()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
// Pass a lazy IEnumerable (not materialized)
|
||||
IEnumerable<Item> lazy = items.Reverse().Where(_ => true);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, lazy, out var removed);
|
||||
|
||||
AssertSequence(original, "C", "B", "A");
|
||||
Assert.AreEqual(0, removed.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IncrementalSearch_ProgressiveFiltering()
|
||||
{
|
||||
// Simulate typing a search query character by character
|
||||
var all = MakeItems("Apple", "Banana", "Avocado", "Blueberry", "Apricot");
|
||||
var original = new ObservableCollection<Item>(all);
|
||||
|
||||
// First keystroke "A" — filter to A items
|
||||
var filtered1 = new[] { all[0], all[2], all[4] }; // Apple, Avocado, Apricot
|
||||
ListHelpers.InPlaceUpdateList(original, filtered1, out var removed1);
|
||||
AssertSequence(original, "Apple", "Avocado", "Apricot");
|
||||
Assert.AreEqual(2, removed1.Count);
|
||||
|
||||
// Second keystroke "Ap" — filter further
|
||||
var filtered2 = new[] { all[0], all[4] }; // Apple, Apricot
|
||||
ListHelpers.InPlaceUpdateList(original, filtered2, out var removed2);
|
||||
AssertSequence(original, "Apple", "Apricot");
|
||||
Assert.AreEqual(1, removed2.Count);
|
||||
|
||||
// Clear search — back to all
|
||||
ListHelpers.InPlaceUpdateList(original, all, out var removed3);
|
||||
AssertSequence(original, "Apple", "Banana", "Avocado", "Blueberry", "Apricot");
|
||||
Assert.AreEqual(0, removed3.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PageNavigation_CompleteReplacement()
|
||||
{
|
||||
// Simulate navigating from one extension page to another
|
||||
var page1 = MakeItems("P1A", "P1B", "P1C", "P1D");
|
||||
var page2 = MakeItems("P2A", "P2B", "P2C");
|
||||
var original = new ObservableCollection<Item>(page1);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, page2, out var removed1);
|
||||
AssertSequence(original, "P2A", "P2B", "P2C");
|
||||
Assert.AreEqual(4, removed1.Count);
|
||||
|
||||
// Navigate back
|
||||
ListHelpers.InPlaceUpdateList(original, page1, out var removed2);
|
||||
AssertSequence(original, "P1A", "P1B", "P1C", "P1D");
|
||||
Assert.AreEqual(3, removed2.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StableItems_SameInstancePreserved()
|
||||
{
|
||||
var a = new Item("A");
|
||||
var b = new Item("B");
|
||||
var c = new Item("C");
|
||||
var original = new ObservableCollection<Item> { a, b, c };
|
||||
|
||||
// Remove middle item
|
||||
ListHelpers.InPlaceUpdateList(original, [a, c]);
|
||||
|
||||
Assert.AreSame(a, original[0], "A should be the same instance");
|
||||
Assert.AreSame(c, original[1], "C should be the same instance");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ZeroOverlap_UsesReplaceNotInsertRemove()
|
||||
{
|
||||
// Track notifications to verify Replace path is used
|
||||
var oldItems = MakeItems("A", "B", "C");
|
||||
var newItems = MakeItems("X", "Y", "Z");
|
||||
var original = new ObservableCollection<Item>(oldItems);
|
||||
|
||||
var notifications = new List<System.Collections.Specialized.NotifyCollectionChangedAction>();
|
||||
original.CollectionChanged += (_, e) => notifications.Add(e.Action);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newItems, out var removed);
|
||||
|
||||
AssertSequence(original, "X", "Y", "Z");
|
||||
Assert.AreEqual(3, removed.Count);
|
||||
|
||||
// All notifications should be Replace (not Add/Remove pairs)
|
||||
Assert.IsTrue(
|
||||
notifications.All(a => a == System.Collections.Specialized.NotifyCollectionChangedAction.Replace),
|
||||
$"Expected all Replace but got: [{string.Join(", ", notifications)}]");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ZeroOverlap_ShrinkingList_ReplaceThenRemove()
|
||||
{
|
||||
var oldItems = MakeItems("A", "B", "C", "D", "E");
|
||||
var newItems = MakeItems("X", "Y");
|
||||
var original = new ObservableCollection<Item>(oldItems);
|
||||
|
||||
var notifications = new List<System.Collections.Specialized.NotifyCollectionChangedAction>();
|
||||
original.CollectionChanged += (_, e) => notifications.Add(e.Action);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newItems, out var removed);
|
||||
|
||||
AssertSequence(original, "X", "Y");
|
||||
Assert.AreEqual(5, removed.Count);
|
||||
|
||||
// 2 Replace + 3 Remove
|
||||
var replaces = notifications.Count(a => a == System.Collections.Specialized.NotifyCollectionChangedAction.Replace);
|
||||
var removes = notifications.Count(a => a == System.Collections.Specialized.NotifyCollectionChangedAction.Remove);
|
||||
Assert.AreEqual(2, replaces);
|
||||
Assert.AreEqual(3, removes);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ZeroOverlap_GrowingList_ReplaceThenAdd()
|
||||
{
|
||||
var oldItems = MakeItems("A", "B");
|
||||
var newItems = MakeItems("X", "Y", "Z", "W");
|
||||
var original = new ObservableCollection<Item>(oldItems);
|
||||
|
||||
var notifications = new List<System.Collections.Specialized.NotifyCollectionChangedAction>();
|
||||
original.CollectionChanged += (_, e) => notifications.Add(e.Action);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, newItems, out var removed);
|
||||
|
||||
AssertSequence(original, "X", "Y", "Z", "W");
|
||||
Assert.AreEqual(2, removed.Count);
|
||||
|
||||
// 2 Replace + 2 Add
|
||||
var replaces = notifications.Count(a => a == System.Collections.Specialized.NotifyCollectionChangedAction.Replace);
|
||||
var adds = notifications.Count(a => a == System.Collections.Specialized.NotifyCollectionChangedAction.Add);
|
||||
Assert.AreEqual(2, replaces);
|
||||
Assert.AreEqual(2, adds);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_IdenticalLists_NoChanges()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, items.ToList());
|
||||
|
||||
AssertSequence(original, "A", "B", "C");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_EmptyToNonEmpty()
|
||||
{
|
||||
var original = new ObservableCollection<Item>();
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, MakeItems("A", "B", "C"));
|
||||
|
||||
AssertSequence(original, "A", "B", "C");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_NonEmptyToEmpty()
|
||||
{
|
||||
var original = new ObservableCollection<Item>(MakeItems("A", "B", "C"));
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, Array.Empty<Item>());
|
||||
|
||||
Assert.AreEqual(0, original.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_FilterDown()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[0], items[2], items[4] });
|
||||
|
||||
AssertSequence(original, "A", "C", "E");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_FilterDown_EveryOtherItem()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E", "F", "G", "H");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[1], items[3], items[5], items[7] });
|
||||
|
||||
AssertSequence(original, "B", "D", "F", "H");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_Expand()
|
||||
{
|
||||
var items = MakeItems("A", "C", "E");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var b = new Item("B");
|
||||
var d = new Item("D");
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[0], b, items[1], d, items[2] });
|
||||
|
||||
AssertSequence(original, "A", "B", "C", "D", "E");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_Reversed()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, items.Reverse());
|
||||
|
||||
AssertSequence(original, "E", "D", "C", "B", "A");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_MoveFirstToLast()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[1], items[2], items[3], items[0] });
|
||||
|
||||
AssertSequence(original, "B", "C", "D", "A");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_MoveLastToFirst()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[3], items[0], items[1], items[2] });
|
||||
|
||||
AssertSequence(original, "D", "A", "B", "C");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_NoOverlap_ReplacesAll()
|
||||
{
|
||||
var original = new ObservableCollection<Item>(MakeItems("A", "B", "C"));
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, MakeItems("X", "Y", "Z"));
|
||||
|
||||
AssertSequence(original, "X", "Y", "Z");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_NoOverlap_OriginalLarger()
|
||||
{
|
||||
var original = new ObservableCollection<Item>(MakeItems("A", "B", "C", "D", "E"));
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, MakeItems("X", "Y"));
|
||||
|
||||
AssertSequence(original, "X", "Y");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_NoOverlap_NewLarger()
|
||||
{
|
||||
var original = new ObservableCollection<Item>(MakeItems("A", "B"));
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, MakeItems("X", "Y", "Z", "W"));
|
||||
|
||||
AssertSequence(original, "X", "Y", "Z", "W");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_MixedRemoveAndReorder()
|
||||
{
|
||||
var items = MakeItems("A", "X", "Y", "C", "B");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[0], items[4], items[3] });
|
||||
|
||||
AssertSequence(original, "A", "B", "C");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_MixedAddRemoveReorder()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
var e = new Item("E");
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[2], items[0], e });
|
||||
|
||||
AssertSequence(original, "C", "A", "E");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_IncrementalSearch()
|
||||
{
|
||||
var all = MakeItems("Apple", "Banana", "Avocado", "Blueberry", "Apricot");
|
||||
var original = new ObservableCollection<Item>(all);
|
||||
|
||||
// "A" filter
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { all[0], all[2], all[4] });
|
||||
AssertSequence(original, "Apple", "Avocado", "Apricot");
|
||||
|
||||
// "Ap" filter
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { all[0], all[4] });
|
||||
AssertSequence(original, "Apple", "Apricot");
|
||||
|
||||
// Clear
|
||||
ListHelpers.InPlaceUpdateList(original, all);
|
||||
AssertSequence(original, "Apple", "Banana", "Avocado", "Blueberry", "Apricot");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_PageNavigation()
|
||||
{
|
||||
var page1 = MakeItems("P1A", "P1B", "P1C", "P1D");
|
||||
var page2 = MakeItems("P2A", "P2B", "P2C");
|
||||
var original = new ObservableCollection<Item>(page1);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, page2);
|
||||
AssertSequence(original, "P2A", "P2B", "P2C");
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, page1);
|
||||
AssertSequence(original, "P1A", "P1B", "P1C", "P1D");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_WorksWithPlainList()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D");
|
||||
var original = new List<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { items[2], items[0] });
|
||||
|
||||
AssertSequence(original, "C", "A");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_AcceptsLazyEnumerable()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C");
|
||||
var original = new ObservableCollection<Item>(items);
|
||||
|
||||
IEnumerable<Item> lazy = items.Reverse().Where(_ => true);
|
||||
ListHelpers.InPlaceUpdateList(original, lazy);
|
||||
|
||||
AssertSequence(original, "C", "B", "A");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TwoArg_SingleItemReplace()
|
||||
{
|
||||
var original = new ObservableCollection<Item> { new Item("A") };
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original, new[] { new Item("B") });
|
||||
|
||||
AssertSequence(original, "B");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BothOverloads_ProduceSameResult_FilterAndReorder()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E", "F");
|
||||
var newList = new[] { items[4], items[2], items[0], new Item("G") }; // E, C, A, G
|
||||
|
||||
var original1 = new ObservableCollection<Item>(items);
|
||||
var original2 = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original1, newList);
|
||||
ListHelpers.InPlaceUpdateList(original2, newList, out _);
|
||||
|
||||
var names1 = original1.Select(i => i.Name).ToArray();
|
||||
var names2 = original2.Select(i => i.Name).ToArray();
|
||||
CollectionAssert.AreEqual(names1, names2, "Both overloads should produce identical results");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BothOverloads_ProduceSameResult_CompleteReversal()
|
||||
{
|
||||
var items = MakeItems("A", "B", "C", "D", "E");
|
||||
var reversed = items.Reverse().ToArray();
|
||||
|
||||
var original1 = new ObservableCollection<Item>(items);
|
||||
var original2 = new ObservableCollection<Item>(items);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original1, reversed);
|
||||
ListHelpers.InPlaceUpdateList(original2, reversed, out _);
|
||||
|
||||
var names1 = original1.Select(i => i.Name).ToArray();
|
||||
var names2 = original2.Select(i => i.Name).ToArray();
|
||||
CollectionAssert.AreEqual(names1, names2, "Both overloads should produce identical results");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BothOverloads_ProduceSameResult_NoOverlap()
|
||||
{
|
||||
var oldItems = MakeItems("A", "B", "C", "D");
|
||||
var newItems = MakeItems("W", "X", "Y");
|
||||
|
||||
var original1 = new ObservableCollection<Item>(oldItems);
|
||||
var original2 = new ObservableCollection<Item>(oldItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(original1, newItems);
|
||||
ListHelpers.InPlaceUpdateList(original2, newItems, out _);
|
||||
|
||||
var names1 = original1.Select(i => i.Name).ToArray();
|
||||
var names2 = original2.Select(i => i.Name).ToArray();
|
||||
CollectionAssert.AreEqual(names1, names2, "Both overloads should produce identical results");
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||
<VersionMajor>0</VersionMajor>
|
||||
<VersionMinor>8</VersionMinor>
|
||||
<VersionMinor>9</VersionMinor>
|
||||
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Collections.Generic;
|
||||
using Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -39,9 +38,6 @@ public partial class AllAppsCommandProvider : CommandProvider
|
||||
{
|
||||
MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)],
|
||||
};
|
||||
|
||||
// Subscribe to pin state changes to refresh the command provider
|
||||
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
|
||||
}
|
||||
|
||||
public static int TopLevelResultLimit
|
||||
@@ -66,7 +62,7 @@ public partial class AllAppsCommandProvider : CommandProvider
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [_listItem, .. _page.GetPinnedApps()];
|
||||
public override ICommandItem[] TopLevelCommands() => [_listItem];
|
||||
|
||||
public ICommandItem? LookupAppByPackageFamilyName(string packageFamilyName, bool requireSingleMatch)
|
||||
{
|
||||
@@ -197,9 +193,4 @@ public partial class AllAppsCommandProvider : CommandProvider
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnPinStateChanged(object? sender, System.EventArgs e)
|
||||
{
|
||||
RaiseItemsChanged(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -21,9 +20,7 @@ public sealed partial class AllAppsPage : ListPage
|
||||
private readonly Lock _listLock = new();
|
||||
private readonly IAppCache _appCache;
|
||||
|
||||
private AppItem[] allApps = [];
|
||||
private AppListItem[] unpinnedApps = [];
|
||||
private AppListItem[] pinnedApps = [];
|
||||
private AppListItem[] allAppListItems = [];
|
||||
|
||||
public AllAppsPage()
|
||||
: this(AppCache.Instance.Value)
|
||||
@@ -39,9 +36,6 @@ public sealed partial class AllAppsPage : ListPage
|
||||
this.IsLoading = true;
|
||||
this.PlaceholderText = Resources.search_installed_apps_placeholder;
|
||||
|
||||
// Subscribe to pin state changes to refresh the command provider
|
||||
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
lock (_listLock)
|
||||
@@ -51,24 +45,17 @@ public sealed partial class AllAppsPage : ListPage
|
||||
});
|
||||
}
|
||||
|
||||
internal AppListItem[] GetPinnedApps()
|
||||
{
|
||||
BuildListItems();
|
||||
return pinnedApps;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
// Build or update the list if needed
|
||||
BuildListItems();
|
||||
|
||||
AppListItem[] allApps = [.. pinnedApps, .. unpinnedApps];
|
||||
return allApps;
|
||||
return allAppListItems;
|
||||
}
|
||||
|
||||
private void BuildListItems()
|
||||
{
|
||||
if (allApps.Length == 0 || _appCache.ShouldReload())
|
||||
if (allAppListItems.Length == 0 || _appCache.ShouldReload())
|
||||
{
|
||||
lock (_listLock)
|
||||
{
|
||||
@@ -77,10 +64,7 @@ public sealed partial class AllAppsPage : ListPage
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
|
||||
var apps = GetPrograms();
|
||||
this.allApps = apps.AllApps;
|
||||
this.pinnedApps = apps.PinnedItems;
|
||||
this.unpinnedApps = apps.UnpinnedItems;
|
||||
this.allAppListItems = GetPrograms();
|
||||
|
||||
this.IsLoading = false;
|
||||
|
||||
@@ -92,15 +76,15 @@ public sealed partial class AllAppsPage : ListPage
|
||||
}
|
||||
}
|
||||
|
||||
private AppItem[] GetAllApps()
|
||||
private AppListItem[] GetPrograms()
|
||||
{
|
||||
List<AppItem> allApps = new();
|
||||
var items = new List<AppListItem>();
|
||||
|
||||
foreach (var uwpApp in _appCache.UWPs)
|
||||
{
|
||||
if (uwpApp.Enabled)
|
||||
{
|
||||
allApps.Add(uwpApp.ToAppItem());
|
||||
items.Add(new AppListItem(uwpApp.ToAppItem(), true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,101 +92,12 @@ public sealed partial class AllAppsPage : ListPage
|
||||
{
|
||||
if (win32App.Enabled && win32App.Valid)
|
||||
{
|
||||
allApps.Add(win32App.ToAppItem());
|
||||
items.Add(new AppListItem(win32App.ToAppItem(), true));
|
||||
}
|
||||
}
|
||||
|
||||
return [.. allApps];
|
||||
}
|
||||
items.Sort((a, b) => string.Compare(a.Title, b.Title, StringComparison.Ordinal));
|
||||
|
||||
internal (AppItem[] AllApps, AppListItem[] PinnedItems, AppListItem[] UnpinnedItems) GetPrograms()
|
||||
{
|
||||
var allApps = GetAllApps();
|
||||
var pinned = new List<AppListItem>();
|
||||
var unpinned = new List<AppListItem>();
|
||||
|
||||
foreach (var app in allApps)
|
||||
{
|
||||
var isPinned = PinnedAppsManager.Instance.IsAppPinned(app.AppIdentifier);
|
||||
var appListItem = new AppListItem(app, true, isPinned);
|
||||
|
||||
if (isPinned)
|
||||
{
|
||||
appListItem.Tags = [.. appListItem.Tags, new Tag() { Icon = Icons.PinIcon }];
|
||||
pinned.Add(appListItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
unpinned.Add(appListItem);
|
||||
}
|
||||
}
|
||||
|
||||
pinned.Sort((a, b) => string.Compare(a.Title, b.Title, StringComparison.Ordinal));
|
||||
unpinned.Sort((a, b) => string.Compare(a.Title, b.Title, StringComparison.Ordinal));
|
||||
|
||||
return (
|
||||
allApps,
|
||||
pinned.ToArray(),
|
||||
unpinned.ToArray()
|
||||
);
|
||||
}
|
||||
|
||||
private void OnPinStateChanged(object? sender, PinStateChangedEventArgs e)
|
||||
{
|
||||
/*
|
||||
* Rebuilding all the lists is pretty expensive.
|
||||
* So, instead, we'll just compare pinned items to move existing
|
||||
* items between the two lists.
|
||||
*/
|
||||
AppItem? existingAppItem = null;
|
||||
|
||||
foreach (var app in allApps)
|
||||
{
|
||||
if (app.AppIdentifier == e.AppIdentifier)
|
||||
{
|
||||
existingAppItem = app;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingAppItem is not null)
|
||||
{
|
||||
var appListItem = new AppListItem(existingAppItem, true, e.IsPinned);
|
||||
|
||||
var newPinned = new List<AppListItem>(pinnedApps);
|
||||
var newUnpinned = new List<AppListItem>(unpinnedApps);
|
||||
|
||||
if (e.IsPinned)
|
||||
{
|
||||
newPinned.Add(appListItem);
|
||||
|
||||
foreach (var app in newUnpinned)
|
||||
{
|
||||
if (app.AppIdentifier == e.AppIdentifier)
|
||||
{
|
||||
newUnpinned.Remove(app);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
newUnpinned.Add(appListItem);
|
||||
|
||||
foreach (var app in newPinned)
|
||||
{
|
||||
if (app.AppIdentifier == e.AppIdentifier)
|
||||
{
|
||||
newPinned.Remove(app);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pinnedApps = newPinned.ToArray();
|
||||
unpinnedApps = newUnpinned.ToArray();
|
||||
}
|
||||
|
||||
RaiseItemsChanged(0);
|
||||
return [.. items];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
|
||||
|
||||
public AppItem App => _app;
|
||||
|
||||
public AppListItem(AppItem app, bool useThumbnails, bool isPinned)
|
||||
public AppListItem(AppItem app, bool useThumbnails)
|
||||
{
|
||||
Command = _appCommand = new AppCommand(app);
|
||||
_app = app;
|
||||
@@ -95,7 +95,7 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
|
||||
Subtitle = app.Subtitle;
|
||||
Icon = Icons.GenericAppIcon;
|
||||
|
||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||
MoreCommands = _app.Commands?.ToArray() ?? [];
|
||||
|
||||
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails).ConfigureAwait(false));
|
||||
@@ -237,35 +237,6 @@ public sealed partial class AppListItem : ListItem, IPrecomputedListItem
|
||||
return icon;
|
||||
}
|
||||
|
||||
private IContextItem[] AddPinCommands(List<IContextItem> commands, bool isPinned)
|
||||
{
|
||||
var newCommands = new List<IContextItem>();
|
||||
newCommands.AddRange(commands);
|
||||
|
||||
newCommands.Add(new Separator());
|
||||
|
||||
if (isPinned)
|
||||
{
|
||||
newCommands.Add(
|
||||
new CommandContextItem(
|
||||
new UnpinAppCommand(this.AppIdentifier))
|
||||
{
|
||||
RequestedShortcut = KeyChords.TogglePin,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
newCommands.Add(
|
||||
new CommandContextItem(
|
||||
new PinAppCommand(this.AppIdentifier))
|
||||
{
|
||||
RequestedShortcut = KeyChords.TogglePin,
|
||||
});
|
||||
}
|
||||
|
||||
return newCommands.ToArray();
|
||||
}
|
||||
|
||||
private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
|
||||
{
|
||||
return await Task.Run(async () =>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Commands;
|
||||
|
||||
internal sealed partial class PinAppCommand : InvokableCommand
|
||||
{
|
||||
private readonly string _appIdentifier;
|
||||
|
||||
public PinAppCommand(string appIdentifier)
|
||||
{
|
||||
_appIdentifier = appIdentifier;
|
||||
Name = Resources.pin_app;
|
||||
Icon = Icons.PinIcon;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
PinnedAppsManager.Instance.PinApp(_appIdentifier);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Commands;
|
||||
|
||||
internal sealed partial class UnpinAppCommand : InvokableCommand
|
||||
{
|
||||
private readonly string _appIdentifier;
|
||||
|
||||
public UnpinAppCommand(string appIdentifier)
|
||||
{
|
||||
_appIdentifier = appIdentifier;
|
||||
Name = Resources.unpin_app;
|
||||
Icon = Icons.UnpinIcon;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
PinnedAppsManager.Instance.UnpinApp(_appIdentifier);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,10 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.CmdPal.Ext.Apps.State;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(PinnedApps))]
|
||||
[JsonSerializable(typeof(List<string>), TypeInfoPropertyName = "StringList")]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
||||
internal sealed partial class JsonSerializationContext : JsonSerializerContext
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.State;
|
||||
|
||||
public sealed class PinnedApps
|
||||
{
|
||||
public List<string> PinnedAppIdentifiers { get; set; } = [];
|
||||
|
||||
public static PinnedApps ReadFromFile(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return new PinnedApps();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jsonString = File.ReadAllText(path);
|
||||
var result = JsonSerializer.Deserialize<PinnedApps>(jsonString, JsonSerializationContext.Default.PinnedApps);
|
||||
return result ?? new PinnedApps();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new PinnedApps();
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteToFile(string path, PinnedApps data)
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonString = JsonSerializer.Serialize(data, JsonSerializationContext.Default.PinnedApps);
|
||||
File.WriteAllText(path, jsonString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail - we don't want pinning issues to crash the extension
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.State;
|
||||
|
||||
public sealed class PinnedAppsManager
|
||||
{
|
||||
private static readonly Lazy<PinnedAppsManager> _instance = new(() => new PinnedAppsManager());
|
||||
private readonly string _pinnedAppsFilePath;
|
||||
|
||||
public static PinnedAppsManager Instance => _instance.Value;
|
||||
|
||||
private PinnedApps _pinnedApps = new();
|
||||
|
||||
// Add event for when pinning state changes
|
||||
public event EventHandler<PinStateChangedEventArgs>? PinStateChanged;
|
||||
|
||||
private PinnedAppsManager()
|
||||
{
|
||||
_pinnedAppsFilePath = GetPinnedAppsFilePath();
|
||||
LoadPinnedApps();
|
||||
}
|
||||
|
||||
public bool IsAppPinned(string appIdentifier)
|
||||
{
|
||||
return _pinnedApps.PinnedAppIdentifiers.IndexOf(appIdentifier) >= 0;
|
||||
}
|
||||
|
||||
public void PinApp(string appIdentifier)
|
||||
{
|
||||
if (!IsAppPinned(appIdentifier))
|
||||
{
|
||||
_pinnedApps.PinnedAppIdentifiers.Add(appIdentifier);
|
||||
SavePinnedApps();
|
||||
Logger.LogTrace($"Pinned app: {appIdentifier}");
|
||||
PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, true));
|
||||
}
|
||||
}
|
||||
|
||||
public string[] GetPinnedAppIdentifiers()
|
||||
{
|
||||
return _pinnedApps.PinnedAppIdentifiers.ToArray();
|
||||
}
|
||||
|
||||
public void UnpinApp(string appIdentifier)
|
||||
{
|
||||
var removed = _pinnedApps.PinnedAppIdentifiers.RemoveAll(id =>
|
||||
string.Equals(id, appIdentifier, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (removed > 0)
|
||||
{
|
||||
SavePinnedApps();
|
||||
Logger.LogTrace($"Unpinned app: {appIdentifier}");
|
||||
PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, false));
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPinnedApps()
|
||||
{
|
||||
_pinnedApps = PinnedApps.ReadFromFile(_pinnedAppsFilePath);
|
||||
}
|
||||
|
||||
private void SavePinnedApps()
|
||||
{
|
||||
PinnedApps.WriteToFile(_pinnedAppsFilePath, _pinnedApps);
|
||||
}
|
||||
|
||||
private static string GetPinnedAppsFilePath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
return Path.Combine(directory, "apps.pinned.json");
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,12 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<CsWinRTInputs Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.winmd" />
|
||||
<Content Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.winmd" Link="CalculatorEngineCommon.winmd">
|
||||
<None Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.winmd" Link="CalculatorEngineCommon.winmd">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.dll">
|
||||
</None>
|
||||
<None Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -16,7 +16,8 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
|
||||
public ClipboardHistoryCommandsProvider()
|
||||
{
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
|
||||
var page = new ClipboardHistoryListPage(_settingsManager);
|
||||
_clipboardHistoryListItem = new ListItem(page)
|
||||
{
|
||||
Title = Properties.Resources.list_item_title,
|
||||
Icon = Icons.ClipboardListIcon,
|
||||
@@ -24,7 +25,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
new CommandContextItem(_settingsManager.Settings.SettingsPage),
|
||||
],
|
||||
};
|
||||
|
||||
DisplayName = Properties.Resources.provider_display_name;
|
||||
Icon = Icons.ClipboardListIcon;
|
||||
Id = "Windows.ClipboardHistory";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user