CmdPal: Add a dock (#45824)

Add support for a "dock" window in CmdPal. The dock is a toolbar powered
by the `APPBAR` APIs. This gives you a persistent region to display
commands for quick shortcuts or glanceable widgets.

The dock can be pinned to any side of the screen.
The dock can be independently styled with any of the theming controls
cmdpal already has
The dock has three "regions" to pin to - the "start", the "center", and
the "end".
Elements on the dock are grouped as "bands", which contains a set of
"items". Each "band" is one atomic unit. For example, the Media Player
extension produces 4 items, but one _band_.
The dock has only one size (for now)
The dock will only appear on your primary display (for now)

This PR includes support for pinning arbitrary top-level commands to the
dock - however, we're planning on replacing that with a more universal
ability to pin any command to the dock or top level. (see #45191). This
is at least usable for now.

This is definitely still _even more preview_ than usual PowerToys
features, but it's more than usable. I'd love to get it out there and
start collecting feedback on where to improve next. I'll probably add a
follow-up issue for tracking the remaining bugs & nits.

closes #45201

---------

Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Mike Griese
2026-02-27 07:24:23 -06:00
committed by GitHub
parent 494c14fb88
commit 70bf430d9f
90 changed files with 7148 additions and 193 deletions

View File

@@ -315,6 +315,7 @@ xef
xes xes
PACKAGEVERSIONNUMBER PACKAGEVERSIONNUMBER
APPXMANIFESTVERSION APPXMANIFESTVERSION
PROGMAN
# MRU lists # MRU lists
CACHEWRITE CACHEWRITE
@@ -325,6 +326,14 @@ REGSTR
# Misc Win32 APIs and PInvokes # Misc Win32 APIs and PInvokes
INVOKEIDLIST INVOKEIDLIST
MEMORYSTATUSEX MEMORYSTATUSEX
ABE
HTCAPTION
POSCHANGED
QUERYPOS
SETAUTOHIDEBAR
WINDOWPOS
WINEVENTPROC
WORKERW
# PowerRename metadata pattern abbreviations (used in tests and regex patterns) # PowerRename metadata pattern abbreviations (used in tests and regex patterns)
DDDD DDDD
@@ -349,3 +358,6 @@ nostdin
# Performance counter keys # Performance counter keys
engtype engtype
Nonpaged Nonpaged
# XAML
Untargeted

View File

@@ -26,6 +26,11 @@
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd", "input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
"name": "Update template project", "name": "Update template project",
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory." "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."
} }
] ]
} }

View File

@@ -2,8 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.CmdPal.Common; namespace Microsoft.CmdPal.Common;
public static class CoreLogger public static class CoreLogger
@@ -15,6 +13,8 @@ public static class CoreLogger
private static ILogger? _logger; 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) 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); _logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);

View File

@@ -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;
}
}

View File

@@ -72,5 +72,14 @@ namespace Microsoft.CmdPal.Common.Properties {
return ResourceManager.GetString("ErrorReport_Global_Preamble", resourceCulture); 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);
}
}
} }
} }

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
@@ -117,6 +117,10 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </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"> <data name="ErrorReport_Global_Preamble" xml:space="preserve">
<value>This is an error report generated by Windows Command Palette. <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. 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 youre at it, give the details below a quick skim — just to make sure theres nothing personal youd prefer not to share. Its rare, but sometimes little surprises sneak in.)</value> (While youre at it, give the details below a quick skim — just to make sure theres nothing personal youd prefer not to share. Its rare, but sometimes little surprises sneak in.)</value>
</data> </data>
</root> </root>

View File

@@ -18,9 +18,9 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable 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 // row 0
Color.FromArgb(255, 255, 185, 0), // #ffb900 Color.FromArgb(255, 255, 185, 0), // #ffb900

View File

@@ -96,9 +96,10 @@ public partial class CommandBarViewModel : ObservableObject,
SecondaryCommand = SelectedItem.SecondaryCommand; SecondaryCommand = SelectedItem.SecondaryCommand;
ShouldShowContextMenu = SelectedItem.MoreCommands var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
.OfType<CommandContextItemViewModel>() var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
.Count() > 1;
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
OnPropertyChanged(nameof(HasSecondaryCommand)); OnPropertyChanged(nameof(HasSecondaryCommand));
OnPropertyChanged(nameof(SecondaryCommand)); OnPropertyChanged(nameof(SecondaryCommand));

View File

@@ -51,9 +51,11 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
private string _itemTitle = string.Empty; private string _itemTitle = string.Empty;
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle; protected string ItemTitle => _itemTitle;
public string Subtitle { get; private set; } = string.Empty; public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
public virtual string Subtitle { get; private set; } = string.Empty;
private IconInfoViewModel _icon = new(null); private IconInfoViewModel _icon = new(null);
@@ -73,10 +75,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public CommandItemViewModel? PrimaryCommand => this; 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 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 DataPackageView? DataPackage { get; private set; }
public List<IContextItemViewModel> AllCommands public List<IContextItemViewModel> AllCommands
@@ -331,11 +353,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(Name)); UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title)); UpdateProperty(nameof(Title));
UpdateProperty(nameof(Icon)); UpdateProperty(nameof(Icon));
UpdateProperty(nameof(HasText));
break; break;
case nameof(Title): case nameof(Title):
_itemTitle = model.Title; _itemTitle = model.Title;
_titleCache.Invalidate(); _titleCache.Invalidate();
UpdateProperty(nameof(HasText));
break; break;
case nameof(Subtitle): case nameof(Subtitle):
@@ -343,6 +367,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
this.Subtitle = modelSubtitle; this.Subtitle = modelSubtitle;
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle; _defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
_subtitleCache.Invalidate(); _subtitleCache.Invalidate();
UpdateProperty(nameof(HasText));
break; break;
case nameof(Icon): 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 // Command icon takes precedence over our icon on the primary command
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon); _defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
}
private void UpdateTitle(string? title) private void UpdateTitle(string? title)
{ {

View File

@@ -22,7 +22,7 @@ public class CommandPalettePageViewModelFactory
{ {
return page switch 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), IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host, providerContext),
_ => null, _ => null,
}; };

View File

