Compare commits

...

79 Commits

Author SHA1 Message Date
Mike Griese
c0fe992e37 fixes from the upstream merge 2025-12-05 14:14:06 -06:00
Mike Griese
bd316d4d34 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-05 13:24:32 -06:00
Mike Griese
f03eb96b9c the more commands too 2025-12-05 13:16:20 -06:00
Mike Griese
bbd15a3ae8 actually respect the setting 2025-12-05 12:39:30 -06:00
Mike Griese
777a301666 default settings 2025-12-05 10:56:41 -06:00
Mike Griese
f9e3ab4852 fix the title on the settings page 2025-12-05 10:02:29 -06:00
Mike Griese
f311a65708 More small refactoring 2025-12-05 08:14:09 -06:00
Mike Griese
b9040d82c3 refactoring to make code 1% more readable 2025-12-05 07:00:48 -06:00
Mike Griese
1d8b45f824 wholesale steal craig's perf monitor 2025-12-04 10:41:28 -06:00
Mike Griese
221cf083bc Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-04 09:00:59 -06:00
Mike Griese
ccac1e1ac9 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-12-03 12:52:29 -06:00
Mike Griese
fb428b2d61 update wt.json for building SDK 2025-11-25 16:22:30 -06:00
Mike Griese
acb933643a toolkit updated to support these 2025-11-24 09:02:50 -06:00
Mike Griese
f63785d80d initial spec 2025-11-24 06:25:19 -06:00
Mike Griese
87c1a73ecc bunch of loc todos 2025-11-24 06:24:52 -06:00
Mike Griese
44b0b9ac67 cleanup; tooltips 2025-11-21 15:58:38 -06:00
Mike Griese
7629c6fbfa Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-20 16:49:09 -06:00
Mike Griese
b8c024ac07 remove some dead code 2025-11-20 16:49:01 -06:00
Mike Griese
640c1a8388 pull to separate file 2025-11-20 16:30:46 -06:00
Mike Griese
78b2b23764 I'm guessing that niels will want this 2025-11-20 16:21:24 -06:00
Mike Griese
46d26041b9 don't write the settings when opening it 2025-11-20 16:17:57 -06:00
Mike Griese
08454f8b18 rudimentary settings saving 2025-11-20 15:58:33 -06:00
Mike Griese
b7a65ab609 show num items in band 2025-11-20 15:24:42 -06:00
Mike Griese
08d3435a0d Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-20 05:27:46 -06:00
Mike Griese
46b8eea695 more settings for labels 2025-11-18 11:06:01 -06:00
Mike Griese
5b255011c7 We need to have a separate helper for "wrap as band" vs "this is a pinned thing" 2025-11-18 10:48:21 -06:00
Mike Griese
6782829cdd icons are nice 2025-11-18 09:40:40 -06:00
Mike Griese
6ed8d73b50 Make it a combobox 2025-11-18 09:25:08 -06:00
Mike Griese
38dfee0234 This works! 2025-11-18 06:59:51 -06:00
Mike Griese
d547a6f613 Start plumbing the settings for bands through 2025-11-18 06:02:07 -06:00
Mike Griese
58bea1c813 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-17 16:02:12 -06:00
Mike Griese
5ad2bdf6c2 xaml 2025-11-09 09:13:10 -06:00
Mike Griese
44f739a289 that's better 2025-11-09 08:28:25 -06:00
Mike Griese
f3d9fc2342 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-09 07:02:00 -06:00
Mike Griese
90d4ca060e don't show the secondary command if its a separator 2025-11-09 07:01:52 -06:00
Mike Griese
6554a4aaaa add a 'customize' item to the context menu of the bar itself 2025-11-09 06:56:06 -06:00
Mike Griese
cac0048ca7 we start now boys 2025-11-08 06:07:55 -06:00
Mike Griese
ddb28a8606 Pin & Unpin, and fix a closing crash 2025-11-07 14:44:14 -06:00
Mike Griese
a7206863bc gimme dis\nnow 2025-11-07 13:53:35 -06:00
Mike Griese
96def3b79a much simpler settings 2025-11-07 13:19:36 -06:00
Mike Griese
5231543ed2 Rudimentary: This _does_ actually pin commands to the dock 2025-11-07 13:03:47 -06:00
Mike Griese
2462da68bc some nits around the sizing, to make things with subtitles show again 2025-11-07 06:29:46 -06:00
Mike Griese
bbfa6c6ccb hell yea it works 2025-11-07 06:12:48 -06:00
Mike Griese
f0ea908ee6 Merge remote-tracking branch 'origin/main' into dev/migrie/f/powerdock 2025-11-06 15:27:49 -06:00
Mike Griese
6e11230fed Merge branch 'dev/migrie/f/powerdock' of https://github.com/microsoft/powertoys into dev/migrie/f/powerdock 2025-11-06 15:24:10 -06:00
Mike Griese
6c26e86e9a dead end trying to pin new top-level commands 2025-11-06 15:23:56 -06:00
Mike Griese
1d19705568 notes 2025-11-02 06:50:09 -06:00
Mike Griese
e5e20eca9c fix the showdesktop handler 2025-11-02 06:40:35 -06:00
Niels Laute
ef0639602f Minor padding tweaks on left/right 2025-11-01 19:09:45 +01:00
Niels Laute
fdd4416049 Adding design polish and make it look like taskbar (border and button styles) 2025-11-01 19:09:13 +01:00
Mike Griese
0dab46e58f tighten up opening cmdpal in a couple places 2025-10-31 15:03:46 -05:00
Mike Griese
86d1061a25 Yep, open the palette right at the dock 2025-10-31 14:40:31 -05:00
Mike Griese
e0197dd7a5 holy fuck this is cool 2025-10-31 12:55:25 -05:00
Mike Griese
64ea63b77d handle esc to dismiss dock flyout 2025-10-31 12:06:45 -05:00
Mike Griese
bc6b2af03c Add context menus to buttons 2025-10-31 06:10:41 -05:00
Mike Griese
c1af5fdc57 meh settings for icon sizes 2025-10-30 16:56:32 -05:00
Mike Griese
5be208520e update the clock in RT ; accidentally regress clipboard name 2025-10-28 16:12:54 -05:00
Mike Griese
5aaf0e010a add a dedicated VM for DockBandItems 2025-10-28 05:55:57 -05:00
Mike Griese
48eee1b0d9 do less work on the UI thread 2025-10-27 15:43:17 -05:00
Mike Griese
1447a825ee right right, update the observable thing on the UI thread dumbass 2025-10-27 15:35:10 -05:00
Mike Griese
76f7dd3b09 don't flicker the windows so much bub 2025-10-27 10:57:09 -05:00
Mike Griese
ee174ddd1d bands come from the settings 2025-10-27 10:25:24 -05:00
Mike Griese
35c4f8fdaa add my deskband 2025-10-27 09:57:00 -05:00
Mike Griese
2ec7ae664e actually hot-reload dock settings 2025-10-27 06:25:29 -05:00
Mike Griese
1b8ddaa849 add the settings to the main settings, even if they do nothing 2025-10-27 05:59:15 -05:00
Mike Griese
d6bca1d38e different sides? yes pls 2025-10-26 21:43:27 -05:00
Mike Griese
b1d7626ab7 naming for that weird clipboard command 2025-10-26 20:40:34 -05:00
Mike Griese
91598c091e Have a separate provider for dock bands 2025-10-25 10:45:14 -05:00
Mike Griese
fd3e73ee7e listen for window updates 2025-10-24 12:16:54 -05:00
Mike Griese
06a664a53a Couple things
* hide subtitles on WW items;
* Add a clock band
* discover that items need .Names on the commands to appear and that's gross
2025-10-24 06:56:35 -05:00
Mike Griese
87d2509380 a bunch of WW refactoring 2025-10-24 06:17:35 -05:00
Mike Griese
c1dc487f2c Now all the items are initialized straight off of the commands in cmdpal 2025-10-24 05:37:34 -05:00
Mike Griese
e0dd7ad44a Move out a couple of these files 2025-10-23 15:35:16 -05:00
Mike Griese
aaa68fa351 look ma, those are from CmdPal 2025-10-23 15:32:39 -05:00
Mike Griese
d9e4133b5a Revert "probably don't need any of this"
This reverts commit 821b99c4e0.
2025-10-23 14:51:19 -05:00
Mike Griese
821b99c4e0 probably don't need any of this 2025-10-23 14:51:15 -05:00
Mike Griese
8b5a2e9537 need all this 2025-10-23 14:51:01 -05:00
Mike Griese
2e49835b4d didn't need a different project, really 2025-10-23 05:52:22 -05:00
Mike Griese
ef106f6811 stand up project 2025-10-23 05:51:10 -05:00
93 changed files with 5247 additions and 199 deletions

View File

@@ -206,6 +206,10 @@
<Platform Solution="*|x64" Project="x64" />
<Deploy />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />
</Project>
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj">
<Platform Solution="*|ARM64" Project="ARM64" />
<Platform Solution="*|x64" Project="x64" />

View File

@@ -26,6 +26,11 @@
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
"name": "Update template project",
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
},
{
"input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1",
"name": "Build SDK",
"description": "Builds the SDK nuget package with the specified version."
}
]
}

View File

@@ -0,0 +1,23 @@
// 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.Core.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(ICommandItem item, string id)
: base(item, id, item.Title)
{
}
}

View File

@@ -9,4 +9,19 @@
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,72 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Core.Common.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Core.Common.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Pinned.
/// </summary>
public static string PinnedItemSuffix {
get {
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="PinnedItemSuffix" xml:space="preserve">
<value>Pinned</value>
<comment>Suffix shown for pinned items in the dock</comment>
</data>
</root>

View File

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

View File

@@ -39,7 +39,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
private string _itemTitle = string.Empty;
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
protected string ItemTitle => _itemTitle;
public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
public string Subtitle { get; private set; } = string.Empty;
@@ -61,10 +63,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public CommandItemViewModel? PrimaryCommand => this;
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
public CommandItemViewModel? SecondaryCommand // => HasMoreCommands ? ActualCommands[0] : null;
{
get
{
if (HasMoreCommands)
{
if (MoreCommands[0] is CommandContextItemViewModel command)
{
return command;
}
}
return null;
}
}
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
public bool HasTitle => !string.IsNullOrEmpty(Title);
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
public virtual bool HasText => HasTitle || HasSubtitle;
public List<IContextItemViewModel> AllCommands
{
get
@@ -319,16 +341,19 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Title));
UpdateProperty(nameof(Icon));
UpdateProperty(nameof(HasText));
break;
case nameof(Title):
_itemTitle = model.Title;
UpdateProperty(nameof(HasText));
break;
case nameof(Subtitle):
var modelSubtitle = model.Subtitle;
this.Subtitle = modelSubtitle;
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
UpdateProperty(nameof(HasText));
break;
case nameof(Icon):
@@ -412,11 +437,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
}
}
private void UpdateDefaultContextItemIcon()
{
private void UpdateDefaultContextItemIcon() =>
// Command icon takes precedence over our icon on the primary command
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
}
private void UpdateTitle(string? title)
{

View File

@@ -53,11 +53,12 @@ public partial class ContextMenuViewModel : ObservableObject,
{
if (SelectedItem is not null)
{
if (SelectedItem.MoreCommands.Count() > 1)
{
ContextMenuStack.Clear();
PushContextStack(SelectedItem.AllCommands);
}
// if (SelectedItem.MoreCommands.Count() > 1)
// {
ContextMenuStack.Clear();
PushContextStack(SelectedItem.AllCommands);
// }
}
}

View File

@@ -17,7 +17,7 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject
PageContext = new(realContext);
}
internal ExtensionObjectViewModel(WeakReference<IPageContext> context)
protected ExtensionObjectViewModel(WeakReference<IPageContext> context)
{
PageContext = context;
}

View File

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

View File

@@ -4,4 +4,4 @@
namespace Microsoft.CmdPal.Core.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 TransientPage { get; set; }
public PerformCommandMessage(ExtensionObject<ICommand> 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.Core.ViewModels.Messages;
public sealed record WindowHiddenMessage();

View File

@@ -27,7 +27,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
public partial string ErrorMessage { get; protected set; } = string.Empty;
[ObservableProperty]
public partial bool IsNested { get; set; } = true;
public partial bool IsRootPage { get; set; } = true;
[ObservableProperty]
public partial bool HasBackButton { get; set; } = true;
// This is set from the SearchBar
[ObservableProperty]

View File

@@ -121,4 +121,8 @@
<value>Show details</value>
<comment>Name for the command that shows details of an item</comment>
</data>
<data name="PinnedItemSuffix" xml:space="preserve">
<value>Pinned</value>
<comment>Suffix shown for pinned items in the dock</comment>
</data>
</root>

View File

@@ -15,7 +15,8 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ShellViewModel : ObservableObject,
IRecipient<PerformCommandMessage>,
IRecipient<HandleCommandResultMessage>
IRecipient<HandleCommandResultMessage>,
IRecipient<WindowHiddenMessage>
{
private readonly IRootPageService _rootPageService;
private readonly IAppHostService _appHostService;
@@ -78,8 +79,9 @@ public partial class ShellViewModel : ObservableObject,
private IPage? _rootPage;
private bool _isNested;
private bool _currentlyTransient;
public bool IsNested => _isNested;
public bool IsNested => _isNested && !_currentlyTransient;
public PageViewModel NullPage { get; private set; }
@@ -95,11 +97,13 @@ public partial class ShellViewModel : ObservableObject,
_appHostService = appHostService;
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
NullPage.HasBackButton = false;
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
// Register to receive messages
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
WeakReferenceMessenger.Default.Register<WindowHiddenMessage>(this);
}
[RelayCommand]
@@ -258,7 +262,7 @@ public partial class ShellViewModel : ObservableObject,
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
_rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
try
{
@@ -268,6 +272,7 @@ public partial class ShellViewModel : ObservableObject,
var isMainPage = command == _rootPage;
_isNested = !isMainPage;
_currentlyTransient = message.TransientPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host);
@@ -277,6 +282,9 @@ public partial class ShellViewModel : ObservableObject,
throw new NotSupportedException();
}
pageViewModel.IsRootPage = isMainPage;
pageViewModel.HasBackButton = IsNested;
// Clear command bar, ViewModel initialization can already set new commands if it wants to
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
@@ -296,7 +304,8 @@ public partial class ShellViewModel : ObservableObject,
_scheduler);
// While we're loading in the background, immediately move to the next page.
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
WeakReferenceMessenger.Default.Send(msg);
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
// See RootFrame_Navigated event handler.
@@ -447,6 +456,19 @@ public partial class ShellViewModel : ObservableObject,
UnsafeHandleCommandResult(message.Result.Unsafe);
}
public void Receive(WindowHiddenMessage message)
{
// If the window was hidden while we had a transient page, we need to reset that state.
if (_currentlyTransient)
{
_currentlyTransient = false;
// navigate back to the main page without animation
GoHome(withAnimation: false, focusSearch: false);
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
}
}
private void OnUIThread(Action action)
{
_ = Task.Factory.StartNew(

View File

@@ -21,7 +21,7 @@ public class CommandPalettePageViewModelFactory
{
return page switch
{
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsRootPage = !nested },
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
_ => null,
};

View File

@@ -1,12 +1,14 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using ManagedCommon;
using Microsoft.CmdPal.Core.Common;
using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Extensions.DependencyInjection;
using Windows.Foundation;
@@ -27,6 +29,8 @@ public sealed class CommandProviderWrapper
public TopLevelViewModel[] FallbackItems { get; private set; } = [];
public TopLevelViewModel[] DockBandItems { get; private set; } = [];
public string DisplayName { get; private set; } = string.Empty;
public IExtensionWrapper? Extension { get; }
@@ -141,6 +145,7 @@ public sealed class CommandProviderWrapper
ICommandItem[]? commands = null;
IFallbackCommandItem[]? fallbacks = null;
ICommandItem[] dockBands = []; // do not initialize me to null
try
{
@@ -158,6 +163,30 @@ public sealed class CommandProviderWrapper
UnsafePreCacheApiAdditions(two);
}
// if (model is IExtendedAttributesProvider iHaveProperties)
if (model is ICommandProvider3 supportsDockBands)
{
// var props = iHaveProperties.GetProperties();
// var hasBands = props.TryGetValue("DockBands", out var obj);
// if (hasBands && obj is not null)
// {
// // CoreLogger.LogDebug($"Found bands object on {DisplayName} ({ProviderId}) ");
// // var bands = (ICommandItem[])obj;
// var bands = obj as ICommandItem[];
// if (bands is not null)
// {
// CoreLogger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
// dockBands = bands;
// }
// }
var bands = supportsDockBands.GetDockBands();
if (bands is not null)
{
CoreLogger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
dockBands = bands;
}
}
Id = model.Id;
DisplayName = model.DisplayName;
Icon = new(model.Icon);
@@ -168,7 +197,8 @@ public sealed class CommandProviderWrapper
Settings = new(model.Settings, this, _taskScheduler);
// We do need to explicitly initialize commands though
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
var objects = new TopLevelObjects(commands, fallbacks, dockBands);
InitializeCommands(objects, serviceProvider, pageContext);
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
}
@@ -180,32 +210,67 @@ public sealed class CommandProviderWrapper
}
}
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
private record TopLevelObjects(
ICommandItem[]? Commands,
IFallbackCommandItem[]? Fallbacks,
ICommandItem[]? DockBands);
private void InitializeCommands(
TopLevelObjects objects,
IServiceProvider serviceProvider,
WeakReference<IPageContext> pageContext)
{
var settings = serviceProvider.GetService<SettingsModel>()!;
var state = serviceProvider.GetService<AppStateModel>()!;
var providerSettings = GetProviderSettings(settings);
Func<ICommandItem?, bool, TopLevelViewModel> makeAndAdd = (ICommandItem? i, bool fallback) =>
Func<ICommandItem?, TopLevelType, TopLevelViewModel> make = (ICommandItem? i, TopLevelType t) =>
{
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider);
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider);
topLevelViewModel.InitializeProperties();
return topLevelViewModel;
};
if (commands is not null)
if (objects.Commands is not null)
{
TopLevelItems = commands
.Select(c => makeAndAdd(c, false))
TopLevelItems = objects.Commands
.Select(c => make(c, TopLevelType.Normal))
.ToArray();
}
if (fallbacks is not null)
if (objects.Fallbacks is not null)
{
FallbackItems = fallbacks
.Select(c => makeAndAdd(c, true))
FallbackItems = objects.Fallbacks
.Select(c => make(c, TopLevelType.Fallback))
.ToArray();
}
if (objects.DockBands is not null)
{
List<TopLevelViewModel> bands = new();
foreach (var b in objects.DockBands)
{
var bandVm = make(b, TopLevelType.DockBand);
bands.Add(bandVm);
}
foreach (var c in TopLevelItems)
{
foreach (var pinnedId in settings.DockSettings.PinnedCommands)
{
if (pinnedId == c.Id)
{
var bandModel = c.ToPinnedDockBandItem();
var bandVm = make(bandModel, TopLevelType.DockBand);
bands.Add(bandVm);
break;
}
}
}
DockBandItems = bands.ToArray();
}
}
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
@@ -218,6 +283,10 @@ public sealed class CommandProviderWrapper
{
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
}
else if (a is ICommandItem[] commands)
{
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
}
}
}
@@ -234,4 +303,17 @@ public sealed class CommandProviderWrapper
// In handling this, a call will be made to `LoadTopLevelCommands` to
// retrieve the new items.
this.CommandsChanged?.Invoke(this, args);
internal void PinDockBand(TopLevelViewModel bandVm)
{
Logger.LogDebug($"CommandProviderWrapper.PinDockBand: {ProviderId} - {bandVm.Id}");
// var settings = ExtensionHost.ServiceProvider.GetService<SettingsModel>()!;
// settings.DockSettings.PinnedCommands.Add(bandVm.Id);
// SettingsModel.SaveSettings(settings);
var bands = this.DockBandItems.ToList();
bands.Add(bandVm);
this.DockBandItems = bands.ToArray();
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
}
}

