mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-27 14:37:17 +01:00
Compare commits
153 Commits
dev/vanzue
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a328c429b | ||
|
|
41f14c4bf4 | ||
|
|
2913477f07 | ||
|
|
0cc5c08ea4 | ||
|
|
2494bf84b3 | ||
|
|
7f217609f9 | ||
|
|
a0c8b70697 | ||
|
|
7833526186 | ||
|
|
b56e4ea560 | ||
|
|
80e734587b | ||
|
|
daf586b86d | ||
|
|
95247aa6d5 | ||
|
|
947457d20c | ||
|
|
5d4a971bcf | ||
|
|
0cacfa3cc9 | ||
|
|
40f3425a1b | ||
|
|
c32f67bc5a | ||
|
|
ba2ad4b317 | ||
|
|
d46bb66c11 | ||
|
|
ea0af2bb9c | ||
|
|
806d9eebe6 | ||
|
|
e24cf24835 | ||
|
|
30ba1e7aca | ||
|
|
2996d4b9d5 | ||
|
|
efec6cfc03 | ||
|
|
d5017fffe4 | ||
|
|
086c63b6af | ||
|
|
d192672c74 | ||
|
|
1bffcfb6fa | ||
|
|
5d55c5c120 | ||
|
|
90df6f15ae | ||
|
|
aa6acf8145 | ||
|
|
3bae9a57e7 | ||
|
|
e0972996ff | ||
|
|
90afe7e9f5 | ||
|
|
884d1ec6d6 | ||
|
|
0b38be2c01 | ||
|
|
5d7211cf85 | ||
|
|
a5e93da8ad | ||
|
|
17aa472af5 | ||
|
|
96581fd24c | ||
|
|
41d12017b6 | ||
|
|
6e7add6feb | ||
|
|
d55bad1457 | ||
|
|
e02d384051 | ||
|
|
2293b76f7b | ||
|
|
b7ea22e017 | ||
|
|
d83dc94841 | ||
|
|
031f29418e | ||
|
|
fe533cd350 | ||
|
|
d4802fec40 | ||
|
|
12af2770e6 | ||
|
|
d64ccf686f | ||
|
|
17b840c583 | ||
|
|
421df9f1e0 | ||
|
|
806d383272 | ||
|
|
bffce430cc | ||
|
|
b310df6e2e | ||
|
|
1e1f0118d9 | ||
|
|
037062e754 | ||
|
|
19d77a4d02 | ||
|
|
72c0922af7 | ||
|
|
329a6434f8 | ||
|
|
b0250fc0f4 | ||
|
|
029c894ee4 | ||
|
|
f624625344 | ||
|
|
2c61ec1a8c | ||
|
|
3e4e3dd6f1 | ||
|
|
edcce595df | ||
|
|
ef0efcbe2f | ||
|
|
4de34eca96 | ||
|
|
f886d52484 | ||
|
|
017966e3db | ||
|
|
e499f90ee5 | ||
|
|
c0fe992e37 | ||
|
|
bd316d4d34 | ||
|
|
f03eb96b9c | ||
|
|
bbd15a3ae8 | ||
|
|
777a301666 | ||
|
|
f9e3ab4852 | ||
|
|
f311a65708 | ||
|
|
b9040d82c3 | ||
|
|
1d8b45f824 | ||
|
|
221cf083bc | ||
|
|
ccac1e1ac9 | ||
|
|
fb428b2d61 | ||
|
|
acb933643a | ||
|
|
f63785d80d | ||
|
|
87c1a73ecc | ||
|
|
44b0b9ac67 | ||
|
|
7629c6fbfa | ||
|
|
b8c024ac07 | ||
|
|
640c1a8388 | ||
|
|
78b2b23764 | ||
|
|
46d26041b9 | ||
|
|
08454f8b18 | ||
|
|
b7a65ab609 | ||
|
|
08d3435a0d | ||
|
|
46b8eea695 | ||
|
|
5b255011c7 | ||
|
|
6782829cdd | ||
|
|
6ed8d73b50 | ||
|
|
38dfee0234 | ||
|
|
d547a6f613 | ||
|
|
58bea1c813 | ||
|
|
5ad2bdf6c2 | ||
|
|
44f739a289 | ||
|
|
f3d9fc2342 | ||
|
|
90d4ca060e | ||
|
|
6554a4aaaa | ||
|
|
cac0048ca7 | ||
|
|
ddb28a8606 | ||
|
|
a7206863bc | ||
|
|
96def3b79a | ||
|
|
5231543ed2 | ||
|
|
2462da68bc | ||
|
|
bbfa6c6ccb | ||
|
|
f0ea908ee6 | ||
|
|
6e11230fed | ||
|
|
6c26e86e9a | ||
|
|
1d19705568 | ||
|
|
e5e20eca9c | ||
|
|
ef0639602f | ||
|
|
fdd4416049 | ||
|
|
0dab46e58f | ||
|
|
86d1061a25 | ||
|
|
e0197dd7a5 | ||
|
|
64ea63b77d | ||
|
|
bc6b2af03c | ||
|
|
c1af5fdc57 | ||
|
|
5be208520e | ||
|
|
5aaf0e010a | ||
|
|
48eee1b0d9 | ||
|
|
1447a825ee | ||
|
|
76f7dd3b09 | ||
|
|
ee174ddd1d | ||
|
|
35c4f8fdaa | ||
|
|
2ec7ae664e | ||
|
|
1b8ddaa849 | ||
|
|
d6bca1d38e | ||
|
|
b1d7626ab7 | ||
|
|
91598c091e | ||
|
|
fd3e73ee7e | ||
|
|
06a664a53a | ||
|
|
87d2509380 | ||
|
|
c1dc487f2c | ||
|
|
e0dd7ad44a | ||
|
|
aaa68fa351 | ||
|
|
d9e4133b5a | ||
|
|
821b99c4e0 | ||
|
|
8b5a2e9537 | ||
|
|
2e49835b4d | ||
|
|
ef106f6811 |
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -886,6 +886,7 @@ Ldr
|
||||
LEFTALIGN
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
@@ -1011,6 +1012,7 @@ MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
metafile
|
||||
|
||||
@@ -218,6 +218,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.PowerToys/Microsoft.CmdPal.Ext.PowerToys.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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
|
||||
public static class CoreLogger
|
||||
@@ -15,6 +13,8 @@ public static class CoreLogger
|
||||
|
||||
private static ILogger? _logger;
|
||||
|
||||
public static ILogger? Instance => _logger;
|
||||
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.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(IListItem item, string id)
|
||||
: base([item], id, item.Title)
|
||||
{
|
||||
Icon = item.Icon;
|
||||
}
|
||||
}
|
||||
@@ -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,10 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||
|
||||
ShouldShowContextMenu = SelectedItem.MoreCommands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.Count() > 1;
|
||||
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
|
||||
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
|
||||
|
||||
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
|
||||
@@ -42,7 +42,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;
|
||||
|
||||
@@ -64,10 +66,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => this;
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
|
||||
public CommandItemViewModel? SecondaryCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasMoreCommands)
|
||||
{
|
||||
if (MoreCommands[0] is CommandContextItemViewModel command)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
public virtual bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
@@ -331,16 +353,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):
|
||||
@@ -427,11 +452,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,8 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
{
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
if (SelectedItem.MoreCommands.Count() > 1)
|
||||
{
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
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>
|
||||
@@ -16,7 +16,8 @@ namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IDisposable,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<HandleCommandResultMessage>
|
||||
IRecipient<HandleCommandResultMessage>,
|
||||
IRecipient<WindowHiddenMessage>
|
||||
{
|
||||
private readonly IRootPageService _rootPageService;
|
||||
private readonly IAppHostService _appHostService;
|
||||
@@ -79,8 +80,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; }
|
||||
|
||||
@@ -96,11 +98,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]
|
||||
@@ -259,7 +263,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
|
||||
{
|
||||
@@ -269,6 +273,7 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
var isMainPage = command == _rootPage;
|
||||
_isNested = !isMainPage;
|
||||
_currentlyTransient = message.TransientPage;
|
||||
|
||||
// Telemetry: Track extension page navigation for session metrics
|
||||
if (host is not null)
|
||||
@@ -288,6 +293,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)));
|
||||
|
||||
@@ -307,7 +315,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.
|
||||
@@ -478,6 +487,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(
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
internal static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
|
||||
// row 0
|
||||
Color.FromArgb(255, 255, 185, 0), // #ffb900
|
||||
|
||||
@@ -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,16 @@ public sealed class CommandProviderWrapper
|
||||
UnsafePreCacheApiAdditions(two);
|
||||
}
|
||||
|
||||
if (model is ICommandProvider3 supportsDockBands)
|
||||
{
|
||||
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 +183,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,33 +196,68 @@ 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, i);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
|
||||
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)
|
||||
@@ -219,6 +270,10 @@ public sealed class CommandProviderWrapper
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
|
||||
}
|
||||
else if (a is ICommandItem[] commands)
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,4 +290,14 @@ public sealed class CommandProviderWrapper
|
||||
// In handling this, a call will be made to `LoadTopLevelCommands` to
|
||||
// retrieve the new items.
|
||||
this.CommandsChanged?.Invoke(this, args);
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
Logger.LogDebug($"CommandProviderWrapper.PinDockBand: {ProviderId} - {bandVm.Id}");
|
||||
|
||||
var bands = this.DockBandItems.ToList();
|
||||
bands.Add(bandVm);
|
||||
this.DockBandItems = bands.ToArray();
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +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.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;
|
||||
|
||||
@@ -18,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) { },
|
||||
@@ -37,11 +42,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);
|
||||
|
||||
@@ -50,6 +50,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -70,6 +70,15 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
|
||||
RenderCard();
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void RenderCard()
|
||||
{
|
||||
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
|
||||
{
|
||||
Card = builtCard;
|
||||
@@ -93,8 +102,41 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
UpdateProperty(nameof(Card));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._formModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(DataJson):
|
||||
DataJson = model.DataJson;
|
||||
RenderCard();
|
||||
break;
|
||||
case nameof(TemplateJson):
|
||||
TemplateJson = model.TemplateJson;
|
||||
RenderCard();
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the band is pinned to the dock.
|
||||
/// When enabled, pins to Center. When disabled, removes from all sides.
|
||||
/// </summary>
|
||||
public bool IsPinned
|
||||
{
|
||||
get => PinSide != DockPinSide.None;
|
||||
set
|
||||
{
|
||||
if (value && PinSide == DockPinSide.None)
|
||||
{
|
||||
// Pin to Center by default when enabling
|
||||
PinSide = DockPinSide.Center;
|
||||
}
|
||||
else if (!value && PinSide != DockPinSide.None)
|
||||
{
|
||||
// Remove from dock when disabling
|
||||
PinSide = DockPinSide.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DockBandSettingsViewModel(
|
||||
DockBandSettings dockSettingsModel,
|
||||
TopLevelViewModel topLevelAdapter,
|
||||
DockBandViewModel? bandViewModel,
|
||||
SettingsModel settingsModel)
|
||||
{
|
||||
_dockSettingsModel = dockSettingsModel;
|
||||
_adapter = topLevelAdapter;
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsModel = settingsModel;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var inStart = dockSettings.StartBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inStart)
|
||||
{
|
||||
return DockPinSide.Start;
|
||||
}
|
||||
|
||||
var inCenter = dockSettings.CenterBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inCenter)
|
||||
{
|
||||
return DockPinSide.Center;
|
||||
}
|
||||
|
||||
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));
|
||||
OnPropertyChanged(nameof(IsPinned));
|
||||
}
|
||||
|
||||
public void SetBandPosition(DockPinSide side, int? index)
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from all sides first
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
dockSettings.CenterBands.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.Center:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.CenterBands.Count;
|
||||
dockSettings.CenterBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.EndBands.Count;
|
||||
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.None:
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private void OnPinSideChanged(DockPinSide value)
|
||||
{
|
||||
SetBandPosition(value, null);
|
||||
_pinSide = value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DockPinSide
|
||||
{
|
||||
None,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
}
|
||||
|
||||
public enum ShowLabelsOption
|
||||
{
|
||||
Default,
|
||||
ShowLabels,
|
||||
HideLabels,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,196 @@
|
||||
// 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;
|
||||
private readonly DockBandSettings _bandSettings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly Action _saveSettings;
|
||||
|
||||
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
||||
|
||||
private bool _showLabels = true;
|
||||
private bool? _showLabelsSnapshot;
|
||||
|
||||
public string Id => _rootItem.Command.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether labels are shown for items in this band.
|
||||
/// This is a preview value - call <see cref="SaveShowLabels"/> to persist or
|
||||
/// <see cref="RestoreShowLabels"/> to discard changes.
|
||||
/// </summary>
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => _showLabels;
|
||||
set
|
||||
{
|
||||
if (_showLabels != value)
|
||||
{
|
||||
_showLabels = value;
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.ShowLabel = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current ShowLabels value before editing.
|
||||
/// </summary>
|
||||
internal void SnapshotShowLabels()
|
||||
{
|
||||
_showLabelsSnapshot = _showLabels;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current ShowLabels value to settings.
|
||||
/// </summary>
|
||||
internal void SaveShowLabels()
|
||||
{
|
||||
_bandSettings.ShowLabels = _showLabels;
|
||||
_showLabelsSnapshot = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the ShowLabels value from the snapshot.
|
||||
/// </summary>
|
||||
internal void RestoreShowLabels()
|
||||
{
|
||||
if (_showLabelsSnapshot.HasValue)
|
||||
{
|
||||
ShowLabels = _showLabelsSnapshot.Value;
|
||||
_showLabelsSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal DockBandViewModel(
|
||||
CommandItemViewModel commandItemViewModel,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
DockBandSettings settings,
|
||||
DockSettings dockSettings,
|
||||
Action saveSettings)
|
||||
: base(errorContext)
|
||||
{
|
||||
_rootItem = commandItemViewModel;
|
||||
_bandSettings = settings;
|
||||
_dockSettings = dockSettings;
|
||||
_saveSettings = saveSettings;
|
||||
|
||||
_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;
|
||||
|
||||
public bool ShowLabel
|
||||
{
|
||||
get => _showLabel;
|
||||
internal set
|
||||
{
|
||||
if (_showLabel != value)
|
||||
{
|
||||
_showLabel = value;
|
||||
UpdateProperty(nameof(ShowLabel));
|
||||
UpdateProperty(nameof(HasText));
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string Title => _showLabel ? ItemTitle : string.Empty;
|
||||
|
||||
public new string Subtitle => _showLabel ? base.Subtitle : string.Empty;
|
||||
|
||||
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,602 @@
|
||||
// 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 readonly SettingsModel _settingsModel;
|
||||
|
||||
private DockSettings _settings;
|
||||
|
||||
public TaskScheduler Scheduler { get; }
|
||||
|
||||
public ObservableCollection<DockBandViewModel> StartItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> CenterItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
|
||||
|
||||
public DockViewModel(
|
||||
TopLevelCommandManager tlcManager,
|
||||
SettingsModel settings,
|
||||
TaskScheduler scheduler)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_settingsModel = settings;
|
||||
_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.CenterBands, CenterItems);
|
||||
SetupBands(_settings.EndBands, EndItems);
|
||||
}
|
||||
|
||||
private void SetupBands(
|
||||
List<DockBandSettings> bands,
|
||||
ObservableCollection<DockBandViewModel> target)
|
||||
{
|
||||
List<DockBandViewModel> newBands = new();
|
||||
foreach (var band in bands)
|
||||
{
|
||||
var commandId = band.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, SaveSettings);
|
||||
band.InitializeProperties(); // TODO! make async
|
||||
return band;
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||
{
|
||||
var id = tlc.Id;
|
||||
return FindBandById(id);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandById(string id)
|
||||
{
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Syncs the band position in settings after a same-list reorder.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all settings lists
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
|
||||
// Add to target settings list at the correct index
|
||||
var targetSettings = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => dockSettings.StartBands,
|
||||
DockPinSide.Center => dockSettings.CenterBands,
|
||||
DockPinSide.End => dockSettings.EndBands,
|
||||
_ => dockSettings.StartBands,
|
||||
};
|
||||
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
|
||||
targetSettings.Insert(insertIndex, bandSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a dock band to a new position (cross-list drop).
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
Logger.LogWarning($"Could not find band settings for band {bandId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all sides (settings and UI)
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
// Add to the target side at the specified index
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
|
||||
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, StartItems.Count);
|
||||
StartItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.CenterBands.Count);
|
||||
dockSettings.CenterBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
|
||||
CenterItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
|
||||
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, EndItems.Count);
|
||||
EndItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current band order and label settings to settings.
|
||||
/// Call this when exiting edit mode.
|
||||
/// </summary>
|
||||
public void SaveBandOrder()
|
||||
{
|
||||
// Save ShowLabels for all bands
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
band.SaveShowLabels();
|
||||
}
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotPinnedCommands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
Logger.LogDebug("Saved band order to settings");
|
||||
}
|
||||
|
||||
private List<DockBandSettings>? _snapshotStartBands;
|
||||
private List<DockBandSettings>? _snapshotCenterBands;
|
||||
private List<DockBandSettings>? _snapshotEndBands;
|
||||
private List<string>? _snapshotPinnedCommands;
|
||||
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current band order and label settings before editing.
|
||||
/// Call this when entering edit mode.
|
||||
/// </summary>
|
||||
public void SnapshotBandOrder()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
_snapshotStartBands = dockSettings.StartBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotCenterBands = dockSettings.CenterBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotEndBands = dockSettings.EndBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotPinnedCommands = dockSettings.PinnedCommands.ToList();
|
||||
|
||||
// Snapshot band ViewModels so we can restore unpinned bands
|
||||
// Use a dictionary but handle potential duplicates gracefully
|
||||
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
_snapshotBandViewModels.TryAdd(band.Id, band);
|
||||
}
|
||||
|
||||
// Snapshot ShowLabels for all bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.SnapshotShowLabels();
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotCenterBands.Count} center bands, {_snapshotEndBands.Count} end bands");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the band order and label settings from the snapshot taken when entering edit mode.
|
||||
/// Call this when discarding edit mode changes.
|
||||
/// </summary>
|
||||
public void RestoreBandOrder()
|
||||
{
|
||||
if (_snapshotStartBands == null || _snapshotCenterBands == null || _snapshotEndBands == null || _snapshotBandViewModels == null || _snapshotPinnedCommands == null)
|
||||
{
|
||||
Logger.LogWarning("No snapshot to restore from");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore ShowLabels for all snapshotted bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.RestoreShowLabels();
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Restore PinnedCommands from snapshot
|
||||
dockSettings.PinnedCommands.Clear();
|
||||
dockSettings.PinnedCommands.AddRange(_snapshotPinnedCommands);
|
||||
|
||||
// Restore settings from snapshot
|
||||
dockSettings.StartBands.Clear();
|
||||
dockSettings.CenterBands.Clear();
|
||||
dockSettings.EndBands.Clear();
|
||||
|
||||
foreach (var bandSnapshot in _snapshotStartBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotCenterBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotEndBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
// Rebuild UI collections from restored settings using the snapshotted ViewModels
|
||||
RebuildUICollectionsFromSnapshot();
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotPinnedCommands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
Logger.LogDebug("Restored band order from snapshot");
|
||||
}
|
||||
|
||||
private void RebuildUICollectionsFromSnapshot()
|
||||
{
|
||||
if (_snapshotBandViewModels == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildUICollections()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Create a lookup of all current band ViewModels
|
||||
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of dock bands that are not currently pinned to any section.
|
||||
/// </summary>
|
||||
public IEnumerable<TopLevelViewModel> GetAvailableBandsToAdd()
|
||||
{
|
||||
// Get IDs of all bands currently in the dock
|
||||
var pinnedBandIds = new HashSet<string>();
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
// Return all dock bands that are not already pinned
|
||||
return AllItems.Where(tlc => !pinnedBandIds.Contains(tlc.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a band to the specified dock section.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void AddBandToSection(TopLevelViewModel topLevel, DockPinSide targetSide)
|
||||
{
|
||||
var bandId = topLevel.Id;
|
||||
|
||||
// Check if already in the dock
|
||||
if (FindBandById(bandId) != null)
|
||||
{
|
||||
Logger.LogWarning($"Band {bandId} is already in the dock");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { Id = bandId, ShowLabels = null };
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// If this is not an explicit dock band (i.e., it's from TopLevelCommands),
|
||||
// add it to PinnedCommands so it gets loaded as a dock band on restart
|
||||
if (!topLevel.IsDockBand && !dockSettings.PinnedCommands.Contains(bandId))
|
||||
{
|
||||
dockSettings.PinnedCommands.Add(bandId);
|
||||
}
|
||||
|
||||
// Create the band view model
|
||||
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
||||
|
||||
// Add to the appropriate section
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
StartItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.Center:
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
CenterItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.End:
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
EndItems.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
|
||||
// Snapshot the new band so it can be removed on discard
|
||||
bandVm.SnapshotShowLabels();
|
||||
|
||||
Logger.LogDebug($"Added band {bandId} to {targetSide} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpins a band from the dock, removing it from whichever section it's in.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void UnpinBand(DockBandViewModel band)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from settings
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
|
||||
// Also remove from PinnedCommands if it was pinned from TopLevelCommands
|
||||
dockSettings.PinnedCommands.Remove(bandId);
|
||||
|
||||
// Remove from UI collections
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
Logger.LogDebug($"Unpinned band {bandId} (not saved yet)");
|
||||
}
|
||||
|
||||
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 editDockCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new EnterDockEditModeMessage());
|
||||
})
|
||||
{
|
||||
Name = "Edit dock", // TODO!Loc
|
||||
Icon = Icons.EditIcon,
|
||||
};
|
||||
|
||||
var openSettingsCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
|
||||
})
|
||||
{
|
||||
Name = "Dock settings", // TODO!Loc
|
||||
Icon = Icons.SettingsIcon,
|
||||
};
|
||||
|
||||
MoreCommands = new CommandContextItem[]
|
||||
{
|
||||
new CommandContextItem(editDockCommand),
|
||||
new CommandContextItem(openSettingsCommand),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public partial class DockWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ImageSource? BackgroundImageSource { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageOpacity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundImageTint { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageTintIntensity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int BackgroundImageBlurAmount { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageBrightness { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowBackgroundImage { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowColorizationOverlay { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color ColorizationColor { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double ColorizationOpacity { get; private set; }
|
||||
|
||||
public DockWindowViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
UpdateFromThemeSnapshot();
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(UpdateFromThemeSnapshot);
|
||||
}
|
||||
|
||||
private void UpdateFromThemeSnapshot()
|
||||
{
|
||||
var snapshot = _themeService.CurrentDockTheme;
|
||||
|
||||
BackgroundImageSource = snapshot.BackgroundImageSource;
|
||||
BackgroundImageStretch = snapshot.BackgroundImageStretch;
|
||||
BackgroundImageOpacity = snapshot.BackgroundImageOpacity;
|
||||
|
||||
BackgroundImageBrightness = snapshot.BackgroundBrightness;
|
||||
BackgroundImageTint = snapshot.Tint;
|
||||
BackgroundImageTintIntensity = snapshot.TintIntensity;
|
||||
BackgroundImageBlurAmount = snapshot.BlurAmount;
|
||||
|
||||
ShowBackgroundImage = BackgroundImageSource != null;
|
||||
|
||||
// Colorization overlay for transparent backdrop
|
||||
ShowColorizationOverlay = snapshot.Backdrop == DockBackdrop.Transparent && snapshot.TintIntensity > 0;
|
||||
ColorizationColor = snapshot.Tint;
|
||||
ColorizationOpacity = snapshot.TintIntensity;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for dock appearance settings, controlling theme, backdrop, colorization,
|
||||
/// and background image settings for the dock.
|
||||
/// </summary>
|
||||
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private ElementTheme? _elementThemeOverride;
|
||||
private Color _currentSystemAccentColor;
|
||||
|
||||
public ObservableCollection<Color> Swatches => AppearanceSettingsViewModel.WindowsColorSwatches;
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_dockSettings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _dockSettings.Theme;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Theme != value)
|
||||
{
|
||||
_dockSettings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackdropIndex
|
||||
{
|
||||
get => (int)_dockSettings.Backdrop;
|
||||
set => Backdrop = (DockBackdrop)value;
|
||||
}
|
||||
|
||||
public DockBackdrop Backdrop
|
||||
{
|
||||
get => _dockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Backdrop != value)
|
||||
{
|
||||
_dockSettings.Backdrop = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackdropIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _dockSettings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.ColorizationMode != value)
|
||||
{
|
||||
_dockSettings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||
|
||||
if (value == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
|
||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_dockSettings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _dockSettings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.CustomThemeColor != value)
|
||||
{
|
||||
_dockSettings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
if (ColorIntensity == 0)
|
||||
{
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _dockSettings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_dockSettings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _dockSettings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImagePath != value)
|
||||
{
|
||||
_dockSettings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
{
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _dockSettings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _dockSettings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageFit != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageFitIndex
|
||||
{
|
||||
get => BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => 1,
|
||||
_ => 0,
|
||||
};
|
||||
set => BackgroundImageFit = value switch
|
||||
{
|
||||
1 => BackgroundImageFit.Fill,
|
||||
_ => BackgroundImageFit.UniformToFill,
|
||||
};
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor => ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
|
||||
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
|
||||
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
|
||||
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_dockSettings = settings.DockSettings;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
UpdateAccentColor(_uiSettings);
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _dockSettings.ColorizationMode != ColorizationMode.None;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
|
||||
private void UpdateAccentColor(UISettings sender)
|
||||
{
|
||||
_currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
|
||||
if (ColorizationMode == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Reapply()
|
||||
{
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
||||
OnPropertyChanged(nameof(EffectiveThemeColor));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
|
||||
|
||||
// LOAD BEARING:
|
||||
// We need to cycle through the EffectiveTheme property to force reload of resources.
|
||||
_elementThemeOverride = ElementTheme.Light;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = ElementTheme.Dark;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = null;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetBackgroundImageProperties()
|
||||
{
|
||||
BackgroundImageBrightness = 0;
|
||||
BackgroundImageBlurAmount = 0;
|
||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||
BackgroundImageOpacity = 100;
|
||||
ColorIntensity = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
}
|
||||
}
|
||||
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
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
|
||||
|
||||
internal static IconInfo EditIcon => new("\uE70F"); // Edit 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();
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record EnterDockEditModeMessage();
|
||||
@@ -4,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);
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -429,6 +447,42 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fallbacks.
|
||||
/// </summary>
|
||||
|
||||
@@ -239,6 +239,30 @@
|
||||
<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>
|
||||
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
|
||||
<value>Pick background image</value>
|
||||
</data>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a snapshot of dock theme-related visual settings, including accent color, theme preference,
|
||||
/// backdrop, and background image configuration, for use in rendering the Dock UI.
|
||||
/// </summary>
|
||||
public sealed class DockThemeSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Dock visuals.
|
||||
/// </summary>
|
||||
public required Color Tint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the intensity of the accent tint color (0-1 range).
|
||||
/// </summary>
|
||||
public required float TintIntensity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured application theme preference for the Dock.
|
||||
/// </summary>
|
||||
public required ElementTheme Theme { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backdrop type for the Dock.
|
||||
/// </summary>
|
||||
public required DockBackdrop Backdrop { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image source to render as the background, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <see langword="null"/> when no background image is configured.
|
||||
/// </remarks>
|
||||
public required ImageSource? BackgroundImageSource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stretch mode used to lay out the background image.
|
||||
/// </summary>
|
||||
public required Stretch BackgroundImageStretch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the opacity applied to the background image.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
|
||||
/// </value>
|
||||
public required double BackgroundImageOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
|
||||
/// </summary>
|
||||
public required AcrylicBackdropParameters BackdropParameters { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blur amount for the background image.
|
||||
/// </summary>
|
||||
public required int BlurAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brightness adjustment for the background (0-1 range).
|
||||
/// </summary>
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,4 +38,9 @@ public interface IThemeService
|
||||
/// Gets the current theme settings.
|
||||
/// </summary>
|
||||
ThemeSnapshot Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current dock theme settings.
|
||||
/// </summary>
|
||||
DockThemeSnapshot CurrentDockTheme { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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.UI;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
||||
/// settings are in <see cref="DockBandSettings"/>.
|
||||
/// </summary>
|
||||
public class DockSettings
|
||||
{
|
||||
public DockSide Side { get; set; } = DockSide.Top;
|
||||
|
||||
public DockSize DockSize { get; set; } = DockSize.Small;
|
||||
|
||||
public DockSize DockIconsSize { get; set; } = DockSize.Small;
|
||||
|
||||
// Theme settings
|
||||
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
|
||||
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
|
||||
public int BackgroundImageBrightness { get; set; }
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
// /Theme settings
|
||||
public List<string> PinnedCommands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> StartBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> CenterBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> EndBands { get; set; } = [];
|
||||
|
||||
public bool ShowLabels { get; set; } = true;
|
||||
|
||||
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.performanceWidget" });
|
||||
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
|
||||
{
|
||||
Transparent,
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -66,6 +66,11 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public bool EnableDock { get; set; }
|
||||
|
||||
public DockSettings DockSettings { get; set; } = new();
|
||||
|
||||
// Theme settings
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
@@ -84,6 +89,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
// </Theme settings>
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -303,6 +310,7 @@ public partial class SettingsModel : ObservableObject
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(Color))]
|
||||
[JsonSerializable(typeof(HistoryItem))]
|
||||
[JsonSerializable(typeof(SettingsModel))]
|
||||
[JsonSerializable(typeof(WindowPosition))]
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -32,6 +35,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public AppearanceSettingsViewModel Appearance { get; }
|
||||
|
||||
public DockAppearanceSettingsViewModel DockAppearance { get; }
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
@@ -173,6 +178,58 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide Dock_Side
|
||||
{
|
||||
get => _settings.DockSettings.Side;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Side = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockSize Dock_DockSize
|
||||
{
|
||||
get => _settings.DockSettings.DockSize;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.DockSize = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockBackdrop Dock_Backdrop
|
||||
{
|
||||
get => _settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Backdrop = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_ShowLabels
|
||||
{
|
||||
get => _settings.DockSettings.ShowLabels;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.ShowLabels = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableDock
|
||||
{
|
||||
get => _settings.EnableDock;
|
||||
set
|
||||
{
|
||||
_settings.EnableDock = value;
|
||||
Save();
|
||||
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
|
||||
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = new();
|
||||
|
||||
public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();
|
||||
@@ -185,6 +242,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
|
||||
@@ -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,52 @@ 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}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the band to DockBands if not already present
|
||||
if (!DockBands.Any(b => b.Id == bandVm.Id))
|
||||
{
|
||||
DockBands.Add(bandVm);
|
||||
}
|
||||
|
||||
// Notify DockViewModel to update its collections
|
||||
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_reloadCommandsGate.Dispose();
|
||||
|
||||
@@ -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 System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
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.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -22,6 +24,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
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, IEx
|
||||
|
||||
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();
|
||||
@@ -176,9 +168,37 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -192,23 +212,23 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
_commandProviderId = commandProviderId;
|
||||
_commandItemViewModel = item;
|
||||
|
||||
IsFallback = isFallback;
|
||||
IsFallback = topLevelType == TopLevelType.Fallback;
|
||||
IsDockBand = topLevelType == TopLevelType.DockBand;
|
||||
ExtensionHost = extensionHost;
|
||||
if (isFallback && commandItem is FallbackCommandItem fallback)
|
||||
if (IsFallback && commandItem is FallbackCommandItem fallback)
|
||||
{
|
||||
_fallbackId = fallback.Id;
|
||||
}
|
||||
|
||||
item.PropertyChanged += Item_PropertyChanged;
|
||||
|
||||
// UpdateAlias();
|
||||
// UpdateHotkey();
|
||||
// UpdateTags();
|
||||
_dockViewModel = serviceProvider.GetService<DockViewModel>();
|
||||
}
|
||||
|
||||
internal void InitializeProperties()
|
||||
{
|
||||
ItemViewModel.SlowInitializeProperties();
|
||||
GenerateId();
|
||||
|
||||
if (IsFallback)
|
||||
{
|
||||
@@ -260,7 +280,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
return;
|
||||
}
|
||||
|
||||
_initialIcon = _commandItemViewModel.Icon;
|
||||
_initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
|
||||
|
||||
if (raiseNotification)
|
||||
{
|
||||
@@ -420,4 +440,167 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
_commandItemViewModel.Model.Unsafe);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
// It's possible that the top-level command shares an ID with a
|
||||
// band. In that case, we don't want to add it to PinnedCommands.
|
||||
// PinnedCommands is just for top-level commands IDs that aren't
|
||||
// otherwise bands.
|
||||
//
|
||||
// Check the top-level command ID against the bands first.
|
||||
if (_topLevelCommandManager.DockBands.Any(band => band.Id == _topLevelViewModel.Id))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
// In this case, the ID isn't another band, so add it to PinnedCommands.
|
||||
if (!_settings.DockSettings.PinnedCommands.Contains(_topLevelViewModel.Id))
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Add(_topLevelViewModel.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO! Deal with "the command ID is already pinned in
|
||||
// PinnedCommands but not in one of StartBands/EndBands". I think
|
||||
// we're already avoiding adding it to PinnedCommands above, but I
|
||||
// think that PinDockBand below will create a duplicate VM for it.
|
||||
_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"
|
||||
xmlns:services="using:Microsoft.CmdPal.UI.Services">
|
||||
<Application.Resources>
|
||||
@@ -11,13 +12,13 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/Tag.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Dock/DockItemControl.xaml" />
|
||||
<!-- Default theme dictionary -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
|
||||
<services:MutableOverridesDictionary />
|
||||
@@ -26,6 +27,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;
|
||||
@@ -27,6 +28,7 @@ using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -167,6 +169,7 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services)
|
||||
@@ -201,6 +204,7 @@ public partial class App : Application
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<DockViewModel>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ScrollContainer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="ScrollButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{ThemeResource FlipViewNextPreviousButtonBackground}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
Background="{x:Bind Background, Mode=OneWay}"
|
||||
BorderBrush="{x:Bind BorderBrush, Mode=OneWay}"
|
||||
BorderThickness="{x:Bind BorderThickness, Mode=OneWay}"
|
||||
CornerRadius="{x:Bind CornerRadius, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition x:Name="Row2" Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Action button - position controlled by visual states -->
|
||||
<ContentPresenter
|
||||
x:Name="ActionButtonPresenter"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="2"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Content="{x:Bind ActionButton, Mode=OneWay}"
|
||||
Visibility="{x:Bind ActionButtonVisibility, Mode=OneWay}" />
|
||||
|
||||
<Grid
|
||||
x:Name="ScrollerContainer"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1">
|
||||
<ScrollViewer
|
||||
x:Name="scroller"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
HorizontalScrollMode="Enabled"
|
||||
SizeChanged="Scroller_SizeChanged"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
VerticalScrollMode="Disabled"
|
||||
ViewChanging="Scroller_ViewChanging">
|
||||
<Grid x:Name="ContentGrid">
|
||||
<ContentPresenter Content="{x:Bind Source, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<Button
|
||||
x:Name="ScrollBackBtn"
|
||||
Margin="8,0,0,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll left"
|
||||
Click="ScrollBackBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll left"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
x:Name="ScrollBackIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="ScrollForwardBtn"
|
||||
Margin="0,0,8,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll right"
|
||||
Click="ScrollForwardBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll right">
|
||||
<FontIcon
|
||||
x:Name="ScrollForwardIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="OrientationStates">
|
||||
<VisualState x:Name="HorizontalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Enabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Disabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="8,0,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,8,0" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="VerticalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Disabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Enabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="0,8,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,0,8" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="LayoutStates">
|
||||
<!-- Horizontal + Start: button on right -->
|
||||
<VisualState x:Name="HorizontalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="4,0,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Horizontal + End: button on left -->
|
||||
<VisualState x:Name="HorizontalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,4,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + Start: button on bottom -->
|
||||
<VisualState x:Name="VerticalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,4,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + End: button on top -->
|
||||
<VisualState x:Name="VerticalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,0,4" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ScrollContainer : UserControl
|
||||
{
|
||||
public enum ScrollContentAlignment
|
||||
{
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
public ScrollContainer()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += ScrollContainer_Loaded;
|
||||
}
|
||||
|
||||
private void ScrollContainer_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateOrientationState();
|
||||
UpdateLayoutState();
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => (object)GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SourceProperty =
|
||||
DependencyProperty.Register(nameof(Source), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(ScrollContainer), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
|
||||
|
||||
public ScrollContentAlignment ContentAlignment
|
||||
{
|
||||
get => (ScrollContentAlignment)GetValue(ContentAlignmentProperty);
|
||||
set => SetValue(ContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentAlignmentProperty =
|
||||
DependencyProperty.Register(nameof(ContentAlignment), typeof(ScrollContentAlignment), typeof(ScrollContainer), new PropertyMetadata(ScrollContentAlignment.Start, OnContentAlignmentChanged));
|
||||
|
||||
public object ActionButton
|
||||
{
|
||||
get => (object)GetValue(ActionButtonProperty);
|
||||
set => SetValue(ActionButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonProperty =
|
||||
DependencyProperty.Register(nameof(ActionButton), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Visibility ActionButtonVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(ActionButtonVisibilityProperty);
|
||||
set => SetValue(ActionButtonVisibilityProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonVisibilityProperty =
|
||||
DependencyProperty.Register(nameof(ActionButtonVisibility), typeof(Visibility), typeof(ScrollContainer), new PropertyMetadata(Visibility.Collapsed));
|
||||
|
||||
private static void OnContentAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToAlignment()
|
||||
{
|
||||
// Reset button visibility
|
||||
ScrollBackBtn.Visibility = Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (ContentAlignment == ScrollContentAlignment.End)
|
||||
{
|
||||
// Scroll to the end
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.ScrollableWidth, null, null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.ScrollableHeight, null, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scroll to the beginning
|
||||
scroller.ChangeView(0, 0, null, true);
|
||||
}
|
||||
|
||||
// Defer visibility update until after layout
|
||||
void OnLayoutUpdated(object? sender, object args)
|
||||
{
|
||||
scroller.LayoutUpdated -= OnLayoutUpdated;
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
scroller.LayoutUpdated += OnLayoutUpdated;
|
||||
}
|
||||
|
||||
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateOrientationState();
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOrientationState()
|
||||
{
|
||||
var stateName = Orientation == Orientation.Horizontal ? "HorizontalState" : "VerticalState";
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void UpdateLayoutState()
|
||||
{
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var isStart = ContentAlignment == ScrollContentAlignment.Start;
|
||||
|
||||
var stateName = (isHorizontal, isStart) switch
|
||||
{
|
||||
(true, true) => "HorizontalStartState",
|
||||
(true, false) => "HorizontalEndState",
|
||||
(false, true) => "VerticalStartState",
|
||||
(false, false) => "VerticalEndState",
|
||||
};
|
||||
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void Scroller_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility(e.FinalView.HorizontalOffset, e.FinalView.VerticalOffset);
|
||||
}
|
||||
|
||||
private void ScrollBackBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset - scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset - scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollForwardBtn since this button disappears after scrolling to the end.
|
||||
ScrollForwardBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void ScrollForwardBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset + scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset + scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollBackBtn since this button disappears after scrolling to the end.
|
||||
ScrollBackBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void Scroller_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
private void UpdateScrollButtonsVisibility(double? horizontalOffset = null, double? verticalOffset = null)
|
||||
{
|
||||
var hOffset = horizontalOffset ?? scroller.HorizontalOffset;
|
||||
var vOffset = verticalOffset ?? scroller.VerticalOffset;
|
||||
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
ScrollBackBtn.Visibility = hOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableWidth > 0 && hOffset < scroller.ScrollableWidth - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrollBackBtn.Visibility = vOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableHeight > 0 && vOffset < scroller.ScrollableHeight - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
internal sealed partial class DockBandTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DockControl? Control { get; set; }
|
||||
|
||||
public DataTemplate? HorizontalTemplate { get; set; }
|
||||
|
||||
public DataTemplate? VerticalTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
if (Control is null)
|
||||
{
|
||||
return HorizontalTemplate;
|
||||
}
|
||||
|
||||
return Control.ItemsOrientation == Orientation.Horizontal
|
||||
? HorizontalTemplate
|
||||
: VerticalTemplate;
|
||||
}
|
||||
}
|
||||
463
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal file
463
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal file
@@ -0,0 +1,463 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreVm="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ItemsPanelTemplate x:Key="HorizontalItemsPanel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="VerticalItemsPanel">
|
||||
<StackPanel Orientation="Vertical" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
<DataTemplate x:Key="DeskbandTemplate" x:DataType="dockVm:DockItemViewModel">
|
||||
<local:DockItemControl
|
||||
Title="{x:Bind Title, Mode=OneWay}"
|
||||
RightTapped="BandItem_RightTapped"
|
||||
Subtitle="{x:Bind Subtitle, Mode=OneWay}"
|
||||
Tapped="BandItem_Tapped"
|
||||
ToolTip="{x:Bind Tooltip, Mode=OneWay}">
|
||||
<local:DockItemControl.Icon>
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="16"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
</local:DockItemControl.Icon>
|
||||
</local:DockItemControl>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="HorizontalDockBandTemplate" x:DataType="dockVm:DockBandViewModel">
|
||||
<ItemsControl
|
||||
ItemTemplate="{StaticResource DeskbandTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind Items, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="VerticalDockBandTemplate" x:DataType="dockVm:DockBandViewModel">
|
||||
<ItemsControl
|
||||
ItemTemplate="{StaticResource DeskbandTemplate}"
|
||||
ItemsPanel="{StaticResource VerticalItemsPanel}"
|
||||
ItemsSource="{x:Bind Items, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<local:DockBandTemplateSelector
|
||||
x:Key="DockBandTemplateSelector"
|
||||
Control="{x:Bind}"
|
||||
HorizontalTemplate="{StaticResource HorizontalDockBandTemplate}"
|
||||
VerticalTemplate="{StaticResource VerticalDockBandTemplate}" />
|
||||
|
||||
<Style x:Key="DockBandListViewStyle" TargetType="ListView">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="IsItemClickEnabled" Value="False" />
|
||||
<Setter Property="SelectionMode" Value="None" />
|
||||
<!-- Drag properties controlled by code-behind based on IsEditMode -->
|
||||
<Setter Property="CanDragItems" Value="False" />
|
||||
<Setter Property="CanReorderItems" Value="False" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DockBandListViewItemStyle" TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0,0,4,0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Name="ContextMenuFlyoutStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||
</Flyout>
|
||||
|
||||
<!-- Edit mode context menu for dock bands -->
|
||||
<MenuFlyout x:Name="EditModeContextMenu" ShouldConstrainToRootBounds="False">
|
||||
<MenuFlyoutItem
|
||||
x:Name="ShowLabelsMenuItem"
|
||||
Click="ShowLabelsMenuItem_Click"
|
||||
Text="Show labels">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem
|
||||
x:Name="HideLabelsMenuItem"
|
||||
Click="HideLabelsMenuItem_Click"
|
||||
Text="Hide labels">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Name="UnpinBandMenuItem"
|
||||
Click="UnpinBandMenuItem_Click"
|
||||
Text="Unpin">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
|
||||
<!-- Add band flyout - used in edit mode to add bands to dock sections -->
|
||||
<Flyout
|
||||
x:Name="AddBandFlyout"
|
||||
Placement="Bottom"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<StackPanel Width="320">
|
||||
<TextBlock
|
||||
x:Name="NoAvailableBandsText"
|
||||
Padding="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="No commands available to pin"
|
||||
TextAlignment="Center"
|
||||
Visibility="Collapsed" />
|
||||
<ListView
|
||||
x:Name="AddBandListView"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="AddBandListView_ItemClick"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TopLevelViewModel">
|
||||
<Grid Padding="4" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<cpcontrols:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
BorderThickness="0,0,0,1"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<!-- Edit Mode Overlay - shown when in edit mode -->
|
||||
<Grid
|
||||
x:Name="ContentGrid"
|
||||
Margin="4"
|
||||
Padding="4,0,4,0"
|
||||
Background="Transparent"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="StartScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="StartAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="Start"
|
||||
ToolTipService.ToolTip="Add band to Start">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="StartListView"
|
||||
HorizontalAlignment="Stretch"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="StartListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}" />
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="CenterScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1"
|
||||
MinWidth="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Stretch">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="CenterAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="Center"
|
||||
ToolTipService.ToolTip="Add band to Center">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="CenterListView"
|
||||
HorizontalAlignment="Stretch"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="CenterListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.CenterItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}" />
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="EndScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
ContentAlignment="End">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="EndAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="End"
|
||||
ToolTipService.ToolTip="Add band to End">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="EndListView"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="EndListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplateSelector="{StaticResource DockBandTemplateSelector}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<!-- To do: remove the X button -->
|
||||
<TeachingTip
|
||||
x:Name="EditButtonsTeachingTip"
|
||||
MinWidth="0"
|
||||
PreferredPlacement="Bottom"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
Target="{x:Bind ContentGrid}">
|
||||
<!--<TeachingTip.CloseButtonStyle>
|
||||
-->
|
||||
<!-- Workaround to hide the close button -->
|
||||
<!--
|
||||
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="Button">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Setter Property="Width" Value="0" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="Foreground" Value="Red" />
|
||||
<Setter Property="Height" Value="0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
</TeachingTip.CloseButtonStyle>-->
|
||||
<TeachingTip.Content>
|
||||
<StackPanel
|
||||
x:Name="EditButtonsPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="DoneEditingButton_Click"
|
||||
Content="Save"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="DiscardEditingButton_Click"
|
||||
Content="Discard" />
|
||||
</StackPanel>
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="DockOrientation">
|
||||
<VisualState x:Name="DockOnTop">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
|
||||
</VisualState.StateTriggers>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnBottom">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Bottom" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,1,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnLeft">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Left" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="EndScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
|
||||
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,0,1,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnRight">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Right" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="EndScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
|
||||
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="1,0,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
|
||||
<!-- Edit Mode Visual States -->
|
||||
<VisualStateGroup x:Name="EditModeStates">
|
||||
<VisualState x:Name="EditModeOff" />
|
||||
<VisualState x:Name="EditModeOn">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="StartScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="StartScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="StartScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="CenterScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="CenterScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="CenterScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="CenterScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="EndScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="EndScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="EndScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="EndScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="StartScroller.ActionButtonVisibility" Value="Visible" />
|
||||
<Setter Target="CenterScroller.ActionButtonVisibility" Value="Visible" />
|
||||
<Setter Target="EndScroller.ActionButtonVisibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
563
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal file
563
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal file
@@ -0,0 +1,563 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
public sealed partial class DockControl : UserControl, IRecipient<CloseContextMenuMessage>, IRecipient<EnterDockEditModeMessage>
|
||||
{
|
||||
private DockViewModel _viewModel;
|
||||
|
||||
internal DockViewModel ViewModel => _viewModel;
|
||||
|
||||
public static readonly DependencyProperty ItemsOrientationProperty =
|
||||
DependencyProperty.Register(nameof(ItemsOrientation), typeof(Orientation), typeof(DockControl), new PropertyMetadata(Orientation.Horizontal, OnItemsOrientationChanged));
|
||||
|
||||
public Orientation ItemsOrientation
|
||||
{
|
||||
get => (Orientation)GetValue(ItemsOrientationProperty);
|
||||
set => SetValue(ItemsOrientationProperty, value);
|
||||
}
|
||||
|
||||
private static void OnItemsOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockControl control)
|
||||
{
|
||||
control.UpdateBandTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DockSideProperty =
|
||||
DependencyProperty.Register(nameof(DockSide), typeof(DockSide), typeof(DockControl), new PropertyMetadata(DockSide.Top));
|
||||
|
||||
public DockSide DockSide
|
||||
{
|
||||
get => (DockSide)GetValue(DockSideProperty);
|
||||
set => SetValue(DockSideProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsEditModeProperty =
|
||||
DependencyProperty.Register(nameof(IsEditMode), typeof(bool), typeof(DockControl), new PropertyMetadata(false, OnIsEditModeChanged));
|
||||
|
||||
public bool IsEditMode
|
||||
{
|
||||
get => (bool)GetValue(IsEditModeProperty);
|
||||
set => SetValue(IsEditModeProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIsEditModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockControl control && e.NewValue is bool isEditMode)
|
||||
{
|
||||
control.UpdateEditMode(isEditMode);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBandTemplates()
|
||||
{
|
||||
var panelKey = ItemsOrientation == Orientation.Horizontal
|
||||
? "HorizontalItemsPanel"
|
||||
: "VerticalItemsPanel";
|
||||
|
||||
if ((ItemsPanelTemplate)App.Current.Resources[panelKey] is ItemsPanelTemplate panelTemplate)
|
||||
{
|
||||
StartListView.ItemsPanel = panelTemplate;
|
||||
CenterListView.ItemsPanel = panelTemplate;
|
||||
EndListView.ItemsPanel = panelTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
internal DockControl(DockViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
|
||||
|
||||
// Start with edit mode disabled - normal click behavior
|
||||
UpdateEditMode(false);
|
||||
}
|
||||
|
||||
public void Receive(EnterDockEditModeMessage message)
|
||||
{
|
||||
// Message may arrive from a background thread, dispatch to UI thread
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
EnterEditMode();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateEditMode(bool isEditMode)
|
||||
{
|
||||
// Enable/disable drag-and-drop based on edit mode
|
||||
StartListView.CanDragItems = isEditMode;
|
||||
StartListView.CanReorderItems = isEditMode;
|
||||
StartListView.AllowDrop = isEditMode;
|
||||
|
||||
CenterListView.CanDragItems = isEditMode;
|
||||
CenterListView.CanReorderItems = isEditMode;
|
||||
CenterListView.AllowDrop = isEditMode;
|
||||
|
||||
EndListView.CanDragItems = isEditMode;
|
||||
EndListView.CanReorderItems = isEditMode;
|
||||
EndListView.AllowDrop = isEditMode;
|
||||
|
||||
if (isEditMode)
|
||||
{
|
||||
EditButtonsTeachingTip.PreferredPlacement = DockSide switch
|
||||
{
|
||||
DockSide.Left => TeachingTipPlacementMode.Right,
|
||||
DockSide.Right => TeachingTipPlacementMode.Left,
|
||||
DockSide.Top => TeachingTipPlacementMode.Bottom,
|
||||
DockSide.Bottom => TeachingTipPlacementMode.Top,
|
||||
_ => TeachingTipPlacementMode.Auto,
|
||||
};
|
||||
}
|
||||
|
||||
EditButtonsTeachingTip.IsOpen = isEditMode;
|
||||
|
||||
// Update visual state
|
||||
VisualStateManager.GoToState(this, isEditMode ? "EditModeOn" : "EditModeOff", true);
|
||||
}
|
||||
|
||||
internal void EnterEditMode()
|
||||
{
|
||||
// Snapshot current state so we can restore on discard
|
||||
ViewModel.SnapshotBandOrder();
|
||||
IsEditMode = true;
|
||||
}
|
||||
|
||||
internal void ExitEditMode()
|
||||
{
|
||||
IsEditMode = false;
|
||||
|
||||
// Save all changes when exiting edit mode
|
||||
ViewModel.SaveBandOrder();
|
||||
}
|
||||
|
||||
internal void DiscardEditMode()
|
||||
{
|
||||
IsEditMode = false;
|
||||
|
||||
// Restore the original band order from snapshot
|
||||
ViewModel.RestoreBandOrder();
|
||||
}
|
||||
|
||||
private void DoneEditingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitEditMode();
|
||||
}
|
||||
|
||||
private void DiscardEditingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DiscardEditMode();
|
||||
}
|
||||
|
||||
internal void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
DockSide = settings.Side;
|
||||
|
||||
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
|
||||
|
||||
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
|
||||
|
||||
if (settings.Backdrop == DockBackdrop.Transparent)
|
||||
{
|
||||
RootGrid.BorderBrush = new SolidColorBrush(Colors.Transparent);
|
||||
}
|
||||
}
|
||||
|
||||
private void BandItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
|
||||
{
|
||||
// Ignore clicks when in edit mode - allow drag behavior instead
|
||||
if (IsEditMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockItemViewModel item)
|
||||
{
|
||||
// Use the center of the border as the point to open at
|
||||
var borderPos = dockItem.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var borderCenter = new Point(
|
||||
borderPos.X + (dockItem.ActualWidth / 2),
|
||||
borderPos.Y + (dockItem.ActualHeight / 2));
|
||||
|
||||
InvokeItem(item, borderCenter);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Stores the band that was right-clicked for edit mode context menu
|
||||
private DockBandViewModel? _editModeContextBand;
|
||||
|
||||
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockItemViewModel item)
|
||||
{
|
||||
// In edit mode, show the edit mode context menu (show/hide labels)
|
||||
if (IsEditMode)
|
||||
{
|
||||
// Find the parent DockBandViewModel for this item
|
||||
_editModeContextBand = FindParentBand(item);
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
// Update menu item visibility based on current state
|
||||
ShowLabelsMenuItem.Visibility = _editModeContextBand.ShowLabels ? Visibility.Collapsed : Visibility.Visible;
|
||||
HideLabelsMenuItem.Visibility = _editModeContextBand.ShowLabels ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
EditModeContextMenu.ShowAt(
|
||||
dockItem,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal mode - show the command context menu
|
||||
if (item.HasMoreCommands)
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextMenuFlyout.ShowAt(
|
||||
dockItem,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DockBandViewModel? FindParentBand(DockItemViewModel item)
|
||||
{
|
||||
// Search all bands to find which one contains this item
|
||||
foreach (var band in ViewModel.StartItems)
|
||||
{
|
||||
if (band.Items.Contains(item))
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in ViewModel.CenterItems)
|
||||
{
|
||||
if (band.Items.Contains(item))
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in ViewModel.EndItems)
|
||||
{
|
||||
if (band.Items.Contains(item))
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ShowLabelsMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
_editModeContextBand.ShowLabels = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void HideLabelsMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
_editModeContextBand.ShowLabels = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnpinBandMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
ViewModel.UnpinBand(_editModeContextBand);
|
||||
_editModeContextBand = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeItem(DockItemViewModel item, Point pos)
|
||||
{
|
||||
var command = item.Command;
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private DockBandViewModel? _draggedBand;
|
||||
|
||||
private void BandListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
|
||||
{
|
||||
if (e.Items.Count > 0 && e.Items[0] is DockBandViewModel band)
|
||||
{
|
||||
_draggedBand = band;
|
||||
e.Data.RequestedOperation = DataPackageOperation.Move;
|
||||
}
|
||||
}
|
||||
|
||||
private void BandListView_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
if (_draggedBand != null)
|
||||
{
|
||||
e.AcceptedOperation = DataPackageOperation.Move;
|
||||
}
|
||||
}
|
||||
|
||||
private void BandListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
// Reordering within the same list is handled automatically by ListView
|
||||
// We just need to sync the ViewModel order without saving
|
||||
if (args.DropResult == DataPackageOperation.Move && _draggedBand != null)
|
||||
{
|
||||
DockPinSide targetSide;
|
||||
ObservableCollection<DockBandViewModel> targetCollection;
|
||||
|
||||
if (sender == StartListView)
|
||||
{
|
||||
targetSide = DockPinSide.Start;
|
||||
targetCollection = ViewModel.StartItems;
|
||||
}
|
||||
else if (sender == CenterListView)
|
||||
{
|
||||
targetSide = DockPinSide.Center;
|
||||
targetCollection = ViewModel.CenterItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetSide = DockPinSide.End;
|
||||
targetCollection = ViewModel.EndItems;
|
||||
}
|
||||
|
||||
// Find the new index and sync ViewModel (without saving)
|
||||
var newIndex = targetCollection.IndexOf(_draggedBand);
|
||||
if (newIndex >= 0)
|
||||
{
|
||||
ViewModel.SyncBandPosition(_draggedBand, targetSide, newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
_draggedBand = null;
|
||||
}
|
||||
|
||||
private void StartListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.Start, e);
|
||||
}
|
||||
|
||||
private void CenterListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.Center, e);
|
||||
}
|
||||
|
||||
private void EndListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.End, e);
|
||||
}
|
||||
|
||||
private void HandleCrossListDrop(DockPinSide targetSide, DragEventArgs e)
|
||||
{
|
||||
if (_draggedBand == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check which list the band is currently in
|
||||
var isInStart = ViewModel.StartItems.Contains(_draggedBand);
|
||||
var isInCenter = ViewModel.CenterItems.Contains(_draggedBand);
|
||||
var isInEnd = ViewModel.EndItems.Contains(_draggedBand);
|
||||
|
||||
DockPinSide sourceSide;
|
||||
if (isInStart)
|
||||
{
|
||||
sourceSide = DockPinSide.Start;
|
||||
}
|
||||
else if (isInCenter)
|
||||
{
|
||||
sourceSide = DockPinSide.Center;
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceSide = DockPinSide.End;
|
||||
}
|
||||
|
||||
// Only handle cross-list drops here; same-list reorders are handled in DragItemsCompleted
|
||||
if (sourceSide != targetSide)
|
||||
{
|
||||
// Calculate drop index based on drop position
|
||||
var targetListView = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => StartListView,
|
||||
DockPinSide.Center => CenterListView,
|
||||
_ => EndListView,
|
||||
};
|
||||
var targetCollection = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => ViewModel.StartItems,
|
||||
DockPinSide.Center => ViewModel.CenterItems,
|
||||
_ => ViewModel.EndItems,
|
||||
};
|
||||
|
||||
var dropIndex = GetDropIndex(targetListView, e, targetCollection.Count);
|
||||
|
||||
// Move the band to the new side (without saving - save happens on Done)
|
||||
ViewModel.MoveBandWithoutSaving(_draggedBand, targetSide, dropIndex);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetDropIndex(ListView listView, DragEventArgs e, int itemCount)
|
||||
{
|
||||
var position = e.GetPosition(listView);
|
||||
|
||||
// Find the item at the drop position
|
||||
for (var i = 0; i < itemCount; i++)
|
||||
{
|
||||
if (listView.ContainerFromIndex(i) is ListViewItem container)
|
||||
{
|
||||
var itemBounds = container.TransformToVisual(listView).TransformBounds(
|
||||
new Rect(0, 0, container.ActualWidth, container.ActualHeight));
|
||||
|
||||
if (ItemsOrientation == Orientation.Horizontal)
|
||||
{
|
||||
// For horizontal layout, check X position
|
||||
if (position.X < itemBounds.X + (itemBounds.Width / 2))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For vertical layout, check Y position
|
||||
if (position.Y < itemBounds.Y + (itemBounds.Height / 2))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're past all items, insert at the end
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
// Tracks which section (Start/Center/End) the add button was clicked for
|
||||
private DockPinSide _addBandTargetSide;
|
||||
|
||||
private void AddBandButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.Tag is string sideTag)
|
||||
{
|
||||
_addBandTargetSide = sideTag switch
|
||||
{
|
||||
"Start" => DockPinSide.Start,
|
||||
"Center" => DockPinSide.Center,
|
||||
"End" => DockPinSide.End,
|
||||
_ => DockPinSide.Center,
|
||||
};
|
||||
|
||||
// Populate the list with available bands (not already in the dock)
|
||||
var availableBands = ViewModel.GetAvailableBandsToAdd().ToList();
|
||||
AddBandListView.ItemsSource = availableBands;
|
||||
|
||||
// Show/hide empty state text based on whether there are bands to add
|
||||
var hasAvailableBands = availableBands.Count > 0;
|
||||
NoAvailableBandsText.Visibility = hasAvailableBands ? Visibility.Collapsed : Visibility.Visible;
|
||||
AddBandListView.Visibility = hasAvailableBands ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// Show the flyout
|
||||
AddBandFlyout.ShowAt(button);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBandListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is TopLevelViewModel topLevel)
|
||||
{
|
||||
// Add the band to the target section
|
||||
ViewModel.AddBandToSection(topLevel, _addBandTargetSide);
|
||||
|
||||
// Close the flyout
|
||||
AddBandFlyout.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml
Normal file
161
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml
Normal file
@@ -0,0 +1,161 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock">
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#0FFFFFFF" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#0BFFFFFF" />
|
||||
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#0FFFFFFF" />
|
||||
<GradientStop Offset="1.0" Color="#19FFFFFF" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#0BFFFFFF" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#80FFFFFF" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#4DFFFFFF" />
|
||||
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#08000000" />
|
||||
<GradientStop Offset="1.0" Color="#17000000" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#05000000" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPointerOver" Color="{StaticResource SystemColorHighlightColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultDockItemControlStyle}" TargetType="local:DockItemControl" />
|
||||
|
||||
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
|
||||
<Style.Setters>
|
||||
<Setter Property="Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DockItemBorderBrush}" />
|
||||
<Setter Property="Padding" Value="4,0,4,0" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="TextVisibility" Value="Visible" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:DockItemControl">
|
||||
<Grid
|
||||
x:Name="PART_RootGrid"
|
||||
MaxWidth="120"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
ToolTipService.ToolTip="{TemplateBinding ToolTip}">
|
||||
<Grid
|
||||
x:Name="ContentGrid"
|
||||
AutomationProperties.Name="{TemplateBinding Title}"
|
||||
Background="Transparent"
|
||||
ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Icon -->
|
||||
<ContentPresenter
|
||||
x:Name="IconPresenter"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Icon}" />
|
||||
|
||||
<!-- Text (Title + Subtitle) -->
|
||||
<StackPanel
|
||||
x:Name="TextPanel"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{TemplateBinding TextVisibility}">
|
||||
<TextBlock
|
||||
x:Name="TitleText"
|
||||
MaxWidth="100"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Text="{TemplateBinding Title}"
|
||||
TextAlignment="Left"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
x:Name="SubtitleText"
|
||||
MaxWidth="100"
|
||||
Margin="0,-4,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{TemplateBinding Subtitle}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="TitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="SubtitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="TextVisibilityStates">
|
||||
<VisualState x:Name="TextVisible" />
|
||||
<VisualState x:Name="TextHidden">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="IconVisibilityStates">
|
||||
<VisualState x:Name="IconVisible" />
|
||||
<VisualState x:Name="IconHidden">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,212 @@
|
||||
// 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.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
[ContentProperty(Name = nameof(Icon))]
|
||||
public sealed partial class DockItemControl : Control
|
||||
{
|
||||
public DockItemControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(DockItemControl);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ToolTipProperty =
|
||||
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null));
|
||||
|
||||
public string ToolTip
|
||||
{
|
||||
get => (string)GetValue(ToolTipProperty);
|
||||
set => SetValue(ToolTipProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => (string)GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SubtitleProperty =
|
||||
DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public string Subtitle
|
||||
{
|
||||
get => (string)GetValue(SubtitleProperty);
|
||||
set => SetValue(SubtitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DockItemControl), new PropertyMetadata(null, OnIconPropertyChanged));
|
||||
|
||||
public object Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextVisibilityProperty =
|
||||
DependencyProperty.Register(nameof(TextVisibility), typeof(Visibility), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public Visibility TextVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(TextVisibilityProperty);
|
||||
set => SetValue(TextVisibilityProperty, value);
|
||||
}
|
||||
|
||||
private const string IconPresenterName = "IconPresenter";
|
||||
private const string TitleTextName = "TitleText";
|
||||
private const string SubtitleTextName = "SubtitleText";
|
||||
|
||||
private FrameworkElement? _iconPresenter;
|
||||
private FrameworkElement? _titleText;
|
||||
private FrameworkElement? _subtitleText;
|
||||
|
||||
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockItemControl control)
|
||||
{
|
||||
control.UpdateTextVisibility();
|
||||
control.UpdateAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnIconPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockItemControl control)
|
||||
{
|
||||
control.UpdateIconVisibility();
|
||||
control.UpdateAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
internal bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
internal bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
private void UpdateTextVisibility()
|
||||
{
|
||||
if (_titleText is not null)
|
||||
{
|
||||
_titleText.Visibility = HasTitle ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
if (_subtitleText is not null)
|
||||
{
|
||||
_subtitleText.Visibility = HasSubtitle ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
UpdateTextVisibilityState();
|
||||
}
|
||||
|
||||
private void UpdateTextVisibilityState()
|
||||
{
|
||||
var hasText = !string.IsNullOrEmpty(Title) || !string.IsNullOrEmpty(Subtitle);
|
||||
VisualStateManager.GoToState(this, hasText ? "TextVisible" : "TextHidden", true);
|
||||
}
|
||||
|
||||
private void UpdateIconVisibility()
|
||||
{
|
||||
if (_iconPresenter is not null)
|
||||
{
|
||||
// n.b. this might be wrong - I think we always have an Icon (an IconBox),
|
||||
// we need to check if the box has an icon
|
||||
_iconPresenter.Visibility = Icon is null ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
UpdateIconVisibilityState();
|
||||
}
|
||||
|
||||
private void UpdateIconVisibilityState()
|
||||
{
|
||||
var hasIcon = Icon is not null;
|
||||
VisualStateManager.GoToState(this, hasIcon ? "IconVisible" : "IconHidden", true);
|
||||
}
|
||||
|
||||
private void UpdateAlignment()
|
||||
{
|
||||
// If this item has both an icon and a label, left align so that the
|
||||
// icons don't wobble if the text changes.
|
||||
//
|
||||
// Otherwise, center align.
|
||||
var requestedTheme = ActualTheme;
|
||||
var isLight = requestedTheme == ElementTheme.Light;
|
||||
var showText = HasText;
|
||||
if (Icon is IconBox icoBox &&
|
||||
icoBox.DataContext is DockItemViewModel item &&
|
||||
item.Icon is IconInfoViewModel icon)
|
||||
{
|
||||
var showIcon = icon is not null && icon.HasIcon(isLight);
|
||||
if (showText && showIcon)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalAlignment = HorizontalAlignment.Center;
|
||||
}
|
||||
|
||||
private void UpdateAllVisibility()
|
||||
{
|
||||
UpdateTextVisibility();
|
||||
UpdateIconVisibility();
|
||||
UpdateAlignment();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
IsEnabledChanged -= OnIsEnabledChanged;
|
||||
|
||||
PointerEntered += Control_PointerEntered;
|
||||
PointerExited += Control_PointerExited;
|
||||
|
||||
IsEnabledChanged += OnIsEnabledChanged;
|
||||
|
||||
// Get template children for visibility updates
|
||||
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
|
||||
_titleText = GetTemplateChild(TitleTextName) as FrameworkElement;
|
||||
_subtitleText = GetTemplateChild(SubtitleTextName) as FrameworkElement;
|
||||
|
||||
// Set initial visibility
|
||||
UpdateAllVisibility();
|
||||
}
|
||||
|
||||
private void Control_PointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "PointerOver", true);
|
||||
}
|
||||
|
||||
private void Control_PointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "Normal", true);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerRoutedEventArgs e)
|
||||
{
|
||||
if (IsEnabled)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
VisualStateManager.GoToState(this, "Pressed", true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Windows.Win32;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
internal static class DockSettingsToViews
|
||||
{
|
||||
public static double WidthForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 128,
|
||||
DockSize.Medium => 192,
|
||||
DockSize.Large => 256,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double HeightForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 32,
|
||||
DockSize.Medium => 54,
|
||||
DockSize.Large => 76,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double IconSizeForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 32 / 2,
|
||||
DockSize.Medium => 54 / 2,
|
||||
DockSize.Large => 76 / 2,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Microsoft.UI.Xaml.Media.SystemBackdrop? GetSystemBackdrop(DockBackdrop backdrop)
|
||||
{
|
||||
return backdrop switch
|
||||
{
|
||||
DockBackdrop.Transparent => new TransparentTintBackdrop(),
|
||||
DockBackdrop.Acrylic => null, // 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
|
||||
50
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal file
50
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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">
|
||||
|
||||
<!-- Colorization overlay for transparent backdrop -->
|
||||
<Border
|
||||
x:Name="ColorizationOverlay"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="{x:Bind WindowViewModel.ColorizationOpacity, Mode=OneWay}"
|
||||
Visibility="{x:Bind WindowViewModel.ShowColorizationOverlay, Mode=OneWay}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind WindowViewModel.ColorizationColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<cpcontrols:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind WindowViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind WindowViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind WindowViewModel.BackgroundImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind WindowViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind WindowViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
IsHoldingEnabled="False"
|
||||
TintColor="{x:Bind WindowViewModel.BackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind WindowViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind WindowViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
728
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal file
728
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal file
@@ -0,0 +1,728 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Accessibility;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockWindow : WindowEx,
|
||||
IRecipient<BringToTopMessage>,
|
||||
IRecipient<RequestShowPaletteAtMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IDisposable
|
||||
{
|
||||
#pragma warning disable SA1306 // Field names should begin with lower-case letter
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private readonly uint WM_TASKBAR_RESTART;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
#pragma warning restore SA1306 // Field names should begin with lower-case letter
|
||||
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DockWindowViewModel _windowViewModel;
|
||||
|
||||
private HWND _hwnd = HWND.Null;
|
||||
private APPBARDATA _appBarData;
|
||||
private uint _callbackMessageId;
|
||||
|
||||
private DockSettings _settings;
|
||||
private DockViewModel viewModel;
|
||||
private DockControl _dock;
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private DockSize _lastSize;
|
||||
|
||||
// Store the original WndProc
|
||||
private WNDPROC? _originalWndProc;
|
||||
private WNDPROC? _customWndProc;
|
||||
|
||||
// internal Settings CurrentSettings => _settings;
|
||||
public DockWindow()
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var mainSettings = serviceProvider.GetService<SettingsModel>()!;
|
||||
mainSettings.SettingsChanged += SettingsChangedHandler;
|
||||
_settings = mainSettings.DockSettings;
|
||||
_lastSize = _settings.DockSize;
|
||||
|
||||
viewModel = serviceProvider.GetService<DockViewModel>()!;
|
||||
_themeService = serviceProvider.GetRequiredService<IThemeService>();
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
_windowViewModel = new DockWindowViewModel(_themeService);
|
||||
_dock = new DockControl(viewModel);
|
||||
|
||||
InitializeComponent();
|
||||
Root.Children.Add(_dock);
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
AppWindow.IsShownInSwitchers = false;
|
||||
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
|
||||
{
|
||||
overlappedPresenter.SetBorderAndTitleBar(false, false);
|
||||
overlappedPresenter.IsResizable = false;
|
||||
}
|
||||
|
||||
this.Activated += DockWindow_Activated;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
|
||||
_hwnd = GetWindowHandle(this);
|
||||
|
||||
// Subclass the window to intercept messages
|
||||
//
|
||||
// Set up custom window procedure to listen for display changes
|
||||
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
|
||||
// member (and instead like, use a local), then the pointer we marshal
|
||||
// into the WindowLongPtr will be useless after we leave this function,
|
||||
// and our **WindProc will explode**.
|
||||
_customWndProc = CustomWndProc;
|
||||
|
||||
_callbackMessageId = PInvoke.RegisterWindowMessage($"CmdPal_ABM_{_hwnd}");
|
||||
|
||||
// TaskbarCreated is the message that's broadcast when explorer.exe
|
||||
// restarts. We need to know when that happens to be able to bring our
|
||||
// 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();
|
||||
}
|
||||
|
||||
var backdrop = _themeService.CurrentDockTheme.BackdropParameters;
|
||||
_acrylicController = new DesktopAcrylicController
|
||||
{
|
||||
Kind = DesktopAcrylicKind.Thin,
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = backdrop.TintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.LuminosityOpacity,
|
||||
};
|
||||
|
||||
// Enable the system backdrop.
|
||||
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void DisposeAcrylic()
|
||||
{
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null!;
|
||||
_configurationSource = null!;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
// We only need to handle acrylic here.
|
||||
// Transparent background is handled directly in XAML by binding to
|
||||
// the DockWindowViewModel's ColorizationColor properties.
|
||||
if (_settings.Backdrop == DockBackdrop.Acrylic)
|
||||
{
|
||||
UpdateAcrylic();
|
||||
}
|
||||
|
||||
// ActualTheme / RequestedTheme sync,
|
||||
// as pilfered from WindowThemeSynchronizer
|
||||
// LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces
|
||||
// a refresh of the theme.
|
||||
Root.RequestedTheme = ElementTheme.Dark;
|
||||
Root.RequestedTheme = ElementTheme.Light;
|
||||
Root.RequestedTheme = _themeService.CurrentDockTheme.Theme;
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateAppBar(HWND hwnd)
|
||||
{
|
||||
_appBarData = new APPBARDATA
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
|
||||
hWnd = hwnd,
|
||||
uCallbackMessage = _callbackMessageId,
|
||||
};
|
||||
|
||||
// Register this window as an 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 DockWindowViewModel WindowViewModel => _windowViewModel;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAcrylic();
|
||||
viewModel.Dispose();
|
||||
_windowViewModel.Dispose();
|
||||
}
|
||||
|
||||
private void DockWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var settings = serviceProvider.GetService<SettingsModel>();
|
||||
settings?.SettingsChanged -= SettingsChangedHandler;
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
DisposeAcrylic();
|
||||
|
||||
// Remove our 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
|
||||
@@ -587,7 +587,7 @@ public sealed partial class ListPage : Page,
|
||||
var shouldUpdateSelection = false;
|
||||
|
||||
// If it's a top level list update we force the reset to the top useful item
|
||||
if (!sender.IsNested)
|
||||
if (sender.IsRootPage)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -50,6 +51,7 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<DismissMessage>,
|
||||
IRecipient<ShowWindowMessage>,
|
||||
IRecipient<ShowPaletteAtMessage>,
|
||||
IRecipient<HideWindowMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
@@ -134,6 +136,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
|
||||
@@ -332,6 +335,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();
|
||||
|
||||
@@ -350,15 +424,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,
|
||||
@@ -548,6 +616,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.
|
||||
@@ -658,6 +731,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();
|
||||
}
|
||||
@@ -1070,6 +1145,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;
|
||||
}
|
||||
@@ -1120,6 +1196,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
DisposeAcrylic();
|
||||
}
|
||||
|
||||
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
|
||||
|
||||
public void Receive(DragStartedMessage message)
|
||||
{
|
||||
_preventHideWhenDeactivated = true;
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
<Version>$(CmdPalVersion)</Version>
|
||||
|
||||
<!-- For MVVM Toolkit Partial Properties/AOT support -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
<!-- OutputPath is set in CmdPal.Branding.props -->
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
@@ -27,10 +30,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
|
||||
<!--<PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<EnableCmdPalAOT>true</EnableCmdPalAOT>
|
||||
<GeneratePackageLocally>true</GeneratePackageLocally>
|
||||
</PropertyGroup>-->
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -74,6 +77,7 @@
|
||||
<None Remove="Controls\FallbackRankerDialog.xaml" />
|
||||
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
<None Remove="Controls\ScreenPreview.xaml" />
|
||||
<None Remove="Controls\ScrollContainer.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="IsEnabledTextBlock.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
@@ -84,7 +88,6 @@
|
||||
<None Remove="SettingsWindow.xaml" />
|
||||
<None Remove="Settings\AppearancePage.xaml" />
|
||||
<None Remove="ShellPage.xaml" />
|
||||
<None Remove="Styles\Colors.xaml" />
|
||||
<None Remove="Styles\Settings.xaml" />
|
||||
<None Remove="Styles\TextBox.xaml" />
|
||||
<None Remove="Styles\Theme.Normal.xaml" />
|
||||
@@ -140,6 +143,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" />
|
||||
@@ -214,6 +218,18 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Dock\DockItemControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\ScrollContainer.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\CommandPalettePreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -264,11 +280,6 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Colors.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Settings.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -59,11 +59,52 @@ GetModuleHandle
|
||||
GetWindowLong
|
||||
SetWindowLong
|
||||
WINDOW_EX_STYLE
|
||||
|
||||
CreateWindowEx
|
||||
WNDCLASSEXW
|
||||
RegisterClassEx
|
||||
GetStockObject
|
||||
GetModuleHandle
|
||||
|
||||
MoveWindow
|
||||
GetSystemMetrics
|
||||
SHAppBarMessage
|
||||
ABM_NEW
|
||||
ABM_QUERYPOS
|
||||
ABM_SETPOS
|
||||
ABM_REMOVE
|
||||
ABM_SETAUTOHIDEBAR
|
||||
ABS_AUTOHIDE
|
||||
ABN_POSCHANGED
|
||||
APPBARDATA
|
||||
ABE_TOP
|
||||
ABE_BOTTOM
|
||||
ABE_LEFT
|
||||
ABE_RIGHT
|
||||
SYSTEM_METRICS_INDEX
|
||||
GetDpiForWindow
|
||||
SHQueryUserNotificationState
|
||||
SYSTEM_PARAMETERS_INFO_ACTION
|
||||
WINDOWPOS
|
||||
WM_DISPLAYCHANGE
|
||||
WM_SYSCOMMAND
|
||||
WM_SETTINGCHANGE
|
||||
WM_WINDOWPOSCHANGING
|
||||
WM_SHOWWINDOW
|
||||
WM_SIZE
|
||||
WM_GETMINMAXINFO
|
||||
SetWinEventHook
|
||||
WINDOW_STYLE
|
||||
SC_MINIMIZE
|
||||
SC_MAXIMIZE
|
||||
SET_WINDOW_POS_FLAGS
|
||||
SIZE_MAXIMIZED
|
||||
SIZE_MINIMIZED
|
||||
HWND_NOTOPMOST
|
||||
HWND_TOP
|
||||
HTCAPTION
|
||||
GetClassName
|
||||
EVENT_SYSTEM_FOREGROUND
|
||||
WINEVENT_OUTOFCONTEXT
|
||||
GetWindowThreadProcessId
|
||||
AttachThreadInput
|
||||
AttachThreadInput
|
||||
|
||||
@@ -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>
|
||||
@@ -248,26 +261,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();
|
||||
}
|
||||
@@ -328,10 +344,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;
|
||||
|
||||
@@ -400,10 +413,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)
|
||||
{
|
||||
@@ -444,10 +454,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)
|
||||
{
|
||||
@@ -465,6 +472,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)
|
||||
@@ -721,5 +749,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = null;
|
||||
|
||||
_dockWindow?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -36,11 +37,14 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
private bool _isInitialized;
|
||||
private bool _disposed;
|
||||
private InternalThemeState _currentState;
|
||||
private DockThemeSnapshot _currentDockState;
|
||||
|
||||
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||
|
||||
public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot;
|
||||
|
||||
public DockThemeSnapshot CurrentDockTheme => Volatile.Read(ref _currentDockState);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the theme service. Must be called after the application window is activated and on UI thread.
|
||||
/// </summary>
|
||||
@@ -134,6 +138,60 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
// Atomic swap
|
||||
Interlocked.Exchange(ref _currentState, newState);
|
||||
|
||||
// Compute DockThemeSnapshot from DockSettings
|
||||
var dockSettings = _settings.DockSettings;
|
||||
var dockIntensity = Math.Clamp(dockSettings.CustomThemeColorIntensity, 0, 100);
|
||||
IThemeProvider dockProvider = dockIntensity > 0 && dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
|
||||
? _colorfulThemeProvider
|
||||
: _normalThemeProvider;
|
||||
|
||||
var dockTint = dockSettings.ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.CustomColor => dockSettings.CustomThemeColor,
|
||||
ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent),
|
||||
ColorizationMode.Image => dockSettings.CustomThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
var dockEffectiveTheme = GetElementTheme((ElementTheme)dockSettings.Theme);
|
||||
var dockImageSource = dockSettings.ColorizationMode == ColorizationMode.Image
|
||||
? LoadImageSafe(dockSettings.BackgroundImagePath)
|
||||
: null;
|
||||
var dockStretch = dockSettings.BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => Stretch.Fill,
|
||||
_ => Stretch.UniformToFill,
|
||||
};
|
||||
var dockOpacity = Math.Clamp(dockSettings.BackgroundImageOpacity, 0, 100) / 100.0;
|
||||
|
||||
var dockContext = new ThemeContext
|
||||
{
|
||||
Tint = dockTint,
|
||||
ColorIntensity = dockIntensity,
|
||||
Theme = dockEffectiveTheme,
|
||||
BackgroundImageSource = dockImageSource,
|
||||
BackgroundImageStretch = dockStretch,
|
||||
BackgroundImageOpacity = dockOpacity,
|
||||
};
|
||||
var dockBackdrop = dockProvider.GetAcrylicBackdrop(dockContext);
|
||||
var dockBlur = dockSettings.BackgroundImageBlurAmount;
|
||||
var dockBrightness = dockSettings.BackgroundImageBrightness;
|
||||
|
||||
var dockSnapshot = new DockThemeSnapshot
|
||||
{
|
||||
Tint = dockTint,
|
||||
TintIntensity = dockIntensity / 100f,
|
||||
Theme = dockEffectiveTheme,
|
||||
Backdrop = dockSettings.Backdrop,
|
||||
BackgroundImageSource = dockImageSource,
|
||||
BackgroundImageStretch = dockStretch,
|
||||
BackgroundImageOpacity = dockOpacity,
|
||||
BackdropParameters = dockBackdrop,
|
||||
BlurAmount = dockBlur,
|
||||
BackgroundBrightness = dockBrightness / 100f,
|
||||
};
|
||||
|
||||
Interlocked.Exchange(ref _currentDockState, dockSnapshot);
|
||||
|
||||
_resourceSwapper.TryActivateTheme(provider.ThemeKey);
|
||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
|
||||
}
|
||||
@@ -205,6 +263,20 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
},
|
||||
Provider = _normalThemeProvider,
|
||||
};
|
||||
|
||||
_currentDockState = new DockThemeSnapshot
|
||||
{
|
||||
Tint = Colors.Transparent,
|
||||
TintIntensity = 1.0f,
|
||||
Theme = ElementTheme.Light,
|
||||
Backdrop = DockBackdrop.Acrylic,
|
||||
BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
|
||||
BackgroundImageOpacity = 1,
|
||||
BackgroundImageSource = null,
|
||||
BackgroundImageStretch = Stretch.Fill,
|
||||
BlurAmount = 0,
|
||||
BackgroundBrightness = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private void RequestReload()
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<?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>-->
|
||||
|
||||
<!-- Enable Dock -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Appearance Section -->
|
||||
<TextBlock x:Uid="DockAppearanceSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<!-- Dock Size -->
|
||||
|
||||
|
||||
<!-- Dock Position -->
|
||||
<controls:SettingsExpander
|
||||
Description="Choose where the dock appears on your screen"
|
||||
Header="Dock position and appearnce"
|
||||
IsExpanded="True">
|
||||
<controls:SettingsExpander.HeaderIcon>
|
||||
<SymbolIcon Symbol="MoveToFolder" />
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
<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:SettingsExpander.Items>
|
||||
|
||||
<!-- Show Labels -->
|
||||
<controls:SettingsCard ContentAlignment="Vertical">
|
||||
<cpControls:CheckBoxWithDescriptionControl
|
||||
Content="Show labels"
|
||||
Description="Choose whether to show labels for dock items by default"
|
||||
IsChecked="{x:Bind ShowLabels, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- Theme Section -->
|
||||
<TextBlock x:Uid="DockAppearance_ThemeSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_System" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Light_Automation" Tag="Light">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Light" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark_Automation" Tag="Dark">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Backdrop Style -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_Backdrop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
x:Name="BackdropComboBox"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.DockAppearance.BackdropIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Transparent" />
|
||||
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Acrylic" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background / Colorization Section -->
|
||||
<controls:SettingsExpander
|
||||
x:Uid="DockAppearance_Background_SettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.DockAppearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
||||
<ComboBox
|
||||
x:Uid="DockAppearance_ColorizationMode"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
<!-- none -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="DockAppearance_NoBackground_SettingsCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
Visibility="{x:Bind ViewModel.DockAppearance.IsNoBackgroundVisible, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="DockAppearance_NoBackground_DescriptionTextBlock"
|
||||
Margin="24"
|
||||
HorizontalAlignment="Stretch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
HorizontalTextAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- system accent color -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_WindowsAccentColor_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsAccentColorControlsVisible, Mode=OneWay}">
|
||||
<controls:SettingsCard.Description>
|
||||
<TextBlock>
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1" />
|
||||
<Hyperlink
|
||||
Click="OpenWindowsColorsSettings_Click"
|
||||
TextDecorations="None"
|
||||
UnderlineStyle="None">
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_OpenWindowsColorsLinkText" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</controls:SettingsCard.Description>
|
||||
<controls:SettingsCard.Content>
|
||||
<Border
|
||||
MinWidth="32"
|
||||
MinHeight="32"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind ViewModel.DockAppearance.EffectiveThemeColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</controls:SettingsCard.Content>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- background image -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="DockAppearance_BackgroundImage_SettingsCard"
|
||||
Description="{x:Bind ViewModel.DockAppearance.BackgroundImagePath, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="-100"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBrightness, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="50"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background tint color and intensity -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTint_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintVisible, Mode=OneWay}">
|
||||
<cpControls:ColorPickerButton
|
||||
HasSelectedColor="True"
|
||||
IsAlphaEnabled="False"
|
||||
PaletteColors="{x:Bind ViewModel.DockAppearance.Swatches}"
|
||||
SelectedColor="{x:Bind ViewModel.DockAppearance.ThemeColor, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="1"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.ColorIntensity, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Reset background image properties -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- 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}"
|
||||
IsClickEnabled="False">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<cpControls:ContentIcon>
|
||||
<cpControls:ContentIcon.Content>
|
||||
<cpControls:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
</cpControls:ContentIcon.Content>
|
||||
</cpControls:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
|
||||
<ToggleSwitch IsOn="{x:Bind IsPinned, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.Windows.Storage.Pickers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
public sealed partial class DockSettingsPage : Page
|
||||
{
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
internal SettingsViewModel ViewModel { get; }
|
||||
|
||||
public List<DockBandSettingsViewModel> AllDockBandItems => GetAllBandSettings();
|
||||
|
||||
public DockSettingsPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
ViewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
|
||||
|
||||
// Initialize UI state
|
||||
InitializeSettings();
|
||||
}
|
||||
|
||||
private void InitializeSettings()
|
||||
{
|
||||
// Initialize UI controls to match current settings
|
||||
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
|
||||
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
|
||||
}
|
||||
|
||||
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (XamlRoot?.ContentIslandEnvironment is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var windowId = XamlRoot?.ContentIslandEnvironment?.AppWindowId ?? new Microsoft.UI.WindowId(0);
|
||||
|
||||
var picker = new FileOpenPicker(windowId)
|
||||
{
|
||||
CommitButtonText = ViewModels.Properties.Resources.builtin_settings_appearance_pick_background_image_title!,
|
||||
SuggestedStartLocation = PickerLocationId.PicturesLibrary,
|
||||
ViewMode = PickerViewMode.Thumbnail,
|
||||
};
|
||||
|
||||
string[] extensions = [".png", ".bmp", ".jpg", ".jpeg", ".jfif", ".gif", ".tiff", ".tif", ".webp", ".jxr"];
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
picker.FileTypeFilter!.Add(ext);
|
||||
}
|
||||
|
||||
var file = await picker.PickSingleFileAsync()!;
|
||||
if (file != null)
|
||||
{
|
||||
ViewModel.DockAppearance.BackgroundImagePath = file.Path ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to pick background image file for dock", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenWindowsColorsSettings_Click(Hyperlink sender, HyperlinkClickEventArgs args)
|
||||
{
|
||||
// LOAD BEARING (or BEAR LOADING?): Process.Start with UseShellExecute inside a XAML input event can trigger WinUI reentrancy
|
||||
// and cause FailFast crashes. Task.Run moves the call off the UI thread to prevent hard process termination.
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = Process.Start(new ProcessStartInfo("ms-settings:colors") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open Windows Settings", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Property bindings for ComboBoxes
|
||||
public int SelectedDockSizeIndex
|
||||
{
|
||||
get => DockSizeToSelectedIndex(ViewModel.Dock_DockSize);
|
||||
set => ViewModel.Dock_DockSize = SelectedIndexToDockSize(value);
|
||||
}
|
||||
|
||||
public int SelectedSideIndex
|
||||
{
|
||||
get => SideToSelectedIndex(ViewModel.Dock_Side);
|
||||
set => ViewModel.Dock_Side = SelectedIndexToSide(value);
|
||||
}
|
||||
|
||||
public int SelectedBackdropIndex
|
||||
{
|
||||
get => BackdropToSelectedIndex(ViewModel.Dock_Backdrop);
|
||||
set => ViewModel.Dock_Backdrop = SelectedIndexToBackdrop(value);
|
||||
}
|
||||
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => ViewModel.Dock_ShowLabels;
|
||||
set => ViewModel.Dock_ShowLabels = value;
|
||||
}
|
||||
|
||||
// Conversion methods for ComboBox bindings
|
||||
private static int DockSizeToSelectedIndex(DockSize size) => size switch
|
||||
{
|
||||
DockSize.Small => 0,
|
||||
DockSize.Medium => 1,
|
||||
DockSize.Large => 2,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static DockSize SelectedIndexToDockSize(int index) => index switch
|
||||
{
|
||||
0 => DockSize.Small,
|
||||
1 => DockSize.Medium,
|
||||
2 => DockSize.Large,
|
||||
_ => DockSize.Small,
|
||||
};
|
||||
|
||||
private static int SideToSelectedIndex(DockSide side) => side switch
|
||||
{
|
||||
DockSide.Left => 0,
|
||||
DockSide.Top => 1,
|
||||
DockSide.Right => 2,
|
||||
DockSide.Bottom => 3,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
private static DockSide SelectedIndexToSide(int index) => index switch
|
||||
{
|
||||
0 => DockSide.Left,
|
||||
1 => DockSide.Top,
|
||||
2 => DockSide.Right,
|
||||
3 => DockSide.Bottom,
|
||||
_ => DockSide.Top,
|
||||
};
|
||||
|
||||
private static int BackdropToSelectedIndex(DockBackdrop backdrop) => backdrop switch
|
||||
{
|
||||
DockBackdrop.Transparent => 0,
|
||||
DockBackdrop.Acrylic => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static DockBackdrop SelectedIndexToBackdrop(int index) => index switch
|
||||
{
|
||||
0 => DockBackdrop.Transparent,
|
||||
1 => DockBackdrop.Acrylic,
|
||||
_ => DockBackdrop.Acrylic,
|
||||
};
|
||||
|
||||
private List<TopLevelViewModel> GetAllBands()
|
||||
{
|
||||
var allBands = new List<TopLevelViewModel>();
|
||||
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
foreach (var item in tlcManager.DockBands)
|
||||
{
|
||||
if (item.IsDockBand)
|
||||
{
|
||||
allBands.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return allBands;
|
||||
}
|
||||
|
||||
private List<DockBandSettingsViewModel> GetAllBandSettings()
|
||||
{
|
||||
var allSettings = new List<DockBandSettingsViewModel>();
|
||||
|
||||
// var allBands = GetAllBands();
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
var settingsModel = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var dockViewModel = App.Current.Services.GetService<DockViewModel>()!;
|
||||
var allBands = tlcManager.DockBands;
|
||||
foreach (var band in allBands)
|
||||
{
|
||||
var setting = band.DockBandSettings;
|
||||
if (setting is not null)
|
||||
{
|
||||
var bandVm = dockViewModel.FindBandByTopLevel(band);
|
||||
allSettings.Add(new(
|
||||
dockSettingsModel: setting,
|
||||
topLevelAdapter: band,
|
||||
bandViewModel: bandVm,
|
||||
settingsModel: settingsModel
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return allSettings;
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,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,19 +100,36 @@ 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),
|
||||
"Appearance" => typeof(AppearancePage),
|
||||
"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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +282,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 (Preview)</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>
|
||||
@@ -616,6 +631,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_DockPage" xml:space="preserve">
|
||||
<value>Dock</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack.Content" xml:space="preserve">
|
||||
<value>Clear search first, then go back</value>
|
||||
</data>
|
||||
@@ -727,4 +745,73 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
||||
<value>Version {0}</value>
|
||||
</data>
|
||||
<data name="Settings_NavigationViewItem_DockAppearance.Content" xml:space="preserve">
|
||||
<value>Dock Appearance</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_DockAppearancePage" xml:space="preserve">
|
||||
<value>Dock Appearance</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ThemeSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="DockAppearance_AppTheme_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Dock theme mode</value>
|
||||
</data>
|
||||
<data name="DockAppearance_AppTheme_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Select which theme to display for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Backdrop style</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Choose the background effect for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Mica.Content" xml:space="preserve">
|
||||
<value>Mica</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Transparent.Content" xml:space="preserve">
|
||||
<value>Transparent</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Acrylic.Content" xml:space="preserve">
|
||||
<value>Acrylic</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Background_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Background</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Background_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>Choose a custom background color or image for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ColorizationMode.Header" xml:space="preserve">
|
||||
<value>Colorization mode</value>
|
||||
</data>
|
||||
<data name="DockAppearance_NoBackground_SettingsCard.Header" xml:space="preserve">
|
||||
<value>No background</value>
|
||||
</data>
|
||||
<data name="DockAppearance_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
|
||||
<value>No settings</value>
|
||||
</data>
|
||||
<data name="DockAppearance_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
|
||||
<value>System accent color</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImage_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageBrightness_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image brightness</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageBlur_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image blur</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageFit_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image fit</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundTint_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color tint</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color intensity</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImage_ResetProperties_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Restore defaults</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
@@ -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)
|
||||
@@ -2045,6 +2047,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,
|
||||
Icon = Icons.ClipboardListIcon,
|
||||
@@ -24,7 +25,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
new CommandContextItem(_settingsManager.Settings.SettingsPage),
|
||||
],
|
||||
};
|
||||
|
||||
DisplayName = Properties.Resources.provider_display_name;
|
||||
Icon = Icons.ClipboardListIcon;
|
||||
Id = "Windows.ClipboardHistory";
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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 CoreWidgetProvider.Widgets.Enums;
|
||||
|
||||
public enum WidgetDataState
|
||||
{
|
||||
Unknown,
|
||||
Requested, // Request is out, waiting on a response. Current data is stale.
|
||||
Okay, // Received and updated data, stable state.
|
||||
Failed, // Failed retrieving data.
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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 CoreWidgetProvider.Widgets.Enums;
|
||||
|
||||
public enum WidgetPageState
|
||||
{
|
||||
Unknown,
|
||||
Configure,
|
||||
Loading,
|
||||
Content,
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class CPUStats : IDisposable
|
||||
{
|
||||
// CPU counters
|
||||
private readonly PerformanceCounter _procPerf = new("Processor Information", "% Processor Utility", "_Total");
|
||||
private readonly PerformanceCounter _procPerformance = new("Processor Information", "% Processor Performance", "_Total");
|
||||
private readonly PerformanceCounter _procFrequency = new("Processor Information", "Processor Frequency", "_Total");
|
||||
private readonly Dictionary<Process, PerformanceCounter> _cpuCounters = new();
|
||||
|
||||
internal sealed class ProcessStats
|
||||
{
|
||||
public Process? Process { get; set; }
|
||||
|
||||
public float CpuUsage { get; set; }
|
||||
}
|
||||
|
||||
public float CpuUsage { get; set; }
|
||||
|
||||
public float CpuSpeed { get; set; }
|
||||
|
||||
public ProcessStats[] ProcessCPUStats { get; set; }
|
||||
|
||||
public List<float> CpuChartValues { get; set; } = new();
|
||||
|
||||
public CPUStats()
|
||||
{
|
||||
CpuUsage = 0;
|
||||
ProcessCPUStats =
|
||||
[
|
||||
new ProcessStats(),
|
||||
new ProcessStats(),
|
||||
new ProcessStats()
|
||||
];
|
||||
|
||||
InitCPUPerfCounters();
|
||||
}
|
||||
|
||||
private void InitCPUPerfCounters()
|
||||
{
|
||||
var allProcesses = Process.GetProcesses().Where(p => (long)p.MainWindowHandle != 0);
|
||||
|
||||
foreach (var process in allProcesses)
|
||||
{
|
||||
_cpuCounters.Add(process, new PerformanceCounter("Process", "% Processor Time", process.ProcessName, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData(bool includeTopProcesses)
|
||||
{
|
||||
var timer = Stopwatch.StartNew();
|
||||
CpuUsage = _procPerf.NextValue() / 100;
|
||||
var usageMs = timer.ElapsedMilliseconds;
|
||||
CpuSpeed = _procFrequency.NextValue() * (_procPerformance.NextValue() / 100);
|
||||
var speedMs = timer.ElapsedMilliseconds - usageMs;
|
||||
lock (CpuChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(CpuUsage * 100, CpuChartValues);
|
||||
}
|
||||
|
||||
var chartMs = timer.ElapsedMilliseconds - speedMs;
|
||||
|
||||
var processCPUUsages = new Dictionary<Process, float>();
|
||||
|
||||
if (includeTopProcesses)
|
||||
{
|
||||
foreach (var processCounter in _cpuCounters)
|
||||
{
|
||||
try
|
||||
{
|
||||
// process might be terminated
|
||||
processCPUUsages.Add(processCounter.Key, processCounter.Value.NextValue() / Environment.ProcessorCount);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// _log.Information($"ProcessCounter Key {processCounter.Key} no longer exists, removing from _cpuCounters.");
|
||||
_cpuCounters.Remove(processCounter.Key);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// _log.Error(ex, "Error going through process counters.");
|
||||
}
|
||||
}
|
||||
|
||||
var cpuIndex = 0;
|
||||
foreach (var processCPUValue in processCPUUsages.OrderByDescending(x => x.Value).Take(3))
|
||||
{
|
||||
ProcessCPUStats[cpuIndex].Process = processCPUValue.Key;
|
||||
ProcessCPUStats[cpuIndex].CpuUsage = processCPUValue.Value;
|
||||
cpuIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
var total = timer.ElapsedMilliseconds;
|
||||
var processesMs = total - chartMs;
|
||||
|
||||
// CoreLogger.LogDebug($"[{usageMs}]+[{speedMs}]+[{chartMs}]+[{processesMs}]=[{total}]");
|
||||
}
|
||||
|
||||
internal string CreateCPUImageUrl()
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(CpuChartValues, ChartHelper.ChartType.CPU);
|
||||
}
|
||||
|
||||
internal string GetCpuProcessText(int cpuProcessIndex)
|
||||
{
|
||||
if (cpuProcessIndex >= ProcessCPUStats.Length)
|
||||
{
|
||||
return "no data";
|
||||
}
|
||||
|
||||
return $"{ProcessCPUStats[cpuProcessIndex].Process?.ProcessName} ({ProcessCPUStats[cpuProcessIndex].CpuUsage / 100:p})";
|
||||
}
|
||||
|
||||
internal void KillTopProcess(int cpuProcessIndex)
|
||||
{
|
||||
if (cpuProcessIndex >= ProcessCPUStats.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessCPUStats[cpuProcessIndex].Process?.Kill();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_procPerf.Dispose();
|
||||
_procPerformance.Dispose();
|
||||
_procFrequency.Dispose();
|
||||
|
||||
foreach (var counter in _cpuCounters.Values)
|
||||
{
|
||||
counter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed class ChartHelper
|
||||
{
|
||||
public enum ChartType
|
||||
{
|
||||
CPU,
|
||||
GPU,
|
||||
Mem,
|
||||
Net,
|
||||
}
|
||||
|
||||
public const int ChartHeight = 86;
|
||||
public const int ChartWidth = 268;
|
||||
|
||||
private const string LightGrayBoxStyle = "fill:none;stroke:lightgrey;stroke-width:1";
|
||||
|
||||
private const string CPULineStyle = "fill:none;stroke:rgb(57,184,227);stroke-width:1";
|
||||
private const string GPULineStyle = "fill:none;stroke:rgb(222,104,242);stroke-width:1";
|
||||
private const string MemLineStyle = "fill:none;stroke:rgb(92,158,250);stroke-width:1";
|
||||
private const string NetLineStyle = "fill:none;stroke:rgb(245,98,142);stroke-width:1";
|
||||
|
||||
private const string FillStyle = "fill:url(#gradientId);stroke:transparent";
|
||||
|
||||
private const string CPUBrushStop1Style = "stop-color:rgb(57,184,227);stop-opacity:0.4";
|
||||
private const string CPUBrushStop2Style = "stop-color:rgb(0,86,110);stop-opacity:0.25";
|
||||
|
||||
private const string GPUBrushStop1Style = "stop-color:rgb(222,104,242);stop-opacity:0.4";
|
||||
private const string GPUBrushStop2Style = "stop-color:rgb(125,0,138);stop-opacity:0.25";
|
||||
|
||||
private const string MemBrushStop1Style = "stop-color:rgb(92,158,250);stop-opacity:0.4";
|
||||
private const string MemBrushStop2Style = "stop-color:rgb(0,34,92);stop-opacity:0.25";
|
||||
|
||||
private const string NetBrushStop1Style = "stop-color:rgb(245,98,142);stop-opacity:0.4";
|
||||
private const string NetBrushStop2Style = "stop-color:rgb(130,0,47);stop-opacity:0.25";
|
||||
|
||||
private const string SvgElement = "svg";
|
||||
private const string RectElement = "rect";
|
||||
private const string PolylineElement = "polyline";
|
||||
private const string DefsElement = "defs";
|
||||
private const string LinearGradientElement = "linearGradient";
|
||||
private const string StopElement = "stop";
|
||||
|
||||
private const string HeightAttr = "height";
|
||||
private const string WidthAttr = "width";
|
||||
private const string StyleAttr = "style";
|
||||
private const string PointsAttr = "points";
|
||||
private const string OffsetAttr = "offset";
|
||||
private const string X1Attr = "x1";
|
||||
private const string X2Attr = "x2";
|
||||
private const string Y1Attr = "y1";
|
||||
private const string Y2Attr = "y2";
|
||||
private const string IdAttr = "id";
|
||||
|
||||
private const int MaxChartValues = 34;
|
||||
|
||||
public static string CreateImageUrl(List<float> chartValues, ChartType type)
|
||||
{
|
||||
var chartStr = CreateChart(chartValues, type);
|
||||
return "data:image/svg+xml;utf8," + chartStr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an SVG image for the chart.
|
||||
/// </summary>
|
||||
/// <param name="chartValues">The values to plot on the chart</param>
|
||||
/// <param name="type">The type of chart. Each chart type uses different colors.</param>
|
||||
/// <remarks>
|
||||
/// The SVG is made of three shapes: <br/>
|
||||
/// 1. A colored line, plotting the points on the graph <br/>
|
||||
/// 2. A transparent line, outlining the gradient under the graph <br/>
|
||||
/// 3. A grey box, outlining the entire image <br/>
|
||||
/// The SVG also contains a definition for the fill gradient.
|
||||
/// </remarks>
|
||||
/// <returns>A string representing the chart as an SVG image.</returns>
|
||||
public static string CreateChart(List<float> chartValues, ChartType type)
|
||||
{
|
||||
// The SVG created by this method will look similar to this:
|
||||
/*
|
||||
<svg height="102" width="264">
|
||||
<defs>
|
||||
<linearGradient x1="0%" x2="0%" y1="0%" y2="100%" id="gradientId">
|
||||
<stop offset="0%" style="stop-color:rgb(222,104,242);stop-opacity:0.4" />
|
||||
<stop offset="95%" style="stop-color:rgb(125,0,138);stop-opacity:0.25" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polyline points="1,91 10,71 253,51 262,31 262,101 1,101" style="fill:url(#gradientId);stroke:transparent" />
|
||||
<polyline points="1,91 10,71 253,51 262,31" style="fill:none;stroke:rgb(222,104,242);stroke-width:1" />
|
||||
<rect height="102" width="264" style="fill:none;stroke:lightgrey;stroke-width:1" />
|
||||
</svg>
|
||||
*/
|
||||
|
||||
// The following code can be uncommented for testing when a static image is desired.
|
||||
/* chartValues.Clear();
|
||||
chartValues = new List<float>
|
||||
{
|
||||
10, 30, 20, 40, 30, 50, 40, 60, 50, 100,
|
||||
10, 30, 20, 40, 30, 50, 40, 60, 50, 70,
|
||||
0, 30, 20, 40, 30, 50, 40, 60, 50, 70,
|
||||
};*/
|
||||
|
||||
var chartDoc = new XDocument();
|
||||
|
||||
lock (chartValues)
|
||||
{
|
||||
var svgElement = CreateBlankSvg(ChartHeight, ChartWidth);
|
||||
|
||||
// Create the line that will show the points on the graph.
|
||||
var lineElement = new XElement(PolylineElement);
|
||||
var points = TransformPointsToLine(chartValues, out var startX, out var finalX);
|
||||
lineElement.SetAttributeValue(PointsAttr, points.ToString());
|
||||
lineElement.SetAttributeValue(StyleAttr, GetLineStyle(type));
|
||||
|
||||
// Create the line that will contain the gradient fill.
|
||||
TransformPointsToLoop(points, startX, finalX);
|
||||
var fillElement = new XElement(PolylineElement);
|
||||
fillElement.SetAttributeValue(PointsAttr, points.ToString());
|
||||
fillElement.SetAttributeValue(StyleAttr, FillStyle);
|
||||
|
||||
// Add the gradient definition and the three shapes to the svg.
|
||||
svgElement.Add(CreateGradientDefinition(type));
|
||||
svgElement.Add(fillElement);
|
||||
svgElement.Add(lineElement);
|
||||
svgElement.Add(CreateBorderBox(ChartHeight, ChartWidth));
|
||||
|
||||
chartDoc.Add(svgElement);
|
||||
}
|
||||
|
||||
return chartDoc.ToString();
|
||||
}
|
||||
|
||||
private static XElement CreateBlankSvg(int height, int width)
|
||||
{
|
||||
var svgElement = new XElement(SvgElement);
|
||||
svgElement.SetAttributeValue(HeightAttr, height);
|
||||
svgElement.SetAttributeValue(WidthAttr, width);
|
||||
return svgElement;
|
||||
}
|
||||
|
||||
private static XElement CreateGradientDefinition(ChartType type)
|
||||
{
|
||||
var defsElement = new XElement(DefsElement);
|
||||
var gradientElement = new XElement(LinearGradientElement);
|
||||
|
||||
// Vertical gradients are created when x1 and x2 are equal and y1 and y2 differ.
|
||||
gradientElement.SetAttributeValue(X1Attr, "0%");
|
||||
gradientElement.SetAttributeValue(X2Attr, "0%");
|
||||
gradientElement.SetAttributeValue(Y1Attr, "0%");
|
||||
gradientElement.SetAttributeValue(Y2Attr, "100%");
|
||||
gradientElement.SetAttributeValue(IdAttr, "gradientId");
|
||||
|
||||
string stop1Style;
|
||||
string stop2Style;
|
||||
switch (type)
|
||||
{
|
||||
case ChartType.GPU:
|
||||
stop1Style = GPUBrushStop1Style;
|
||||
stop2Style = GPUBrushStop2Style;
|
||||
break;
|
||||
case ChartType.Mem:
|
||||
stop1Style = MemBrushStop1Style;
|
||||
stop2Style = MemBrushStop2Style;
|
||||
break;
|
||||
case ChartType.Net:
|
||||
stop1Style = NetBrushStop1Style;
|
||||
stop2Style = NetBrushStop2Style;
|
||||
break;
|
||||
case ChartType.CPU:
|
||||
default:
|
||||
stop1Style = CPUBrushStop1Style;
|
||||
stop2Style = CPUBrushStop2Style;
|
||||
break;
|
||||
}
|
||||
|
||||
var stop1 = new XElement(StopElement);
|
||||
stop1.SetAttributeValue(OffsetAttr, "0%");
|
||||
stop1.SetAttributeValue(StyleAttr, stop1Style);
|
||||
|
||||
var stop2 = new XElement(StopElement);
|
||||
stop2.SetAttributeValue(OffsetAttr, "95%");
|
||||
stop2.SetAttributeValue(StyleAttr, stop2Style);
|
||||
|
||||
gradientElement.Add(stop1);
|
||||
gradientElement.Add(stop2);
|
||||
defsElement.Add(gradientElement);
|
||||
|
||||
return defsElement;
|
||||
}
|
||||
|
||||
private static XElement CreateBorderBox(int height, int width)
|
||||
{
|
||||
var boxElement = new XElement(RectElement);
|
||||
boxElement.SetAttributeValue(HeightAttr, height);
|
||||
boxElement.SetAttributeValue(WidthAttr, width);
|
||||
boxElement.SetAttributeValue(StyleAttr, LightGrayBoxStyle);
|
||||
return boxElement;
|
||||
}
|
||||
|
||||
private static string GetLineStyle(ChartType type)
|
||||
{
|
||||
var lineStyle = type switch
|
||||
{
|
||||
ChartType.CPU => CPULineStyle,
|
||||
ChartType.GPU => GPULineStyle,
|
||||
ChartType.Mem => MemLineStyle,
|
||||
ChartType.Net => NetLineStyle,
|
||||
_ => CPULineStyle,
|
||||
};
|
||||
|
||||
return lineStyle;
|
||||
}
|
||||
|
||||
private static StringBuilder TransformPointsToLine(List<float> chartValues, out int startX, out int finalX)
|
||||
{
|
||||
var points = new StringBuilder();
|
||||
|
||||
// The X value where the graph starts must be adjusted so that the graph is right-aligned.
|
||||
// The max available width of the widget is 268. Since there is a 1 px border around the chart, the width of the chart's line must be <=266.
|
||||
// To create a chart of exactly the right size, we'll have 34 points with 8 pixels in between:
|
||||
// 1 px left border + 1 px for first point + 33 segments * 8 px per segment + 1 px right border = 267 pixels total in width.
|
||||
const int pxBetweenPoints = 8;
|
||||
|
||||
// When the chart doesn't have all points yet, move the chart over to the right by increasing the starting X coordinate.
|
||||
// For a chart with only 1 point, the svg will not render a polyline.
|
||||
// For a chart with 2 points, starting X coordinate == 2 + (34 - 2) * 8 == 1 + 32 * 8 == 1 + 256 == 257
|
||||
// For a chart with 30 points, starting X coordinate == 2 + (34 - 34) * 8 == 1 + 0 * 8 == 1 + 0 == 2
|
||||
startX = 2 + ((MaxChartValues - chartValues.Count) * pxBetweenPoints);
|
||||
finalX = startX;
|
||||
|
||||
// Extend graph by one pixel to cover gap on the left when the chart is otherwise full.
|
||||
if (startX == 2)
|
||||
{
|
||||
var invertedHeight = 100 - chartValues[0];
|
||||
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
|
||||
points.Append(CultureInfo.InvariantCulture, $"1,{finalY} ");
|
||||
}
|
||||
|
||||
foreach (var origY in chartValues)
|
||||
{
|
||||
// We receive the height as a number up from the X axis (bottom of the chart), but we have to invert it
|
||||
// since the Y coordinate is relative to the top of the chart.
|
||||
var invertedHeight = 100 - origY;
|
||||
|
||||
// Scale the final Y to whatever the chart height is.
|
||||
var finalY = (invertedHeight * (ChartHeight / 100.0)) - 1;
|
||||
|
||||
points.Append(CultureInfo.InvariantCulture, $"{finalX},{finalY} ");
|
||||
finalX += pxBetweenPoints;
|
||||
}
|
||||
|
||||
// Remove the trailing space.
|
||||
if (points.Length > 0)
|
||||
{
|
||||
points.Remove(points.Length - 1, 1);
|
||||
finalX -= pxBetweenPoints;
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private static void TransformPointsToLoop(StringBuilder points, int startX, int finalX)
|
||||
{
|
||||
// Close the loop.
|
||||
// Add a point at the most recent X value that corresponds with y = 0
|
||||
points.Append(CultureInfo.InvariantCulture, $" {finalX},{ChartHeight - 1}");
|
||||
|
||||
// Add a point at the start of the chart that corresponds with y = 0
|
||||
points.Append(CultureInfo.InvariantCulture, $" {startX},{ChartHeight - 1}");
|
||||
}
|
||||
|
||||
public static void AddNextChartValue(float value, List<float> chartValues)
|
||||
{
|
||||
if (chartValues.Count >= MaxChartValues)
|
||||
{
|
||||
chartValues.RemoveAt(0);
|
||||
}
|
||||
|
||||
chartValues.Add(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// 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 Timer = System.Timers.Timer;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class DataManager : IDisposable
|
||||
{
|
||||
private readonly SystemData _systemData;
|
||||
private readonly DataType _dataType;
|
||||
private readonly Timer _updateTimer;
|
||||
private readonly Action _updateAction;
|
||||
|
||||
private const int OneSecondInMilliseconds = 1000;
|
||||
|
||||
public DataManager(DataType type, Action updateWidget)
|
||||
{
|
||||
_systemData = new SystemData();
|
||||
_updateAction = updateWidget;
|
||||
_dataType = type;
|
||||
|
||||
_updateTimer = new Timer(OneSecondInMilliseconds);
|
||||
_updateTimer.Elapsed += UpdateTimer_Elapsed;
|
||||
_updateTimer.AutoReset = true;
|
||||
_updateTimer.Enabled = false;
|
||||
}
|
||||
|
||||
private void GetMemoryData()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
{
|
||||
SystemData.MemStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetNetworkData()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
{
|
||||
SystemData.NetStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetGPUData()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
{
|
||||
SystemData.GPUStats.GetData();
|
||||
}
|
||||
}
|
||||
|
||||
private void GetCPUData(bool includeTopProcesses)
|
||||
{
|
||||
lock (SystemData.CpuStats)
|
||||
{
|
||||
SystemData.CpuStats.GetData(includeTopProcesses);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
switch (_dataType)
|
||||
{
|
||||
case DataType.CPU:
|
||||
case DataType.CpuWithTopProcesses:
|
||||
{
|
||||
// CPU
|
||||
GetCPUData(_dataType == DataType.CpuWithTopProcesses);
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.GPU:
|
||||
{
|
||||
// gpu
|
||||
GetGPUData();
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Memory:
|
||||
{
|
||||
// memory
|
||||
GetMemoryData();
|
||||
break;
|
||||
}
|
||||
|
||||
case DataType.Network:
|
||||
{
|
||||
// network
|
||||
GetNetworkData();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_updateAction?.Invoke();
|
||||
}
|
||||
|
||||
internal MemoryStats GetMemoryStats()
|
||||
{
|
||||
lock (SystemData.MemStats)
|
||||
{
|
||||
return SystemData.MemStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal NetworkStats GetNetworkStats()
|
||||
{
|
||||
lock (SystemData.NetStats)
|
||||
{
|
||||
return SystemData.NetStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal GPUStats GetGPUStats()
|
||||
{
|
||||
lock (SystemData.GPUStats)
|
||||
{
|
||||
return SystemData.GPUStats;
|
||||
}
|
||||
}
|
||||
|
||||
internal CPUStats GetCPUStats()
|
||||
{
|
||||
lock (SystemData.CpuStats)
|
||||
{
|
||||
return SystemData.CpuStats;
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_updateTimer.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_updateTimer.Stop();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_systemData.Dispose();
|
||||
_updateTimer.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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 CoreWidgetProvider.Helpers;
|
||||
|
||||
public enum DataType
|
||||
{
|
||||
/// <summary>
|
||||
/// CPU related data.
|
||||
/// </summary>
|
||||
CPU,
|
||||
|
||||
/// <summary>
|
||||
/// CPU related data, including the top processes.
|
||||
/// Calculating the top processes takes a lot longer,
|
||||
/// so by default we don't.
|
||||
/// </summary>
|
||||
CpuWithTopProcesses,
|
||||
|
||||
/// <summary>
|
||||
/// Memory related data.
|
||||
/// </summary>
|
||||
Memory,
|
||||
|
||||
/// <summary>
|
||||
/// GPU related data.
|
||||
/// </summary>
|
||||
GPU,
|
||||
|
||||
/// <summary>
|
||||
/// Network related data.
|
||||
/// </summary>
|
||||
Network,
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class GPUStats : IDisposable
|
||||
{
|
||||
// GPU counters
|
||||
private readonly Dictionary<int, List<PerformanceCounter>> _gpuCounters = new();
|
||||
|
||||
private readonly List<Data> _stats = new();
|
||||
|
||||
public sealed class Data
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public int PhysId { get; set; }
|
||||
|
||||
public float Usage { get; set; }
|
||||
|
||||
public float Temperature { get; set; }
|
||||
|
||||
public List<float> GpuChartValues { get; set; } = new();
|
||||
}
|
||||
|
||||
public GPUStats()
|
||||
{
|
||||
GetGPUPerfCounters();
|
||||
LoadGPUsFromCounters();
|
||||
}
|
||||
|
||||
public void GetGPUPerfCounters()
|
||||
{
|
||||
// There are really 4 different things we should be tracking the usage
|
||||
// of. Similar to how the instance name ends with `3D`, the following
|
||||
// suffixes are important.
|
||||
//
|
||||
// * `3D`
|
||||
// * `VideoEncode`
|
||||
// * `VideoDecode`
|
||||
// * `VideoProcessing`
|
||||
//
|
||||
// We could totally put each of those sets of counters into their own
|
||||
// set. That's what we should do, so that we can report the sum of those
|
||||
// numbers as the total utilization, and then have them broken out in
|
||||
// the card template and in the details metadata.
|
||||
_gpuCounters.Clear();
|
||||
|
||||
var pcg = new PerformanceCounterCategory("GPU Engine");
|
||||
var instanceNames = pcg.GetInstanceNames();
|
||||
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
if (!instanceName.EndsWith("3D", StringComparison.InvariantCulture))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var utilizationCounters = pcg.GetCounters(instanceName)
|
||||
.Where(x => x.CounterName.StartsWith("Utilization Percentage", StringComparison.InvariantCulture));
|
||||
|
||||
foreach (var counter in utilizationCounters)
|
||||
{
|
||||
var counterKey = counter.InstanceName;
|
||||
|
||||
// skip these values
|
||||
GetKeyValueFromCounterKey("pid", ref counterKey);
|
||||
GetKeyValueFromCounterKey("luid", ref counterKey);
|
||||
|
||||
int phys;
|
||||
var success = int.TryParse(GetKeyValueFromCounterKey("phys", ref counterKey), out phys);
|
||||
if (success)
|
||||
{
|
||||
GetKeyValueFromCounterKey("eng", ref counterKey);
|
||||
var engtype = GetKeyValueFromCounterKey("engtype", ref counterKey);
|
||||
if (engtype != "3D")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_gpuCounters.TryGetValue(phys, out var value))
|
||||
{
|
||||
value = new();
|
||||
_gpuCounters.Add(phys, value);
|
||||
}
|
||||
|
||||
value.Add(counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadGPUsFromCounters()
|
||||
{
|
||||
// The old dev home code tracked GPU stats by querying WMI for the list
|
||||
// of GPUs, and then matching them up with the performance counter IDs.
|
||||
//
|
||||
// We can't use WMI here, because it drags in a dependency on
|
||||
// Microsoft.Management.Infrastructure, which is not compatible with
|
||||
// AOT.
|
||||
//
|
||||
// For now, we'll just use the indicies as the GPU names.
|
||||
_stats.Clear();
|
||||
foreach (var (k, v) in _gpuCounters)
|
||||
{
|
||||
var id = k;
|
||||
var counters = v;
|
||||
_stats.Add(new Data() { PhysId = id, Name = "GPU " + id });
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
foreach (var gpu in _stats)
|
||||
{
|
||||
List<PerformanceCounter>? counters;
|
||||
var success = _gpuCounters.TryGetValue(gpu.PhysId, out counters);
|
||||
|
||||
if (success && counters != null)
|
||||
{
|
||||
// TODO: This outer try/catch should be replaced with more secure locking around shared resources.
|
||||
try
|
||||
{
|
||||
var sum = 0.0f;
|
||||
var countersToRemove = new List<PerformanceCounter>();
|
||||
foreach (var counter in counters)
|
||||
{
|
||||
try
|
||||
{
|
||||
// NextValue() can throw an InvalidOperationException if the counter is no longer there.
|
||||
sum += counter.NextValue();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// We can't modify the list during the loop, so save it to remove at the end.
|
||||
// _log.Information(ex, "Failed to get next value, remove");
|
||||
countersToRemove.Add(counter);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// _log.Error(ex, "Error going through process counters.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var counter in countersToRemove)
|
||||
{
|
||||
counters.Remove(counter);
|
||||
counter.Dispose();
|
||||
}
|
||||
|
||||
gpu.Usage = sum / 100;
|
||||
lock (gpu.GpuChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(sum, gpu.GpuChartValues);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// _log.Error(ex, "Error summing process counters.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal string CreateGPUImageUrl(int gpuChartIndex)
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(_stats.ElementAt(gpuChartIndex).GpuChartValues, ChartHelper.ChartType.GPU);
|
||||
}
|
||||
|
||||
internal string GetGPUName(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _stats[gpuActiveIndex].Name ?? string.Empty;
|
||||
}
|
||||
|
||||
internal int GetPrevGPUIndex(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gpuActiveIndex == 0)
|
||||
{
|
||||
return _stats.Count - 1;
|
||||
}
|
||||
|
||||
return gpuActiveIndex - 1;
|
||||
}
|
||||
|
||||
internal int GetNextGPUIndex(int gpuActiveIndex)
|
||||
{
|
||||
if (_stats.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gpuActiveIndex == _stats.Count - 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return gpuActiveIndex + 1;
|
||||
}
|
||||
|
||||
internal float GetGPUUsage(int gpuActiveIndex, string gpuActiveEngType)
|
||||
{
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _stats[gpuActiveIndex].Usage;
|
||||
}
|
||||
|
||||
internal string GetGPUTemperature(int gpuActiveIndex)
|
||||
{
|
||||
// MG Jan 2026: This code was lifted from the old Dev Home codebase.
|
||||
// However, the performance counters for GPU temperature are not being
|
||||
// collected. So this function always returns "--" for now.
|
||||
//
|
||||
// I have not done the code archeology to figure out why they were
|
||||
// removed.
|
||||
if (_stats.Count <= gpuActiveIndex)
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
|
||||
var temperature = _stats[gpuActiveIndex].Temperature;
|
||||
if (temperature == 0)
|
||||
{
|
||||
return "--";
|
||||
}
|
||||
|
||||
return temperature.ToString("0.", CultureInfo.InvariantCulture) + " \x00B0C";
|
||||
}
|
||||
|
||||
private string GetKeyValueFromCounterKey(string key, ref string counterKey)
|
||||
{
|
||||
if (!counterKey.StartsWith(key, StringComparison.InvariantCulture))
|
||||
{
|
||||
return "error";
|
||||
}
|
||||
|
||||
counterKey = counterKey.Substring(key.Length + 1);
|
||||
if (key.Equals("engtype", StringComparison.Ordinal))
|
||||
{
|
||||
return counterKey;
|
||||
}
|
||||
|
||||
var pos = counterKey.IndexOf('_');
|
||||
if (key.Equals("luid", StringComparison.Ordinal))
|
||||
{
|
||||
pos = counterKey.IndexOf('_', pos + 1);
|
||||
}
|
||||
|
||||
var retValue = counterKey.Substring(0, pos);
|
||||
counterKey = counterKey.Substring(pos + 1);
|
||||
return retValue;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var counterPair in _gpuCounters)
|
||||
{
|
||||
foreach (var counter in counterPair.Value)
|
||||
{
|
||||
counter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class MemoryStats : IDisposable
|
||||
{
|
||||
private readonly PerformanceCounter _memCommitted = new("Memory", "Committed Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memCached = new("Memory", "Cache Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memCommittedLimit = new("Memory", "Commit Limit", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolPaged = new("Memory", "Pool Paged Bytes", string.Empty);
|
||||
private readonly PerformanceCounter _memPoolNonPaged = new("Memory", "Pool Nonpaged Bytes", string.Empty);
|
||||
|
||||
public float MemUsage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong AllMem
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong UsedMem
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemCommitted
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemCommitLimit
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemCached
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemPagedPool
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public ulong MemNonPagedPool
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<float> MemChartValues { get; set; } = new();
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
Windows.Win32.System.SystemInformation.MEMORYSTATUSEX memStatus = default;
|
||||
memStatus.dwLength = (uint)Marshal.SizeOf<Windows.Win32.System.SystemInformation.MEMORYSTATUSEX>();
|
||||
if (PInvoke.GlobalMemoryStatusEx(ref memStatus))
|
||||
{
|
||||
AllMem = memStatus.ullTotalPhys;
|
||||
var availableMem = memStatus.ullAvailPhys;
|
||||
UsedMem = AllMem - availableMem;
|
||||
|
||||
MemUsage = (float)UsedMem / AllMem;
|
||||
lock (MemChartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(MemUsage * 100, MemChartValues);
|
||||
}
|
||||
}
|
||||
|
||||
MemCached = (ulong)_memCached.NextValue();
|
||||
MemCommitted = (ulong)_memCommitted.NextValue();
|
||||
MemCommitLimit = (ulong)_memCommittedLimit.NextValue();
|
||||
MemPagedPool = (ulong)_memPoolPaged.NextValue();
|
||||
MemNonPagedPool = (ulong)_memPoolNonPaged.NextValue();
|
||||
}
|
||||
|
||||
public string CreateMemImageUrl()
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(MemChartValues, ChartHelper.ChartType.Mem);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_memCommitted.Dispose();
|
||||
_memCached.Dispose();
|
||||
_memCommittedLimit.Dispose();
|
||||
_memPoolPaged.Dispose();
|
||||
_memPoolNonPaged.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class NetworkStats : IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, List<PerformanceCounter>> _networkCounters = new();
|
||||
|
||||
private Dictionary<string, Data> NetworkUsages { get; set; } = new();
|
||||
|
||||
private Dictionary<string, List<float>> NetChartValues { get; set; } = new();
|
||||
|
||||
public sealed class Data
|
||||
{
|
||||
public float Usage
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public float Sent
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public float Received
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
|
||||
public NetworkStats()
|
||||
{
|
||||
InitNetworkPerfCounters();
|
||||
}
|
||||
|
||||
private void InitNetworkPerfCounters()
|
||||
{
|
||||
var pcc = new PerformanceCounterCategory("Network Interface");
|
||||
var instanceNames = pcc.GetInstanceNames();
|
||||
foreach (var instanceName in instanceNames)
|
||||
{
|
||||
var instanceCounters = new List<PerformanceCounter>();
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Sent/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Bytes Received/sec", instanceName));
|
||||
instanceCounters.Add(new PerformanceCounter("Network Interface", "Current Bandwidth", instanceName));
|
||||
_networkCounters.Add(instanceName, instanceCounters);
|
||||
NetChartValues.Add(instanceName, new List<float>());
|
||||
NetworkUsages.Add(instanceName, new Data());
|
||||
}
|
||||
}
|
||||
|
||||
public void GetData()
|
||||
{
|
||||
float maxUsage = 0;
|
||||
foreach (var networkCounterWithName in _networkCounters)
|
||||
{
|
||||
try
|
||||
{
|
||||
var sent = networkCounterWithName.Value[0].NextValue();
|
||||
var received = networkCounterWithName.Value[1].NextValue();
|
||||
var bandWidth = networkCounterWithName.Value[2].NextValue();
|
||||
if (bandWidth == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var usage = 8 * (sent + received) / bandWidth;
|
||||
var name = networkCounterWithName.Key;
|
||||
NetworkUsages[name].Sent = sent;
|
||||
NetworkUsages[name].Received = received;
|
||||
NetworkUsages[name].Usage = usage;
|
||||
|
||||
var chartValues = NetChartValues[name];
|
||||
lock (chartValues)
|
||||
{
|
||||
ChartHelper.AddNextChartValue(usage * 100, chartValues);
|
||||
}
|
||||
|
||||
if (usage > maxUsage)
|
||||
{
|
||||
maxUsage = usage;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Log.Error(ex, "Error getting network data.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string CreateNetImageUrl(int netChartIndex)
|
||||
{
|
||||
return ChartHelper.CreateImageUrl(NetChartValues.ElementAt(netChartIndex).Value, ChartHelper.ChartType.Net);
|
||||
}
|
||||
|
||||
public string GetNetworkName(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count <= networkIndex)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return NetChartValues.ElementAt(networkIndex).Key;
|
||||
}
|
||||
|
||||
public Data GetNetworkUsage(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count <= networkIndex)
|
||||
{
|
||||
return new Data();
|
||||
}
|
||||
|
||||
var currNetworkName = NetChartValues.ElementAt(networkIndex).Key;
|
||||
if (!NetworkUsages.TryGetValue(currNetworkName, out var value))
|
||||
{
|
||||
return new Data();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public int GetPrevNetworkIndex(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (networkIndex == 0)
|
||||
{
|
||||
return NetChartValues.Count - 1;
|
||||
}
|
||||
|
||||
return networkIndex - 1;
|
||||
}
|
||||
|
||||
public int GetNextNetworkIndex(int networkIndex)
|
||||
{
|
||||
if (NetChartValues.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (networkIndex == NetChartValues.Count - 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return networkIndex + 1;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var counterPair in _networkCounters)
|
||||
{
|
||||
foreach (var counter in counterPair.Value)
|
||||
{
|
||||
counter.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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.Text;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
// This class was pilfered from devhome, but changed much more substantially to
|
||||
// get the resources out of our resources.pri the way we need.
|
||||
public static class Resources
|
||||
{
|
||||
private static readonly Windows.ApplicationModel.Resources.Core.ResourceMap? _map;
|
||||
|
||||
private static readonly string ResourcesPath = "Microsoft.CmdPal.Ext.PerformanceMonitor/Resources";
|
||||
|
||||
static Resources()
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
|
||||
if (currentResourceManager.MainResourceMap is not null)
|
||||
{
|
||||
_map = currentResourceManager.MainResourceMap;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Resource map not available (e.g., during unit tests)
|
||||
_map = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetResource(string identifier, ILogger? log = null)
|
||||
{
|
||||
if (_map is null)
|
||||
{
|
||||
return identifier;
|
||||
}
|
||||
|
||||
var fullKey = $"{ResourcesPath}/{identifier}";
|
||||
|
||||
var val = _map.GetValue(fullKey);
|
||||
#if DEBUG
|
||||
if (val == null)
|
||||
{
|
||||
log?.LogError($"Failed loading resource: {identifier}");
|
||||
|
||||
DebugResources(log);
|
||||
}
|
||||
#endif
|
||||
return val!.ValueAsString;
|
||||
}
|
||||
|
||||
public static string ReplaceIdentifersFast(
|
||||
string original,
|
||||
ILogger? log = null)
|
||||
{
|
||||
// walk the string, looking for a pair of '%' characters
|
||||
StringBuilder sb = new();
|
||||
var length = original.Length;
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
if (original[i] == '%')
|
||||
{
|
||||
var end = original.IndexOf('%', i + 1);
|
||||
if (end > i)
|
||||
{
|
||||
var identifier = original.Substring(i + 1, end - i - 1);
|
||||
var resourceString = GetResource(identifier, log);
|
||||
sb.Append(resourceString);
|
||||
i = end; // move index to the end '%'
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(original[i]);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// These are all the string identifiers that appear in widgets.
|
||||
public static string[] GetWidgetResourceIdentifiers()
|
||||
{
|
||||
return
|
||||
[
|
||||
"Widget_Template/Loading",
|
||||
"Widget_Template_Tooltip/Submit",
|
||||
"Memory_Widget_Template/SystemMemory",
|
||||
"Memory_Widget_Template/MemoryUsage",
|
||||
"Memory_Widget_Template/AllMemory",
|
||||
"Memory_Widget_Template/UsedMemory",
|
||||
"Memory_Widget_Template/Committed",
|
||||
"Memory_Widget_Template/Cached",
|
||||
"Memory_Widget_Template/NonPagedPool",
|
||||
"Memory_Widget_Template/PagedPool",
|
||||
"NetworkUsage_Widget_Template/Network_Usage",
|
||||
"NetworkUsage_Widget_Template/Sent",
|
||||
"NetworkUsage_Widget_Template/Received",
|
||||
"NetworkUsage_Widget_Template/Network_Name",
|
||||
"NetworkUsage_Widget_Template/Previous_Network",
|
||||
"NetworkUsage_Widget_Template/Next_Network",
|
||||
"NetworkUsage_Widget_Template/Ethernet_Heading",
|
||||
"GPUUsage_Widget_Template/GPU_Usage",
|
||||
"GPUUsage_Widget_Template/GPU_Name",
|
||||
"GPUUsage_Widget_Template/GPU_Temperature",
|
||||
"GPUUsage_Widget_Template/Previous_GPU",
|
||||
"GPUUsage_Widget_Template/Next_GPU",
|
||||
"CPUUsage_Widget_Template/CPU_Usage",
|
||||
"CPUUsage_Widget_Template/CPU_Speed",
|
||||
"CPUUsage_Widget_Template/Processes",
|
||||
"CPUUsage_Widget_Template/End_Process",
|
||||
"Widget_Template_Button/Preview",
|
||||
"Widget_Template_Button/Save",
|
||||
"Widget_Template_Button/Cancel",
|
||||
];
|
||||
}
|
||||
|
||||
private static void DebugResources(ILogger? log)
|
||||
{
|
||||
var currentResourceManager = Windows.ApplicationModel.Resources.Core.ResourceManager.Current;
|
||||
StringBuilder sb = new();
|
||||
|
||||
foreach (var (k, v) in currentResourceManager.AllResourceMaps)
|
||||
{
|
||||
sb.AppendLine(k);
|
||||
foreach (var (k2, v2) in v)
|
||||
{
|
||||
sb.Append('\t');
|
||||
sb.AppendLine(k2);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
log?.LogDebug($"Resource maps:");
|
||||
log?.LogDebug(sb.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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;
|
||||
|
||||
namespace CoreWidgetProvider.Helpers;
|
||||
|
||||
internal sealed partial class SystemData : IDisposable
|
||||
{
|
||||
public static MemoryStats MemStats { get; set; } = new MemoryStats();
|
||||
|
||||
public static NetworkStats NetStats { get; set; } = new NetworkStats();
|
||||
|
||||
public static GPUStats GPUStats { get; set; } = new GPUStats();
|
||||
|
||||
public static CPUStats CpuStats { get; set; } = new CPUStats();
|
||||
|
||||
public SystemData()
|
||||
{
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
The code in this directory was largely lifted from the [DevHome repo].
|
||||
|
||||
The specific directory we're using is
|
||||
https://github.com/microsoft/devhome/tree/main/extensions/CoreWidgetProvider
|
||||
This has code for all the DevHome performance widgets.
|
||||
|
||||
Minimal changes have been made to match our style guidelines.
|
||||
Additionally, a much larger change was made to Resources.cs, to match our own
|
||||
resource loading needs.
|
||||
|
||||
The code was lifted as of commit d52734ce0e33a82af3313d24c3c2979c37b68bab
|
||||
|
||||
|
||||
[DevHome repo]: https://github.com/microsoft/devhome/
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "AdaptiveCard",
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.5",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "%Widget_Template/Loading%",
|
||||
"wrap": true,
|
||||
"horizontalAlignment": "center"
|
||||
}
|
||||
],
|
||||
"verticalContentAlignment": "center",
|
||||
"height": "stretch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"type": "AdaptiveCard",
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.6",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"id": "ConfigFile",
|
||||
"label": "%SSH_Widget_Template/ConfigFilePath%",
|
||||
"inlineAction": {
|
||||
"type": "Action.ChooseFile"
|
||||
},
|
||||
"spacing": "Medium",
|
||||
"style": "Url",
|
||||
"placeholder": "${$root.configuration.currentOrDefaultConfigFile}",
|
||||
"value": "${$root.configuration.currentOrDefaultConfigFile}"
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"spacing": "Medium",
|
||||
"horizontalAlignment": "center",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"width": "auto",
|
||||
"items": [
|
||||
{
|
||||
"type": "ActionSet",
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Execute",
|
||||
"title": "%Widget_Template_Button/Preview%",
|
||||
"verb": "CheckPath",
|
||||
"associatedInputs": "Auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${errorMessage}",
|
||||
"wrap": true,
|
||||
"horizontalAlignment": "Center"
|
||||
}
|
||||
],
|
||||
"$when": "${errorMessage != null}",
|
||||
"separator": true,
|
||||
"verticalContentAlignment": "Center",
|
||||
"style": "warning"
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "%SSH_Widget_Template/NumOfHosts%",
|
||||
"wrap": true,
|
||||
"spacing": "Medium",
|
||||
"size": "Small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${numOfEntries}",
|
||||
"wrap": true,
|
||||
"size": "medium",
|
||||
"spacing": "None"
|
||||
}
|
||||
],
|
||||
"spacing": "Medium",
|
||||
"$data": "${$root.configuration}",
|
||||
"$when": "${$root.hasConfiguration}",
|
||||
"bleed": true
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"spacing": "Large",
|
||||
"horizontalAlignment": "center",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"width": "auto",
|
||||
"items": [
|
||||
{
|
||||
"type": "Container",
|
||||
"items": [
|
||||
{
|
||||
"type": "ActionSet",
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Execute",
|
||||
"title": "%Widget_Template_Button/Save%",
|
||||
"verb": "Save",
|
||||
"isEnabled": "${$root.hasConfiguration}"
|
||||
},
|
||||
{
|
||||
"type": "Action.Execute",
|
||||
"title": "%Widget_Template_Button/Cancel%",
|
||||
"verb": "Cancel",
|
||||
"isEnabled": "${$root.savedConfigFile != \"\"}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage != null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${errorMessage}",
|
||||
"wrap": true,
|
||||
"size": "small"
|
||||
}
|
||||
],
|
||||
"style": "warning"
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage == null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${(count(hosts) == 0)}",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "%SSH_Widget_Template/EmptyHosts%",
|
||||
"wrap": true,
|
||||
"weight": "Bolder",
|
||||
"horizontalAlignment": "Center"
|
||||
}
|
||||
],
|
||||
"spacing": "Medium",
|
||||
"verticalContentAlignment": "Center"
|
||||
},
|
||||
{
|
||||
"$data": "${hosts}",
|
||||
"type": "ColumnSet",
|
||||
"style": "emphasis",
|
||||
"selectAction": {
|
||||
"type": "Action.Execute",
|
||||
"verb": "Connect",
|
||||
"data": "${host}"
|
||||
},
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"verticalContentAlignment": "Center",
|
||||
"width": "auto",
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"url": "data:image/png;base64,${icon}",
|
||||
"size": "small",
|
||||
"horizontalAlignment": "left"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"width": "stretch",
|
||||
"verticalContentAlignment": "Center",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${host}",
|
||||
"size": "medium",
|
||||
"wrap": true,
|
||||
"horizontalAlignment": "left"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.5"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage != null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${errorMessage}",
|
||||
"wrap": true,
|
||||
"size": "small"
|
||||
}
|
||||
],
|
||||
"style": "warning"
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage == null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"url": "${cpuGraphUrl}",
|
||||
"height": "${chartHeight}",
|
||||
"width": "${chartWidth}",
|
||||
"$when": "${$host.widgetSize != \"small\"}",
|
||||
"horizontalAlignment": "center"
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"isSubtle": true,
|
||||
"text": "%CPUUsage_Widget_Template/CPU_Usage%"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "large",
|
||||
"weight": "bolder",
|
||||
"text": "${cpuUsage}"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"isSubtle": true,
|
||||
"horizontalAlignment": "right",
|
||||
"text": "%CPUUsage_Widget_Template/CPU_Speed%"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "large",
|
||||
"horizontalAlignment": "right",
|
||||
"text": "${cpuSpeed}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": false,
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"isSubtle": true,
|
||||
"text": "%CPUUsage_Widget_Template/Processes%",
|
||||
"wrap": true
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"text": "${cpuProc1}"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"text": "${cpuProc2}"
|
||||
},
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"text": "${cpuProc3}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.5"
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage != null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${errorMessage}",
|
||||
"wrap": true,
|
||||
"size": "small"
|
||||
}
|
||||
],
|
||||
"style": "warning"
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage == null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"url": "${gpuGraphUrl}",
|
||||
"height": "${chartHeight}",
|
||||
"width": "${chartWidth}",
|
||||
"$when": "${$host.widgetSize != \"small\"}",
|
||||
"horizontalAlignment": "center"
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%GPUUsage_Widget_Template/GPU_Usage%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"text": "${gpuUsage}",
|
||||
"type": "TextBlock",
|
||||
"size": "large",
|
||||
"weight": "bolder"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%GPUUsage_Widget_Template/GPU_Temperature%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true,
|
||||
"horizontalAlignment": "right"
|
||||
},
|
||||
{
|
||||
"text": "${gpuTemp}",
|
||||
"type": "TextBlock",
|
||||
"size": "large",
|
||||
"weight": "bolder",
|
||||
"horizontalAlignment": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "%GPUUsage_Widget_Template/GPU_Name%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"text": "${gpuName}",
|
||||
"type": "TextBlock",
|
||||
"size": "medium"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.5"
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage != null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${errorMessage}",
|
||||
"wrap": true,
|
||||
"size": "small"
|
||||
}
|
||||
],
|
||||
"style": "warning"
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage == null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"url": "${memGraphUrl}",
|
||||
"height": "${chartHeight}",
|
||||
"width": "${chartWidth}",
|
||||
"$when": "${$host.widgetSize != \"small\"}",
|
||||
"horizontalAlignment": "center"
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%Memory_Widget_Template/UsedMemory%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"text": "${usedMem}",
|
||||
"type": "TextBlock",
|
||||
"size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
|
||||
"weight": "bolder"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%Memory_Widget_Template/AllMemory%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true,
|
||||
"horizontalAlignment": "right"
|
||||
},
|
||||
{
|
||||
"text": "${allMem}",
|
||||
"type": "TextBlock",
|
||||
"size": "${if($host.widgetSize == \"small\", \"medium\", \"large\")}",
|
||||
"weight": "bolder",
|
||||
"horizontalAlignment": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%Memory_Widget_Template/Committed%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"text": "${committedMem}/${committedLimitMem}",
|
||||
"type": "TextBlock",
|
||||
"size": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%Memory_Widget_Template/Cached%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true,
|
||||
"horizontalAlignment": "right"
|
||||
},
|
||||
{
|
||||
"text": "${cachedMem}",
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"horizontalAlignment": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"$when": "${$host.widgetSize == \"large\"}",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%Memory_Widget_Template/PagedPool%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"text": "${pagedPoolMem}",
|
||||
"type": "TextBlock",
|
||||
"size": "medium"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%Memory_Widget_Template/NonPagedPool%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true,
|
||||
"horizontalAlignment": "right"
|
||||
},
|
||||
{
|
||||
"text": "${nonPagedPoolMem}",
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"horizontalAlignment": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"$when": "${$host.widgetSize != \"small\"}",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%Memory_Widget_Template/MemoryUsage%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true,
|
||||
"horizontalAlignment": "right"
|
||||
},
|
||||
{
|
||||
"text": "${memUsage}",
|
||||
"type": "TextBlock",
|
||||
"size": "medium",
|
||||
"horizontalAlignment": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.5"
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage != null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "${errorMessage}",
|
||||
"wrap": true,
|
||||
"size": "small"
|
||||
}
|
||||
],
|
||||
"style": "warning"
|
||||
},
|
||||
{
|
||||
"type": "Container",
|
||||
"$when": "${errorMessage == null}",
|
||||
"items": [
|
||||
{
|
||||
"type": "Image",
|
||||
"url": "${netGraphUrl}",
|
||||
"height": "${chartHeight}",
|
||||
"width": "${chartWidth}",
|
||||
"$when": "${$host.widgetSize != \"small\"}",
|
||||
"horizontalAlignment": "center"
|
||||
},
|
||||
{
|
||||
"type": "ColumnSet",
|
||||
"columns": [
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%NetworkUsage_Widget_Template/Sent%",
|
||||
"type": "TextBlock",
|
||||
"spacing": "none",
|
||||
"size": "small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"text": "${netSent}",
|
||||
"type": "TextBlock",
|
||||
"size": "large",
|
||||
"weight": "bolder"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Column",
|
||||
"items": [
|
||||
{
|
||||
"text": "%NetworkUsage_Widget_Template/Received%",
|
||||
"type": "TextBlock",
|
||||
"spacing": "none",
|
||||
"size": "small",
|
||||
"isSubtle": true,
|
||||
"horizontalAlignment": "right"
|
||||
},
|
||||
{
|
||||
"text": "${netReceived}",
|
||||
"type": "TextBlock",
|
||||
"size": "large",
|
||||
"weight": "bolder",
|
||||
"horizontalAlignment": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "%NetworkUsage_Widget_Template/Network_Name%",
|
||||
"type": "TextBlock",
|
||||
"size": "small",
|
||||
"isSubtle": true
|
||||
},
|
||||
{
|
||||
"text": "${networkName}",
|
||||
"type": "TextBlock",
|
||||
"size": "medium"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"version": "1.5"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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
|
||||
|
||||
internal static IconInfo GpuIcon => new("\uE950"); // Component icon
|
||||
|
||||
internal static IconInfo NavigateBackwardIcon => new("\uE72B"); // Previous icon
|
||||
|
||||
internal static IconInfo NavigateForwardIcon => new("\uE72A"); // Next icon
|
||||
}
|
||||
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,58 @@
|
||||
<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>
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<None Update="DevHome\Templates\SystemCPUUsageTemplate.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DevHome\Templates\SystemGPUUsageTemplate.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DevHome\Templates\SystemMemoryTemplate.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="DevHome\Templates\SystemNetworkUsageTemplate.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1 @@
|
||||
GlobalMemoryStatusEx
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for creating ListPage's which can listen for when they're
|
||||
/// loaded and unloaded. This works because CmdPal will attach an event handler
|
||||
/// to the ItemsChanged event when the page is added to the UI, and remove it
|
||||
/// when the page is removed from the UI.
|
||||
///
|
||||
/// Subclasses should override the Loaded and Unloaded methods to start/stop
|
||||
/// any background work needed to populate the page.
|
||||
/// </summary>
|
||||
internal abstract partial class OnLoadStaticListPage : OnLoadBasePage, IListPage
|
||||
{
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
public virtual string PlaceholderText { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual string SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
|
||||
|
||||
public virtual bool ShowDetails { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual bool HasMoreItems { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IFilters? Filters { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IGridProperties? GridProperties { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual ICommandItem? EmptyContent { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public void LoadMore()
|
||||
{
|
||||
}
|
||||
|
||||
protected void SetSearchNoUpdate(string newSearchText)
|
||||
{
|
||||
_searchText = newSearchText;
|
||||
}
|
||||
|
||||
public abstract IListItem[] GetItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for creating ContentPage's which can listen for when they're
|
||||
/// loaded and unloaded. This works because CmdPal will attach an event handler
|
||||
/// to the ItemsChanged event when the page is added to the UI, and remove it
|
||||
/// when the page is removed from the UI.
|
||||
///
|
||||
/// Subclasses should override the Loaded and Unloaded methods to start/stop
|
||||
/// any background work needed to populate the page.
|
||||
/// </summary>
|
||||
internal abstract partial class OnLoadContentPage : OnLoadBasePage, IContentPage
|
||||
{
|
||||
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IContextItem[] Commands { get; set => SetProperty(ref field, value); } = [];
|
||||
|
||||
public abstract IContent[] GetContent();
|
||||
}
|
||||
|
||||
internal abstract partial class OnLoadBasePage : Page
|
||||
{
|
||||
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();
|
||||
|
||||
protected void RaiseItemsChanged(int totalItems = -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO #181 - This is the same thing that BaseObservable has to deal with.
|
||||
InternalItemsChanged?.Invoke(this, new ItemsChangedEventArgs(totalItems));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#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 PerformanceWidgetsPage(false);
|
||||
var band = new PerformanceWidgetsPage(true);
|
||||
_band = new CommandItem(band) { Title = DisplayName };
|
||||
_commands = [
|
||||
new CommandItem(page) { Title = DisplayName },
|
||||
];
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands()
|
||||
{
|
||||
return _commands;
|
||||
}
|
||||
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
return new ICommandItem[] { _band };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,927 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using CoreWidgetProvider.Helpers;
|
||||
using CoreWidgetProvider.Widgets.Enums;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
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 PerformanceWidgetsPage : OnLoadStaticListPage, IDisposable
|
||||
{
|
||||
public override string Id => "com.microsoft.cmdpal.performanceWidget";
|
||||
|
||||
public override string Title => Resources.GetResource("Performance_Monitor_Title");
|
||||
|
||||
public override IconInfo Icon => Icons.StackedAreaIcon;
|
||||
|
||||
private readonly bool _isBandPage;
|
||||
|
||||
private readonly SystemCPUUsageWidgetPage _cpuPage = new();
|
||||
private readonly ListItem _cpuItem;
|
||||
|
||||
private readonly SystemMemoryUsageWidgetPage _memoryPage = new();
|
||||
private readonly ListItem _memoryItem;
|
||||
|
||||
private readonly SystemNetworkUsageWidgetPage _networkPage = new();
|
||||
private readonly ListItem _networkItem;
|
||||
|
||||
private readonly SystemGPUUsageWidgetPage _gpuPage = new();
|
||||
private readonly ListItem _gpuItem;
|
||||
|
||||
// For bands, we want two bands, one for up and one for down
|
||||
private ListItem? _networkUpItem;
|
||||
private ListItem? _networkDownItem;
|
||||
private string _networkUpSpeed = string.Empty;
|
||||
private string _networkDownSpeed = string.Empty;
|
||||
|
||||
public PerformanceWidgetsPage(bool isBandPage = false)
|
||||
{
|
||||
_isBandPage = isBandPage;
|
||||
_cpuItem = new ListItem(_cpuPage)
|
||||
{
|
||||
Title = _cpuPage.GetItemTitle(isBandPage),
|
||||
MoreCommands = _cpuPage.Commands,
|
||||
};
|
||||
|
||||
_cpuPage.Updated += (s, e) =>
|
||||
{
|
||||
_cpuItem.Title = _cpuPage.GetItemTitle(isBandPage);
|
||||
};
|
||||
|
||||
_memoryItem = new ListItem(_memoryPage)
|
||||
{
|
||||
Title = _memoryPage.GetItemTitle(isBandPage),
|
||||
MoreCommands = _memoryPage.Commands,
|
||||
};
|
||||
|
||||
_memoryPage.Updated += (s, e) =>
|
||||
{
|
||||
_memoryItem.Title = _memoryPage.GetItemTitle(isBandPage);
|
||||
};
|
||||
|
||||
_networkItem = new ListItem(_networkPage)
|
||||
{
|
||||
Title = _networkPage.GetItemTitle(isBandPage),
|
||||
MoreCommands = _networkPage.Commands,
|
||||
};
|
||||
|
||||
_networkPage.Updated += (s, e) =>
|
||||
{
|
||||
_networkItem.Title = _networkPage.GetItemTitle(isBandPage);
|
||||
_networkUpSpeed = _networkPage.GetUpSpeed();
|
||||
_networkDownSpeed = _networkPage.GetDownSpeed();
|
||||
_networkDownItem?.Title = $"{_networkDownSpeed}";
|
||||
_networkUpItem?.Title = $"{_networkUpSpeed}";
|
||||
};
|
||||
|
||||
_gpuItem = new ListItem(_gpuPage)
|
||||
{
|
||||
Title = _gpuPage.GetItemTitle(isBandPage),
|
||||
MoreCommands = _gpuPage.Commands,
|
||||
};
|
||||
|
||||
_gpuPage.Updated += (s, e) =>
|
||||
{
|
||||
_gpuItem.Title = _gpuPage.GetItemTitle(isBandPage);
|
||||
};
|
||||
|
||||
if (_isBandPage)
|
||||
{
|
||||
// add subtitles to them all
|
||||
_cpuItem.Subtitle = Resources.GetResource("CPU_Usage_Subtitle");
|
||||
_memoryItem.Subtitle = Resources.GetResource("Memory_Usage_Subtitle");
|
||||
_networkItem.Subtitle = Resources.GetResource("Network_Usage_Subtitle");
|
||||
_gpuItem.Subtitle = Resources.GetResource("GPU_Usage_Subtitle");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Loaded()
|
||||
{
|
||||
_cpuPage.PushActivate();
|
||||
_memoryPage.PushActivate();
|
||||
_networkPage.PushActivate();
|
||||
_gpuPage.PushActivate();
|
||||
}
|
||||
|
||||
protected override void Unloaded()
|
||||
{
|
||||
_cpuPage.PopActivate();
|
||||
_memoryPage.PopActivate();
|
||||
_networkPage.PopActivate();
|
||||
_gpuPage.PopActivate();
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (!_isBandPage)
|
||||
{
|
||||
// TODO add details
|
||||
return new[] { _cpuItem, _memoryItem, _networkItem, _gpuItem };
|
||||
}
|
||||
else
|
||||
{
|
||||
_networkUpItem = new ListItem(_networkPage)
|
||||
{
|
||||
Title = $"{_networkUpSpeed}",
|
||||
Subtitle = Resources.GetResource("Network_Send_Subtitle"),
|
||||
MoreCommands = _networkPage.Commands,
|
||||
};
|
||||
|
||||
_networkDownItem = new ListItem(_networkPage)
|
||||
{
|
||||
Title = $"{_networkDownSpeed}",
|
||||
Subtitle = Resources.GetResource("Network_Receive_Subtitle"),
|
||||
MoreCommands = _networkPage.Commands,
|
||||
};
|
||||
|
||||
return new[] { _cpuItem, _memoryItem, _networkDownItem, _networkUpItem, _gpuItem };
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cpuPage.Dispose();
|
||||
_memoryPage.Dispose();
|
||||
_networkPage.Dispose();
|
||||
_gpuPage.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all the performance monitor widget pages.
|
||||
/// This handles common stuff like loading their widget JSON
|
||||
/// and updating it when needed.
|
||||
/// </summary>
|
||||
internal abstract partial class WidgetPage : OnLoadContentPage
|
||||
{
|
||||
internal event EventHandler? Updated;
|
||||
|
||||
protected Dictionary<string, string> ContentData { get; } = new();
|
||||
|
||||
protected WidgetPageState Page { get; set; } = WidgetPageState.Unknown;
|
||||
|
||||
protected Dictionary<WidgetPageState, string> Template { get; set; } = new();
|
||||
|
||||
protected JsonObject ContentDataJson
|
||||
{
|
||||
get
|
||||
{
|
||||
var json = new JsonObject();
|
||||
lock (ContentData)
|
||||
{
|
||||
foreach (var kvp in ContentData)
|
||||
{
|
||||
if (kvp.Value is not null)
|
||||
{
|
||||
json[kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly FormContent _formContent = new();
|
||||
|
||||
public void UpdateWidget()
|
||||
{
|
||||
lock (ContentData)
|
||||
{
|
||||
LoadContentData();
|
||||
}
|
||||
|
||||
_formContent.DataJson = ContentDataJson.ToJsonString();
|
||||
|
||||
Updated?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected abstract void LoadContentData();
|
||||
|
||||
protected abstract string GetTemplatePath(WidgetPageState page);
|
||||
|
||||
protected string GetTemplateForPage(WidgetPageState page)
|
||||
{
|
||||
if (Template.TryGetValue(page, out var value))
|
||||
{
|
||||
CoreLogger.LogDebug($"Using cached template for {page}");
|
||||
return value;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(Package.Current.EffectivePath, GetTemplatePath(page));
|
||||
var template = File.ReadAllText(path, Encoding.Default) ?? throw new FileNotFoundException(path);
|
||||
|
||||
template = Resources.ReplaceIdentifersFast(template, CoreLogger.Instance);
|
||||
CoreLogger.LogDebug($"Caching template for {page}");
|
||||
Template[page] = template;
|
||||
return template;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CoreLogger.LogError("Error getting template.", e);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public override IContent[] GetContent()
|
||||
{
|
||||
_formContent.TemplateJson = GetTemplateForPage(WidgetPageState.Content);
|
||||
|
||||
return [_formContent];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increment our tracker of how many pages have needed us active. This is a
|
||||
/// little wackier than just OnLoad/Unload. Both the ListPage for
|
||||
/// PerformanceWidgetsPage itself, AND the widget itself need the stats to
|
||||
/// be updating. So we use a counter to track how many "clients" need us
|
||||
/// active. When either is activated, we'll start updating. When both are
|
||||
/// removed, we'll stop updating.
|
||||
/// </summary>
|
||||
internal virtual void PushActivate()
|
||||
{
|
||||
_loadCount++;
|
||||
}
|
||||
|
||||
internal virtual void PopActivate()
|
||||
{
|
||||
_loadCount--;
|
||||
}
|
||||
|
||||
private int _loadCount;
|
||||
|
||||
protected bool IsActive => _loadCount > 0;
|
||||
|
||||
protected override void Loaded()
|
||||
{
|
||||
PushActivate();
|
||||
}
|
||||
|
||||
protected override void Unloaded()
|
||||
{
|
||||
PopActivate();
|
||||
}
|
||||
|
||||
internal static string FloatToPercentString(float value)
|
||||
{
|
||||
return ((int)(value * 100)).ToString(CultureInfo.InvariantCulture) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class SystemCPUUsageWidgetPage : WidgetPage, IDisposable
|
||||
{
|
||||
public override string Title => Resources.GetResource("CPU_Usage_Title");
|
||||
|
||||
public override string Id => "com.microsoft.cmdpal.cpu_widget";
|
||||
|
||||
public override IconInfo Icon => Icons.CpuIcon;
|
||||
|
||||
private readonly DataManager _dataManager;
|
||||
|
||||
public SystemCPUUsageWidgetPage()
|
||||
{
|
||||
_dataManager = new(DataType.CPU, () => UpdateWidget());
|
||||
Commands = [
|
||||
new CommandContextItem(OpenTaskManagerCommand.Instance),
|
||||
];
|
||||
}
|
||||
|
||||
protected override void LoadContentData()
|
||||
{
|
||||
// CoreLogger.LogDebug("Getting CPU stats");
|
||||
try
|
||||
{
|
||||
ContentData.Clear();
|
||||
|
||||
var timer = Stopwatch.StartNew();
|
||||
|
||||
var currentData = _dataManager.GetCPUStats();
|
||||
|
||||
var dataDuration = timer.ElapsedMilliseconds;
|
||||
|
||||
ContentData["cpuUsage"] = FloatToPercentString(currentData.CpuUsage);
|
||||
ContentData["cpuSpeed"] = SpeedToString(currentData.CpuSpeed);
|
||||
ContentData["cpuGraphUrl"] = currentData.CreateCPUImageUrl();
|
||||
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
|
||||
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
|
||||
|
||||
// ContentData["cpuProc1"] = currentData.GetCpuProcessText(0);
|
||||
// ContentData["cpuProc2"] = currentData.GetCpuProcessText(1);
|
||||
// ContentData["cpuProc3"] = currentData.GetCpuProcessText(2);
|
||||
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
|
||||
|
||||
// CoreLogger.LogDebug($"CPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
|
||||
// DataState = WidgetDataState.Okay;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Log.Error(e, "Error retrieving stats.");
|
||||
ContentData.Clear();
|
||||
ContentData["errorMessage"] = e.Message;
|
||||
|
||||
// ContentData = content.ToJsonString();
|
||||
// DataState = WidgetDataState.Failed;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetTemplatePath(WidgetPageState page)
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
WidgetPageState.Content => @"DevHome\Templates\SystemCPUUsageTemplate.json",
|
||||
WidgetPageState.Loading => @"DevHome\Templates\SystemCPUUsageTemplate.json",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public string GetItemTitle(bool isBandPage)
|
||||
{
|
||||
if (ContentData.TryGetValue("cpuUsage", out var usage))
|
||||
{
|
||||
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("CPU_Usage_Label"), usage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return isBandPage ? Resources.GetResource("CPU_Usage_Unknown") : Resources.GetResource("CPU_Usage_Unknown_Label");
|
||||
}
|
||||
}
|
||||
|
||||
private string SpeedToString(float cpuSpeed)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0.00} GHz", cpuSpeed / 1000);
|
||||
}
|
||||
|
||||
internal override void PushActivate()
|
||||
{
|
||||
base.PushActivate();
|
||||
if (IsActive)
|
||||
{
|
||||
_dataManager.Start();
|
||||
}
|
||||
}
|
||||
|
||||
internal override void PopActivate()
|
||||
{
|
||||
base.PopActivate();
|
||||
if (!IsActive)
|
||||
{
|
||||
_dataManager.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dataManager.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class SystemMemoryUsageWidgetPage : WidgetPage, IDisposable
|
||||
{
|
||||
public override string Id => "com.microsoft.cmdpal.memory_widget";
|
||||
|
||||
public override string Title => Resources.GetResource("Memory_Usage_Title");
|
||||
|
||||
public override IconInfo Icon => Icons.MemoryIcon;
|
||||
|
||||
private readonly DataManager _dataManager;
|
||||
|
||||
public SystemMemoryUsageWidgetPage()
|
||||
{
|
||||
_dataManager = new(DataType.Memory, () => UpdateWidget());
|
||||
Commands = [
|
||||
new CommandContextItem(OpenTaskManagerCommand.Instance),
|
||||
];
|
||||
}
|
||||
|
||||
protected override void LoadContentData()
|
||||
{
|
||||
// CoreLogger.LogDebug("Getting Memory stats");
|
||||
try
|
||||
{
|
||||
ContentData.Clear();
|
||||
|
||||
var timer = Stopwatch.StartNew();
|
||||
|
||||
var currentData = _dataManager.GetMemoryStats();
|
||||
|
||||
var dataDuration = timer.ElapsedMilliseconds;
|
||||
|
||||
ContentData["allMem"] = MemUlongToString(currentData.AllMem);
|
||||
ContentData["usedMem"] = MemUlongToString(currentData.UsedMem);
|
||||
ContentData["memUsage"] = FloatToPercentString(currentData.MemUsage);
|
||||
ContentData["committedMem"] = MemUlongToString(currentData.MemCommitted);
|
||||
ContentData["committedLimitMem"] = MemUlongToString(currentData.MemCommitLimit);
|
||||
ContentData["cachedMem"] = MemUlongToString(currentData.MemCached);
|
||||
ContentData["pagedPoolMem"] = MemUlongToString(currentData.MemPagedPool);
|
||||
ContentData["nonPagedPoolMem"] = MemUlongToString(currentData.MemNonPagedPool);
|
||||
ContentData["memGraphUrl"] = currentData.CreateMemImageUrl();
|
||||
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
|
||||
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
|
||||
|
||||
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
|
||||
|
||||
// CoreLogger.LogDebug($"Memory stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ContentData.Clear();
|
||||
ContentData["errorMessage"] = e.Message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetTemplatePath(WidgetPageState page)
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
WidgetPageState.Content => @"DevHome\Templates\SystemMemoryTemplate.json",
|
||||
WidgetPageState.Loading => @"DevHome\Templates\SystemMemoryTemplate.json",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public string GetItemTitle(bool isBandPage)
|
||||
{
|
||||
if (ContentData.TryGetValue("memUsage", out var usage))
|
||||
{
|
||||
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Memory_Usage_Label"), usage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return isBandPage ? Resources.GetResource("Memory_Usage_Unknown") : Resources.GetResource("Memory_Usage_Unknown_Label");
|
||||
}
|
||||
}
|
||||
|
||||
private string MemUlongToString(ulong memBytes)
|
||||
{
|
||||
if (memBytes < 1024)
|
||||
{
|
||||
return memBytes.ToString(CultureInfo.InvariantCulture) + " B";
|
||||
}
|
||||
|
||||
var memSize = memBytes / 1024.0;
|
||||
if (memSize < 1024)
|
||||
{
|
||||
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " kB";
|
||||
}
|
||||
|
||||
memSize /= 1024;
|
||||
if (memSize < 1024)
|
||||
{
|
||||
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " MB";
|
||||
}
|
||||
|
||||
memSize /= 1024;
|
||||
return memSize.ToString("0.00", CultureInfo.InvariantCulture) + " GB";
|
||||
}
|
||||
|
||||
internal override void PushActivate()
|
||||
{
|
||||
base.PushActivate();
|
||||
if (IsActive)
|
||||
{
|
||||
_dataManager.Start();
|
||||
}
|
||||
}
|
||||
|
||||
internal override void PopActivate()
|
||||
{
|
||||
base.PopActivate();
|
||||
if (!IsActive)
|
||||
{
|
||||
_dataManager.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dataManager.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class SystemNetworkUsageWidgetPage : WidgetPage, IDisposable
|
||||
{
|
||||
public override string Id => "com.microsoft.cmdpal.network_widget";
|
||||
|
||||
public override string Title => Resources.GetResource("Network_Usage_Title");
|
||||
|
||||
public override IconInfo Icon => Icons.NetworkIcon;
|
||||
|
||||
private readonly DataManager _dataManager;
|
||||
private int _networkIndex;
|
||||
|
||||
public SystemNetworkUsageWidgetPage()
|
||||
{
|
||||
_dataManager = new(DataType.Network, () => UpdateWidget());
|
||||
Commands = [
|
||||
new CommandContextItem(new PrevNetworkCommand(this) { Name = Resources.GetResource("Previous_Network_Title") }),
|
||||
new CommandContextItem(new NextNetworkCommand(this) { Name = Resources.GetResource("Next_Network_Title") }),
|
||||
new CommandContextItem(OpenTaskManagerCommand.Instance),
|
||||
];
|
||||
}
|
||||
|
||||
protected override void LoadContentData()
|
||||
{
|
||||
// CoreLogger.LogDebug("Getting Network stats");
|
||||
try
|
||||
{
|
||||
ContentData.Clear();
|
||||
|
||||
var timer = Stopwatch.StartNew();
|
||||
|
||||
var currentData = _dataManager.GetNetworkStats();
|
||||
|
||||
var dataDuration = timer.ElapsedMilliseconds;
|
||||
|
||||
var netName = currentData.GetNetworkName(_networkIndex);
|
||||
var networkStats = currentData.GetNetworkUsage(_networkIndex);
|
||||
|
||||
ContentData["networkUsage"] = FloatToPercentString(networkStats.Usage);
|
||||
ContentData["netSent"] = BytesToBitsPerSecString(networkStats.Sent);
|
||||
ContentData["netReceived"] = BytesToBitsPerSecString(networkStats.Received);
|
||||
ContentData["networkName"] = netName;
|
||||
ContentData["netGraphUrl"] = currentData.CreateNetImageUrl(_networkIndex);
|
||||
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
|
||||
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
|
||||
|
||||
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
|
||||
|
||||
// CoreLogger.LogDebug($"Network stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ContentData.Clear();
|
||||
ContentData["errorMessage"] = e.Message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetTemplatePath(WidgetPageState page)
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
WidgetPageState.Content => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
|
||||
WidgetPageState.Loading => @"DevHome\Templates\SystemNetworkUsageTemplate.json",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public string GetItemTitle(bool isBandPage)
|
||||
{
|
||||
if (ContentData.TryGetValue("networkName", out var name) && ContentData.TryGetValue("networkUsage", out var usage))
|
||||
{
|
||||
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("Network_Usage_Label"), name, usage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return isBandPage ? Resources.GetResource("Network_Usage_Unknown") : Resources.GetResource("Network_Usage_Unknown_Label");
|
||||
}
|
||||
}
|
||||
|
||||
// up/down speed is always used for bands
|
||||
public string GetUpSpeed()
|
||||
{
|
||||
if (ContentData.TryGetValue("netSent", out var upSpeed))
|
||||
{
|
||||
return upSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "???";
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDownSpeed()
|
||||
{
|
||||
if (ContentData.TryGetValue("netReceived", out var downSpeed))
|
||||
{
|
||||
return downSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "???";
|
||||
}
|
||||
}
|
||||
|
||||
private string BytesToBitsPerSecString(float value)
|
||||
{
|
||||
// Bytes to bits
|
||||
value *= 8;
|
||||
|
||||
// bits to Kbits
|
||||
value /= 1024;
|
||||
if (value < 1024)
|
||||
{
|
||||
if (value < 100)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Kbps", value);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0} Kbps", value);
|
||||
}
|
||||
|
||||
// Kbits to Mbits
|
||||
value /= 1024;
|
||||
if (value < 1024)
|
||||
{
|
||||
if (value < 100)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Mbps", value);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0} Mbps", value);
|
||||
}
|
||||
|
||||
// Mbits to Gbits
|
||||
value /= 1024;
|
||||
if (value < 100)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0.0} Gbps", value);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:0} Gbps", value);
|
||||
}
|
||||
|
||||
internal override void PushActivate()
|
||||
{
|
||||
base.PushActivate();
|
||||
if (IsActive)
|
||||
{
|
||||
_dataManager.Start();
|
||||
}
|
||||
}
|
||||
|
||||
internal override void PopActivate()
|
||||
{
|
||||
base.PopActivate();
|
||||
if (!IsActive)
|
||||
{
|
||||
_dataManager.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePrevNetwork()
|
||||
{
|
||||
_networkIndex = _dataManager.GetNetworkStats().GetPrevNetworkIndex(_networkIndex);
|
||||
UpdateWidget();
|
||||
}
|
||||
|
||||
private void HandleNextNetwork()
|
||||
{
|
||||
_networkIndex = _dataManager.GetNetworkStats().GetNextNetworkIndex(_networkIndex);
|
||||
UpdateWidget();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dataManager.Dispose();
|
||||
}
|
||||
|
||||
private sealed partial class PrevNetworkCommand : InvokableCommand
|
||||
{
|
||||
private readonly SystemNetworkUsageWidgetPage _page;
|
||||
|
||||
public PrevNetworkCommand(SystemNetworkUsageWidgetPage page)
|
||||
{
|
||||
_page = page;
|
||||
}
|
||||
|
||||
public override string Id => "com.microsoft.cmdpal.network_widget.prev";
|
||||
|
||||
public override IconInfo Icon => Icons.NavigateBackwardIcon;
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
_page.HandlePrevNetwork();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed partial class NextNetworkCommand : InvokableCommand
|
||||
{
|
||||
private readonly SystemNetworkUsageWidgetPage _page;
|
||||
|
||||
public NextNetworkCommand(SystemNetworkUsageWidgetPage page)
|
||||
{
|
||||
_page = page;
|
||||
}
|
||||
|
||||
public override string Id => "com.microsoft.cmdpal.network_widget.next";
|
||||
|
||||
public override IconInfo Icon => Icons.NavigateForwardIcon;
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
_page.HandleNextNetwork();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class SystemGPUUsageWidgetPage : WidgetPage, IDisposable
|
||||
{
|
||||
public override string Id => "com.microsoft.cmdpal.gpu_widget";
|
||||
|
||||
public override string Title => Resources.GetResource("GPU_Usage_Title");
|
||||
|
||||
public override IconInfo Icon => Icons.GpuIcon;
|
||||
|
||||
private readonly DataManager _dataManager;
|
||||
private readonly string _gpuActiveEngType = "3D";
|
||||
private int _gpuActiveIndex;
|
||||
|
||||
public SystemGPUUsageWidgetPage()
|
||||
{
|
||||
_dataManager = new(DataType.GPU, () => UpdateWidget());
|
||||
|
||||
Commands = [
|
||||
new CommandContextItem(new PrevGPUCommand(this) { Name = Resources.GetResource("Previous_GPU_Title") }),
|
||||
new CommandContextItem(new NextGPUCommand(this) { Name = Resources.GetResource("Next_GPU_Title") }),
|
||||
new CommandContextItem(OpenTaskManagerCommand.Instance),
|
||||
];
|
||||
}
|
||||
|
||||
protected override void LoadContentData()
|
||||
{
|
||||
// CoreLogger.LogDebug("Getting GPU stats");
|
||||
try
|
||||
{
|
||||
ContentData.Clear();
|
||||
|
||||
var timer = Stopwatch.StartNew();
|
||||
|
||||
var stats = _dataManager.GetGPUStats();
|
||||
|
||||
var dataDuration = timer.ElapsedMilliseconds;
|
||||
|
||||
var gpuName = stats.GetGPUName(_gpuActiveIndex);
|
||||
|
||||
ContentData["gpuUsage"] = FloatToPercentString(stats.GetGPUUsage(_gpuActiveIndex, _gpuActiveEngType));
|
||||
ContentData["gpuName"] = gpuName;
|
||||
ContentData["gpuTemp"] = stats.GetGPUTemperature(_gpuActiveIndex);
|
||||
ContentData["gpuGraphUrl"] = stats.CreateGPUImageUrl(_gpuActiveIndex);
|
||||
ContentData["chartHeight"] = ChartHelper.ChartHeight + "px";
|
||||
ContentData["chartWidth"] = ChartHelper.ChartWidth + "px";
|
||||
|
||||
var contentDuration = timer.ElapsedMilliseconds - dataDuration;
|
||||
|
||||
// CoreLogger.LogDebug($"GPU stats retrieved in {dataDuration} ms, content prepared in {contentDuration} ms. (Total {timer.ElapsedMilliseconds} ms)");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ContentData.Clear();
|
||||
ContentData["errorMessage"] = e.Message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetTemplatePath(WidgetPageState page)
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
WidgetPageState.Content => @"DevHome\Templates\SystemGPUUsageTemplate.json",
|
||||
WidgetPageState.Loading => @"DevHome\Templates\SystemGPUUsageTemplate.json",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public string GetItemTitle(bool isBandPage)
|
||||
{
|
||||
if (ContentData.TryGetValue("gpuName", out var name) && ContentData.TryGetValue("gpuUsage", out var usage))
|
||||
{
|
||||
return isBandPage ? usage : string.Format(CultureInfo.CurrentCulture, Resources.GetResource("GPU_Usage_Label"), name, usage);
|
||||
}
|
||||
else
|
||||
{
|
||||
return isBandPage ? Resources.GetResource("GPU_Usage_Unknown") : Resources.GetResource("GPU_Usage_Unknown_Label");
|
||||
}
|
||||
}
|
||||
|
||||
internal override void PushActivate()
|
||||
{
|
||||
base.PushActivate();
|
||||
if (IsActive)
|
||||
{
|
||||
_dataManager.Start();
|
||||
}
|
||||
}
|
||||
|
||||
internal override void PopActivate()
|
||||
{
|
||||
base.PopActivate();
|
||||
if (!IsActive)
|
||||
{
|
||||
_dataManager.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePrevGPU()
|
||||
{
|
||||
_gpuActiveIndex = _dataManager.GetGPUStats().GetPrevGPUIndex(_gpuActiveIndex);
|
||||
UpdateWidget();
|
||||
}
|
||||
|
||||
private void HandleNextGPU()
|
||||
{
|
||||
_gpuActiveIndex = _dataManager.GetGPUStats().GetNextGPUIndex(_gpuActiveIndex);
|
||||
UpdateWidget();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dataManager.Dispose();
|
||||
}
|
||||
|
||||
private sealed partial class PrevGPUCommand : InvokableCommand
|
||||
{
|
||||
private readonly SystemGPUUsageWidgetPage _page;
|
||||
|
||||
public PrevGPUCommand(SystemGPUUsageWidgetPage page)
|
||||
{
|
||||
_page = page;
|
||||
}
|
||||
|
||||
public override string Id => "com.microsoft.cmdpal.gpu_widget.prev";
|
||||
|
||||
public override IconInfo Icon => Icons.NavigateBackwardIcon;
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
_page.HandlePrevGPU();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed partial class NextGPUCommand : InvokableCommand
|
||||
{
|
||||
private readonly SystemGPUUsageWidgetPage _page;
|
||||
|
||||
public NextGPUCommand(SystemGPUUsageWidgetPage page)
|
||||
{
|
||||
_page = page;
|
||||
}
|
||||
|
||||
public override string Id => "com.microsoft.cmdpal.gpu_widget.next";
|
||||
|
||||
public override IconInfo Icon => Icons.NavigateForwardIcon;
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
_page.HandleNextGPU();
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed partial class OpenTaskManagerCommand : InvokableCommand
|
||||
{
|
||||
internal static readonly OpenTaskManagerCommand Instance = new();
|
||||
|
||||
public override string Id => "com.microsoft.cmdpal.open_task_manager";
|
||||
|
||||
public override IconInfo Icon => Icons.StackedAreaIcon; // StackedAreaIcon looks like task manager's icon
|
||||
|
||||
public override string Name => Resources.GetResource("Open_Task_Manager_Title");
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "taskmgr.exe",
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CoreLogger.LogError("Error launching Task Manager.", e);
|
||||
}
|
||||
|
||||
return CommandResult.Hide();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user