@@ -6,6 +6,7 @@ using ManagedCommon;
using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -29,6 +30,8 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
public TopLevelViewModel[] FallbackItems { get; private set; } = []; public TopLevelViewModel[] FallbackItems { get; private set; } = [];
public TopLevelViewModel[] DockBandItems { get; private set; } = [];
public string DisplayName { get; private set; } = string.Empty; public string DisplayName { get; private set; } = string.Empty;
public IExtensionWrapper? Extension { get; } public IExtensionWrapper? Extension { get; }
@@ -57,7 +60,7 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// calls are pretty dang safe actually. // calls are pretty dang safe actually.
_commandProvider = new(provider); _commandProvider = new(provider);
_taskScheduler = mainThread; _taskScheduler = mainThread;
TopLevelPageContext = new TopLevelItemPageContext(this, _taskScheduler); TopLevelPageContext = new(this, _taskScheduler);
// Hook the extension back into us // Hook the extension back into us
ExtensionHost = new CommandPaletteHost(provider); ExtensionHost = new CommandPaletteHost(provider);
@@ -82,7 +85,7 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
{ {
_taskScheduler = mainThread; _taskScheduler = mainThread;
_commandProviderCache = commandProviderCache; _commandProviderCache = commandProviderCache;
TopLevelPageContext = new TopLevelItemPageContext(this, _taskScheduler); TopLevelPageContext = new(this, _taskScheduler);
Extension = extension; Extension = extension;
ExtensionHost = new CommandPaletteHost(extension); ExtensionHost = new CommandPaletteHost(extension);
@@ -146,26 +149,42 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
return; return;
} }
ICommandItem[]? commands = null;
IFallbackCommandItem[]? fallbacks = null;
ICommandItem[] dockBands = []; // do not initialize me to null
var displayInfoInitialized = false; var displayInfoInitialized = false;
try try
{ {
var model = _commandProvider.Unsafe!; var model = _commandProvider.Unsafe!;
Task<ICommandItem[]> loadTopLevelCommandsTask = new(model.TopLevelCommands); Task<ICommandItem[]> loadTopLevelCommandsTask = new(model.TopLevelCommands);
loadTopLevelCommandsTask.Start(); loadTopLevelCommandsTask.Start();
var commands = await loadTopLevelCommandsTask.ConfigureAwait(false); commands = await loadTopLevelCommandsTask.ConfigureAwait(false);
// On a BG thread here // On a BG thread here
var fallbacks = model.FallbackCommands(); fallbacks = model.FallbackCommands();
if (model is ICommandProvider2 two) if (model is ICommandProvider2 two)
{ {
UnsafePreCacheApiAdditions(two); UnsafePreCacheApiAdditions(two);
} }
ICommandItem[] pinnedCommands = []; if (model is ICommandProvider3 supportsDockBands)
if (model is ICommandProvider4 four)
{ {
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 definitelyFour)
{
four = definitelyFour; // stash this away so we don't need to QI again
SupportsPinning = true; SupportsPinning = true;
// Load pinned commands from saved settings // Load pinned commands from saved settings
@@ -189,7 +208,8 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
Settings = new(model.Settings, this, _taskScheduler); Settings = new(model.Settings, this, _taskScheduler);
// We do need to explicitly initialize commands though // We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, pinnedCommands, serviceProvider); var objects = new TopLevelObjects(commands, fallbacks, pinnedCommands, dockBands);
InitializeCommands(objects, serviceProvider, four);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})"); Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
} }
@@ -220,21 +240,27 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
} }
} }
private record TopLevelObjects(
ICommandItem[]? Commands,
IFallbackCommandItem[]? Fallbacks,
ICommandItem[]? PinnedCommands,
ICommandItem[]? DockBands);
private void InitializeCommands( private void InitializeCommands(
ICommandItem[] commands, TopLevelObjects objects,
IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider,
ICommandItem[] pinnedCommands, ICommandProvider4? four)
IServiceProvider serviceProvider)
{ {
var settings = serviceProvider.GetService<SettingsModel>()!; var settings = serviceProvider.GetService<SettingsModel>()!;
var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!; var contextMenuFactory = serviceProvider.GetService<IContextMenuFactory>()!;
var state = serviceProvider.GetService<AppStateModel>()!;
var providerSettings = GetProviderSettings(settings); var providerSettings = GetProviderSettings(settings);
var ourContext = GetProviderContext(); var ourContext = GetProviderContext();
var pageContext = new WeakReference<IPageContext>(TopLevelPageContext); WeakReference<IPageContext> pageContext = new(this.TopLevelPageContext);
var makeAndAdd = (ICommandItem? i, bool fallback) => var make = (ICommandItem? i, TopLevelType t) =>
{ {
CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory); CommandItemViewModel commandItemViewModel = new(new(i), pageContext, contextMenuFactory: contextMenuFactory);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i); TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ourContext, settings, providerSettings, serviceProvider, i, contextMenuFactory: contextMenuFactory);
topLevelViewModel.InitializeProperties(); topLevelViewModel.InitializeProperties();
return topLevelViewModel; return topLevelViewModel;
@@ -242,24 +268,103 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
var topLevelList = new List<TopLevelViewModel>(); 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(); TopLevelItems = topLevelList.ToArray();
if (fallbacks is not null) if (objects.Fallbacks is not null)
{ {
FallbackItems = fallbacks FallbackItems = objects.Fallbacks
.Select(c => makeAndAdd(c, true)) .Select(c => make(c, TopLevelType.Fallback))
.ToArray(); .ToArray();
} }
List<TopLevelViewModel> bands = new();
if (objects.DockBands is not null)
{
// 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 = four.GetCommandItem(commandId);
if (commandItem is not null)
{
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 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) private ICommandItem[] LoadPinnedCommands(ICommandProvider4 model, ProviderSettings providerSettings)
@@ -295,6 +400,10 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
{ {
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider"); Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
} }
else if (a is ICommandItem[] commands)
{
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
}
} }
} }
@@ -306,10 +415,10 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
if (!providerSettings.PinnedCommandIds.Contains(commandId)) if (!providerSettings.PinnedCommandIds.Contains(commandId))
{ {
providerSettings.PinnedCommandIds.Add(commandId); providerSettings.PinnedCommandIds.Add(commandId);
SettingsModel.SaveSettings(settings);
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands // Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1)); this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
SettingsModel.SaveSettings(settings, false);
} }
} }
@@ -320,13 +429,41 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
if (providerSettings.PinnedCommandIds.Remove(commandId)) if (providerSettings.PinnedCommandIds.Remove(commandId))
{ {
SettingsModel.SaveSettings(settings);
// Raise CommandsChanged so the TopLevelCommandManager reloads our commands // Raise CommandsChanged so the TopLevelCommandManager reloads our commands
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1)); this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs(-1));
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 ICommandProviderContext GetProviderContext() => this;
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid; public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
@@ -342,4 +479,14 @@ public sealed class CommandProviderWrapper : ICommandProviderContext
// In handling this, a call will be made to `LoadTopLevelCommands` to // In handling this, a call will be made to `LoadTopLevelCommands` to
// retrieve the new items. // retrieve the new items.
this.CommandsChanged?.Invoke(this, args); 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());
}
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
@@ -18,6 +18,8 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
private readonly FallbackLogItem _fallbackLogItem = new(); private readonly FallbackLogItem _fallbackLogItem = new();
private readonly NewExtensionPage _newExtension = new(); private readonly NewExtensionPage _newExtension = new();
private readonly IRootPageService _rootPageService;
public override ICommandItem[] TopLevelCommands() => public override ICommandItem[] TopLevelCommands() =>
[ [
new CommandItem(openSettings) { }, new CommandItem(openSettings) { },
@@ -37,11 +39,22 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
_fallbackLogItem, _fallbackLogItem,
]; ];
public BuiltInsCommandProvider() public BuiltInsCommandProvider(IRootPageService rootPageService)
{ {
Id = "com.microsoft.cmdpal.builtin.core"; Id = "com.microsoft.cmdpal.builtin.core";
DisplayName = Properties.Resources.builtin_display_name; 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); public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);

View File

@@ -65,6 +65,7 @@ public sealed partial class MainListPage : DynamicListPage,
AppStateModel appStateModel, AppStateModel appStateModel,
IFuzzyMatcherProvider fuzzyMatcherProvider) IFuzzyMatcherProvider fuzzyMatcherProvider)
{ {
Id = "com.microsoft.cmdpal.home";
Title = Resources.builtin_home_name; Title = Resources.builtin_home_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder; PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
@@ -70,6 +70,15 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
StateJson = model.StateJson; StateJson = model.StateJson;
DataJson = model.DataJson; DataJson = model.DataJson;
RenderCard();
UpdateProperty(nameof(Card));
model.PropChanged += Model_PropChanged;
}
private void RenderCard()
{
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError)) if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
{ {
Card = builtCard; Card = builtCard;
@@ -93,8 +102,41 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
UpdateProperty(nameof(Card)); UpdateProperty(nameof(Card));
return; 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))] [DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]

View File

@@ -59,11 +59,8 @@ public partial class ContextMenuViewModel : ObservableObject,
{ {
if (SelectedItem is not null) if (SelectedItem is not null)
{ {
if (SelectedItem.PrimaryCommand is not null || SelectedItem.HasMoreCommands) ContextMenuStack.Clear();
{ PushContextStack(SelectedItem.AllCommands);
ContextMenuStack.Clear();
PushContextStack(SelectedItem.AllCommands);
}
} }
} }

View File

@@ -40,4 +40,12 @@ public partial class DefaultContextMenuFactory : IContextMenuFactory
return results; return results;
} }
public void AddMoreCommandsToTopLevel(
TopLevelViewModel topLevelItem,
ICommandProviderContext providerContext,
List<IContextItem?> contextItems)
{
// do nothing
}
} }

View File

@@ -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 bandVm.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,
}

View File

@@ -0,0 +1,300 @@
// 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;
}
UpdateProperty(nameof(ShowTitles));
}
}
}
/// <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;
}
UpdateProperty(nameof(ShowSubtitles));
}
}
}
/// <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
{
var dockItem = new DockItemViewModel(_rootItem, _showTitles, _showSubtitles, _contextMenuFactory);
dockItem.SlowInitializeProperties();
DoOnUiThread(() =>
{
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 override 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

View File

@@ -0,0 +1,633 @@
// 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;
DoOnUiThread(() =>
{
List<DockBandViewModel> removed = new();
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");
// then, back to a BG thread:
Task.Run(() =>
{
if (removed is not null)
{
foreach (var removedItem in removed)
{
removedItem.SafeCleanup();
}
}
});
});
// Initialize properties on BG thread
Task.Run(() =>
{
foreach (var band in newBands)
{
band.SafeInitializePropertiesSynchronous();
}
});
}
/// <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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -59,7 +59,7 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject, IBatc
LogIfDefaultScheduler(); LogIfDefaultScheduler();
} }
private protected ExtensionObjectViewModel(WeakReference<IPageContext> contextRef) protected ExtensionObjectViewModel(WeakReference<IPageContext> contextRef)
{ {
ArgumentNullException.ThrowIfNull(contextRef); ArgumentNullException.ThrowIfNull(contextRef);

View File

@@ -9,4 +9,9 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public interface IContextMenuFactory public interface IContextMenuFactory
{ {
List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(IContextItem[] items, CommandItemViewModel commandItem); List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(IContextItem[] items, CommandItemViewModel commandItem);
void AddMoreCommandsToTopLevel(
TopLevelViewModel topLevelItem,
ICommandProviderContext providerContext,
List<IContextItem?> contextItems);
} }

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels; namespace Microsoft.CmdPal.UI.ViewModels;
public sealed partial class ItemsUpdatedEventArgs : EventArgs public sealed partial class ItemsUpdatedEventArgs : EventArgs
{ {

View File

@@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common; using Microsoft.CmdPal.Common;
using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
@@ -371,7 +370,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
UpdateEmptyContent(); UpdateEmptyContent();
} }
ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(!IsNested)); ItemsUpdated?.Invoke(this, new ItemsUpdatedEventArgs(IsRootPage));
_isLoading.Clear(); _isLoading.Clear();
}); });
} }

View File

@@ -12,6 +12,7 @@ public partial class LoadingPageViewModel : PageViewModel
: base(model, scheduler, host, CommandProviderContext.Empty) : base(model, scheduler, host, CommandProviderContext.Empty)
{ {
ModelIsLoading = true; ModelIsLoading = true;
HasBackButton = false;
IsInitialized = false; IsInitialized = false;
} }
} }

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record EnterDockEditModeMessage();

View File

@@ -4,4 +4,4 @@
namespace Microsoft.CmdPal.UI.ViewModels.Messages; 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);

View File