View File

@@ -1,10 +1,12 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
@@ -19,6 +21,8 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
private readonly FallbackLogItem _fallbackLogItem = new();
private readonly NewExtensionPage _newExtension = new();
private readonly IRootPageService _rootPageService;
public override ICommandItem[] TopLevelCommands() =>
[
new CommandItem(openSettings) { },
@@ -32,11 +36,22 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
_fallbackLogItem,
];
public BuiltInsCommandProvider()
public BuiltInsCommandProvider(IRootPageService rootPageService)
{
Id = "com.microsoft.cmdpal.builtin.core";
DisplayName = Properties.Resources.builtin_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
_rootPageService = rootPageService;
}
public override ICommandItem[]? GetDockBands()
{
var rootPage = _rootPageService.GetRootPage();
List<ICommandItem> bandItems = new();
bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
return bandItems.ToArray();
}
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);

View File

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

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.
// See the LICENSE file in the project root for more information.
@@ -19,7 +19,7 @@ public partial class OpenSettingsCommand : InvokableCommand
public override ICommandResult Invoke()
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
return CommandResult.KeepOpen();
}
}

View File

@@ -0,0 +1,216 @@
// 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.Core.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Settings;
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
#pragma warning disable SA1402 // File may only contain a single type
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;
}
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.Id == _dockSettingsModel.Id);
if (inStart)
{
return DockPinSide.Start;
}
var inEnd = dockSettings.EndBands.Any(b => b.Id == _dockSettingsModel.Id);
if (inEnd)
{
return DockPinSide.End;
}
return DockPinSide.None;
}
private int NumItemsInBand()
{
var bandVm = _bandViewModel;
if (bandVm is null)
{
return 0;
}
return _bandViewModel!.Items.Count;
}
private void Save()
{
SettingsModel.SaveSettings(_settingsModel);
}
private void UpdatePinSide(DockPinSide value)
{
OnPinSideChanged(value);
OnPropertyChanged(nameof(PinSideIndex));
OnPropertyChanged(nameof(PinSide));
}
public void SetBandPosition(DockPinSide side, int? index)
{
var dockSettings = _settingsModel.DockSettings;
// Remove from both sides first
dockSettings.StartBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
dockSettings.EndBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
// Add to the selected side
switch (side)
{
case DockPinSide.Start:
{
var insertIndex = index ?? dockSettings.StartBands.Count;
dockSettings.StartBands.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);
}
}
public enum DockPinSide
{
None,
Start,
End,
}
public enum ShowLabelsOption
{
Default,
ShowLabels,
HideLabels,
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,130 @@
// 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.Core.ViewModels;
using Microsoft.CmdPal.Core.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;
public ObservableCollection<DockItemViewModel> Items { get; } = new();
private bool _showLabels = true;
public string Id => _rootItem.Command.Id;
internal DockBandViewModel(
CommandItemViewModel commandItemViewModel,
WeakReference<IPageContext> errorContext,
DockBandSettings settings,
DockSettings dockSettings)
: base(errorContext)
{
_rootItem = commandItemViewModel;
_showLabels = settings.ResolveShowLabels(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, _showLabels);
newItemVm.SlowInitializeProperties();
newViewModels.Add(newItemVm);
}
DoOnUiThread(() =>
{
ListHelpers.InPlaceUpdateList(Items, newViewModels, out var removed);
});
// TODO! dispose removed VMs
}
public override void InitializeProperties()
{
var command = _rootItem.Command;
var list = command.Model.Unsafe as IListPage;
if (list is not null)
{
InitializeFromList(list);
list.ItemsChanged += HandleItemsChanged;
}
else
{
DoOnUiThread(() =>
{
var dockItem = new DockItemViewModel(_rootItem, _showLabels);
dockItem.SlowInitializeProperties();
Items.Add(dockItem);
});
}
}
private void HandleItemsChanged(object sender, IItemsChangedEventArgs args)
{
if (_rootItem.Command.Model.Unsafe is IListPage p)
{
InitializeFromList(p);
}
}
}
public partial class DockItemViewModel : CommandItemViewModel
{
private bool _showLabel = true;
internal bool ShowLabel
{
get => _showLabel;
set
{
_showLabel = value;
UpdateProperty(nameof(HasText));
}
}
public override string Title => ItemTitle;
public override bool HasText => _showLabel ? base.HasText : false;
/// <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(Title) && !string.IsNullOrEmpty(Subtitle) ?
$"{Title}\n{Subtitle}" :
Title + Subtitle;
public DockItemViewModel(CommandItemViewModel root, bool showLabel)
: this(root.Model, root.PageContext, showLabel)
{
}
public DockItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext, bool showLabel)
: base(item, errorContext)
{
_showLabel = showLabel;
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,185 @@
// 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.Core.Common;
using Microsoft.CmdPal.Core.ViewModels;
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 : IDisposable,
IRecipient<CommandsReloadedMessage>,
IPageContext
{
private readonly TopLevelCommandManager _topLevelCommandManager;
private DockSettings _settings;
// private DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
// private DispatcherQueue _updateWindowsQueue = DispatcherQueueController.CreateOnDedicatedThread().DispatcherQueue;
public TaskScheduler Scheduler { get; }
public ObservableCollection<DockBandViewModel> StartItems { get; } = new();
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
public DockViewModel(
TopLevelCommandManager tlcManager,
SettingsModel settings,
TaskScheduler scheduler)
{
_topLevelCommandManager = tlcManager;
_settings = settings.DockSettings;
Scheduler = scheduler;
WeakReferenceMessenger.Default.Register<CommandsReloadedMessage>(this);
}
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.EndBands, EndItems);
}
private void SetupBands(
List<DockBandSettings> bands,
ObservableCollection<DockBandViewModel> target)
{
List<DockBandViewModel> newBands = new();
foreach (var band in bands)
{
var commandId = band.Id;
var topLevelCommand = _topLevelCommandManager.LookupDockBand(commandId);
if (topLevelCommand is null)
{
Logger.LogWarning($"Failed to find band {commandId}");
}
if (topLevelCommand is not null)
{
var bandVm = CreateBandItem(band, topLevelCommand.ItemViewModel);
newBands.Add(bandVm);
}
}
var beforeCount = target.Count;
var afterCount = newBands.Count;
DoOnUiThread(() =>
{
ListHelpers.InPlaceUpdateList(target, newBands, out var removed);
var isStartBand = target == StartItems;
var label = isStartBand ? "Start bands:" : "End bands:";
Logger.LogDebug($"{label} ({beforeCount}) -> ({afterCount}), Removed {removed?.Count ?? 0} items");
});
}
public void Dispose()
{
}
public void Receive(CommandsReloadedMessage message)
{
SetupBands();
CoreLogger.LogDebug("Bands reloaded");
}
private DockBandViewModel CreateBandItem(
DockBandSettings bandSettings,
CommandItemViewModel commandItem)
{
DockBandViewModel band = new(commandItem, new(this), bandSettings, _settings);
band.InitializeProperties(); // TODO! make async
return band;
}
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 EndItems)
{
if (band.Id == id)
{
return band;
}
}
return null;
}
public void ShowException(Exception ex, string? extensionHint = null)
{
var extensionText = extensionHint ?? "<unknown>";
CoreLogger.LogError($"Error in extension {extensionText}", ex);
}
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(this));
vm.SlowInitializeProperties();
return vm;
}
private sealed partial class DockContextMenuItem : CommandItem
{
public DockContextMenuItem()
{
var openSettingsCommand = new AnonymousCommand(
action: () =>
{
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
})
{
Name = "Customize", // TODO!Loc
Icon = Icons.SettingsIcon,
};
MoreCommands = new CommandContextItem[]
{
new CommandContextItem(openSettingsCommand),
};
}
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.UI.ViewModels;
internal sealed class Icons
{
internal static IconInfo PinIcon => new("\uE718"); // Pin icon
internal static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon
internal static IconInfo SettingsIcon => new("\uE713"); // Settings icon
}

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 CommandsReloadedMessage();

View File

@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.UI.Messages;
public record OpenSettingsMessage()
public record OpenSettingsMessage(string? Page = null)
{
}

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

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Open Command Palette.
/// </summary>
public static string builtin_command_palette_title {
get {
return ResourceManager.GetString("builtin_command_palette_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Create another.
/// </summary>
@@ -205,7 +214,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
/// <summary>
/// Looks up a localized string similar to Create a new extension.
/// Looks up a localized string similar to Create extension.
/// </summary>
public static string builtin_create_extension_title {
get {
@@ -285,6 +294,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Built-in.
/// </summary>
public static string builtin_extension_name_fallback {
get {
return ResourceManager.GetString("builtin_extension_name_fallback", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Home.
/// </summary>
@@ -349,7 +367,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
/// <summary>
/// Looks up a localized string similar to Creates a project for a new Command Palette extension.
/// Looks up a localized string similar to Generate a new Command Palette extension project.
/// </summary>
public static string builtin_new_extension_subtitle {
get {
@@ -358,7 +376,7 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
/// <summary>
/// Looks up a localized string similar to Open Settings.
/// Looks up a localized string similar to Open Command Palette settings.
/// </summary>
public static string builtin_open_settings_name {
get {
@@ -366,15 +384,6 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Open Command Palette settings.
/// </summary>
public static string builtin_open_settings_subtitle {
get {
return ResourceManager.GetString("builtin_open_settings_subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exit Command Palette.
/// </summary>
@@ -428,5 +437,41 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
return ResourceManager.GetString("builtin_settings_extension_n_extensions_installed", 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 Pin to dock.
/// </summary>
public static string dock_pin_command_name {
get {
return ResourceManager.GetString("dock_pin_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unpin from dock.
/// </summary>
public static string dock_unpin_command_name {
get {
return ResourceManager.GetString("dock_unpin_command_name", resourceCulture);
}
}
}
}

View File

@@ -239,4 +239,28 @@
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
<value>{0} extensions installed</value>
</data>
<data name="builtin_extension_name_fallback" xml:space="preserve">
<value>Built-in</value>
<comment>Fallback name for built-in extensions</comment>
</data>
<data name="dock_pin_command_name" xml:space="preserve">
<value>Pin to dock</value>
<comment>Command name for pinning an item to the dock</comment>
</data>
<data name="dock_unpin_command_name" xml:space="preserve">
<value>Unpin from dock</value>
<comment>Command name for unpinning an item from the dock</comment>
</data>
<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>
</root>

View File

@@ -0,0 +1,86 @@
// 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.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;
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
public List<string> PinnedCommands { get; set; } = [];
public List<DockBandSettings> StartBands { get; set; } = [];
public List<DockBandSettings> EndBands { get; set; } = [];
public bool ShowLabels { get; set; } = true;
public DockSettings()
{
// Initialize with default values
PinnedCommands = [
"com.microsoft.cmdpal.winget"
];
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.home" });
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.winget", ShowLabels = false });
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.performanceMonitor" });
EndBands.Add(new DockBandSettings { Id = "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 string Id { get; set; } = string.Empty;
public bool? ShowLabels { get; set; }
/// <summary>
/// Resolves the effective value of <see cref="ShowLabels"/> 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 ResolveShowLabels(bool defaultValue) => ShowLabels ?? defaultValue;
}
public enum DockSide
{
Left = 0,
Top = 1,
Right = 2,
Bottom = 3,
}
public enum DockSize
{
Small,
Medium,
Large,
}
public enum DockBackdrop
{
Mica,
Transparent,
Acrylic,
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -60,6 +60,10 @@ public partial class SettingsModel : ObservableObject
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
public bool EnableDock { get; set; }
public DockSettings DockSettings { get; set; } = new();
// END SETTINGS
///////////////////////////////////////////////////////////////////////////

View File

@@ -4,6 +4,8 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.Extensions.DependencyInjection;
@@ -160,6 +162,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; } = [];
public SettingsExtensionsViewModel Extensions { get; }

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.
// See the LICENSE file in the project root for more information.
@@ -44,6 +44,8 @@ public partial class TopLevelCommandManager : ObservableObject,
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
[ObservableProperty]
public partial bool IsLoading { get; private set; } = true;
@@ -79,12 +81,23 @@ public partial class TopLevelCommandManager : ObservableObject,
_builtInCommands.Add(wrapper);
}
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
var objects = await LoadTopLevelCommandsFromProvider(wrapper);
lock (TopLevelCommands)
{
foreach (var c in commands)
if (objects.Commands is IEnumerable<TopLevelViewModel> commands)
{
TopLevelCommands.Add(c);
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
if (objects.DockBands is IEnumerable<TopLevelViewModel> bands)
{
foreach (var c in bands)
{
DockBands.Add(c);
}
}
}
}
@@ -97,7 +110,7 @@ public partial class TopLevelCommandManager : ObservableObject,
}
// May be called from a background thread
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
{
WeakReference<IPageContext> weakSelf = new(this);
@@ -107,6 +120,7 @@ public partial class TopLevelCommandManager : ObservableObject,
() =>
{
List<TopLevelViewModel> commands = [];
List<TopLevelViewModel> bands = [];
foreach (var item in commandProvider.TopLevelItems)
{
commands.Add(item);
@@ -120,7 +134,15 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
return commands;
foreach (var item in commandProvider.DockBandItems)
{
bands.Add(item);
}
var commandsCount = commands.Count;
var bandsCount = bands.Count;
Logger.LogDebug($"{commandProvider.ProviderId}: Loaded {commandsCount} commands, {bandsCount} bands");
return new TopLevelObjectSets(commands, bands);
},
CancellationToken.None,
TaskCreationOptions.None,
@@ -160,6 +182,8 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
List<TopLevelViewModel> newBands = [.. sender.DockBandItems];
// modify the TopLevelCommands under shared lock; event if we clone it, we don't want
// TopLevelCommands to get modified while we're working on it. Otherwise, we might
// out clone would be stale at the end of this method.
@@ -176,6 +200,13 @@ public partial class TopLevelCommandManager : ObservableObject,
clone.InsertRange(startIndex, newItems);
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
// 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;
@@ -222,6 +253,7 @@ public partial class TopLevelCommandManager : ObservableObject,
lock (TopLevelCommands)
{
TopLevelCommands.Clear();
DockBands.Clear();
}
await LoadBuiltinsAsync();
@@ -300,17 +332,34 @@ public partial class TopLevelCommandManager : ObservableObject,
lock (TopLevelCommands)
{
foreach (var commands in commandSets)
foreach (var providerObjects in commandSets)
{
foreach (var c in commands)
var commandsCount = providerObjects.Commands?.Count() ?? 0;
var bandsCount = providerObjects.DockBands?.Count() ?? 0;
Logger.LogDebug($"(some provider) Loaded {commandsCount} commands and {bandsCount} bands");
if (providerObjects.Commands is IEnumerable<TopLevelViewModel> commands)
{
TopLevelCommands.Add(c);
foreach (var c in commands)
{
TopLevelCommands.Add(c);
}
}
if (providerObjects.DockBands is IEnumerable<TopLevelViewModel> bands)
{
foreach (var c in bands)
{
DockBands.Add(c);
}
}
}
}
timer.Stop();
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
}
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
@@ -328,7 +377,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
{
@@ -408,6 +459,23 @@ public partial class TopLevelCommandManager : ObservableObject,
return null;
}
public TopLevelViewModel? LookupDockBand(string id)
{
// TODO! bad that we're using TopLevelCommands as the object to lock, even for bands
lock (TopLevelCommands)
{
foreach (var command in DockBands)
{
if (command.Id == id)
{
return command;
}
}
}
return null;
}
public void Receive(ReloadCommandsMessage message) =>
ReloadAllCommandsAsync().ConfigureAwait(false);
@@ -426,6 +494,41 @@ public partial class TopLevelCommandManager : ObservableObject,
}
}
internal void PinDockBand(TopLevelViewModel bandVm)
{
lock (DockBands)
{
foreach (var existing in DockBands)
{
if (existing.Id == bandVm.Id)
{
// already pinned
Logger.LogDebug($"Dock band '{bandVm.Id}' is already pinned.");
return;
}
}
Logger.LogDebug($"Attempting to pin dock band '{bandVm.Id}' from provider '{bandVm.CommandProviderId}'.");
var providerId = bandVm.CommandProviderId;
var foundProvider = false;
foreach (var provider in CommandProviders)
{
if (provider.Id == providerId)
{
Logger.LogDebug($"Found provider '{providerId}' to pin dock band '{bandVm.Id}'.");
provider.PinDockBand(bandVm);
foundProvider = true;
break;
}
}
if (!foundProvider)
{
Logger.LogWarning($"Could not find provider '{providerId}' to pin dock band '{bandVm.Id}'.");
}
}
}
public void Dispose()
{
_reloadCommandsGate.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.
// See the LICENSE file in the project root for more information.
@@ -6,8 +6,10 @@ using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
@@ -24,6 +26,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
private readonly ProviderSettings _providerSettings;
private readonly IServiceProvider _serviceProvider;
private readonly CommandItemViewModel _commandItemViewModel;
private readonly DockViewModel? _dockViewModel;
private readonly string _commandProviderId;
@@ -45,39 +48,28 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
public CommandPaletteHost ExtensionHost { get; private set; }
public string ExtensionName => ExtensionHost.Extension?.ExtensionDisplayName ?? Properties.Resources.builtin_extension_name_fallback;
public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
public string CommandProviderId => _commandProviderId;
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
////// ICommandItem
public string Title => _commandItemViewModel.Title;
public string Subtitle => _commandItemViewModel.Subtitle;
public IIconInfo Icon => _commandItemViewModel.Icon;
public IIconInfo Icon => (IIconInfo)IconViewModel;
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
.Select(item =>
{
if (item is ISeparatorContextItem)
{
return item as IContextItem;
}
else if (item is CommandContextItemViewModel commandItem)
{
return commandItem.Model.Unsafe;
}
else
{
return null;
}
}).ToArray();
IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();
////// IListItem
ITag[] IListItem.Tags => Tags.ToArray();
@@ -170,9 +162,37 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
}
}
// Dock properties
public bool IsDockBand { get; private set; }
public DockBandSettings? DockBandSettings
{
get
{
if (!IsDockBand)
{
return null;
}
var bandSettings = _settings.DockSettings.StartBands
.Concat(_settings.DockSettings.EndBands)
.FirstOrDefault(band => band.Id == this.Id);
if (bandSettings is null)
{
return new DockBandSettings()
{
Id = this.Id,
ShowLabels = true,
};
}
return bandSettings;
}
}
public TopLevelViewModel(
CommandItemViewModel item,
bool isFallback,
TopLevelType topLevelType,
CommandPaletteHost extensionHost,
string commandProviderId,
SettingsModel settings,
@@ -185,19 +205,19 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
_commandProviderId = commandProviderId;
_commandItemViewModel = item;
IsFallback = isFallback;
IsFallback = topLevelType == TopLevelType.Fallback;
IsDockBand = topLevelType == TopLevelType.DockBand;
ExtensionHost = extensionHost;
item.PropertyChanged += Item_PropertyChanged;
// UpdateAlias();
// UpdateHotkey();
// UpdateTags();
_dockViewModel = serviceProvider.GetService<DockViewModel>();
}
internal void InitializeProperties()
{
ItemViewModel.SlowInitializeProperties();
GenerateId();
if (IsFallback)
{
@@ -242,7 +262,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
return;
}
_initialIcon = _commandItemViewModel.Icon;
_initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
if (raiseNotification)
{
@@ -394,4 +414,150 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
{
return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}";
}
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);
}
}
var dockEnabled = _settings.EnableDock;
if (dockEnabled && _dockViewModel is not null)
{
// Add a separator
contextItems.Add(new Separator());
var inStartBands = _settings.DockSettings.StartBands.Any(band => band.Id == this.Id);
var inEndBands = _settings.DockSettings.EndBands.Any(band => band.Id == this.Id);
var alreadyPinned = (inStartBands || inEndBands) &&
_settings.DockSettings.PinnedCommands.Contains(this.Id);
var pinCommand = new PinToDockCommand(
this,
!alreadyPinned,
_dockViewModel,
_settings,
_serviceProvider.GetService<TopLevelCommandManager>()!);
var contextItem = new CommandContextItem(pinCommand);
contextItems.Add(contextItem);
}
return contextItems.ToArray();
}
internal ICommandItem ToPinnedDockBandItem()
{
var item = new PinnedDockItem(item: this, id: Id);
return item;
}
internal TopLevelViewModel CloneAsBand()
{
return new TopLevelViewModel(
_commandItemViewModel,
TopLevelType.DockBand,
ExtensionHost,
_commandProviderId,
_settings,
_providerSettings,
_serviceProvider);
}
private sealed partial class PinToDockCommand : InvokableCommand
{
private readonly TopLevelViewModel _topLevelViewModel;
private readonly DockViewModel _dockViewModel;
private readonly SettingsModel _settings;
private readonly TopLevelCommandManager _topLevelCommandManager;
private readonly bool _pin;
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
public override string Name => _pin ? Properties.Resources.dock_pin_command_name : Properties.Resources.dock_unpin_command_name;
public PinToDockCommand(
TopLevelViewModel topLevelViewModel,
bool pin,
DockViewModel dockViewModel,
SettingsModel settings,
TopLevelCommandManager topLevelCommandManager)
{
_topLevelViewModel = topLevelViewModel;
_dockViewModel = dockViewModel;
_settings = settings;
_topLevelCommandManager = topLevelCommandManager;
_pin = pin;
}
public override CommandResult Invoke()
{
Logger.LogDebug($"PinToDockCommand.Invoke({_pin}): {_topLevelViewModel.Id}");
if (_pin)
{
PinToDock();
}
else
{
UnpinFromDock();
}
// Notify that the MoreCommands have changed, so the context menu updates
_topLevelViewModel.PropChanged?.Invoke(
_topLevelViewModel,
new PropChangedEventArgs(nameof(ICommandItem.MoreCommands)));
return CommandResult.GoHome();
}
private void PinToDock()
{
// TODO! Deal with "the command ID is already pinned in PinnedCommands but not in one of StartBands/EndBands"
if (!_settings.DockSettings.PinnedCommands.Contains(_topLevelViewModel.Id))
{
_settings.DockSettings.PinnedCommands.Add(_topLevelViewModel.Id);
}
_settings.DockSettings.StartBands.Add(new DockBandSettings()
{
Id = _topLevelViewModel.Id,
ShowLabels = true,
});
// Create a new band VM from our current TLVM. This will allow us to
// update the bands in the CommandProviderWrapper and the TLCM,
// without forcing a whole reload
var bandVm = _topLevelViewModel.CloneAsBand();
_topLevelCommandManager.PinDockBand(bandVm);
_topLevelViewModel.Save();
}
private void UnpinFromDock()
{
_settings.DockSettings.PinnedCommands.Remove(_topLevelViewModel.Id);
_settings.DockSettings.StartBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
_settings.DockSettings.EndBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
_topLevelViewModel.Save();
}
}
}
public enum TopLevelType
{
Normal,
Fallback,
DockBand,
}

View File

@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:local="using:Microsoft.CmdPal.UI">
<Application.Resources>
<ResourceDictionary>
@@ -11,6 +12,7 @@
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
<ResourceDictionary Source="Styles/Colors.xaml" />
<ResourceDictionary Source="Styles/Button.xaml" />
<ResourceDictionary Source="Styles/TextBlock.xaml" />
<ResourceDictionary Source="Styles/TextBox.xaml" />
<ResourceDictionary Source="Styles/Settings.xaml" />
@@ -22,6 +24,14 @@
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="controls:CheckBoxWithDescriptionControl" />
<converters:StringVisibilityConverter
x:Key="StringNotEmptyToVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -12,6 +12,7 @@ using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CmdPal.Ext.PerformanceMonitor;
using Microsoft.CmdPal.Ext.Registry;
using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell;
@@ -26,6 +27,7 @@ using Microsoft.CmdPal.Ext.WinGet;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;
@@ -154,6 +156,7 @@ public partial class App : Application
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
// Models
services.AddSingleton<TopLevelCommandManager>();
@@ -173,6 +176,7 @@ public partial class App : Application
// ViewModels
services.AddSingleton<ShellViewModel>();
services.AddSingleton<DockViewModel>();
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
return services.BuildServiceProvider();

View File

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

View File

@@ -126,7 +126,7 @@ public sealed partial class CommandBar : UserControl,
private void SettingsIcon_Clicked(object sender, RoutedEventArgs e)
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
}
private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e)

View File

@@ -0,0 +1,261 @@
<?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"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<StackLayout
x:Key="ItemsOrientation"
Orientation="{x:Bind ItemsOrientation, Mode=OneWay}"
Spacing="4" />
<Style x:Key="ResizingIconStyle" TargetType="cpcontrols:IconBox">
<Setter Property="Height" Value="{x:Bind IconSize, Mode=OneWay}" />
<Setter Property="MaxWidth" Value="{x:Bind IconSize, Mode=OneWay}" />
<Setter Property="MinWidth" Value="{x:Bind IconMinWidth, Mode=OneWay}" />
</Style>
<Style x:Key="ResizingTitleTextBlock" TargetType="TextBlock">
<Setter Property="FontSize" Value="{x:Bind TitleTextFontSize, Mode=OneWay}" />
<Setter Property="MaxWidth" Value="{x:Bind TitleTextMaxWidth, Mode=OneWay}" />
</Style>
<local:IconInfoVisibilityConverter x:Key="IconInfoVisibilityConverter" />
<DataTemplate x:Key="DeskbandTemplate" x:DataType="dockVm:DockItemViewModel">
<Button
VerticalAlignment="Stretch"
DataContext="{x:Bind}"
RightTapped="BandItem_RightTapped"
Style="{StaticResource TaskBarButtonStyle}"
Tapped="BandItem_Tapped"
ToolTipService.ToolTip="{x:Bind Tooltip, Mode=OneWay}">
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel
Grid.Column="0"
VerticalAlignment="Center"
Orientation="Vertical"
Visibility="{x:Bind Icon, Converter={StaticResource IconInfoVisibilityConverter}, Mode=OneWay}">
<cpcontrols:IconBox
x:Name="IconBorder"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
Style="{StaticResource ResizingIconStyle}" />
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="8,0,8,0"
VerticalAlignment="Center"
Visibility="{x:Bind HasText, Mode=OneWay}">
<TextBlock
x:Name="TitleTextBlock"
Margin="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="12"
Style="{StaticResource ResizingTitleTextBlock}"
Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Left"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
x:Name="SubTitleTextBlock"
MaxWidth="100"
Margin="0,-4,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="10"
Foreground="{ThemeResource TextFillColorTertiary}"
Text="{x:Bind Subtitle, Mode=OneWay}"
TextAlignment="Center"
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringNotEmptyToVisibilityConverter}}" />
</StackPanel>
</Grid>
</Button>
</DataTemplate>
<DataTemplate x:Key="DockBandTemplate" x:DataType="dockVm:DockBandViewModel">
<ItemsRepeater
HorizontalAlignment="Center"
ItemTemplate="{StaticResource DeskbandTemplate}"
ItemsSource="{x:Bind Items, Mode=OneWay}"
Layout="{StaticResource ItemsOrientation}">
<ItemsRepeater.Transitions>
<TransitionCollection />
</ItemsRepeater.Transitions>
</ItemsRepeater>
</DataTemplate>
<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>
</ResourceDictionary>
</UserControl.Resources>
<Grid
x:Name="RootGrid"
BorderBrush="{ThemeResource SurfaceStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
RightTapped="RootGrid_RightTapped">
<Grid x:Name="ContentGrid" Padding="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition x:Name="EndColumn" Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition x:Name="EndRow" Height="Auto" />
</Grid.RowDefinitions>
<ScrollViewer
x:Name="StartItemsView"
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalScrollBarVisibility="Hidden"
HorizontalScrollMode="Enabled"
VerticalScrollBarVisibility="Auto"
VerticalScrollMode="Disabled">
<ItemsRepeater
x:Name="StartItemsRepeater"
HorizontalAlignment="Left"
ItemTemplate="{StaticResource DockBandTemplate}"
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
Layout="{StaticResource ItemsOrientation}">
<ItemsRepeater.Transitions>
<TransitionCollection />
</ItemsRepeater.Transitions>
</ItemsRepeater>
</ScrollViewer>
<ItemsRepeater
x:Name="EndItemsRepeater"
Grid.Column="1"
ItemTemplate="{StaticResource DockBandTemplate}"
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
Layout="{StaticResource ItemsOrientation}">
<ItemsRepeater.Transitions>
<TransitionCollection />
</ItemsRepeater.Transitions>
</ItemsRepeater>
</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.Setters>
<Setter Target="StartItemsView.(Grid.Row)" Value="0" />
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="3" />
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="1" />
<Setter Target="EndItemsRepeater.(Grid.Row)" Value="0" />
<Setter Target="EndItemsRepeater.(Grid.Column)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.RowSpan)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.ColumnSpan)" Value="1" />
<Setter Target="EndItemsRepeater.HorizontalAlignment" Value="Right" />
<Setter Target="ContentGrid.Margin" Value="8,0,8,0" />
<Setter Target="RootGrid.BorderThickness" Value="0,0,0,1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="DockOnBottom">
<VisualState.StateTriggers>
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Bottom" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="StartItemsView.(Grid.Row)" Value="0" />
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="3" />
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="1" />
<Setter Target="EndItemsRepeater.(Grid.Row)" Value="0" />
<Setter Target="EndItemsRepeater.(Grid.Column)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.RowSpan)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.ColumnSpan)" Value="1" />
<Setter Target="EndItemsRepeater.HorizontalAlignment" Value="Right" />
<Setter Target="ContentGrid.Margin" Value="8,0,8,0" />
<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="StartItemsView.(Grid.Row)" Value="0" />
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="1" />
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.Row)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.Column)" Value="0" />
<Setter Target="EndItemsRepeater.(Grid.RowSpan)" Value="1" />
<Setter Target="EndItemsRepeater.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndItemsRepeater.HorizontalAlignment" Value="Stretch" />
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
<Setter Target="RootGrid.BorderThickness" Value="0,0,1,0" />
</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="StartItemsView.(Grid.Row)" Value="0" />
<Setter Target="StartItemsView.(Grid.Column)" Value="0" />
<Setter Target="StartItemsView.(Grid.RowSpan)" Value="1" />
<Setter Target="StartItemsView.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.Row)" Value="3" />
<Setter Target="EndItemsRepeater.(Grid.Column)" Value="0" />
<Setter Target="EndItemsRepeater.(Grid.RowSpan)" Value="1" />
<Setter Target="EndItemsRepeater.(Grid.ColumnSpan)" Value="3" />
<Setter Target="EndItemsRepeater.HorizontalAlignment" Value="Stretch" />
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
<Setter Target="RootGrid.BorderThickness" Value="1,0,0,0" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,231 @@
// 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.ComponentModel;
using System.Runtime.InteropServices;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.UI;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Media;
using Windows.Foundation;
namespace Microsoft.CmdPal.UI.Dock;
#pragma warning disable SA1402 // File may only contain a single type
public sealed partial class DockControl : UserControl, INotifyPropertyChanged, IRecipient<CloseContextMenuMessage>
{
private DockViewModel _viewModel;
internal DockViewModel ViewModel => _viewModel;
public event PropertyChangedEventHandler? PropertyChanged;
public Orientation ItemsOrientation
{
get => field;
set
{
if (field != value)
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(ItemsOrientation)));
}
}
}
public DockSide DockSide
{
get => field;
set
{
if (field != value)
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(DockSide)));
}
}
}
public double IconSize
{
get => field;
set
{
if (field != value)
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(IconSize)));
PropertyChanged?.Invoke(this, new(nameof(IconMinWidth)));
}
}
}
= 16.0;
public double IconMinWidth => IconSize / 2;
public double TitleTextMaxWidth
{
get => field;
set
{
if (field != value)
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(TitleTextMaxWidth)));
}
}
}
= 100;
public double TitleTextFontSize
{
get => field;
set
{
if (field != value)
{
field = value;
PropertyChanged?.Invoke(this, new(nameof(TitleTextFontSize)));
}
}
}
= 12;
internal DockControl(DockViewModel viewModel)
{
_viewModel = viewModel;
InitializeComponent();
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
}
internal void UpdateSettings(DockSettings settings)
{
DockSide = settings.Side;
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
IconSize = DockSettingsToViews.IconSizeForSize(settings.DockIconsSize);
TitleTextFontSize = DockSettingsToViews.TitleTextFontSizeForSize(settings.DockSize);
TitleTextMaxWidth = DockSettingsToViews.TitleTextMaxWidthForSize(settings.DockSize);
if (settings.Backdrop == DockBackdrop.Transparent)
{
RootGrid.BorderBrush = new SolidColorBrush(Colors.Transparent);
}
}
private void BandItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
var pos = e.GetPosition(null);
var button = sender as Button;
var item = button?.DataContext as DockItemViewModel;
if (item is not null)
{
// Use the center of the button as the point to open at. This is
// more reliable than using the tap position. This allows multiple
// clicks anywhere in the button to open the palette in a consistent
// location.
var buttonPos = button!.TransformToVisual(null).TransformPoint(new Point(0, 0));
var buttonCenter = new Point(
buttonPos.X + (button.ActualWidth / 2),
buttonPos.Y + (button.ActualHeight / 2));
InvokeItem(item, buttonCenter);
}
}
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
{
var pos = e.GetPosition(null);
var button = sender as Button;
var item = button?.DataContext as DockItemViewModel;
if (item is not null)
{
if (item.HasMoreCommands)
{
ContextControl.ViewModel.SelectedItem = item;
ContextMenuFlyout.ShowAt(
button,
new FlyoutShowOptions()
{
ShowMode = FlyoutShowMode.Standard,
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
});
e.Handled = true;
}
}
}
private void InvokeItem(DockItemViewModel item, global::Windows.Foundation.Point pos)
{
var command = item.Command;
try
{
var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
if (isPage)
{
WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos));
}
PerformCommandMessage m = new(command.Model);
m.WithAnimation = false;
m.TransientPage = true;
WeakReferenceMessenger.Default.Send(m);
}
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;
}
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,86 @@
// 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.Media;
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 TitleTextFontSizeForSize(DockSize size)
{
return size switch
{
DockSize.Small => 12,
DockSize.Medium => 16,
DockSize.Large => 20,
_ => throw new NotImplementedException(),
};
}
public static double TitleTextMaxWidthForSize(DockSize size)
{
return WidthForSize(size) - TitleTextFontSizeForSize(size);
}
public static double HeightForSize(DockSize size)
{
return size switch
{
DockSize.Small => 40,
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.Mica => new MicaBackdrop(),
DockBackdrop.Transparent => new TransparentTintBackdrop(),
DockBackdrop.Acrylic => null, // new DesktopAcrylicBackdrop(),
_ => 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(),
};
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,22 @@
<?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:toolkit="using:CommunityToolkit.WinUI.Controls"
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" />
</winuiex:WindowEx>

View File

@@ -0,0 +1,711 @@
// 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.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.UI;
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 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>()!;
_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
// appbar 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);
UpdateSettings();
}
private void SettingsChangedHandler(SettingsModel sender, object? args)
{
_settings = sender.DockSettings;
UpdateSettings();
}
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 UpdateSettings()
{
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();
}
_acrylicController = GetAcrylicConfig(Content);
// 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 static DesktopAcrylicController GetAcrylicConfig(UIElement content)
{
var feContent = content as FrameworkElement;
return feContent?.ActualTheme == ElementTheme.Light
? new DesktopAcrylicController()
{
Kind = DesktopAcrylicKind.Thin,
TintColor = Color.FromArgb(255, 243, 243, 243),
LuminosityOpacity = 0.90f,
TintOpacity = 0.0f,
FallbackColor = Color.FromArgb(255, 238, 238, 238),
}
: new DesktopAcrylicController()
{
Kind = DesktopAcrylicKind.Thin,
TintColor = Color.FromArgb(255, 32, 32, 32),
LuminosityOpacity = 0.96f,
TintOpacity = 0.5f,
FallbackColor = Color.FromArgb(255, 28, 28, 28),
};
}
private void CreateAppBar(HWND hwnd)
{
_appBarData = new APPBARDATA
{
cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
hWnd = hwnd,
uCallbackMessage = _callbackMessageId,
};
// Register this window as an appbar
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 autohide bars.
// I think it's something like this, but I don't totally know
// // _appBarData.lParam = ABS_ALWAYSONTOP;
// _appBarData.lParam = (LPARAM)(int)PInvoke.ABS_AUTOHIDE;
// PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
// PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);
// Account for system borders when moving the window
// Adjust position to account for window frame/border
var adjustedLeft = _appBarData.rc.left - frameWidth;
var adjustedTop = _appBarData.rc.top - frameWidth;
var adjustedWidth = (_appBarData.rc.right - _appBarData.rc.left) + (2 * frameWidth);
var adjustedHeight = (_appBarData.rc.bottom - _appBarData.rc.top) + (2 * frameWidth);
// Move the actual window
PInvoke.MoveWindow(
_hwnd,
adjustedLeft,
adjustedTop,
adjustedWidth,
adjustedHeight,
true);
}
private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
{
Logger.LogDebug("UpdateAppBarDataForEdge");
var horizontalHeightDips = DockSettingsToViews.HeightForSize(size);
var verticalWidthDips = DockSettingsToViews.WidthForSize(size);
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
if (side == DockSide.Top)
{
_appBarData.uEdge = PInvoke.ABE_TOP;
_appBarData.rc.left = 0;
_appBarData.rc.top = 0;
_appBarData.rc.right = screenWidth;
_appBarData.rc.bottom = (int)(horizontalHeightDips * scaleFactor);
}
else if (side == DockSide.Bottom)
{
var heightPixels = (int)(horizontalHeightDips * scaleFactor);
_appBarData.uEdge = PInvoke.ABE_BOTTOM;
_appBarData.rc.left = 0;
_appBarData.rc.top = screenHeight - heightPixels;
_appBarData.rc.right = screenWidth;
_appBarData.rc.bottom = screenHeight;
}
else if (side == DockSide.Left)
{
var widthPixels = (int)(verticalWidthDips * scaleFactor);
_appBarData.uEdge = PInvoke.ABE_LEFT;
_appBarData.rc.left = 0;
_appBarData.rc.top = 0;
_appBarData.rc.right = widthPixels;
_appBarData.rc.bottom = screenHeight;
}
else if (side == DockSide.Right)
{
var widthPixels = (int)(verticalWidthDips * scaleFactor);
_appBarData.uEdge = PInvoke.ABE_RIGHT;
_appBarData.rc.left = screenWidth - widthPixels;
_appBarData.rc.top = 0;
_appBarData.rc.right = screenWidth;
_appBarData.rc.bottom = screenHeight;
}
else
{
return;
}
}
private LRESULT CustomWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
{
// check settings changed
if (msg == PInvoke.WM_SETTINGCHANGE)
{
var isFullscreen = IsWindowFullscreen();
Logger.LogDebug($"WM_SETTINGCHANGE ({isFullscreen})");
if (isFullscreen)
{
this.Hide();
}
else
{
this.Show();
}
if (wParam == (uint)SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETWORKAREA)
{
Logger.LogDebug($"WM_SETTINGCHANGE(SPI_SETWORKAREA)");
// Use debounced call to throttle rapid successive calls
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
}
}
else if (msg == PInvoke.WM_DISPLAYCHANGE)
{
Logger.LogDebug("WM_DISPLAYCHANGE");
// Use dispatcher to ensure we're on the UI thread
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
}
// Intercept WM_SYSCOMMAND to prevent minimize and maximize
else if (msg == PInvoke.WM_SYSCOMMAND)
{
var command = (int)(wParam.Value & 0xFFF0);
if (command == PInvoke.SC_MINIMIZE || command == PInvoke.SC_MAXIMIZE)
{
// Block minimize and maximize commands
return new LRESULT(0);
}
}
// Stop min/max on WM_WINDOWPOSCHANGING too
else if (msg == PInvoke.WM_WINDOWPOSCHANGING)
{
unsafe
{
var pWindowPos = (WINDOWPOS*)lParam.Value;
// Check if the window is being hidden (minimized) or if flags suggest minimize/maximize
if ((pWindowPos->flags & SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW) != 0)
{
// Prevent hiding the window (minimize)
pWindowPos->flags &= ~SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW;
}
// Additional check: if the window position suggests it's being minimized or maximized
// by checking for dramatic size changes
if (pWindowPos->cx <= 0 || pWindowPos->cy <= 0)
{
// Prevent zero or negative size changes (minimize)
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
}
}
}
// Handle WM_SIZE to prevent minimize/maximize state changes
else if (msg == PInvoke.WM_SIZE)
{
var sizeType = (int)wParam.Value;
if (sizeType == PInvoke.SIZE_MINIMIZED || sizeType == PInvoke.SIZE_MAXIMIZED)
{
// Block the size change by not calling the original window procedure
return new LRESULT(0);
}
}
// Handle WM_SHOWWINDOW to prevent hiding (minimize)
else if (msg == PInvoke.WM_SHOWWINDOW)
{
var isBeingShown = wParam.Value != 0;
if (!isBeingShown)
{
// Prevent hiding the window
return new LRESULT(0);
}
}
// Handle double-click on title bar (non-client area)
else if (msg == PInvoke.WM_NCLBUTTONDBLCLK)
{
var hitTest = (int)wParam.Value;
if (hitTest == PInvoke.HTCAPTION)
{
// Block double-click on title bar to prevent maximize
return new LRESULT(0);
}
}
// Handle WM_GETMINMAXINFO to control window size limits
else if (msg == PInvoke.WM_GETMINMAXINFO)
{
// We can modify the min/max tracking info here if needed
// For now, let it pass through but we could restrict max size
}
// Handle the AppBarMessage message
// This is needed to update the position when the work area changes.
// (notably, when the user toggles auto-hide taskbars)
else if (msg == _callbackMessageId)
{
if (wParam.Value == PInvoke.ABN_POSCHANGED)
{
UpdateWindowPosition();
}
}
else if (msg == WM_TASKBAR_RESTART)
{
Logger.LogDebug("WM_TASKBAR_RESTART");
DispatcherQueue.TryEnqueue(() => CreateAppBar(_hwnd));
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(false));
}
// Call the original window procedure for all other messages
return PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
}
void IRecipient<BringToTopMessage>.Receive(BringToTopMessage message)
{
DispatcherQueue.TryEnqueue(() =>
{
var onTop = message.OnTop ? HWND.HWND_TOPMOST : HWND.HWND_NOTOPMOST;
PInvoke.SetWindowPos(_hwnd, onTop, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
});
}
public static bool IsWindowFullscreen()
{
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
if (Marshal.GetExceptionForHR(PInvoke.SHQueryUserNotificationState(out var state)) is null)
{
if (state == QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN ||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY ||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_PRESENTATION_MODE)
{
return true;
}
}
return false;
}
public void Receive(QuitMessage message)
{
DispatcherQueue.TryEnqueue(() =>
{
DestroyAppBar(_hwnd);
this.Close();
});
}
void IRecipient<RequestShowPaletteAtMessage>.Receive(RequestShowPaletteAtMessage message)
{
DispatcherQueue.TryEnqueue(() => 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 void Dispose()
{
DisposeAcrylic();
viewModel.Dispose();
}
private void DockWindow_Closed(object sender, WindowEventArgs args)
{
var serviceProvider = App.Current.Services;
var settings = serviceProvider.GetService<SettingsModel>();
settings?.SettingsChanged -= SettingsChangedHandler;
DisposeAcrylic();
// Remove our appbar 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

@@ -0,0 +1,34 @@
// 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.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Microsoft.CmdPal.UI.Dock;
public sealed partial class IconInfoVisibilityConverter : IValueConverter
{
private static bool IsVisible(IconInfoViewModel iconInfoViewModel, ElementTheme requestedTheme) =>
iconInfoViewModel?.HasIcon(requestedTheme == Microsoft.UI.Xaml.ElementTheme.Light) ?? false;
private static bool IsVisible(IconInfoViewModel iconInfoViewModel, ApplicationTheme requestedTheme) =>
iconInfoViewModel?.HasIcon(requestedTheme == ApplicationTheme.Light) ?? false;
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is IconInfoViewModel iconInfoVM)
{
return IsVisible(iconInfoVM, Application.Current.RequestedTheme) ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
public IconInfoVisibilityConverter()
{
}
}

View File

@@ -479,7 +479,7 @@ public sealed partial class ListPage : Page,
// Always reset the selected item when the top-level list page changes
// its items
if (!sender.IsNested)
if (sender.IsRootPage)
{
ItemView.SelectedIndex = 0;
}

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.
// See the LICENSE file in the project root for more information.
@@ -152,7 +152,7 @@ internal sealed partial class TrayIconService
{
if (wParam == PInvoke.WM_USER + 1)
{
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
}
else if (wParam == PInvoke.WM_USER + 2)
{

View File

@@ -12,6 +12,7 @@ using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
@@ -48,6 +49,7 @@ namespace Microsoft.CmdPal.UI;
public sealed partial class MainWindow : WindowEx,
IRecipient<DismissMessage>,
IRecipient<ShowWindowMessage>,
IRecipient<ShowPaletteAtMessage>,
IRecipient<HideWindowMessage>,
IRecipient<QuitMessage>,
IDisposable
@@ -105,6 +107,7 @@ public sealed partial class MainWindow : WindowEx,
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
// Hide our titlebar.
@@ -303,6 +306,77 @@ public sealed partial class MainWindow : WindowEx,
}
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
{
var positionWindowForTargetMonitor = (HWND hwnd) =>
{
if (target == MonitorBehavior.ToLast)
{
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(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();
@@ -321,15 +395,9 @@ public sealed partial class MainWindow : WindowEx,
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
}
if (target == MonitorBehavior.ToLast)
if (positionWindow is not null)
{
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
AppWindow.MoveAndResize(newRect);
}
else
{
var display = GetScreen(hwnd, target);
PositionCentered(display);
positionWindow(hwnd);
}
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
@@ -514,6 +582,11 @@ public sealed partial class MainWindow : WindowEx,
ShowHwnd(message.Hwnd, settings.SummonOn);
}
internal void Receive(ShowPaletteAtMessage message)
{
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
}
public void Receive(HideWindowMessage message)
{
// This might come in off the UI thread. Make sure to hop back.
@@ -561,6 +634,8 @@ public sealed partial class MainWindow : WindowEx,
// Sure, it's not ideal, but at least it's not visible.
}
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
// Start auto-go-home timer
RestartAutoGoHome();
}
@@ -953,6 +1028,7 @@ public sealed partial class MainWindow : WindowEx,
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
Cloak();
this.Hide();
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
return;
}
@@ -1001,4 +1077,6 @@ public sealed partial class MainWindow : WindowEx,
_localKeyboardListener.Dispose();
DisposeAcrylic();
}
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
}

View File

@@ -19,6 +19,9 @@
<Version>$(CmdPalVersion)</Version>
<!-- For MVVM Toolkit Partial Properties/AOT support -->
<LangVersion>preview</LangVersion>
<!-- OutputPath is set in CmdPal.Branding.props -->
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
@@ -78,6 +81,7 @@
<None Remove="Pages\Settings\GeneralPage.xaml" />
<None Remove="SettingsWindow.xaml" />
<None Remove="ShellPage.xaml" />
<None Remove="Styles\Button.xaml" />
<None Remove="Styles\Colors.xaml" />
<None Remove="Styles\Settings.xaml" />
<None Remove="Styles\TextBox.xaml" />
@@ -132,6 +136,7 @@
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.PerformanceMonitor\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />
@@ -202,6 +207,12 @@
</Content>
</ItemGroup>
<ItemGroup>
<Page Update="Styles\Button.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="IsEnabledTextBlock.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -59,8 +59,50 @@ GetModuleHandle
GetWindowLong
SetWindowLong
WINDOW_EX_STYLE
CreateWindowEx
WNDCLASSEXW
RegisterClassEx
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

View File

@@ -200,14 +200,19 @@
<!-- Back button -->
<StackPanel Orientation="Horizontal">
<!--
This border is to hold a bit of padding we need when
the back button is hidden
-->
<Border Margin="20,0,0,0" Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<Image
Width="20"
Margin="20,0,6,0"
Margin="0,0,6,0"
HorizontalAlignment="Center"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
AutomationProperties.AccessibilityView="Raw"
Source="ms-appx:///Assets/icon.svg"
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
EasingMode="EaseIn"
@@ -250,7 +255,7 @@
FontSize=14}"
FontSize="16"
Style="{StaticResource SubtleButtonStyle}"
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
EasingMode="EaseIn"
@@ -297,7 +302,7 @@
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<animations:Implicit.ShowAnimations>
<animations:OpacityAnimation
From="0"

View File

@@ -10,11 +10,13 @@ using CommunityToolkit.WinUI;
using ManagedCommon;
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.UI.Dock;
using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages;
using Microsoft.CmdPal.UI.Settings;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.PowerToys.Telemetry;
@@ -25,6 +27,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media.Animation;
using Windows.UI.Core;
using WinUIEx;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using VirtualKey = Windows.System.VirtualKey;
@@ -47,6 +50,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
IRecipient<ShowConfirmationMessage>,
IRecipient<ShowToastMessage>,
IRecipient<NavigateToPageMessage>,
IRecipient<ShowHideDockMessage>,
INotifyPropertyChanged,
IDisposable
{
@@ -64,6 +68,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
private readonly CompositeFormat _pageNavigatedAnnouncement;
private SettingsWindow? _settingsWindow;
private DockWindow? _dockWindow;
private CancellationTokenSource? _focusAfterLoadedCts;
private WeakReference<Page>? _lastNavigatedPageRef;
@@ -94,6 +99,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
@@ -102,6 +109,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
if (App.Current.Services.GetService<SettingsModel>()!.EnableDock)
{
_dockWindow = new DockWindow();
_dockWindow.Show();
}
}
/// <summary>
@@ -245,26 +258,29 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
}
private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
{
vm.SafeInitializePropertiesSynchronous();
}
private void InitializeConfirmationDialog(ConfirmResultViewModel vm) => vm.SafeInitializePropertiesSynchronous();
public void Receive(OpenSettingsMessage message)
{
_ = DispatcherQueue.TryEnqueue(() =>
{
OpenSettings();
OpenSettings(message.Page);
});
}
public void OpenSettings()
public void OpenSettings(string? page = null)
{
if (_settingsWindow is null)
{
_settingsWindow = new SettingsWindow();
}
if (page is not null)
{
_settingsWindow.OpenToPage = page;
_settingsWindow.Navigate(page);
}
_settingsWindow.Activate();
_settingsWindow.BringToFront();
}
@@ -325,10 +341,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
public void Receive(HotkeySummonMessage message)
{
_ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
}
public void Receive(HotkeySummonMessage message) => _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
@@ -397,10 +410,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
}
public void Receive(GoBackMessage message)
{
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
}
public void Receive(GoBackMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
private void GoBack(bool withAnimation = true, bool focusSearch = true)
{
@@ -441,10 +451,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
}
public void Receive(GoHomeMessage message)
{
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
}
public void Receive(GoHomeMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
private void GoHome(bool withAnimation = true, bool focusSearch = true)
{
@@ -462,6 +469,27 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
}
public void Receive(ShowHideDockMessage message)
{
_ = DispatcherQueue.TryEnqueue(() =>
{
if (message.ShowDock)
{
if (_dockWindow is null)
{
_dockWindow = new DockWindow();
}
_dockWindow.Show();
}
else if (_dockWindow is not null)
{
_dockWindow.Close();
_dockWindow = null;
}
});
}
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
@@ -718,5 +746,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
_focusAfterLoadedCts?.Cancel();
_focusAfterLoadedCts?.Dispose();
_focusAfterLoadedCts = null;
_dockWindow?.Dispose();
}
}

