mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-03 02:46:37 +01:00
Compare commits
79 Commits
main
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0fe992e37 | ||
|
|
bd316d4d34 | ||
|
|
f03eb96b9c | ||
|
|
bbd15a3ae8 | ||
|
|
777a301666 | ||
|
|
f9e3ab4852 | ||
|
|
f311a65708 | ||
|
|
b9040d82c3 | ||
|
|
1d8b45f824 | ||
|
|
221cf083bc | ||
|
|
ccac1e1ac9 | ||
|
|
fb428b2d61 | ||
|
|
acb933643a | ||
|
|
f63785d80d | ||
|
|
87c1a73ecc | ||
|
|
44b0b9ac67 | ||
|
|
7629c6fbfa | ||
|
|
b8c024ac07 | ||
|
|
640c1a8388 | ||
|
|
78b2b23764 | ||
|
|
46d26041b9 | ||
|
|
08454f8b18 | ||
|
|
b7a65ab609 | ||
|
|
08d3435a0d | ||
|
|
46b8eea695 | ||
|
|
5b255011c7 | ||
|
|
6782829cdd | ||
|
|
6ed8d73b50 | ||
|
|
38dfee0234 | ||
|
|
d547a6f613 | ||
|
|
58bea1c813 | ||
|
|
5ad2bdf6c2 | ||
|
|
44f739a289 | ||
|
|
f3d9fc2342 | ||
|
|
90d4ca060e | ||
|
|
6554a4aaaa | ||
|
|
cac0048ca7 | ||
|
|
ddb28a8606 | ||
|
|
a7206863bc | ||
|
|
96def3b79a | ||
|
|
5231543ed2 | ||
|
|
2462da68bc | ||
|
|
bbfa6c6ccb | ||
|
|
f0ea908ee6 | ||
|
|
6e11230fed | ||
|
|
6c26e86e9a | ||
|
|
1d19705568 | ||
|
|
e5e20eca9c | ||
|
|
ef0639602f | ||
|
|
fdd4416049 | ||
|
|
0dab46e58f | ||
|
|
86d1061a25 | ||
|
|
e0197dd7a5 | ||
|
|
64ea63b77d | ||
|
|
bc6b2af03c | ||
|
|
c1af5fdc57 | ||
|
|
5be208520e | ||
|
|
5aaf0e010a | ||
|
|
48eee1b0d9 | ||
|
|
1447a825ee | ||
|
|
76f7dd3b09 | ||
|
|
ee174ddd1d | ||
|
|
35c4f8fdaa | ||
|
|
2ec7ae664e | ||
|
|
1b8ddaa849 | ||
|
|
d6bca1d38e | ||
|
|
b1d7626ab7 | ||
|
|
91598c091e | ||
|
|
fd3e73ee7e | ||
|
|
06a664a53a | ||
|
|
87d2509380 | ||
|
|
c1dc487f2c | ||
|
|
e0dd7ad44a | ||
|
|
aaa68fa351 | ||
|
|
d9e4133b5a | ||
|
|
821b99c4e0 | ||
|
|
8b5a2e9537 | ||
|
|
2e49835b4d | ||
|
|
ef106f6811 |
@@ -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" />
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
72
src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Properties/Resources.Designer.cs
generated
Normal file
72
src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Properties/Resources.Designer.cs
generated
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public partial class LoadingPageViewModel : PageViewModel
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
ModelIsLoading = true;
|
||||
HasBackButton = false;
|
||||
IsInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -18,6 +18,8 @@ public record PerformCommandMessage
|
||||
|
||||
public bool WithAnimation { get; set; } = true;
|
||||
|
||||
public bool TransientPage { get; set; }
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command)
|
||||
{
|
||||
Command = command;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public sealed record WindowHiddenMessage();
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
16
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
16
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal 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
|
||||
}
|
||||
@@ -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();
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record OpenSettingsMessage()
|
||||
public record OpenSettingsMessage(string? Page = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
261
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal file
261
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal 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>
|
||||
231
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal file
231
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal 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
|
||||
@@ -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
|
||||
22
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal file
22
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal 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>
|
||||
711
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal file
711
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal 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
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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="" />
|
||||
<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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.EnableDock, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- 'For Developers' section -->
|
||||
|
||||
<TextBlock x:Uid="ForDevelopersSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Extensions" />
|
||||
<!-- xF596 is HolePunchLandscapeTop -->
|
||||
<NavigationViewItem
|
||||
x:Name="DockSettingsPageNavItem"
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Dock"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Dock" />
|
||||
</NavigationView.MenuItems>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
100
src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml
Normal file
100
src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/Button.xaml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -391,6 +391,11 @@ namespace Microsoft.CommandPalette.Extensions
|
||||
{
|
||||
Object[] GetApiExtensionStubs();
|
||||
};
|
||||
|
||||
|
||||
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
|
||||
interface ICommandProvider3 requires ICommandProvider2
|
||||
{
|
||||
ICommandItem[] GetDockBands();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user