@@ -18,6 +18,8 @@ public record PerformCommandMessage
public bool WithAnimation { get; set; } = true; public bool WithAnimation { get; set; } = true;
public bool TransientPage { get; set; }
public PerformCommandMessage(ExtensionObject<ICommand> command) public PerformCommandMessage(ExtensionObject<ICommand> command)
{ {
Command = command; Command = command;

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record PinToDockMessage(string ProviderId, string CommandId, bool Pin);

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record ShowHideDockMessage(bool ShowDock);

View File

@@ -4,6 +4,4 @@
namespace Microsoft.CmdPal.UI.ViewModels.Messages; namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record UnpinCommandItemMessage(string ProviderId, string CommandId) public record UnpinCommandItemMessage(string ProviderId, string CommandId);
{
}

View File

@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.ViewModels.Messages;
public sealed record WindowHiddenMessage();

View File

@@ -4,5 +4,11 @@
namespace Microsoft.CmdPal.UI.ViewModels; namespace Microsoft.CmdPal.UI.ViewModels;
internal sealed partial class NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost) internal sealed partial class NullPageViewModel : PageViewModel
: PageViewModel(null, scheduler, extensionHost, CommandProviderContext.Empty); {
internal NullPageViewModel(TaskScheduler scheduler, AppExtensionHost extensionHost)
: base(null, scheduler, extensionHost, CommandProviderContext.Empty)
{
HasBackButton = false;
}
}

View File

@@ -26,8 +26,25 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
[ObservableProperty] [ObservableProperty]
public partial string ErrorMessage { get; protected set; } = string.Empty; public partial string ErrorMessage { get; protected set; } = string.Empty;
/// <summary>
/// Explicitly: is this page, the VM for the root page. This is used
/// slightly differently than being "nested". When we open CmdPal as a
/// transient window, we want that page to not have a back button, but that
/// page is _not_ the root page.
///
/// Later in ListViewModel, we will have logic that checks if it is the root
/// page, and modify how selection is handled when the list changes.
/// </summary>
[ObservableProperty] [ObservableProperty]
public partial bool IsNested { get; set; } = true; public partial bool IsRootPage { get; set; } = true;
/// <summary>
/// This is used to determine whether to show the back button on this page.
/// When a nested page is opened for the transient "dock flyout" window,
/// then we don't want to show the back button.
/// </summary>
[ObservableProperty]
public partial bool HasBackButton { get; set; } = true;
// This is set from the SearchBar // This is set from the SearchBar
[ObservableProperty] [ObservableProperty]

View File

@@ -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> /// <summary>
/// Looks up a localized string similar to Create another. /// Looks up a localized string similar to Create another.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to {0}, {1} commands. /// Looks up a localized string similar to {0}, {1} commands.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to Fallbacks. /// Looks up a localized string similar to Fallbacks.
/// </summary> /// </summary>
@@ -475,6 +529,14 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
} }
/// <summary> /// <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. /// Looks up a localized string similar to Results.
/// </summary> /// </summary>
public static string results { public static string results {

View File

@@ -254,16 +254,44 @@
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve"> <data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
<value>{0} extensions installed</value> <value>{0} extensions installed</value>
</data> </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"> <data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
<value>Pick background image</value> <value>Pick background image</value>
</data> </data>
<data name="fallbacks" xml:space="preserve"> <data name="fallbacks" xml:space="preserve">
<value>Fallbacks</value> <value>Fallbacks</value>
</data> </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"> <data name="ShowDetailsCommand" xml:space="preserve">
<value>Show details</value> <value>Show details</value>
<comment>Name for the command that shows details of an item</comment> <comment>Name for the command that shows details of an item</comment>
</data> </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"> <data name="results" xml:space="preserve">
<value>Results</value> <value>Results</value>
<comment>Section title for list of all search results that doesn't fall into any other category</comment> <comment>Section title for list of all search results that doesn't fall into any other category</comment>

View File

@@ -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; }
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels.Settings;
namespace Microsoft.CmdPal.UI.ViewModels.Services; namespace Microsoft.CmdPal.UI.ViewModels.Services;
/// <summary> /// <summary>
@@ -36,4 +38,9 @@ public interface IThemeService
/// Gets the current theme settings. /// Gets the current theme settings.
/// </summary> /// </summary>
ThemeSnapshot Current { get; } ThemeSnapshot Current { get; }
/// <summary>
/// Gets the current dock theme settings.
/// </summary>
DockThemeSnapshot CurrentDockTheme { get; }
} }

View File