View File

@@ -0,0 +1,164 @@
<?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: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>-->
<!-- Appearance Section -->
<TextBlock x:Uid="DockAppearanceSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<!-- Dock Size -->
<controls:SettingsCard Header="Dock Size">
<controls:SettingsCard.Description>
Choose the size of your dock
</controls:SettingsCard.Description>
<ComboBox
x:Name="DockSizeComboBox"
MinWidth="120"
SelectedIndex="{x:Bind SelectedDockSizeIndex, Mode=TwoWay}">
<ComboBoxItem Content="Small" />
<ComboBoxItem Content="Medium" />
<ComboBoxItem Content="Large" />
</ComboBox>
</controls:SettingsCard>
<!-- Dock Position -->
<controls:SettingsCard Header="Dock Position">
<controls:SettingsCard.HeaderIcon>
<SymbolIcon Symbol="MoveToFolder" />
</controls:SettingsCard.HeaderIcon>
<controls:SettingsCard.Description>
Choose where the dock appears on your screen
</controls:SettingsCard.Description>
<ComboBox
x:Name="DockPositionComboBox"
MinWidth="120"
SelectedIndex="{x:Bind SelectedSideIndex, Mode=TwoWay}">
<ComboBoxItem Content="Left" />
<ComboBoxItem Content="Top" />
<ComboBoxItem Content="Right" />
<ComboBoxItem Content="Bottom" />
</ComboBox>
</controls:SettingsCard>
<!-- Backdrop Style -->
<controls:SettingsCard Header="Background Style">
<controls:SettingsCard.Description>
Choose the background effect for your dock
</controls:SettingsCard.Description>
<ComboBox
x:Name="BackdropComboBox"
MinWidth="120"
SelectedIndex="{x:Bind SelectedBackdropIndex, Mode=TwoWay}">
<ComboBoxItem Content="Mica" />
<ComboBoxItem Content="Transparent" />
<ComboBoxItem Content="Acrylic" />
</ComboBox>
</controls:SettingsCard>
<!-- Show Labels -->
<controls:SettingsCard Header="Show Labels">
<controls:SettingsCard.Description>
Choose whether to show labels for dock items by default.
</controls:SettingsCard.Description>
<ToggleSwitch
IsOn="{x:Bind ShowLabels, Mode=TwoWay}"
OffContent="Hide labels"
OnContent="Show Labels" />
</controls:SettingsCard>
<!-- Bands Section -->
<TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
<ItemsRepeater ItemsSource="{x:Bind AllDockBandItems, Mode=OneWay}">
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="dockVm:DockBandSettingsViewModel">
<controls:SettingsCard Description="{x:Bind Description, Mode=OneWay}" Header="{x:Bind Title, Mode=OneWay}">
<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.SourceRequested}" />
</cpControls:ContentIcon.Content>
</cpControls:ContentIcon>
</controls:SettingsCard.HeaderIcon>
<StackPanel
VerticalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<TextBlock VerticalAlignment="Center" Text="Pin to" />
<ComboBox MinWidth="120" SelectedIndex="{x:Bind PinSideIndex, Mode=TwoWay}">
<ComboBoxItem>
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="&#xED1A;" />
<TextBlock Text="None" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem>
<StackPanel Orientation="Horizontal" Spacing="8">
<SymbolIcon Symbol="AlignLeft" />
<TextBlock Text="Start" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem>
<StackPanel Orientation="Horizontal" Spacing="8">
<SymbolIcon Symbol="AlignRight" />
<TextBlock Text="End" />
</StackPanel>
</ComboBoxItem>
</ComboBox>
<ComboBox MinWidth="120" SelectedIndex="{x:Bind ShowLabelsIndex, Mode=TwoWay}">
<ComboBoxItem Content="Default" />
<ComboBoxItem Content="Show Labels" />
<ComboBoxItem Content="Hide Labels" />
</ComboBox>
</StackPanel>
</controls:SettingsCard>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,159 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Dock;
using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI.Settings;
public sealed partial class DockSettingsPage : Page
{
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
private readonly SettingsViewModel viewModel;
public List<DockBandSettingsViewModel> AllDockBandItems => GetAllBandSettings();
public DockSettingsPage()
{
this.InitializeComponent();
var settings = App.Current.Services.GetService<SettingsModel>()!;
viewModel = new SettingsViewModel(settings, App.Current.Services, _mainTaskScheduler);
// Initialize UI state
InitializeSettings();
}
private void InitializeSettings()
{
// Initialize UI controls to match current settings
DockSizeComboBox.SelectedIndex = SelectedDockSizeIndex;
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
}
// 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.Mica => 0,
DockBackdrop.Transparent => 1,
DockBackdrop.Acrylic => 2,
_ => 2,
};
private static DockBackdrop SelectedIndexToBackdrop(int index) => index switch
{
0 => DockBackdrop.Mica,
1 => DockBackdrop.Transparent,
2 => 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

@@ -101,6 +101,10 @@
<ToggleSwitch IsOn="{x:Bind viewModel.DisableAnimations, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xF596;}">
<ToggleSwitch IsOn="{x:Bind viewModel.EnableDock, Mode=TwoWay}" />
</controls:SettingsCard>
<!-- 'For Developers' section -->
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />

View File

@@ -67,6 +67,12 @@
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
Icon="{ui:FontIcon Glyph=&#xEA86;}"
Tag="Extensions" />
<!-- xF596 is HolePunchLandscapeTop -->
<NavigationViewItem
x:Name="DockSettingsPageNavItem"
x:Uid="Settings_GeneralPage_NavigationViewItem_Dock"
Icon="{ui:FontIcon Glyph=&#xF596;}"
Tag="Dock" />
</NavigationView.MenuItems>
<Grid>
<Grid.RowDefinitions>

View File

@@ -33,7 +33,9 @@ public sealed partial class SettingsWindow : WindowEx,
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
// Gets or sets optional action invoked after NavigationView is loaded.
public Action NavigationViewLoaded { get; set; } = () => { };
public Action? NavigationViewLoaded { get; set; }
internal string? OpenToPage { get; set; }
public SettingsWindow()
{
@@ -69,7 +71,9 @@ public sealed partial class SettingsWindow : WindowEx,
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
NavView.SelectedItem = NavView.MenuItems[0];
Navigate("General");
Navigate(OpenToPage);
OpenToPage = null;
if (sender is NavigationView navigationView)
{
@@ -96,18 +100,35 @@ public sealed partial class SettingsWindow : WindowEx,
Navigate((selectedItem.Tag as string)!);
}
private void Navigate(string page)
internal void Navigate(string? page)
{
var pageType = page switch
{
null => typeof(GeneralPage),
"General" => typeof(GeneralPage),
"Extensions" => typeof(ExtensionsPage),
"Dock" => typeof(DockSettingsPage),
_ => null,
};
var actualPage = page ?? "General";
if (pageType is not null)
{
// BreadCrumbs.Clear();
// BreadCrumbs.Add(new(actualPage, actualPage));
NavFrame.Navigate(pageType);
// Now, make sure to actually select the correct menu item too
foreach (var obj in NavView.MenuItems)
{
if (obj is NavigationViewItem item)
{
if (item.Tag is string s && s == page)
{
NavView.SelectedItem = item;
}
}
}
}
}
@@ -254,6 +275,12 @@ public sealed partial class SettingsWindow : WindowEx,
var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
BreadCrumbs.Add(new(pageType, pageType));
}
else if (e.SourcePageType == typeof(DockSettingsPage))
{
NavView.SelectedItem = DockSettingsPageNavItem;
var pageType = RS_.GetString("Settings_PageTitles_DockPage");
BreadCrumbs.Add(new(pageType, pageType));
}
else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm)
{
NavView.SelectedItem = ExtensionPageNavItem;

View File

@@ -389,6 +389,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_GeneralPage_NavigationViewItem_Extensions.Content" xml:space="preserve">
<value>Extensions</value>
</data>
<data name="Settings_GeneralPage_NavigationViewItem_Dock.Content" xml:space="preserve">
<value>Dock</value>
</data>
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Open Command Palette settings</value>
</data>
@@ -398,6 +401,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
<value>Behavior</value>
</data>
<data name="DockAppearanceSettingsHeader.Text" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="DockBandsSettingsHeader.Text" xml:space="preserve">
<value>Bands</value>
</data>
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
<value>Search commands...</value>
</data>
@@ -412,6 +421,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
</data>
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve">
<value>Disable animations when switching between pages</value>
</data>
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Header" xml:space="preserve">
<value>Enable dock</value>
</data>
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve">
<value>Enable a toolbar with quick access to commands</value>
</data>
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Back</value>
@@ -556,4 +571,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
<value>Extensions</value>
</data>
<data name="Settings_PageTitles_DockPage" xml:space="preserve">
<value>Dock</value>
</data>
</root>

View File

@@ -0,0 +1,100 @@
<?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">
<Style x:Key="TaskBarButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
<Setter Property="Padding" Value="4,2,4,2" />
<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}"
Foreground="{TemplateBinding Foreground}">
<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 TaskBarButtonBackgroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TaskBarButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPointerOver}" />
</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 TaskBarButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TaskBarButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundPressed}" />
</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="Transparent" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
</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>