@@ -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

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
@@ -68,6 +68,11 @@ public partial class SettingsModel : ObservableObject
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack; 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 UserTheme Theme { get; set; } = UserTheme.Default;
public ColorizationMode ColorizationMode { get; set; } public ColorizationMode ColorizationMode { get; set; }
@@ -92,6 +97,8 @@ public partial class SettingsModel : ObservableObject
public int BackdropOpacity { get; set; } = 100; public int BackdropOpacity { get; set; } = 100;
// </Theme settings>
// END SETTINGS // END SETTINGS
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@@ -230,7 +237,7 @@ public partial class SettingsModel : ObservableObject
return false; return false;
} }
public static void SaveSettings(SettingsModel model) public static void SaveSettings(SettingsModel model, bool hotReload = true)
{ {
if (string.IsNullOrEmpty(FilePath)) if (string.IsNullOrEmpty(FilePath))
{ {
@@ -265,7 +272,10 @@ public partial class SettingsModel : ObservableObject
// TODO: Instead of just raising the event here, we should // TODO: Instead of just raising the event here, we should
// have a file change watcher on the settings file, and // have a file change watcher on the settings file, and
// reload the settings then // reload the settings then
model.SettingsChanged?.Invoke(model, null); if (hotReload)
{
model.SettingsChanged?.Invoke(model, null);
}
} }
else else
{ {
@@ -311,6 +321,7 @@ public partial class SettingsModel : ObservableObject
[JsonSerializable(typeof(int))] [JsonSerializable(typeof(int))]
[JsonSerializable(typeof(string))] [JsonSerializable(typeof(string))]
[JsonSerializable(typeof(bool))] [JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(Color))]
[JsonSerializable(typeof(HistoryItem))] [JsonSerializable(typeof(HistoryItem))]
[JsonSerializable(typeof(SettingsModel))] [JsonSerializable(typeof(SettingsModel))]
[JsonSerializable(typeof(WindowPosition))] [JsonSerializable(typeof(WindowPosition))]

View File

@@ -4,6 +4,8 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -32,6 +34,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public AppearanceSettingsViewModel Appearance { get; } public AppearanceSettingsViewModel Appearance { get; }
public DockAppearanceSettingsViewModel DockAppearance { get; }
public HotkeySettings? Hotkey public HotkeySettings? Hotkey
{ {
get => _settings.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<ProviderSettingsViewModel> CommandProviders { get; } = new();
public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new(); public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();
@@ -195,6 +251,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
_topLevelCommandManager = topLevelCommandManager; _topLevelCommandManager = topLevelCommandManager;
Appearance = new AppearanceSettingsViewModel(themeService, _settings); Appearance = new AppearanceSettingsViewModel(themeService, _settings);
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
var activeProviders = GetCommandProviders(); var activeProviders = GetCommandProviders();
var allProviderSettings = _settings.ProviderSettings; var allProviderSettings = _settings.ProviderSettings;

View File

@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.Common; using Microsoft.CmdPal.Common;
using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels; namespace Microsoft.CmdPal.UI.ViewModels;
@@ -16,7 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class ShellViewModel : ObservableObject, public partial class ShellViewModel : ObservableObject,
IDisposable, IDisposable,
IRecipient<PerformCommandMessage>, IRecipient<PerformCommandMessage>,
IRecipient<HandleCommandResultMessage> IRecipient<HandleCommandResultMessage>,
IRecipient<WindowHiddenMessage>
{ {
private readonly IRootPageService _rootPageService; private readonly IRootPageService _rootPageService;
private readonly IAppHostService _appHostService; private readonly IAppHostService _appHostService;
@@ -79,8 +81,9 @@ public partial class ShellViewModel : ObservableObject,
private IPage? _rootPage; private IPage? _rootPage;
private bool _isNested; private bool _isNested;
private bool _currentlyTransient;
public bool IsNested => _isNested; public bool IsNested => _isNested && !_currentlyTransient;
public PageViewModel NullPage { get; private set; } public PageViewModel NullPage { get; private set; }
@@ -101,6 +104,7 @@ public partial class ShellViewModel : ObservableObject,
// Register to receive messages // Register to receive messages
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this); WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this); WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
WeakReferenceMessenger.Default.Register<WindowHiddenMessage>(this);
} }
[RelayCommand] [RelayCommand]
@@ -260,7 +264,7 @@ public partial class ShellViewModel : ObservableObject,
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost); var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
var providerContext = _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext); var providerContext = _appHostService.GetProviderContextForCommand(message.Context, CurrentPage.ProviderContext);
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host); _rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
try try
{ {
@@ -270,6 +274,7 @@ public partial class ShellViewModel : ObservableObject,
var isMainPage = command == _rootPage; var isMainPage = command == _rootPage;
_isNested = !isMainPage; _isNested = !isMainPage;
_currentlyTransient = message.TransientPage;
// Telemetry: Track extension page navigation for session metrics // Telemetry: Track extension page navigation for session metrics
if (host is not null) if (host is not null)
@@ -289,6 +294,9 @@ public partial class ShellViewModel : ObservableObject,
throw new NotSupportedException(); throw new NotSupportedException();
} }
pageViewModel.IsRootPage = isMainPage;
pageViewModel.HasBackButton = IsNested;
// Clear command bar, ViewModel initialization can already set new commands if it wants to // Clear command bar, ViewModel initialization can already set new commands if it wants to
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null))); OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
@@ -308,7 +316,8 @@ public partial class ShellViewModel : ObservableObject,
_scheduler); _scheduler);
// While we're loading in the background, immediately move to the next page. // 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 // 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. // See RootFrame_Navigated event handler.
@@ -479,6 +488,19 @@ public partial class ShellViewModel : ObservableObject,
UnsafeHandleCommandResult(message.Result.Unsafe); 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) private void OnUIThread(Action action)
{ {
_ = Task.Factory.StartNew( _ = Task.Factory.StartNew(

View File

@@ -23,6 +23,7 @@ public partial class TopLevelCommandManager : ObservableObject,
IRecipient<ReloadCommandsMessage>, IRecipient<ReloadCommandsMessage>,
IRecipient<PinCommandItemMessage>, IRecipient<PinCommandItemMessage>,
IRecipient<UnpinCommandItemMessage>, IRecipient<UnpinCommandItemMessage>,
IRecipient<PinToDockMessage>,
IDisposable IDisposable
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
@@ -32,6 +33,11 @@ public partial class TopLevelCommandManager : ObservableObject,
private readonly List<CommandProviderWrapper> _builtInCommands = []; private readonly List<CommandProviderWrapper> _builtInCommands = [];
private readonly List<CommandProviderWrapper> _extensionCommandProviders = []; private readonly List<CommandProviderWrapper> _extensionCommandProviders = [];
private readonly Lock _commandProvidersLock = new(); private readonly Lock _commandProvidersLock = new();
// watch out: if you add code that locks CommandProviders, be sure to always
// lock CommandProviders before locking DockBands, or you will cause a
// deadlock.
private readonly Lock _dockBandsLock = new();
private readonly SupersedingAsyncGate _reloadCommandsGate; private readonly SupersedingAsyncGate _reloadCommandsGate;
public TopLevelCommandManager(IServiceProvider serviceProvider, ICommandProviderCache commandProviderCache) public TopLevelCommandManager(IServiceProvider serviceProvider, ICommandProviderCache commandProviderCache)
@@ -42,11 +48,14 @@ public partial class TopLevelCommandManager : ObservableObject,
WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this); WeakReferenceMessenger.Default.Register<ReloadCommandsMessage>(this);
WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this); WeakReferenceMessenger.Default.Register<PinCommandItemMessage>(this);
WeakReferenceMessenger.Default.Register<UnpinCommandItemMessage>(this); WeakReferenceMessenger.Default.Register<UnpinCommandItemMessage>(this);
WeakReferenceMessenger.Default.Register<PinToDockMessage>(this);
_reloadCommandsGate = new(ReloadAllCommandsAsyncCore); _reloadCommandsGate = new(ReloadAllCommandsAsyncCore);
} }
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = []; public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
[ObservableProperty] [ObservableProperty]
public partial bool IsLoading { get; private set; } = true; public partial bool IsLoading { get; private set; } = true;
@@ -82,12 +91,26 @@ public partial class TopLevelCommandManager : ObservableObject,
_builtInCommands.Add(wrapper); _builtInCommands.Add(wrapper);
} }
var commands = await LoadTopLevelCommandsFromProvider(wrapper); var objects = await LoadTopLevelCommandsFromProvider(wrapper);
lock (TopLevelCommands) 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);
}
} }
} }
} }
@@ -100,7 +123,7 @@ public partial class TopLevelCommandManager : ObservableObject,
} }
// May be called from a background thread // May be called from a background thread
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider) private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{ {
await commandProvider.LoadTopLevelCommands(_serviceProvider); await commandProvider.LoadTopLevelCommands(_serviceProvider);
@@ -108,6 +131,7 @@ public partial class TopLevelCommandManager : ObservableObject,
() => () =>
{ {
List<TopLevelViewModel> commands = []; List<TopLevelViewModel> commands = [];
List<TopLevelViewModel> bands = [];
foreach (var item in commandProvider.TopLevelItems) foreach (var item in commandProvider.TopLevelItems)
{ {
commands.Add(item); commands.Add(item);
@@ -121,7 +145,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, CancellationToken.None,
TaskCreationOptions.None, TaskCreationOptions.None,
@@ -160,6 +192,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 // 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 // TopLevelCommands to get modified while we're working on it. Otherwise, we might
// out clone would be stale at the end of this method. // out clone would be stale at the end of this method.
@@ -178,6 +212,16 @@ public partial class TopLevelCommandManager : ObservableObject,
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone); 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; return;
static int FindIndexForFirstProviderItem(List<TopLevelViewModel> topLevelItems, string providerId) static int FindIndexForFirstProviderItem(List<TopLevelViewModel> topLevelItems, string providerId)
@@ -224,6 +268,11 @@ public partial class TopLevelCommandManager : ObservableObject,
TopLevelCommands.Clear(); TopLevelCommands.Clear();
} }
lock (_dockBandsLock)
{
DockBands.Clear();
}
await LoadBuiltinsAsync(); await LoadBuiltinsAsync();
_ = Task.Run(LoadExtensionsAsync); _ = Task.Run(LoadExtensionsAsync);
} }
@@ -298,13 +347,31 @@ public partial class TopLevelCommandManager : ObservableObject,
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results is not null).Select(r => r!).ToList(); 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);
}
} }
} }
} }
@@ -328,7 +395,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 try
{ {
@@ -354,6 +423,7 @@ public partial class TopLevelCommandManager : ObservableObject,
{ {
// Then find all the top-level commands that belonged to that extension // Then find all the top-level commands that belonged to that extension
List<TopLevelViewModel> commandsToRemove = []; List<TopLevelViewModel> commandsToRemove = [];
List<TopLevelViewModel> bandsToRemove = [];
lock (TopLevelCommands) lock (TopLevelCommands)
{ {
foreach (var extension in extensions) foreach (var extension in extensions)
@@ -366,6 +436,15 @@ public partial class TopLevelCommandManager : ObservableObject,
commandsToRemove.Add(command); commandsToRemove.Add(command);
} }
} }
foreach (var band in DockBands)
{
var host = band.ExtensionHost;
if (host?.Extension == extension)
{
bandsToRemove.Add(band);
}
}
} }
} }
@@ -385,6 +464,17 @@ public partial class TopLevelCommandManager : ObservableObject,
} }
} }
} }
lock (_dockBandsLock)
{
if (bandsToRemove.Count != 0)
{
foreach (var deleted in bandsToRemove)
{
DockBands.Remove(deleted);
}
}
}
}, },
CancellationToken.None, CancellationToken.None,
TaskCreationOptions.None, TaskCreationOptions.None,
@@ -408,6 +498,22 @@ public partial class TopLevelCommandManager : ObservableObject,
return null; 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) => public void Receive(ReloadCommandsMessage message) =>
ReloadAllCommandsAsync().ConfigureAwait(false); ReloadAllCommandsAsync().ConfigureAwait(false);
@@ -423,6 +529,21 @@ public partial class TopLevelCommandManager : ObservableObject,
wrapper?.UnpinCommand(message.CommandId, _serviceProvider); 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) public CommandProviderWrapper? LookupProvider(string providerId)
{ {
lock (_commandProvidersLock) lock (_commandProvidersLock)
@@ -441,6 +562,53 @@ 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;
// WATCH OUT: This locks CommandProviders. If you add code that
// locks CommandProviders first, before locking DockBands, you will
// cause a deadlock.
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() public void Dispose()
{ {
_reloadCommandsGate.Dispose(); _reloadCommandsGate.Dispose();

View File

@@ -25,6 +25,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
private readonly ProviderSettings _providerSettings; private readonly ProviderSettings _providerSettings;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel; private readonly CommandItemViewModel _commandItemViewModel;
private readonly IContextMenuFactory _contextMenuFactory;
public ICommandProviderContext ProviderContext { get; private set; } public ICommandProviderContext ProviderContext { get; private set; }
@@ -52,39 +53,28 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
public CommandPaletteHost ExtensionHost { get; private set; } public CommandPaletteHost ExtensionHost { get; private set; }
public string ExtensionName => ExtensionHost.GetExtensionDisplayName() ?? string.Empty;
public CommandViewModel CommandViewModel => _commandItemViewModel.Command; public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
public CommandItemViewModel ItemViewModel => _commandItemViewModel; public CommandItemViewModel ItemViewModel => _commandItemViewModel;
public string CommandProviderId => ProviderContext.ProviderId; public string CommandProviderId => ProviderContext.ProviderId;
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
////// ICommandItem ////// ICommandItem
public string Title => _commandItemViewModel.Title; public string Title => _commandItemViewModel.Title;
public string Subtitle => _commandItemViewModel.Subtitle; public string Subtitle => _commandItemViewModel.Subtitle;
public IIconInfo Icon => _commandItemViewModel.Icon; public IIconInfo Icon => (IIconInfo)IconViewModel;
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon; public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe; ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();
.Select(item =>
{
if (item is ISeparatorContextItem)
{
return item as IContextItem;
}
else if (item is CommandContextItemViewModel commandItem)
{
return commandItem.Model.Unsafe;
}
else
{
return null;
}
}).ToArray();
////// IListItem ////// IListItem
ITag[] IListItem.Tags => Tags.ToArray(); ITag[] IListItem.Tags => Tags.ToArray();
@@ -183,17 +173,46 @@ 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.CenterBands)
.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( public TopLevelViewModel(
CommandItemViewModel item, CommandItemViewModel item,
bool isFallback, TopLevelType topLevelType,
CommandPaletteHost extensionHost, CommandPaletteHost extensionHost,
ICommandProviderContext commandProviderContext, ICommandProviderContext commandProviderContext,
SettingsModel settings, SettingsModel settings,
ProviderSettings providerSettings, ProviderSettings providerSettings,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ICommandItem? commandItem) ICommandItem? commandItem,
IContextMenuFactory? contextMenuFactory)
{ {
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_settings = settings; _settings = settings;
@@ -201,22 +220,26 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
ProviderContext = commandProviderContext; ProviderContext = commandProviderContext;
_commandItemViewModel = item; _commandItemViewModel = item;
IsFallback = isFallback; _contextMenuFactory = contextMenuFactory ?? DefaultContextMenuFactory.Instance;
IsFallback = topLevelType == TopLevelType.Fallback;
IsDockBand = topLevelType == TopLevelType.DockBand;
ExtensionHost = extensionHost; ExtensionHost = extensionHost;
if (isFallback && commandItem is FallbackCommandItem fallback) if (IsFallback && commandItem is FallbackCommandItem fallback)
{ {
_fallbackId = fallback.Id; _fallbackId = fallback.Id;
} }
item.PropertyChangedBackground += Item_PropertyChanged; item.PropertyChangedBackground += Item_PropertyChanged;
// UpdateAlias();
// UpdateHotkey();
// UpdateTags();
} }
internal void InitializeProperties() internal void InitializeProperties()
{ {
// Init first, so that we get the ID & titles,
// then generate the ID,
// then slow init for the context menu
ItemViewModel.InitializeProperties();
GenerateId();
ItemViewModel.SlowInitializeProperties(); ItemViewModel.SlowInitializeProperties();
if (IsFallback) if (IsFallback)
@@ -278,7 +301,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
return; return;
} }
_initialIcon = _commandItemViewModel.Icon; _initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
if (raiseNotification) if (raiseNotification)
{ {
@@ -452,4 +475,43 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
{ {
return ToString(); 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);
}
}
_contextMenuFactory.AddMoreCommandsToTopLevel(this, this.ProviderContext, contextItems);
return contextItems.ToArray();
}
internal ICommandItem ToPinnedDockBandItem()
{
var item = new PinnedDockItem(item: this, id: Id);
return item;
}
}
public enum TopLevelType
{
Normal,
Fallback,
DockBand,
} }

View File

@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls" xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:local="using:Microsoft.CmdPal.UI" xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls" xmlns:ptcontrols="using:Microsoft.PowerToys.Common.UI.Controls"
xmlns:services="using:Microsoft.CmdPal.UI.Services"> xmlns:services="using:Microsoft.CmdPal.UI.Services">
@@ -12,7 +13,7 @@
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here --> <!-- 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/TextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" /> <ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
<ResourceDictionary Source="ms-appx:///Styles/Settings.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/KeyVisual/KeyCharPresenter.xaml" />
<ResourceDictionary Source="ms-appx:///PowerToys.Common.UI.Controls/Controls/ShortcutWithTextLabelControl/ShortcutWithTextLabelControl.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:///PowerToys.Common.UI.Controls/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml" />
<ResourceDictionary Source="ms-appx:///Dock/DockItemControl.xaml" />
<!-- Default theme dictionary --> <!-- Default theme dictionary -->
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" /> <ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
<services:MutableOverridesDictionary /> <services:MutableOverridesDictionary />
@@ -29,6 +31,12 @@
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double> <x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="ptcontrols:CheckBoxWithDescriptionControl" /> <Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="ptcontrols:CheckBoxWithDescriptionControl" />
<converters:StringVisibilityConverter
x:Key="StringNotEmptyToVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -28,6 +28,7 @@ using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Services; using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Models; using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
@@ -220,6 +221,7 @@ public partial class App : Application, IDisposable
// ViewModels // ViewModels
services.AddSingleton<ShellViewModel>(); services.AddSingleton<ShellViewModel>();
services.AddSingleton<DockViewModel>();
services.AddSingleton<IContextMenuFactory, CommandPaletteContextMenuFactory>(); services.AddSingleton<IContextMenuFactory, CommandPaletteContextMenuFactory>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>(); services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
} }

View File

@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
@@ -23,22 +24,45 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
_topLevelCommandManager = topLevelCommandManager; _topLevelCommandManager = topLevelCommandManager;
} }
/// <summary>
/// Constructs the view models for the MoreCommands of a
/// CommandItemViewModel. In our case, we can use our settings to add a
/// contextually-relevant pin/unpin command to this item.
///
/// This is called on all CommandItemViewModels. There are however some
/// weird edge cases we need to handle, concerning
/// </summary>
public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands( public List<IContextItemViewModel> UnsafeBuildAndInitMoreCommands(
IContextItem[] items, IContextItem[] items,
CommandItemViewModel commandItem) CommandItemViewModel commandItem)
{ {
var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem); var results = DefaultContextMenuFactory.Instance.UnsafeBuildAndInitMoreCommands(items, commandItem);
IPageContext? page = null;
var succeeded = commandItem.PageContext.TryGetTarget(out page);
if (!succeeded || page is null)
{
return results;
}
var isTopLevelItem = page is TopLevelItemPageContext;
if (isTopLevelItem)
{
// Bail early. We'll handle it below.
return results;
}
List<IContextItem> moreCommands = []; List<IContextItem> moreCommands = [];
var itemId = commandItem.Command.Id; var itemId = commandItem.Command.Id;
var providerContext = page.ProviderContext;
var supportsPinning = providerContext.SupportsPinning;
if (commandItem.PageContext.TryGetTarget(out var page) && if (supportsPinning &&
page.ProviderContext.SupportsPinning &&
!string.IsNullOrEmpty(itemId)) !string.IsNullOrEmpty(itemId))
{ {
// Add pin/unpin commands for pinning items to the top-level or to // Add pin/unpin commands for pinning items to the top-level or to
// the dock. // the dock.
var providerId = page.ProviderContext.ProviderId; var providerId = providerContext.ProviderId;
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider) if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
{ {
var providerSettings = _settingsModel.GetProviderSettings(provider); var providerSettings = _settingsModel.GetProviderSettings(provider);
@@ -51,8 +75,6 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
// We can't look up if this command item is in the top level // 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 // items in the manager, because we are being called _before_ we
// get added to the manager's list of commands. // get added to the manager's list of commands.
var isTopLevelItem = page is TopLevelItemPageContext;
if (!isTopLevelItem || alreadyPinnedToTopLevel) if (!isTopLevelItem || alreadyPinnedToTopLevel)
{ {
var pinToTopLevelCommand = new PinToCommand( var pinToTopLevelCommand = new PinToCommand(
@@ -66,6 +88,8 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem); var contextItem = new PinToContextItem(pinToTopLevelCommand, commandItem);
moreCommands.Add(contextItem); moreCommands.Add(contextItem);
} }
TryAddPinToDockCommand(providerSettings, itemId, providerId, moreCommands, commandItem);
} }
} }
@@ -79,6 +103,99 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
return results; return results;
} }
/// <summary>
/// Called to create the context menu on TopLevelViewModels.
///
/// These are handled differently from everyone else. With
/// TopLevelViewModels, the ID isn't on the Command, it is on the
/// TopLevelViewModel itself. Basically, we can't figure out how to add
/// pin/unpin commands directly attached to the ICommandItems that we get
/// from the API.
///
/// Instead, this method is used to extend the set of IContextItems that are
/// added to the TopLevelViewModel itself. This lets us pin/unpin the
/// generated ID of the TopLevelViewModel, even if the command didn't have
/// one.
/// </summary>
public void AddMoreCommandsToTopLevel(
TopLevelViewModel topLevelItem,
ICommandProviderContext providerContext,
List<IContextItem?> contextItems)
{
var itemId = topLevelItem.Id;
var supportsPinning = providerContext.SupportsPinning;
List<IContextItem> moreCommands = [];
var commandItem = topLevelItem.ItemViewModel;
// Add pin/unpin commands for pinning items to the top-level or to
// the dock.
var providerId = providerContext.ProviderId;
if (_topLevelCommandManager.LookupProvider(providerId) is CommandProviderWrapper provider)
{
var providerSettings = _settingsModel.GetProviderSettings(provider);
var isPinnedSubCommand = providerSettings.PinnedCommandIds.Contains(itemId);
if (isPinnedSubCommand)
{
var pinToTopLevelCommand = new PinToCommand(
commandId: itemId,
providerId: providerId,
pin: !isPinnedSubCommand,
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);
contextItems.AddRange(moreCommands);
}
}
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 internal enum PinLocation
{ {
TopLevel, TopLevel,
@@ -119,9 +236,13 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
private readonly bool _pin; private readonly bool _pin;
private readonly PinLocation _pinLocation; private readonly PinLocation _pinLocation;
private bool IsPinToDock => _pinLocation == PinLocation.Dock;
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon; public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
public override string Name => _pin ? RS_.GetString("top_level_pin_command_name") : RS_.GetString("top_level_unpin_command_name"); 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; internal event EventHandler? PinStateChanged;
@@ -152,10 +273,9 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
PinToTopLevel(); PinToTopLevel();
break; break;
// TODO: After dock is added: case PinLocation.Dock:
// case PinLocation.Dock: PinToDock();
// PinToDock(); break;
// break;
} }
} }
else else
@@ -166,9 +286,9 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
UnpinFromTopLevel(); UnpinFromTopLevel();
break; break;
// case PinLocation.Dock: case PinLocation.Dock:
// UnpinFromDock(); UnpinFromDock();
// break; break;
} }
} }
@@ -188,5 +308,17 @@ internal sealed partial class CommandPaletteContextMenuFactory : IContextMenuFac
UnpinCommandItemMessage message = new(_providerId, _commandId); UnpinCommandItemMessage message = new(_providerId, _commandId);
WeakReferenceMessenger.Default.Send(message); 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);
}
} }
} }

View File

@@ -97,7 +97,7 @@
<Grid <Grid
x:Name="IconRoot" x:Name="IconRoot"
Margin="3,0,-5,0" Margin="3,0,-5,0"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}"> Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<Button <Button
x:Name="StatusMessagesButton" x:Name="StatusMessagesButton"
x:Uid="StatusMessagesButton" x:Uid="StatusMessagesButton"
@@ -134,7 +134,7 @@
x:Uid="SettingsButton" x:Uid="SettingsButton"
Click="SettingsIcon_Clicked" Click="SettingsIcon_Clicked"
Style="{StaticResource SubtleButtonStyle}" 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"> <StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon <FontIcon
VerticalAlignment="Center" VerticalAlignment="Center"
@@ -153,7 +153,7 @@
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}" Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" TextWrapping="NoWrap"
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" /> Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<StackPanel <StackPanel
Grid.Column="2" Grid.Column="2"
Padding="0,0,4,0" Padding="0,0,4,0"

View File