View File

@@ -1,7 +1,5 @@
<?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" />
<!-- Other merged dictionaries here -->
@@ -19,6 +17,15 @@
x:Key="LayerOnAcrylicSecondaryBackgroundBrush"
Opacity="0.0"
Color="#222222" />
<SolidColorBrush x:Key="TaskBarButtonBackgroundPointerOver" Color="#0FFFFFFF" />
<SolidColorBrush x:Key="TaskBarButtonBackgroundPressed" Color="#0BFFFFFF" />
<LinearGradientBrush x:Key="TaskBarButtonBorderBrushPointerOver" 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="TaskBarButtonBorderBrushPressed" Color="#0BFFFFFF" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<SolidColorBrush
@@ -30,11 +37,24 @@
x:Key="LayerOnAcrylicSecondaryBackgroundBrush"
Opacity="0.4"
Color="#FFFFFF" />
<SolidColorBrush x:Key="TaskBarButtonBackgroundPointerOver" Color="#80FFFFFF" />
<SolidColorBrush x:Key="TaskBarButtonBackgroundPressed" Color="#4DFFFFFF" />
<LinearGradientBrush x:Key="TaskBarButtonBorderBrushPointerOver" 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="TaskBarButtonBorderBrushPressed" Color="#05000000" />
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<!-- This is a local copy of LayerOnAcrylicFillColorDefaultBrush -->
<SolidColorBrush x:Key="LayerOnAcrylicPrimaryBackgroundBrush" Color="{ThemeResource LayerOnAcrylicFillColorDefault}" />
<SolidColorBrush x:Key="LayerOnAcrylicSecondaryBackgroundBrush" Color="Transparent" />
<SolidColorBrush x:Key="TaskBarButtonBackgroundPointerOver" Color="{StaticResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="TaskBarButtonBackgroundPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
<SolidColorBrush x:Key="TaskBarButtonBorderBrushPointerOver" Color="{StaticResource SystemColorHighlightColor}" />
<SolidColorBrush x:Key="TaskBarButtonBorderBrushPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View File