@@ -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="&#xEDD9;" />
</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="&#xEDDA;" />
</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="&#xEDD9;" />
<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="&#xEDDA;" />
</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="&#xEDDB;" />
<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="&#xEDDC;" />
</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>

View File

@@ -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;
}
}
}

View 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="&#xE8EC;" />
</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="&#xE77A;" />
</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="&#xE710;" />
</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="&#xE710;" />
</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="&#xE710;" />
</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>

View 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();
}
}
}

View 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>

View File

@@ -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);
}
}

View File

@@ -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(),
};
}
}

View 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>

View File

@@ -0,0 +1,698 @@
// 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)
{
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 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

View File

@@ -2,10 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Diagnostics;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels;

View File

@@ -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 | | 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 _provider20 = null!;
private static IIconSourceProvider _provider32 = null!; private static IIconSourceProvider _provider32 = null!;
private static IIconSourceProvider _provider64 = null!; private static IIconSourceProvider _provider64 = null!;
@@ -31,6 +32,7 @@ public static partial class IconCacheProvider
public static void Initialize(IServiceProvider serviceProvider) public static void Initialize(IServiceProvider serviceProvider)
{ {
_provider16 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size16);
_provider20 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size20); _provider20 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size20);
_provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32); _provider32 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size32);
_provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64); _provider64 = serviceProvider.GetRequiredKeyedService<IIconSourceProvider>(WellKnownIconSize.Size64);
@@ -64,6 +66,9 @@ public static partial class IconCacheProvider
} }
#pragma warning disable IDE0060 // Remove unused parameter #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) public static void SourceRequested20(IconBox sender, SourceRequestedEventArgs args)
=> SourceRequestedCore(_provider20, args); => SourceRequestedCore(_provider20, args);

View File

@@ -16,6 +16,10 @@ internal static class IconServiceRegistration
services.AddSingleton<IIconLoaderService>(loader); services.AddSingleton<IIconLoaderService>(loader);
// Keyed providers by size // Keyed providers by size
services.AddKeyedSingleton<IIconSourceProvider>(
WellKnownIconSize.Size16,
(_, _) => new IconSourceProvider(loader, 16));
services.AddKeyedSingleton<IIconSourceProvider>( services.AddKeyedSingleton<IIconSourceProvider>(
WellKnownIconSize.Size20, WellKnownIconSize.Size20,
(_, _) => new CachedIconSourceProvider(loader, 20, 1024)); (_, _) => new CachedIconSourceProvider(loader, 20, 1024));

View File

@@ -6,6 +6,7 @@ namespace Microsoft.CmdPal.UI.Helpers;
internal enum WellKnownIconSize internal enum WellKnownIconSize
{ {
Size16 = 16,
Size20 = 20, Size20 = 20,
Size32 = 32, Size32 = 32,
Size64 = 64, Size64 = 64,

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.

View File

@@ -11,6 +11,7 @@ using Microsoft.CmdPal.Common.Helpers;
using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages; using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Controls; using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events; using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.Messages;
@@ -18,6 +19,7 @@ using Microsoft.CmdPal.UI.Services;
using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.ViewModels.Messages;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry; using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Composition; using Microsoft.UI.Composition;
@@ -45,6 +47,7 @@ namespace Microsoft.CmdPal.UI;
public sealed partial class MainWindow : WindowEx, public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>, IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>, IRecipient<ShowWindowMessage>,
IRecipient<ShowPaletteAtMessage>,
IRecipient<HideWindowMessage>, IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>, IRecipient<QuitMessage>,
IRecipient<NavigateToPageMessage>, IRecipient<NavigateToPageMessage>,
@@ -145,6 +148,7 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Register<DismissMessage>(this); WeakReferenceMessenger.Default.Register<DismissMessage>(this);
WeakReferenceMessenger.Default.Register<QuitMessage>(this); WeakReferenceMessenger.Default.Register<QuitMessage>(this);
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this); WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this); WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this); WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(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) private void RootElementLoaded(object sender, RoutedEventArgs e)
{ {
@@ -496,6 +503,78 @@ public sealed partial class MainWindow : WindowEx,
} }
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target) 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(); StopAutoGoHome();
@@ -514,16 +593,9 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE); 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); positionWindow(hwnd);
var newRect = WindowPositionHelper.AdjustRectForVisibility(_currentWindowPosition.ToPhysicalWindowRectangle(), originalScreen, _currentWindowPosition.Dpi);
MoveAndResizeDpiAware(newRect);
}
else
{
var display = GetScreen(hwnd, target);
PositionCentered(display);
} }
// Check if the debugger is attached. If it is, we don't want to apply the tool window style, // 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); ShowHwnd(message.Hwnd, settings.SummonOn);
} }
internal void Receive(ShowPaletteAtMessage message)
{
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
}
public void Receive(HideWindowMessage message) public void Receive(HideWindowMessage message)
{ {
// This might come in off the UI thread. Make sure to hop back. // 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. // Sure, it's not ideal, but at least it's not visible.
} }
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
// Start auto-go-home timer // Start auto-go-home timer
RestartAutoGoHome(); 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. // but that's the price to pay for having the HWND not light-dismiss while we're debugging.
Cloak(); Cloak();
this.Hide(); this.Hide();
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
return; return;
} }
@@ -1178,6 +1258,8 @@ public sealed partial class MainWindow : WindowEx,
DisposeAcrylic(); DisposeAcrylic();
} }
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
public void Receive(ToggleDevRibbonMessage message) public void Receive(ToggleDevRibbonMessage message)
{ {
_devRibbon?.Visibility = _devRibbon.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible; _devRibbon?.Visibility = _devRibbon.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;

View File

@@ -19,6 +19,9 @@
<Version>$(CmdPalVersion)</Version> <Version>$(CmdPalVersion)</Version>
<!-- For MVVM Toolkit Partial Properties/AOT support -->
<LangVersion>preview</LangVersion>
<!-- OutputPath is set in CmdPal.Branding.props --> <!-- OutputPath is set in CmdPal.Branding.props -->
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -27,10 +30,10 @@
</PropertyGroup> </PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds --> <!-- For debugging purposes, uncomment this block to enable AOT builds -->
<!--<PropertyGroup> <!-- <PropertyGroup>
<EnableCmdPalAOT>true</EnableCmdPalAOT> <EnableCmdPalAOT>true</EnableCmdPalAOT>
<GeneratePackageLocally>true</GeneratePackageLocally> <GeneratePackageLocally>true</GeneratePackageLocally>
</PropertyGroup>--> </PropertyGroup> -->
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'"> <PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
@@ -39,13 +42,13 @@
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'"> <PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild> <GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
<AppxBundle>Never</AppxBundle> <AppxBundle>Never</AppxBundle>
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir> <AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<!-- This lets us actually reference types from Microsoft.Terminal.UI and CmdPalKeyboardService --> <!-- This lets us actually reference types from Microsoft.Terminal.UI and CmdPalKeyboardService -->
<CsWinRTIncludes>Microsoft.Terminal.UI;CmdPalKeyboardService</CsWinRTIncludes> <CsWinRTIncludes>Microsoft.Terminal.UI;CmdPalKeyboardService</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir> <CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
@@ -73,6 +76,7 @@
<None Remove="Controls\DevRibbon.xaml" /> <None Remove="Controls\DevRibbon.xaml" />
<None Remove="Controls\FallbackRankerDialog.xaml" /> <None Remove="Controls\FallbackRankerDialog.xaml" />
<None Remove="Controls\ScreenPreview.xaml" /> <None Remove="Controls\ScreenPreview.xaml" />
<None Remove="Controls\ScrollContainer.xaml" />
<None Remove="Controls\SearchBar.xaml" /> <None Remove="Controls\SearchBar.xaml" />
<None Remove="ListDetailPage.xaml" /> <None Remove="ListDetailPage.xaml" />
<None Remove="LoadingPage.xaml" /> <None Remove="LoadingPage.xaml" />
@@ -83,8 +87,8 @@
<None Remove="Settings\AppearancePage.xaml" /> <None Remove="Settings\AppearancePage.xaml" />
<None Remove="Settings\InternalPage.xaml" /> <None Remove="Settings\InternalPage.xaml" />
<None Remove="ShellPage.xaml" /> <None Remove="ShellPage.xaml" />
<None Remove="Styles\Colors.xaml" />
<None Remove="Styles\Settings.xaml" /> <None Remove="Styles\Settings.xaml" />
<None Remove="Styles\TeachingTip.xaml" />
<None Remove="Styles\TextBox.xaml" /> <None Remove="Styles\TextBox.xaml" />
<None Remove="Styles\Theme.Normal.xaml" /> <None Remove="Styles\Theme.Normal.xaml" />
</ItemGroup> </ItemGroup>
@@ -214,6 +218,24 @@
</Content> </Content>
</ItemGroup> </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> <ItemGroup>
<Page Update="Controls\CommandPalettePreview.xaml"> <Page Update="Controls\CommandPalettePreview.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>

View File

@@ -59,15 +59,56 @@ GetModuleHandle
GetWindowLong GetWindowLong
SetWindowLong SetWindowLong
WINDOW_EX_STYLE WINDOW_EX_STYLE
CreateWindowEx CreateWindowEx
WNDCLASSEXW WNDCLASSEXW
RegisterClassEx RegisterClassEx
GetStockObject GetStockObject
GetModuleHandle 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 GetWindowThreadProcessId
AttachThreadInput AttachThreadInput
GetWindowPlacement GetWindowPlacement
WINDOWPLACEMENT WINDOWPLACEMENT
WM_DPICHANGED WM_DPICHANGED

View File

@@ -200,14 +200,19 @@
<!-- Back button --> <!-- Back button -->
<StackPanel Orientation="Horizontal"> <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 <Image
Width="20" Width="20"
Margin="20,0,6,0" Margin="0,0,6,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5" ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
AutomationProperties.AccessibilityView="Raw" AutomationProperties.AccessibilityView="Raw"
Source="ms-appx:///Assets/icon.svg" 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:Implicit.ShowAnimations>
<animations:OpacityAnimation <animations:OpacityAnimation
EasingMode="EaseIn" EasingMode="EaseIn"
@@ -250,7 +255,7 @@
FontSize=14}" FontSize=14}"
FontSize="16" FontSize="16"
Style="{StaticResource SubtleButtonStyle}" Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}"> Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay}">
<animations:Implicit.ShowAnimations> <animations:Implicit.ShowAnimations>
<animations:OpacityAnimation <animations:OpacityAnimation
EasingMode="EaseIn" EasingMode="EaseIn"
@@ -297,7 +302,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}" SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested20}" 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:Implicit.ShowAnimations>
<animations:OpacityAnimation <animations:OpacityAnimation
From="0" From="0"