@@ -75,6 +75,8 @@ functionality.
- [Advanced scenarios](#advanced-scenarios)
- [Status messages](#status-messages)
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
- [Class diagram](#class-diagram)
- [Future considerations](#future-considerations)
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
@@ -2046,6 +2048,87 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
developers won't have to do anything. The toolkit will just do the right thing
for them.
## Addenda IV: Dock bands
The "dock" is another way to surface commands to the user. This is a
toolbar-like window that can be docked to the side of the screen, or floated as
its own window. It enables another surface for extensions to display real-time
information and shortcuts to users.
Bands are powered by the same interfaces as DevPal itself. Extensions can provide
bands via the new `DockBand` property on `ICommandProvider3`.
```csharp
interface ICommandProvider3 requires ICommandProvider2
{
ICommandItem[] GetDockBands();
};
```
A **Dock Band** is one "strip of items" in the dock. Each band can have multiple
items. This allows an extension to create a strip of buttons that should all be
treated as a single unit. For example, a media player band will want probably
four items:
* one for the previous track
* one for play/pause
* one for next track
* and one to display the album art and track title
`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem`
represents one band in the dock. These represent all of the bands that an
extension would allow the user to add to their dock.
All of the `ICommandItem`s returned from `GetDockBands` **must** have a
`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will
ignore that band.
Bands are not automatically added to the dock. Instead, the user must choose
which bands they want to add. This is done via the DevPal settings page.
Furthermore, bands are not displayed in the list of commands in DevPal itself.
This allows extension authors to create objects that are only intended for the
dock, without cluttering up the main DevPal UI, and vice versa.
DevPal will then create UI in the dock for each band the user has chosen to add.
What that looks like will depend on the `Command` in the `ICommandItem`:
* A `IInvokableCommand` will be rendered as a single button. Think "the
time/date" button on the taskbar, that opens the notification center.
* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem`
in the list. Think "media controls" for a music player.
* A `IContentPage` will be rendered as a single button. Clicking that button
will open a flyout with that content rendered in it. Think "weather" or "news"
flyouts.
If the `Command` in the `IListItem`s of a band are pages, then clicking those
buttons will open DevPal to that page, as if it were a flyout from the dock.
The `.Title` property of the top-level `ICommandItem` representing the band will
be used as the name of the band in the settings. So a media player band might
want to set the `Title` to "Contoso Music Player", even if the individual
buttons in the band don't show that title.
Users may also "pin" a top-level command from DevPal into the dock. DevPal will
take care of creating a new band (owned by devpal) with that command in it. This
allows users to add quick shortcuts to their favorite commands in the dock.
Think: pinning an app, or pinning a particular GitHub query.
Bands are added via ID. An extension may choose to have a TopLevelCommand and a
DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand
to the dock, DevPal will pin the band from `GetDockBands`, rather than creating
a simple pinned command. This allows extension authors to seamlessly have a
top-level command present a palette-specific experience, while also having a
dock-specific experience. In our ongoing media player example, the top-level
command might open DevPal to a full-featured music control page, while the dock
band has simpler buttons on it (without a title/subtitle).
Users may choose to have:
* the orientation of the dock: vertical or horizontal
* the size of the dock
* which bands are shown in the dock
* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are
shown or hidden.
- Dock bands will still display the `Title` & `Subtitle` of each item in the
band as the tooltip on those items, even when the "labels" are hidden.
## Class diagram
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)

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.
// See the LICENSE file in the project root for more information.
@@ -16,7 +16,8 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
public ClipboardHistoryCommandsProvider()
{
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
var page = new ClipboardHistoryListPage(_settingsManager);
_clipboardHistoryListItem = new ListItem(page)
{
Title = Properties.Resources.list_item_title,
Subtitle = Properties.Resources.list_item_subtitle,
@@ -25,7 +26,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
new CommandContextItem(_settingsManager.Settings.SettingsPage),
],
};
DisplayName = Properties.Resources.provider_display_name;
Icon = Icons.ClipboardListIcon;
Id = "Windows.ClipboardHistory";

View File

@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
internal sealed class Icons
{
internal static IconInfo CpuIcon => new("\uE9D9"); // CPU icon
internal static IconInfo MemoryIcon => new("\uE964"); // Memory icon
internal static IconInfo DiskIcon => new("\uE977"); // PC1 icon
internal static IconInfo HardDriveIcon => new("\uEDA2"); // HardDrive icon
internal static IconInfo NetworkIcon => new("\uEC05"); // Network icon
internal static IconInfo StackedAreaIcon => new("\uE9D2"); // StackedArea icon
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.PerformanceMonitor</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.PerformanceMonitor.pri</ProjectPriFileName>
<nullable>enable</nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Diagnostics.PerformanceCounter" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\RemoteDesktop.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RemoteDesktop.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,140 @@
// 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 Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
internal abstract partial class OnLoadStaticPage : Page, IListPage
{
private string _placeholderText = string.Empty;
private string _searchText = string.Empty;
private bool _showDetails;
private bool _hasMore;
private IFilters? _filters;
private IGridProperties? _gridProperties;
private ICommandItem? _emptyContent;
private int _loadCount;
#pragma warning disable CS0067 // The event is never used
private event TypedEventHandler<object, IItemsChangedEventArgs>? InternalItemsChanged;
#pragma warning restore CS0067 // The event is never used
public event TypedEventHandler<object, IItemsChangedEventArgs> ItemsChanged
{
add
{
InternalItemsChanged += value;
if (_loadCount == 0)
{
Loaded();
}
_loadCount++;
}
remove
{
InternalItemsChanged -= value;
_loadCount--;
_loadCount = Math.Max(0, _loadCount);
if (_loadCount == 0)
{
Unloaded();
}
}
}
protected abstract void Loaded();
protected abstract void Unloaded();
public virtual string PlaceholderText
{
get => _placeholderText;
set
{
_placeholderText = value;
OnPropertyChanged(nameof(PlaceholderText));
}
}
public virtual string SearchText
{
get => _searchText;
set
{
_searchText = value;
OnPropertyChanged(nameof(SearchText));
}
}
public virtual bool ShowDetails
{
get => _showDetails;
set
{
_showDetails = value;
OnPropertyChanged(nameof(ShowDetails));
}
}
public virtual bool HasMoreItems
{
get => _hasMore;
set
{
_hasMore = value;
OnPropertyChanged(nameof(HasMoreItems));
}
}
public virtual IFilters? Filters
{
get => _filters;
set
{
_filters = value;
OnPropertyChanged(nameof(Filters));
}
}
public virtual IGridProperties? GridProperties
{
get => _gridProperties;
set
{
_gridProperties = value;
OnPropertyChanged(nameof(GridProperties));
}
}
public virtual ICommandItem? EmptyContent
{
get => _emptyContent;
set
{
_emptyContent = value;
OnPropertyChanged(nameof(EmptyContent));
}
}
public void LoadMore()
{
}
protected void SetSearchNoUpdate(string newSearchText)
{
_searchText = newSearchText;
}
public abstract IListItem[] GetItems();
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,38 @@
// 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.Ext.PerformanceMonitor;
public partial class PerformanceMonitorCommandsProvider : CommandProvider
{
private readonly ICommandItem[] _commands;
private readonly ICommandItem _band;
public PerformanceMonitorCommandsProvider()
{
DisplayName = "Performance Monitor";
Id = "PerformanceMonitor";
Icon = Icons.StackedAreaIcon;
var page = new PerformanceMonitorPage(false);
var band = new PerformanceMonitorPage(true);
_band = new CommandItem(band) { Title = "Performance monitor" }; // TODO!Loc
_commands = [
new CommandItem(page) { Title = DisplayName },
];
}
public override ICommandItem[] TopLevelCommands()
{
return _commands;
}
public override ICommandItem[]? GetDockBands()
{
return new ICommandItem[] { _band };
}
}

View File

@@ -0,0 +1,632 @@
// 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.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.PerformanceMonitor;
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// asdfasdf
/// </summary>
/// <remarks>
/// Intentionally, we're using IListPage rather than ListPage. This is so we
/// can get the onload/onunload
/// </remarks>
internal sealed partial class PerformanceMonitorPage : OnLoadStaticPage, IDisposable
{
private readonly PerformanceCounter? _cpuCounter;
private readonly PerformanceCounter? _memoryCounter;
private readonly PerformanceCounter[]? _diskCounters;
private readonly PerformanceCounter? _networkSentCounter;
private readonly PerformanceCounter? _networkReceivedCounter;
private readonly bool _isBandPage;
// System performance data object to store all metrics
private SystemPerformanceData _performanceData = new SystemPerformanceData();
private int _loadCount;
private bool IsActive => _loadCount > 0;
private List<ListItem> _items = new List<ListItem>();
private ListItem _cpuItem;
private ListItem _memoryItem;
private ListItem _diskItem;
private ListItem? _networkItem;
public override string Id => "com.microsoft.cmdpal.performanceMonitor";
public override string Title => "Performance monitor";
// public override string PlaceholderText => "Performance monitor";
public override IconInfo Icon => Icons.StackedAreaIcon;
private Task? _updateTask;
// Start of code
public PerformanceMonitorPage(bool asBandPage = false)
{
_isBandPage = asBandPage;
ShowDetails = !_isBandPage;
// Create all the perf counters
// Initialize CPU counter
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
_cpuCounter.NextValue(); // First call always returns 0
_memoryCounter = new PerformanceCounter("Memory", "Available MBytes");
_memoryCounter.NextValue(); // First call always returns 0
// Initialize Disk counters (for all physical disks)
var diskNames = GetPhysicalDiskNames();
_diskCounters = new PerformanceCounter[diskNames.Length];
for (var i = 0; i < diskNames.Length; i++)
{
_diskCounters[i] = new PerformanceCounter("PhysicalDisk", "% Disk Time", diskNames[i]);
_diskCounters[i].NextValue(); // First call always returns 0
}
// Also, instantiate all the items we'll need
_cpuItem = new ListItem(new NoOpCommand() { Name = _isBandPage ? "CPU" : string.Empty })
{
Icon = Icons.CpuIcon,
Title = "CPU",
};
_memoryItem = new ListItem(new NoOpCommand() { Name = _isBandPage ? "Memory" : string.Empty })
{
Icon = Icons.MemoryIcon,
Title = "Memory",
};
_diskItem = new ListItem(new NoOpCommand() { Name = _isBandPage ? "Disk" : string.Empty })
{
Icon = Icons.HardDriveIcon,
Title = "Disk",
};
_items.Add(_cpuItem);
_items.Add(_memoryItem);
_items.Add(_diskItem);
// Try to initialize Network counters (may not be available on all systems)
try
{
var networkInterface = GetMostActiveNetworkInterface();
if (!string.IsNullOrEmpty(networkInterface))
{
_networkSentCounter = new PerformanceCounter("Network Interface", "Bytes Sent/sec", networkInterface);
_networkReceivedCounter = new PerformanceCounter("Network Interface", "Bytes Received/sec", networkInterface);
_networkSentCounter.NextValue(); // First call always returns 0
_networkReceivedCounter.NextValue(); // First call always returns 0
_networkItem = new ListItem(new NoOpCommand() { Name = _isBandPage ? "Network" : string.Empty })
{
Icon = Icons.NetworkIcon,
Title = "Network",
};
_items.Add(_networkItem);
}
}
catch (Exception)
{
}
if (!_isBandPage)
{
InitializeDetailsToLoading();
}
// Initialize performance data
_performanceData.DiskInformation = GetDiskInformation();
}
protected override void Loaded()
{
_loadCount++;
_updateTask ??= Task.Run(() =>
{
UpdateValues();
});
}
protected override void Unloaded()
{
_loadCount--;
// TODO! cancel the update task
}
private void InitializeDetailsToLoading()
{
_cpuItem.Details ??= new Details() { Body = "Loading..." };
_memoryItem.Details ??= new Details() { Body = "Loading..." };
_diskItem.Details ??= new Details() { Body = "Loading..." };
if (_networkItem != null)
{
_networkItem.Details ??= new Details() { Body = "Loading..." };
}
}
private Details GetCPUDetails()
{
return new Details()
{
Title = "CPU Details",
Body = $@"## Top CPU Processes
{_performanceData.TopCpuProcesses}
## Processor Information
- Number of Cores: {Environment.ProcessorCount}
- Architecture: {RuntimeInformation.ProcessArchitecture}",
};
}
private Details GetMemoryDetails()
{
return new Details()
{
Title = "Memory Details",
Body = $@"## Top Memory Processes
{_performanceData.TopMemoryProcesses}
## Memory info
- Total Physical Memory: {GetTotalPhysicalMemoryGB():0.00} GB
- Available Memory: {_performanceData.AvailableMemoryMB / 1024:0.00} GB
- Memory In Use: {GetUsedMemoryGB():0.00} GB",
};
}
private Details GetDiskDetails()
{
return new Details()
{
Title = "Disk Details",
Body = $@"## Top Disk Processes
{_performanceData.TopDiskProcesses}
## Disk Information
{_performanceData.DiskInformation}",
};
}
private Details GetNetworkDetails()
{
return new Details()
{
Title = "Network Details",
Body = $@"To be added in the future.",
};
}
private async void UpdateValues()
{
// Update interval in milliseconds
const int updateInterval = 1000;
// TODO: Fix this behaviour which is needed cause of a bug
while (_loadCount > 0)
{
// Record start time of update cycle
var startTime = DateTime.Now;
var tasks = new List<Task>();
// Start all update tasks in parallel
if (_cpuItem != null)
{
tasks.Add(Task.Run(() => UpdateCpuValues()));
}
if (_memoryItem != null)
{
tasks.Add(Task.Run(() => UpdateMemoryValues()));
}
if (_diskCounters?.Length > 0 && _diskItem != null)
{
tasks.Add(Task.Run(() => UpdateDiskValues()));
}
if (_networkItem != null)
{
tasks.Add(Task.Run(() => UpdateNetworkValues()));
}
if (!_isBandPage)
{
// TODO!: This is unbelievably loud
tasks.Add(GetProcessInfo());
}
// Wait for all tasks to complete
await Task.WhenAll(tasks);
// Calculate how much time has passed
var elapsedTime = (DateTime.Now - startTime).TotalMilliseconds;
// If we completed faster than our desired interval, wait the remaining time
if (elapsedTime < updateInterval)
{
await Task.Delay((int)(updateInterval - elapsedTime));
}
}
}
private void UpdateCpuValues()
{
if (_cpuCounter is null)
{
return;
}
// Quick update
_performanceData.CurrentCpuUsage = _cpuCounter.NextValue();
if (_isBandPage)
{
_cpuItem.Title = $"{_performanceData.CurrentCpuUsage:0.0}%";
_cpuItem.Subtitle = "CPU";
}
else
{
_cpuItem.Title = $"CPU - {_performanceData.CurrentCpuUsage:0.0}%";
}
_cpuItem.Details = GetCPUDetails();
}
private void UpdateMemoryValues()
{
if (_memoryCounter is null)
{
return;
}
// Quick update
_performanceData.AvailableMemoryMB = _memoryCounter.NextValue();
_performanceData.CurrentMemoryUsage = 100f - (_performanceData.AvailableMemoryMB / GetTotalPhysicalMemory() * 100f);
if (_isBandPage)
{
_memoryItem.Title = $"{_performanceData.CurrentMemoryUsage:0.0}%";
_memoryItem.Subtitle = "Memory";
}
else
{
_memoryItem.Title = $"Memory - {_performanceData.CurrentMemoryUsage:0.0}%";
}
_memoryItem.Details = GetMemoryDetails();
}
private void UpdateDiskValues()
{
if (_diskCounters is null)
{
return;
}
// Quick update
if (_diskCounters.Length > 0)
{
_performanceData.CurrentDiskUsage = _diskCounters.Average(counter => counter.NextValue());
}
if (_isBandPage)
{
_diskItem.Title = $"{_performanceData.CurrentDiskUsage:0.0}%";
_diskItem.Subtitle = "Disk";
}
else
{
_diskItem.Title = $"Disk - {_performanceData.CurrentDiskUsage:0.0}%";
}
_diskItem.Details = GetDiskDetails();
}
private void UpdateNetworkValues()
{
if (_networkSentCounter is null ||
_networkReceivedCounter is null ||
_networkItem is null)
{
return;
}
// Quick update
_performanceData.CurrentNetworkSentKBps = _networkSentCounter.NextValue() / 1024; // Convert to KB/s
_performanceData.CurrentNetworkReceivedKBps = _networkReceivedCounter.NextValue() / 1024; // Convert to KB/s
if (_isBandPage)
{
_networkItem.Title = $"{_performanceData.CurrentNetworkReceivedKBps:0.0} KB/s ↓, {_performanceData.CurrentNetworkSentKBps:0.0} KB/s ↑";
_networkItem.Subtitle = "Network";
}
else
{
_networkItem.Title = $"Network - {_performanceData.CurrentNetworkReceivedKBps:0.0} KB/s ↓, {_performanceData.CurrentNetworkSentKBps:0.0} KB/s ↑";
}
_networkItem.Details = GetNetworkDetails();
}
public override IListItem[] GetItems()
{
return _items.ToArray();
}
internal sealed record ProcessPerfData(int Id, string Name, ulong ReadVal, ulong WriteVal, TimeSpan TotalProcessTime, long WorkingSet);
// === Helper functions ===
private async Task<bool> GetProcessInfo()
{
var pollingTime = 750;
try
{
var initialProcessValues = Process.GetProcesses()
.Where(p => !string.IsNullOrEmpty(p.ProcessName))
.Select(GetPerfData)
.Where(p => p != null)
.Select(p => p!)
.ToDictionary(p => p.Id, p => p);
await Task.Delay(pollingTime); // Wait a bit to measure usage
var finalProcessValues = Process.GetProcesses()
.Where(p => !string.IsNullOrEmpty(p.ProcessName))
.Select(GetPerfData)
.Where(p => p != null)
.Select(p => p!)
.ToDictionary(p => p.Id, p => p);
// Make new dictionary with finalizedProcesses
var finalizedProcesses = new Dictionary<int, ProcessPerfData>();
foreach (var (key, value) in finalProcessValues)
{
if (value is null)
{
continue;
}
if (initialProcessValues.TryGetValue(key, out var initialValue))
{
var readVal = value.ReadVal - initialValue.ReadVal;
var writeVal = value.WriteVal - initialValue.WriteVal;
var totalProcessTime = value.TotalProcessTime - initialValue.TotalProcessTime;
finalizedProcesses[key] = new ProcessPerfData(initialValue.Id, initialValue.Name, readVal, writeVal, totalProcessTime, initialValue.WorkingSet);
}
}
var secondConversion = 1000.0 / pollingTime;
// Format the string for CPU usage
var cpuString = new StringBuilder();
var topCPUProcesses = finalizedProcesses
.OrderByDescending(p => p.Value.TotalProcessTime)
.Take(5)
.ToDictionary(p => p.Key, p => p.Value);
foreach (var (key, value) in topCPUProcesses)
{
var cpuUsage = value.TotalProcessTime.TotalMilliseconds * 100.0 / (pollingTime * Environment.ProcessorCount);
cpuUsage = Math.Min(100, Math.Max(0, cpuUsage)); // Clamp between 0-100%
var line = $"- {value.Name}: {cpuUsage:0.0}% CPU";
cpuString.AppendLine(line);
}
_performanceData.TopCpuProcesses = cpuString.ToString();
// Format the string for memory usage
var memoryString = new StringBuilder();
var topMemoryProcesses = finalizedProcesses
.OrderByDescending(p => p.Value.WorkingSet)
.Take(5)
.ToDictionary(p => p.Key, p => p.Value);
foreach (var (key, value) in topMemoryProcesses)
{
var line = $"- {value.Name}: {value.WorkingSet / 1024 / 1024:0.0} MB";
memoryString.AppendLine(line);
}
_performanceData.TopMemoryProcesses = memoryString.ToString();
// Format the string for disk usage
var diskString = new StringBuilder();
var topDiskProcesses = finalizedProcesses
.OrderByDescending(p => p.Value.ReadVal)
.Take(5)
.ToDictionary(p => p.Key, p => p.Value);
foreach (var (key, value) in topDiskProcesses)
{
var line = $"- {value.Name}: R: {value.ReadVal * secondConversion / 1024 / 1024:0.0} , W: {value.WriteVal * secondConversion / 1024 / 1024:0.0} MB";
diskString.AppendLine(line);
}
_performanceData.TopDiskProcesses = diskString.ToString();
}
catch (Exception)
{
return false;
}
return true;
}
private ProcessPerfData? GetPerfData(Process p)
{
try
{
if (p.HasExited)
{
return null;
}
if (GetProcessIoCounters(p.Handle, out var counters))
{
var readVal = counters.ReadTransferCount;
var writeVal = counters.WriteTransferCount;
return new ProcessPerfData(
p.Id,
p.ProcessName,
readVal,
writeVal,
p.TotalProcessorTime,
p.WorkingSet64);
}
}
catch (Win32Exception)
{
return null;
}
catch (InvalidOperationException)
{
return null;
}
return null;
}
private float GetTotalPhysicalMemoryGB()
{
return GetTotalPhysicalMemory() / 1024; // Convert MB to GB
}
private float GetUsedMemoryGB()
{
return (GetTotalPhysicalMemory() - _performanceData.AvailableMemoryMB) / 1024; // Convert MB to GB
}
private string GetDiskInformation()
{
var result = new System.Text.StringBuilder();
foreach (var drive in DriveInfo.GetDrives().Where(d => d.IsReady && d.DriveType == DriveType.Fixed))
{
var freeSpaceGB = drive.TotalFreeSpace / (1024.0 * 1024 * 1024);
var totalSpaceGB = drive.TotalSize / (1024.0 * 1024 * 1024);
var usedPercent = 100 - (freeSpaceGB / totalSpaceGB * 100);
var usedBlocks = (int)(usedPercent / 10);
var body = $"""
### Drive {drive.Name} ({drive.VolumeLabel}):
{freeSpaceGB:0.00} GB free of {totalSpaceGB:0.00} GB
{usedPercent:0.0}% used
\\[{new string('⬛', usedBlocks)}{new string('⬜', 10 - usedBlocks)}\\]
""";
result.AppendLine(body);
}
return result.ToString();
}
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
[StructLayout(LayoutKind.Sequential)]
private struct MIB_TCPROW_OWNER_PID
{
public uint state;
public uint localAddr;
public byte localPort1;
public byte localPort2;
public byte localPort3;
public byte localPort4;
public uint remoteAddr;
public byte remotePort1;
public byte remotePort2;
public byte remotePort3;
public byte remotePort4;
public int owningPid;
}
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
[DllImport("iphlpapi.dll", SetLastError = true)]
private static extern uint GetExtendedTcpTable(
IntPtr pTcpTable,
ref int dwOutBufLen,
bool sort,
int ipVersion,
int tblClass,
int reserved);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetProcessIoCounters(IntPtr hProcess, out IO_COUNTERS lpIoCounters);
private static string[] GetPhysicalDiskNames()
{
var category = new PerformanceCounterCategory("PhysicalDisk");
var instanceNames = category.GetInstanceNames();
return instanceNames.Where(name => name != "_Total").ToArray();
}
private static string? GetMostActiveNetworkInterface()
{
var category = new PerformanceCounterCategory("Network Interface");
return category.GetInstanceNames().FirstOrDefault(name => !name.Contains("Loopback"));
}
private static float GetTotalPhysicalMemory()
{
return (float)GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / (1024 * 1024); // Convert bytes to MB
}
public void Dispose()
{
_cpuCounter?.Dispose();
_memoryCounter?.Dispose();
if (_diskCounters != null)
{
foreach (var counter in _diskCounters)
{
counter?.Dispose();
}
}
_networkSentCounter?.Dispose();
_networkReceivedCounter?.Dispose();
}
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -0,0 +1,40 @@
// 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.Ext.PerformanceMonitor;
/// <summary>
/// Class for storing all performance metrics in one place
/// </summary>
internal sealed class SystemPerformanceData
{
// Process information
public string TopCpuProcesses { get; set; } = "Loading process data...";
public string TopMemoryProcesses { get; set; } = "Loading process data...";
public string TopDiskProcesses { get; set; } = "Loading process data...";
public string TopNetworkProcesses { get; set; } = "Loading process data...";
// Current values
public float CurrentCpuUsage { get; set; }
public float CurrentMemoryUsage { get; set; }
public float AvailableMemoryMB { get; set; }
public float CurrentDiskUsage { get; set; }
public float CurrentNetworkSentKBps { get; set; }
public float CurrentNetworkReceivedKBps { get; set; }
// System information
public string ProcessorName { get; set; } = string.Empty;
public string DiskInformation { get; set; } = string.Empty;
public string NetworkInformation { get; set; } = string.Empty;
}

View File

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

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -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>
/// Looks up a localized string similar to Era.
/// </summary>

View File

@@ -432,4 +432,8 @@
<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>
</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>
</root>

View File

@@ -5,6 +5,7 @@
using System;
using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Ext.TimeDate.Helpers;
using Microsoft.CmdPal.Ext.TimeDate.Pages;
using Microsoft.CommandPalette.Extensions;
@@ -20,6 +21,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager);
private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager);
private readonly CommandItem _bandItem;
public TimeDateCommandsProvider()
{
DisplayName = Resources.Microsoft_plugin_timedate_plugin_name;
@@ -34,6 +37,8 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
Icon = _timeDateExtensionPage.Icon;
Settings = _settingsManager.Settings;
_bandItem = new NowDockBand();
}
private string GetTranslatedPluginDescription()
@@ -48,4 +53,84 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
public override ICommandItem[] TopLevelCommands() => [_command];
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 : CommandItem
{
private CopyTextCommand _copyTimeCommand;
private CopyTextCommand _copyDateCommand;
public NowDockBand()
{
Command = new NoOpCommand() { Id = "com.microsoft.cmdpal.timedate.dockband" };
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = "Copy Time" };
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = "Copy Date" };
MoreCommands = [
new CommandContextItem(_copyTimeCommand),
new CommandContextItem(_copyDateCommand)
];
UpdateText();
// Create a timer to update the time every minute
System.Timers.Timer timer = new(60000); // 60000 ms = 1 minute
// but we want it to tick on the minute, so calculate the initial delay
var now = DateTime.Now;
timer.Interval = 60000 - ((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 = 60000;
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 = "")
{
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;
_tag = tag;
ShowDetails = true;

View File

@@ -78,7 +78,7 @@ internal sealed class OpenWindows
lock (_enumWindowsLock)
{
windows.Clear();
EnumWindowsProc callbackptr = new EnumWindowsProc(WindowEnumerationCallBack);
var callbackptr = new EnumWindowsProc(WindowEnumerationCallBack);
_ = NativeMethods.EnumWindows(callbackptr, tokenHandleParam);
}
}
@@ -109,11 +109,12 @@ internal sealed class OpenWindows
return false;
}
Window newWindow = new Window(hwnd);
var newWindow = new Window(hwnd);
if (newWindow.IsWindow && newWindow.Visible && newWindow.IsOwner &&
(!newWindow.IsToolWindow || newWindow.IsAppWindow) && !newWindow.TaskListDeleted &&
(newWindow.Desktop.IsVisible || !SettingsManager.Instance.ResultsFromVisibleDesktopOnly || WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.GetDesktopCount() < 2) &&
// (newWindow.Desktop.IsVisible || !SettingsManager.Instance.ResultsFromVisibleDesktopOnly || WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.GetDesktopCount() < 2) &&
newWindow.ClassName != "Windows.UI.Core.CoreWindow" && newWindow.Process.Name != _powerLauncherExe)
{
// To hide (not add) preloaded uwp app windows that are invisible to the user and other cloaked windows, we check the cloak state. (Issue #13637.)

View File

@@ -21,7 +21,10 @@ internal static class ResultHelper
/// </summary>
/// <param name="searchControllerResults">List with all search controller matches</param>
/// <returns>List of results</returns>
internal static List<WindowWalkerListItem> GetResultList(List<SearchResult> searchControllerResults, bool isKeywordSearch)
internal static List<WindowWalkerListItem> GetResultList(
List<SearchResult> searchControllerResults,
bool isKeywordSearch,
SettingsManager settings)
{
if (searchControllerResults is null || searchControllerResults.Count == 0)
{
@@ -40,7 +43,15 @@ internal static class ResultHelper
.Select(x => CreateResultFromSearchResult(x))
.ToList();
if (addExplorerInfo && !SettingsManager.Instance.HideExplorerSettingInfo)
if (!settings.ShowSubtitles)
{
foreach (var li in resultsList)
{
li.Subtitle = string.Empty;
}
}
if (addExplorerInfo && !settings.HideExplorerSettingInfo)
{
resultsList.Insert(0, GetExplorerInfoResult());
}

View File

@@ -17,6 +17,8 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Components;
/// </summary>
internal sealed class SearchController
{
private readonly SettingsManager _settings;
/// <summary>
/// the current search text
/// </summary>
@@ -27,10 +29,10 @@ internal sealed class SearchController
/// </summary>
private List<SearchResult>? searchMatches;
/// <summary>
/// Singleton pattern
/// </summary>
private static SearchController? instance;
///// <summary>
///// Singleton pattern
///// </summary>
// private static SearchController? instance;
/// <summary>
/// Gets or sets the current search text
@@ -48,26 +50,27 @@ internal sealed class SearchController
/// </summary>
internal List<SearchResult> SearchMatches => new List<SearchResult>(searchMatches ?? []).OrderByDescending(x => x.Score).ToList();
/// <summary>
/// Gets singleton Pattern
/// </summary>
internal static SearchController Instance
{
get
{
instance ??= new SearchController();
///// <summary>
///// Gets singleton Pattern
///// </summary>
// internal static SearchController Instance
// {
// get
// {
// instance ??= new SearchController();
return instance;
}
}
// return instance;
// }
// }
/// <summary>
/// Initializes a new instance of the <see cref="SearchController"/> class.
/// Initializes the search controller object
/// </summary>
private SearchController()
internal SearchController(SettingsManager settings)
{
searchText = string.Empty;
_settings = settings;
}
/// <summary>
@@ -87,8 +90,13 @@ internal sealed class SearchController
System.Diagnostics.Debug.Print("Syncing WindowSearch result with OpenWindows Model");
var snapshotOfOpenWindows = OpenWindows.Instance.Windows;
var openWindowsCorrectDesktop = snapshotOfOpenWindows.Where(DesktopMatchesSetting).ToList();
searchMatches = string.IsNullOrWhiteSpace(SearchText) ? AllOpenWindows(openWindowsCorrectDesktop) : FuzzySearchOpenWindows(openWindowsCorrectDesktop);
}
searchMatches = string.IsNullOrWhiteSpace(SearchText) ? AllOpenWindows(snapshotOfOpenWindows) : FuzzySearchOpenWindows(snapshotOfOpenWindows);
private bool DesktopMatchesSetting(Window w)
{
return w.Desktop.IsVisible || !_settings.ResultsFromVisibleDesktopOnly || WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.GetDesktopCount() < 2;
}
/// <summary>
@@ -134,7 +142,7 @@ internal sealed class SearchController
}
}
return SettingsManager.Instance.InMruOrder
return _settings.InMruOrder
? result.ToList()
: result
.OrderBy(w => w.Result.Title)

View File

@@ -51,7 +51,7 @@ internal sealed class Window
var sizeOfTitle = NativeMethods.GetWindowTextLength(hwnd);
if (sizeOfTitle++ > 0)
{
StringBuilder titleBuffer = new StringBuilder(sizeOfTitle);
var titleBuffer = new StringBuilder(sizeOfTitle);
var numCharactersWritten = NativeMethods.GetWindowText(hwnd, titleBuffer, sizeOfTitle);
if (numCharactersWritten == 0)
{
@@ -260,7 +260,7 @@ internal sealed class Window
/// <returns>The state (minimized, maximized, etc..) of the window</returns>
internal WindowSizeState GetWindowSizeState()
{
NativeMethods.GetWindowPlacement(Hwnd, out WINDOWPLACEMENT placement);
NativeMethods.GetWindowPlacement(Hwnd, out var placement);
switch (placement.ShowCmd)
{
@@ -295,21 +295,30 @@ internal sealed class Window
/// <returns>The state (none, app, ...) of the window</returns>
internal WindowCloakState GetWindowCloakState()
{
_ = NativeMethods.DwmGetWindowAttribute(Hwnd, (int)DwmWindowAttributes.Cloaked, out var isCloakedState, sizeof(uint));
switch (isCloakedState)
try
{
case (int)DwmWindowCloakStates.None:
return WindowCloakState.None;
case (int)DwmWindowCloakStates.CloakedApp:
return WindowCloakState.App;
case (int)DwmWindowCloakStates.CloakedShell:
return WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.IsWindowCloakedByVirtualDesktopManager(hwnd, Desktop.Id) ? WindowCloakState.OtherDesktop : WindowCloakState.Shell;
case (int)DwmWindowCloakStates.CloakedInherited:
return WindowCloakState.Inherited;
default:
return WindowCloakState.Unknown;
_ = NativeMethods.DwmGetWindowAttribute(Hwnd, (int)DwmWindowAttributes.Cloaked, out var isCloakedState, sizeof(uint));
switch (isCloakedState)
{
case (int)DwmWindowCloakStates.None:
return WindowCloakState.None;
case (int)DwmWindowCloakStates.CloakedApp:
return WindowCloakState.App;
case (int)DwmWindowCloakStates.CloakedShell:
return WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.IsWindowCloakedByVirtualDesktopManager(hwnd, Desktop.Id) ? WindowCloakState.OtherDesktop : WindowCloakState.Shell;
case (int)DwmWindowCloakStates.CloakedInherited:
return WindowCloakState.Inherited;
default:
return WindowCloakState.Unknown;
}
}
catch
{
// Log?
}
return WindowCloakState.Unknown;
}
/// <summary>
@@ -332,7 +341,7 @@ internal sealed class Window
/// <returns>Class name</returns>
private static string GetWindowClassName(IntPtr hwnd)
{
StringBuilder windowClassName = new StringBuilder(300);
var windowClassName = new StringBuilder(300);
var numCharactersWritten = NativeMethods.GetClassName(hwnd, windowClassName, windowClassName.MaxCapacity);
if (numCharactersWritten == 0)
@@ -384,7 +393,7 @@ internal sealed class Window
{
new Task(() =>
{
EnumWindowsProc callbackptr = new EnumWindowsProc((IntPtr hwnd, IntPtr lParam) =>
var callbackptr = new EnumWindowsProc((IntPtr hwnd, IntPtr lParam) =>
{
// Every uwp app main window has at least three child windows. Only the one we are interested in has a class starting with "Windows.UI.Core." and is assigned to the real app process.
// (The other ones have a class name that begins with the string "ApplicationFrame".)
@@ -410,4 +419,21 @@ internal sealed class Window
return _handlesToProcessCache[hWindow];
}
}
public override bool Equals(object? obj)
{
if (obj is Window other)
{
return this.hwnd == other.hwnd &&
this.Title == other.Title &&
this.Visible == other.Visible;
}
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}

View File

@@ -76,7 +76,13 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.windowwalker_SettingUseWindowIcon_Description,
true);
public bool ResultsFromVisibleDesktopOnly => _resultsFromVisibleDesktopOnly.Value;
private readonly ToggleSetting _showTitlesOnDock = new(
Namespaced(nameof(ShowTitlesOnDock)),
Resources.windowwalker_SettingShowTitlesOnDock,
Resources.windowwalker_SettingShowTitlesOnDock_Description,
true);
public bool ResultsFromVisibleDesktopOnly { get => _resultsFromVisibleDesktopOnly.Value; set => _resultsFromVisibleDesktopOnly.Value = value; }
public bool SubtitleShowPid => _subtitleShowPid.Value;
@@ -88,13 +94,17 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
public bool OpenAfterKillAndClose => _openAfterKillAndClose.Value;
public bool HideKillProcessOnElevatedProcesses => _hideKillProcessOnElevatedProcesses.Value;
public bool HideKillProcessOnElevatedProcesses { get => _hideKillProcessOnElevatedProcesses.Value; set => _hideKillProcessOnElevatedProcesses.Value = value; }
public bool HideExplorerSettingInfo => _hideExplorerSettingInfo.Value;
public bool HideExplorerSettingInfo { get => _hideExplorerSettingInfo.Value; set => _hideExplorerSettingInfo.Value = value; }
public bool InMruOrder => _inMruOrder.Value;
public bool InMruOrder { get => _inMruOrder.Value; set => _inMruOrder.Value = value; }
public bool UseWindowIcon => _useWindowIcon.Value;
public bool UseWindowIcon { get => _useWindowIcon.Value; set => _useWindowIcon.Value = value; }
public bool ShowSubtitles { get; set; } = true;
public bool ShowTitlesOnDock { get => _showTitlesOnDock.Value; set => _showTitlesOnDock.Value = value; }
internal static string SettingsJsonPath()
{
@@ -119,6 +129,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_hideExplorerSettingInfo);
Settings.Add(_inMruOrder);
Settings.Add(_useWindowIcon);
Settings.Add(_showTitlesOnDock);
// Load settings from file upon initialization
LoadSettings();

View File

@@ -18,6 +18,14 @@
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<!-- WASDK, WebView2, CmdPal Toolkit references now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>

View File

@@ -0,0 +1,14 @@
GetCurrentThreadId
SetWinEventHook
SetWindowsHookEx
UnhookWindowsHookEx
CallNextHookEx
EVENT_OBJECT_CREATE
EVENT_OBJECT_NAMECHANGE
EVENT_OBJECT_SHOW
EVENT_OBJECT_DESTROY
EVENT_OBJECT_HIDE
WINEVENT_OUTOFCONTEXT
WINEVENT_SKIPOWNPROCESS
OBJECT_IDENTIFIER

View File

@@ -4,22 +4,30 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.CmdPal.Ext.WindowWalker.Components;
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Dispatching;
namespace Microsoft.CmdPal.Ext.WindowWalker.Pages;
internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposable
{
private readonly List<WindowWalkerListItem> _results = new();
private readonly SettingsManager _settingsManager;
private readonly SearchController _searchController;
private System.Threading.CancellationTokenSource _cancellationTokenSource = new();
private DispatcherQueue _updateWindowsQueue = DispatcherQueueController.CreateOnDedicatedThread().DispatcherQueue;
private bool _disposed;
public WindowWalkerListPage()
public WindowWalkerListPage(SettingsManager settings)
{
_settingsManager = settings;
_searchController = new(_settingsManager);
Icon = Icons.WindowWalkerIcon;
Name = Resources.windowwalker_name;
Id = "com.microsoft.cmdpal.windowwalker";
@@ -31,12 +39,20 @@ internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposabl
Title = Resources.window_walker_top_level_command_title,
Subtitle = Resources.windowwalker_NoResultsMessage,
};
Query(string.Empty);
}
public override void UpdateSearchText(string oldSearch, string newSearch) =>
RaiseItemsChanged(0);
public override void UpdateSearchText(string oldSearch, string newSearch)
{
_updateWindowsQueue.TryEnqueue(() =>
{
Query(newSearch);
});
}
public List<WindowWalkerListItem> Query(string query)
// public List<WindowWalkerListItem> Query(string query)
public void Query(string query)
{
ArgumentNullException.ThrowIfNull(query);
@@ -46,13 +62,27 @@ internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposabl
WindowWalkerCommandsProvider.VirtualDesktopHelperInstance.UpdateDesktopList();
OpenWindows.Instance.UpdateOpenWindowsList(_cancellationTokenSource.Token);
SearchController.Instance.UpdateSearchText(query);
var searchControllerResults = SearchController.Instance.SearchMatches;
_searchController.UpdateSearchText(query);
var searchControllerResults = _searchController.SearchMatches;
return ResultHelper.GetResultList(searchControllerResults, !string.IsNullOrEmpty(query));
var newListItems = ResultHelper.GetResultList(searchControllerResults, !string.IsNullOrEmpty(query), _settingsManager);
var oldCount = _results.Count;
var newCount = newListItems.Count;
ListHelpers.InPlaceUpdateList(_results, newListItems, out var removedItems);
if (newCount == oldCount && removedItems.Count == 0)
{
// do nothing - windows didn't change
}
else
{
RaiseItemsChanged(_results.Count);
}
}
public override IListItem[] GetItems() => Query(SearchText).ToArray();
public override IListItem[] GetItems()
{
return _results.ToArray();
}
public void Dispose()
{
@@ -71,4 +101,9 @@ internal sealed partial class WindowWalkerListPage : DynamicListPage, IDisposabl
}
}
}
internal void UpdateWindows()
{
UpdateSearchText(string.Empty, string.Empty);
}
}

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
@@ -375,6 +375,24 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Dock: Show window titles.
/// </summary>
public static string windowwalker_SettingShowTitlesOnDock {
get {
return ResourceManager.GetString("windowwalker_SettingShowTitlesOnDock", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Show the window titles on windows in the dock.
/// </summary>
public static string windowwalker_SettingShowTitlesOnDock_Description {
get {
return ResourceManager.GetString("windowwalker_SettingShowTitlesOnDock_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This information is only shown in subtitle and tool tip, if you have at least two desktops..
/// </summary>

View File

@@ -241,4 +241,10 @@
<data name="windowwalker_SettingUseWindowIcon_Description" xml:space="preserve">
<value>Show the actual window icon instead of the process icon</value>
</data>
<data name="windowwalker_SettingShowTitlesOnDock" xml:space="preserve">
<value>Dock: Show window titles</value>
</data>
<data name="windowwalker_SettingShowTitlesOnDock_Description" xml:space="preserve">
<value>Show the window titles on windows in the dock</value>
</data>
</root>

View File

@@ -1,29 +1,36 @@
// Copyright (c) Microsoft Corporation
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.CmdPal.Ext.WindowWalker.Helpers;
using Microsoft.CmdPal.Ext.WindowWalker.Pages;
using Microsoft.CmdPal.Ext.WindowWalker.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.Accessibility;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Microsoft.CmdPal.Ext.WindowWalker;
public partial class WindowWalkerCommandsProvider : CommandProvider
{
private readonly CommandItem _windowWalkerPageItem;
private readonly CommandItem _bandItem;
private readonly SettingsManager _settings = SettingsManager.Instance;
internal static readonly VirtualDesktopHelper VirtualDesktopHelperInstance = new();
public WindowWalkerCommandsProvider()
{
_settings = new();
Id = "WindowWalker";
DisplayName = Resources.windowwalker_name;
Icon = Icons.WindowWalkerIcon;
Settings = SettingsManager.Instance.Settings;
Settings = _settings.Settings;
_windowWalkerPageItem = new CommandItem(new WindowWalkerListPage())
_windowWalkerPageItem = new CommandItem(new WindowWalkerListPage(_settings))
{
Title = Resources.window_walker_top_level_command_title,
Subtitle = Resources.windowwalker_name,
@@ -31,7 +38,91 @@ public partial class WindowWalkerCommandsProvider : CommandProvider
new CommandContextItem(Settings.SettingsPage),
],
};
_bandItem = new WindowsDockBand();
}
public override ICommandItem[] TopLevelCommands() => [_windowWalkerPageItem];
public override ICommandItem[]? GetDockBands()
{
return new ICommandItem[] { _bandItem };
}
}
#pragma warning disable SA1402 // File may only contain a single type
/// <summary>
/// The Window Walker band is a single command item that is used to create a
/// band in the dock. The command for this band item is a ListPage for the open
/// windows. The dock will then display each of the items on this list page as
/// individual buttons.
/// </summary>
internal sealed partial class WindowsDockBand : CommandItem
{
private readonly IntPtr _hookHandle;
private WINEVENTPROC _hookProc;
private WindowWalkerListPage _page;
public WindowsDockBand()
{
// TODO!Loc
Title = "EXPERIMENTAL: Open windows"; // Resources.window_walker_top_level_command_title;
Subtitle = Resources.windowwalker_name;
var testSettings = new SettingsManager();
testSettings.HideExplorerSettingInfo = true;
testSettings.InMruOrder = false;
testSettings.ResultsFromVisibleDesktopOnly = true;
testSettings.UseWindowIcon = true;
testSettings.ShowSubtitles = false;
testSettings.ShowTitlesOnDock = SettingsManager.Instance.ShowTitlesOnDock;
var testPage = new WindowWalkerListPage(testSettings);
testPage.Id = "com.microsoft.cmdpal.windowwalker.dockband";
_page = testPage;
Command = testPage;
// install window event hook
_hookProc = (WINEVENTPROC)WinEventCallback;
_hookHandle = PInvoke.SetWinEventHook(
PInvoke.EVENT_OBJECT_CREATE,
PInvoke.EVENT_OBJECT_NAMECHANGE, // include name/title changes
HMODULE.Null,
_hookProc,
0,
0,
PInvoke.WINEVENT_OUTOFCONTEXT | PInvoke.WINEVENT_SKIPOWNPROCESS);
}
private void WinEventCallback(
HWINEVENTHOOK hWinEventHook,
uint eventType,
HWND hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime)
{
if (idObject != (int)OBJECT_IDENTIFIER.OBJID_WINDOW ||
hwnd == IntPtr.Zero)
{
return;
}
switch (eventType)
{
case PInvoke.EVENT_OBJECT_CREATE:
case PInvoke.EVENT_OBJECT_SHOW:
// TryAddWindow(hwnd);
// break;
case PInvoke.EVENT_OBJECT_DESTROY:
case PInvoke.EVENT_OBJECT_HIDE:
// TryRemoveWindow(hwnd);
// break;
case PInvoke.EVENT_OBJECT_NAMECHANGE:
// TryUpdateWindow(hwnd);
_page.UpdateWindows();
break;
}
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -2,11 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.WindowWalker.Commands;
using Microsoft.CmdPal.Ext.WindowWalker.Components;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -24,4 +19,24 @@ internal sealed partial class WindowWalkerListItem : ListItem
{
_window = window;
}
public override bool Equals(object? obj)
{
if (obj is WindowWalkerListItem other)
{
if (this._window is null)
{
return other._window is null;
}
return this._window.Equals(other._window);
}
return base.Equals(obj);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}

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.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -27,4 +28,15 @@ public partial class SamplePagesCommandsProvider : CommandProvider
{
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

View File

@@ -6,7 +6,10 @@ using Windows.Foundation;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
public abstract partial class CommandProvider :
ICommandProvider,
ICommandProvider2,
ICommandProvider3
{
public virtual string Id { get; protected set; } = string.Empty;
@@ -48,6 +51,11 @@ public abstract partial class CommandProvider : ICommandProvider, ICommandProvid
}
}
public virtual ICommandItem[]? GetDockBands()
{
return null;
}
/// <summary>
/// This is used to manually populate the WinRT type cache in CmdPal with
/// any interfaces that might not follow a straight linear path of requires.

View File

@@ -0,0 +1,124 @@
// 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.CommandPalette.Extensions.Toolkit;
#pragma warning disable SA1402 // File may only contain a single type
public partial class WrappedDockItem : CommandItem
{
public override string Title => _itemTitle;
public override IIconInfo? Icon => _icon;
public override ICommand? Command => _backingList;
private readonly string _itemTitle;
private readonly IIconInfo? _icon;
private readonly WrappedDockList _backingList;
public IListItem[] Items { get => _backingList.GetItems(); set => _backingList.SetItems(value); }
public WrappedDockItem(
ICommand command,
string displayTitle)
{
_backingList = new WrappedDockList(command);
_itemTitle = string.IsNullOrEmpty(displayTitle) ? command.Name : displayTitle;
_icon = command.Icon;
}
public WrappedDockItem(
ICommandItem item,
string id,
string displayTitle)
{
_backingList = new WrappedDockList(item, id);
_itemTitle = string.IsNullOrEmpty(displayTitle) ? item.Title : displayTitle;
_icon = item.Icon;
}
public WrappedDockItem(IListItem[] items, string id, string displayTitle)
{
_backingList = new WrappedDockList(items, id, displayTitle);
_itemTitle = displayTitle;
}
}
public partial class WrappedDockList : ListPage
{
private string _id;
public override string Id => _id;
// private ICommand _command;
private List<IListItem> _items;
public WrappedDockList(ICommand command)
{
// _command = command;
_items = new() { new ListItem(command) };
Name = command.Name;
_id = command.Id;
}
public WrappedDockList(ICommandItem item, string id)
{
var command = item.Command;
// TODO! This isn't _totally correct, because the wrapping item will not
// listen for property changes on the inner item.
_items = new()
{
new ListItem(command)
{
Title = item.Title,
Subtitle = item.Subtitle,
Icon = item.Icon,
MoreCommands = item.MoreCommands,
},
};
Name = command.Name;
_id = string.IsNullOrEmpty(id) ? command.Id : id;
}
public WrappedDockList(IListItem[] items, string id, string name)
{
_items = new(items);
Name = name;
_id = id;
}
public WrappedDockList(ICommand[] items, string id, string name)
{
_items = new();
foreach (var item in items)
{
_items.Add(new ListItem(item));
}
Name = name;
_id = id;
}
public override IListItem[] GetItems()
{
return _items.ToArray();
}
internal void SetItems(IListItem[]? newItems)
{
if (newItems == null)
{
_items = [];
RaiseItemsChanged(0);
return;
}
ListHelpers.InPlaceUpdateList(_items, newItems);
RaiseItemsChanged(_items.Count);
}
}
#pragma warning restore SA1402 // File may only contain a single type

View File

@@ -391,6 +391,11 @@ namespace Microsoft.CommandPalette.Extensions
{
Object[] GetApiExtensionStubs();
};
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface ICommandProvider3 requires ICommandProvider2
{
ICommandItem[] GetDockBands();
};
}