View File

@@ -8,6 +8,7 @@ using System.Text;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI; using CommunityToolkit.WinUI;
using ManagedCommon; using ManagedCommon;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events; using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.Messages;
@@ -25,6 +26,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation; using Microsoft.UI.Xaml.Media.Animation;
using Windows.UI.Core; using Windows.UI.Core;
using WinUIEx;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using VirtualKey = Windows.System.VirtualKey; using VirtualKey = Windows.System.VirtualKey;
@@ -47,6 +49,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
IRecipient<ShowConfirmationMessage>, IRecipient<ShowConfirmationMessage>,
IRecipient<ShowToastMessage>, IRecipient<ShowToastMessage>,
IRecipient<NavigateToPageMessage>, IRecipient<NavigateToPageMessage>,
IRecipient<ShowHideDockMessage>,
INotifyPropertyChanged, INotifyPropertyChanged,
IDisposable IDisposable
{ {
@@ -64,6 +67,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private readonly CompositeFormat _pageNavigatedAnnouncement; private readonly CompositeFormat _pageNavigatedAnnouncement;
private SettingsWindow? _settingsWindow; private SettingsWindow? _settingsWindow;
private DockWindow? _dockWindow;
private CancellationTokenSource? _focusAfterLoadedCts; private CancellationTokenSource? _focusAfterLoadedCts;
private WeakReference<Page>? _lastNavigatedPageRef; 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<ShowToastMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this); WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true); AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false); AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true); 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"); var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat); _pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
if (App.Current.Services.GetService<SettingsModel>()!.EnableDock)
{
_dockWindow = new DockWindow();
_dockWindow.Show();
}
} }
/// <summary> /// <summary>
@@ -145,7 +157,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public void Receive(NavigateToPageMessage message) public void Receive(NavigateToPageMessage message)
{ {
// TODO GH #526 This needs more better locking too // 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 // Also hide our details pane about here, if we had one
HideDetails(); HideDetails();
@@ -250,10 +262,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
} }
} }
private void InitializeConfirmationDialog(ConfirmResultViewModel vm) private void InitializeConfirmationDialog(ConfirmResultViewModel vm) => vm.SafeInitializePropertiesSynchronous();
{
vm.SafeInitializePropertiesSynchronous();
}
public void Receive(OpenSettingsMessage message) public void Receive(OpenSettingsMessage message)
{ {
@@ -346,10 +355,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch(); public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
public void Receive(HotkeySummonMessage message) public void Receive(HotkeySummonMessage message) => _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
{
_ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
}
public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null; public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
@@ -418,10 +424,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>(); WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
} }
public void Receive(GoBackMessage message) public void Receive(GoBackMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
{
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
}
private void GoBack(bool withAnimation = true, bool focusSearch = true) private void GoBack(bool withAnimation = true, bool focusSearch = true)
{ {
@@ -462,10 +465,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
} }
} }
public void Receive(GoHomeMessage message) public void Receive(GoHomeMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
{
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
}
private void GoHome(bool withAnimation = true, bool focusSearch = true) private void GoHome(bool withAnimation = true, bool focusSearch = true)
{ {
@@ -483,6 +483,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 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) private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
@@ -754,5 +775,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
_focusAfterLoadedCts?.Cancel(); _focusAfterLoadedCts?.Cancel();
_focusAfterLoadedCts?.Dispose(); _focusAfterLoadedCts?.Dispose();
_focusAfterLoadedCts = null; _focusAfterLoadedCts = null;
_dockWindow?.Dispose();
} }
} }

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
@@ -7,6 +7,7 @@ using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Services; using Microsoft.CmdPal.UI.ViewModels.Services;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.UI; using Microsoft.UI;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
@@ -36,11 +37,14 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
private bool _isInitialized; private bool _isInitialized;
private bool _disposed; private bool _disposed;
private InternalThemeState _currentState; private InternalThemeState _currentState;
private DockThemeSnapshot _currentDockState;
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged; public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot; public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot;
public DockThemeSnapshot CurrentDockTheme => Volatile.Read(ref _currentDockState);
/// <summary> /// <summary>
/// Initializes the theme service. Must be called after the application window is activated and on UI thread. /// Initializes the theme service. Must be called after the application window is activated and on UI thread.
/// </summary> /// </summary>
@@ -144,6 +148,60 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
// Atomic swap // Atomic swap
Interlocked.Exchange(ref _currentState, newState); 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); _resourceSwapper.TryActivateTheme(provider.ThemeKey);
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs()); ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
} }
@@ -223,6 +281,20 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
}, },
Provider = _normalThemeProvider, 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() private void RequestReload()

View File

@@ -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=&#xF596;}">
<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=&#xE793;}">
<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="&#xE770;" />
<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="&#xE706;" />
<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="&#xE708;" />
<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=&#xF5EF;}">
<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=&#xE790;}"
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>

View File

@@ -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,
_ => 1,
};
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;
}
}

View File

@@ -71,6 +71,12 @@
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions" x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
Icon="{ui:FontIcon Glyph=&#xEA86;}" Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" /> Tag="Extensions" />
<!-- xF596 is HolePunchLandscapeTop -->
<NavigationViewItem
x:Name="DockSettingsPageNavItem"
x:Uid="Settings_GeneralPage_NavigationViewItem_Dock"
Icon="{ui:FontIcon Glyph=&#xF596;}"
Tag="Dock" />
<!-- "Internal Tools" page item is added dynamically from code --> <!-- "Internal Tools" page item is added dynamically from code -->
</NavigationView.MenuItems> </NavigationView.MenuItems>
<Grid> <Grid>

View File

@@ -35,7 +35,7 @@ public sealed partial class SettingsWindow : WindowEx,
public ObservableCollection<Crumb> BreadCrumbs { get; } = []; public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
// Gets or sets optional action invoked after NavigationView is loaded. // Gets or sets optional action invoked after NavigationView is loaded.
public Action NavigationViewLoaded { get; set; } = () => { }; public Action? NavigationViewLoaded { get; set; }
public SettingsWindow() public SettingsWindow()
{ {
@@ -125,6 +125,9 @@ public sealed partial class SettingsWindow : WindowEx,
case "Extensions": case "Extensions":
pageType = typeof(ExtensionsPage); pageType = typeof(ExtensionsPage);
break; break;
case "Dock":
pageType = typeof(DockSettingsPage);
break;
case "Internal": case "Internal":
pageType = typeof(InternalPage); pageType = typeof(InternalPage);
break; break;
@@ -142,6 +145,18 @@ public sealed partial class SettingsWindow : WindowEx,
if (pageType is not null) if (pageType is not null)
{ {
NavFrame.Navigate(pageType); 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 +307,12 @@ public sealed partial class SettingsWindow : WindowEx,
var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage"); var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
BreadCrumbs.Add(new(pageType, pageType)); 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) else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm)
{ {
NavView.SelectedItem = ExtensionPageNavItem; NavView.SelectedItem = ExtensionPageNavItem;

View File

@@ -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"> <data name="Settings_GeneralPage_NavigationViewItem_Extensions.Content" xml:space="preserve">
<value>Extensions</value> <value>Extensions</value>
</data> </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"> <data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Open Command Palette settings</value> <value>Open Command Palette settings</value>
</data> </data>
@@ -404,6 +407,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="BehaviorSettingsHeader.Text" xml:space="preserve"> <data name="BehaviorSettingsHeader.Text" xml:space="preserve">
<value>Behavior</value> <value>Behavior</value>
</data> </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"> <data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
<value>Search commands...</value> <value>Search commands...</value>
</data> </data>
@@ -418,6 +427,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
</data> </data>
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve"> <data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve">
<value>Disable animations when switching between pages</value> <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>
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve"> <data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Back</value> <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"> <data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
<value>Extensions</value> <value>Extensions</value>
</data> </data>
<data name="Settings_PageTitles_DockPage" xml:space="preserve">
<value>Dock</value>
</data>
<data name="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack.Content" xml:space="preserve"> <data name="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack.Content" xml:space="preserve">
<value>Clear search first, then go back</value> <value>Clear search first, then go back</value>
</data> </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"> <data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
<value>Version {0}</value> <value>Version {0}</value>
</data> </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"> <data name="Settings_GeneralPage_BackdropOpacity_SettingsCard.Header" xml:space="preserve">
<value>Opacity</value> <value>Opacity</value>
</data> </data>
@@ -807,6 +894,30 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="ConfigureShortcutText.Text" xml:space="preserve"> <data name="ConfigureShortcutText.Text" xml:space="preserve">
<value>Assign shortcut</value> <value>Assign shortcut</value>
</data> </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"> <data name="top_level_pin_command_name" xml:space="preserve">
<value>Pin to home</value> <value>Pin to home</value>
<comment>Command name for pinning an item to the top level list of commands</comment> <comment>Command name for pinning an item to the top level list of commands</comment>

View File

@@ -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>

View 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>

View File

@@ -5,7 +5,7 @@
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning> <XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion> <XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
<VersionMajor>0</VersionMajor> <VersionMajor>0</VersionMajor>
<VersionMinor>8</VersionMinor> <VersionMinor>9</VersionMinor>
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName> <VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@@ -22,12 +22,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<CsWinRTInputs Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.winmd" /> <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> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </None>
<Content Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.dll"> <None Include="..\..\..\..\..\$(Platform)\$(Configuration)\CalculatorEngineCommon.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Properties\Resources.Designer.cs"> <Compile Update="Properties\Resources.Designer.cs">

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation // Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
@@ -16,7 +16,8 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
public ClipboardHistoryCommandsProvider() public ClipboardHistoryCommandsProvider()
{ {
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager)) var page = new ClipboardHistoryListPage(_settingsManager);
_clipboardHistoryListItem = new ListItem(page)
{ {
Title = Properties.Resources.list_item_title, Title = Properties.Resources.list_item_title,
Icon = Icons.ClipboardListIcon, Icon = Icons.ClipboardListIcon,
@@ -24,7 +25,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
new CommandContextItem(_settingsManager.Settings.SettingsPage), new CommandContextItem(_settingsManager.Settings.SettingsPage),
], ],
}; };
DisplayName = Properties.Resources.provider_display_name; DisplayName = Properties.Resources.provider_display_name;
Icon = Icons.ClipboardListIcon; Icon = Icons.ClipboardListIcon;
Id = "Windows.ClipboardHistory"; Id = "Windows.ClipboardHistory";

View File

@@ -32,9 +32,8 @@ public partial class PerformanceMonitorCommandsProvider : CommandProvider
return _commands; return _commands;
} }
// Soon... public override ICommandItem[]? GetDockBands()
// public override ICommandItem[]? GetDockBands() {
// { return new ICommandItem[] { _band };
// return new ICommandItem[] { _band }; }
// }
} }

View File

@@ -22,6 +22,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" /> <ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props --> <!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup> </ItemGroup>

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // 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.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources { public class Resources {
@@ -159,6 +159,15 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
} }
} }
/// <summary>
/// Looks up a localized string similar to Clock.
/// </summary>
public static string Microsoft_plugin_timedate_dock_band_title {
get {
return ResourceManager.GetString("Microsoft_plugin_timedate_dock_band_title", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Era. /// Looks up a localized string similar to Era.
/// </summary> /// </summary>
@@ -932,5 +941,23 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
return ResourceManager.GetString("Microsoft_plugin_timedate_Year", resourceCulture); return ResourceManager.GetString("Microsoft_plugin_timedate_Year", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Copy date.
/// </summary>
public static string timedate_copy_date_command_name {
get {
return ResourceManager.GetString("timedate_copy_date_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy time.
/// </summary>
public static string timedate_copy_time_command_name {
get {
return ResourceManager.GetString("timedate_copy_time_command_name", resourceCulture);
}
}
} }
} }

View File

@@ -432,4 +432,16 @@
<data name="Microsoft_plugin_timedate_SettingEnableFallbackItems_Description" xml:space="preserve"> <data name="Microsoft_plugin_timedate_SettingEnableFallbackItems_Description" xml:space="preserve">
<value>Show time and date results when typing keywords like "week", "year", "now", "time", or "date"</value> <value>Show time and date results when typing keywords like "week", "year", "now", "time", or "date"</value>
</data> </data>
<data name="Microsoft_plugin_timedate_dock_band_title" xml:space="preserve">
<value>Clock</value>
<comment>Title for the time and date dock band</comment>
</data>
<data name="timedate_copy_time_command_name" xml:space="preserve">
<value>Copy time</value>
<comment>Name of a command to copy the current time to the clipboard</comment>
</data>
<data name="timedate_copy_date_command_name" xml:space="preserve">
<value>Copy date</value>
<comment>Name of a command to copy the current date to the clipboard</comment>
</data>
</root> </root>

View File

@@ -20,6 +20,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager); private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager);
private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager); private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager);
private readonly ListItem _bandItem;
public TimeDateCommandsProvider() public TimeDateCommandsProvider()
{ {
DisplayName = Resources.Microsoft_plugin_timedate_plugin_name; DisplayName = Resources.Microsoft_plugin_timedate_plugin_name;
@@ -33,6 +35,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
Icon = _timeDateExtensionPage.Icon; Icon = _timeDateExtensionPage.Icon;
Settings = _settingsManager.Settings; Settings = _settingsManager.Settings;
_bandItem = new NowDockBand();
} }
private string GetTranslatedPluginDescription() private string GetTranslatedPluginDescription()
@@ -47,4 +51,85 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() => [_command]; public override ICommandItem[] TopLevelCommands() => [_command];
public override IFallbackCommandItem[] FallbackCommands() => [_fallbackTimeDateItem]; public override IFallbackCommandItem[] FallbackCommands() => [_fallbackTimeDateItem];
public override ICommandItem[] GetDockBands()
{
var wrappedBand = new WrappedDockItem(
[_bandItem],
"com.microsoft.cmdpal.timedate.dockBand",
Resources.Microsoft_plugin_timedate_dock_band_title);
return new ICommandItem[] { wrappedBand };
}
} }
#pragma warning disable SA1402 // File may only contain a single type
internal sealed partial class NowDockBand : ListItem
{
private static readonly TimeSpan UpdateInterval = TimeSpan.FromSeconds(60);
private CopyTextCommand _copyTimeCommand;
private CopyTextCommand _copyDateCommand;
public NowDockBand()
{
Command = new NoOpCommand() { Id = "com.microsoft.cmdpal.timedate.dockBand" };
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_time_command_name };
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_date_command_name };
MoreCommands = [
new CommandContextItem(_copyTimeCommand),
new CommandContextItem(_copyDateCommand)
];
UpdateText();
// Create a timer to update the time every minute
System.Timers.Timer timer = new(UpdateInterval.TotalMilliseconds);
// but we want it to tick on the minute, so calculate the initial delay
var now = DateTime.Now;
timer.Interval = UpdateInterval.TotalMilliseconds - ((now.Second * 1000) + now.Millisecond);
// then after the first tick, set it to 60 seconds
timer.Elapsed += Timer_ElapsedFirst;
timer.Start();
}
private void Timer_ElapsedFirst(object sender, System.Timers.ElapsedEventArgs e)
{
// After the first tick, set the interval to 60 seconds
if (sender is System.Timers.Timer timer)
{
timer.Interval = UpdateInterval.TotalMilliseconds;
timer.Elapsed -= Timer_ElapsedFirst;
timer.Elapsed += Timer_Elapsed;
// Still call the callback, so that we update the clock
Timer_Elapsed(sender, e);
}
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
UpdateText();
}
private void UpdateText()
{
var timeExtended = false; // timeLongFormat ?? settings.TimeWithSecond;
var dateExtended = false; // dateLongFormat ?? settings.DateWithWeekday;
var dateTimeNow = DateTime.Now;
var timeString = dateTimeNow.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Time, timeExtended, dateExtended),
CultureInfo.CurrentCulture);
var dateString = dateTimeNow.ToString(
TimeAndDateHelper.GetStringFormat(FormatStringType.Date, timeExtended, dateExtended),
CultureInfo.CurrentCulture);
Title = timeString;
Subtitle = dateString;
_copyDateCommand.Text = dateString;
_copyTimeCommand.Text = timeString;
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -42,6 +42,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
public WinGetExtensionPage(string tag = "") public WinGetExtensionPage(string tag = "")
{ {
Icon = tag == ExtensionsTag ? Icons.ExtensionsIcon : Icons.WinGetIcon; Icon = tag == ExtensionsTag ? Icons.ExtensionsIcon : Icons.WinGetIcon;
Id = tag == ExtensionsTag ? "com.microsoft.cmdpal.winget-extensions" : "com.microsoft.cmdpal.winget";
Name = Properties.Resources.winget_page_name; Name = Properties.Resources.winget_page_name;
_tag = tag; _tag = tag;
ShowDetails = true; ShowDetails = true;

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
/// <summary>
/// A sample dock band with multiple buttons.
/// Each button shows a toast message when clicked.
/// </summary>
internal sealed partial class SampleButtonsDockBand : WrappedDockItem
{
public SampleButtonsDockBand()
: base([], "com.microsoft.cmdpal.samples.buttons_band", "Sample Buttons Band")
{
ListItem[] buttons = [
new(new ShowToastCommand("Button 1")) { Title = "1" },
new(new ShowToastCommand("Button B")) { Icon = new IconInfo("\uF094") }, // B button
new(new ShowToastCommand("Button 3")) { Title = "Items have Icons &", Icon = new IconInfo("\uED1E"), Subtitle = "titles & subtitles" }, // Subtitles
];
Icon = new IconInfo("\uEECA"); // ButtonView2
Items = buttons;
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,22 @@
// 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 SamplePagesExtension;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// A sample dock band with one button.
/// Clicking on this button will open the palette to the samples list page
/// </summary>
internal sealed partial class SampleDockBand : WrappedDockItem
{
public SampleDockBand()
: base(new SamplesListPage(), "Command Palette Samples")
{
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -27,4 +28,15 @@ public partial class SamplePagesCommandsProvider : CommandProvider
{ {
return _commands; return _commands;
} }
public override ICommandItem[] GetDockBands()
{
List<ICommandItem> bands = new()
{
new SampleDockBand(),
new SampleButtonsDockBand(),
};
return bands.ToArray();
}
} }

View File

@@ -0,0 +1,19 @@
// 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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
internal sealed partial class ShowToastCommand(string message) : InvokableCommand
{
public override ICommandResult Invoke()
{
return CommandResult.ShowToast(message);
}
}
#pragma warning restore SA1402 // File may only contain a single type