mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-31 09:27:03 +01:00
Compare commits
34 Commits
leilzh/fuz
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7efbd2f013 | ||
|
|
ba230eca07 | ||
|
|
30df5e0df2 | ||
|
|
9a6c64f9c0 | ||
|
|
7dc2a05c45 | ||
|
|
26fe36ab8d | ||
|
|
06b56a10bd | ||
|
|
fc804a8156 | ||
|
|
f63fcfd91c | ||
|
|
195ff24a85 | ||
|
|
5691c5754b | ||
|
|
100d560f9e | ||
|
|
25c29ade8e | ||
|
|
8dfa55fe28 | ||
|
|
6c317c4ee1 | ||
|
|
54e058e82d | ||
|
|
b0e7473760 | ||
|
|
f085ba0cd2 | ||
|
|
884bfc71d3 | ||
|
|
252cf2670f | ||
|
|
4be6129835 | ||
|
|
583614449d | ||
|
|
d4e577bb81 | ||
|
|
e1ad7e39c6 | ||
|
|
e8b02cd797 | ||
|
|
232e1b79bd | ||
|
|
5ec2728dea | ||
|
|
d314fa075e | ||
|
|
90723d5b12 | ||
|
|
f2a5505601 | ||
|
|
311ab88ec3 | ||
|
|
c04400e7df | ||
|
|
05218e8af6 | ||
|
|
6cf73ce839 |
2
.github/actions/spell-check/allow/code.txt
vendored
2
.github/actions/spell-check/allow/code.txt
vendored
@@ -273,4 +273,4 @@ mengyuanchen
|
||||
testhost
|
||||
|
||||
#Tools
|
||||
OIP
|
||||
OIP
|
||||
20
.github/actions/spell-check/expect.txt
vendored
20
.github/actions/spell-check/expect.txt
vendored
@@ -8,6 +8,7 @@ Acceleratorkeys
|
||||
ACCEPTFILES
|
||||
ACCESSDENIED
|
||||
ACCESSTOKEN
|
||||
acfs
|
||||
AClient
|
||||
AColumn
|
||||
acrt
|
||||
@@ -197,6 +198,7 @@ CLIPBOARDUPDATE
|
||||
CLIPCHILDREN
|
||||
CLIPSIBLINGS
|
||||
closesocket
|
||||
clp
|
||||
CLSCTX
|
||||
clsids
|
||||
Clusion
|
||||
@@ -273,6 +275,7 @@ CURSORINFO
|
||||
cursorpos
|
||||
customaction
|
||||
CUSTOMACTIONTEST
|
||||
CUSTOMFORMATPLACEHOLDER
|
||||
CVal
|
||||
cvd
|
||||
CVirtual
|
||||
@@ -523,6 +526,7 @@ FZE
|
||||
gacutil
|
||||
Gaeilge
|
||||
Gaidhlig
|
||||
gameid
|
||||
GC'ed
|
||||
GCLP
|
||||
gdi
|
||||
@@ -713,6 +717,7 @@ INPUTSINK
|
||||
INPUTTYPE
|
||||
INSTALLDESKTOPSHORTCUT
|
||||
INSTALLDIR
|
||||
installdir
|
||||
INSTALLFOLDER
|
||||
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
|
||||
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
|
||||
@@ -1007,6 +1012,7 @@ netsh
|
||||
newcolor
|
||||
NEWDIALOGSTYLE
|
||||
NEWFILE
|
||||
NEWFILEHEADER
|
||||
newitem
|
||||
newpath
|
||||
newplus
|
||||
@@ -1040,6 +1046,7 @@ NOINHERITLAYOUT
|
||||
NOINTERFACE
|
||||
NOINVERT
|
||||
NOLINKINFO
|
||||
nologo
|
||||
NOMCX
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
@@ -1272,6 +1279,7 @@ pstm
|
||||
PStr
|
||||
pstream
|
||||
pstrm
|
||||
pswd
|
||||
PSYSTEM
|
||||
psz
|
||||
ptb
|
||||
@@ -1302,6 +1310,7 @@ QUNS
|
||||
QXZ
|
||||
RAII
|
||||
RAlt
|
||||
Rappl
|
||||
randi
|
||||
Rasterization
|
||||
Rasterize
|
||||
@@ -1417,6 +1426,7 @@ searchterm
|
||||
SEARCHUI
|
||||
SECONDARYDISPLAY
|
||||
secpol
|
||||
securestring
|
||||
SEEMASKINVOKEIDLIST
|
||||
SELCHANGE
|
||||
SENDCHANGE
|
||||
@@ -1570,6 +1580,7 @@ stdcpp
|
||||
stdcpplatest
|
||||
STDMETHODCALLTYPE
|
||||
STDMETHODIMP
|
||||
steamapps
|
||||
STGC
|
||||
STGM
|
||||
STGMEDIUM
|
||||
@@ -1651,6 +1662,7 @@ telephon
|
||||
templatenamespace
|
||||
testprocess
|
||||
TEXCOORD
|
||||
TEXTBOXNEWLINE
|
||||
TEXTEXTRACTOR
|
||||
TEXTINCLUDE
|
||||
tfopen
|
||||
@@ -1971,3 +1983,11 @@ ZOOMITX
|
||||
ZXk
|
||||
ZXNs
|
||||
zzz
|
||||
ACIE
|
||||
AOklab
|
||||
BCIE
|
||||
BOklab
|
||||
culori
|
||||
Evercoder
|
||||
LCh
|
||||
CIELCh
|
||||
|
||||
@@ -220,6 +220,7 @@
|
||||
"WinUI3Apps\\PowerToys.Settings.exe",
|
||||
|
||||
"PowerToys.CmdPalModuleInterface.dll",
|
||||
"CmdPalKeyboardService.dll",
|
||||
"*Microsoft.CmdPal.UI_*.msix"
|
||||
],
|
||||
"SigningInfo": {
|
||||
@@ -330,6 +331,8 @@
|
||||
"TestableIO.System.IO.Abstractions.Wrappers.dll",
|
||||
"WinUI3Apps\\TestableIO.System.IO.Abstractions.Wrappers.dll",
|
||||
"WinUI3Apps\\OpenAI.dll",
|
||||
"Testably.Abstractions.FileSystem.Interface.dll",
|
||||
"WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll",
|
||||
"ColorCode.Core.dll",
|
||||
"ColorCode.UWP.dll",
|
||||
"UnitsNet.dll",
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
Param(
|
||||
# Using the default value of 1.6 for winAppSdkVersionNumber and useExperimentalVersion as false
|
||||
# Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
|
||||
[Parameter(Mandatory=$False,Position=1)]
|
||||
[string]$winAppSdkVersionNumber = "1.6",
|
||||
[string]$winAppSdkVersionNumber = "1.7",
|
||||
|
||||
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
|
||||
[Parameter(Mandatory=$False,Position=2)]
|
||||
[boolean]$useExperimentalVersion = $False
|
||||
[boolean]$useExperimentalVersion = $False,
|
||||
|
||||
# Root folder Path for processing
|
||||
[Parameter(Mandatory=$False,Position=3)]
|
||||
[string]$rootPath = $(Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
|
||||
|
||||
# Root folder Path for processing
|
||||
[Parameter(Mandatory=$False,Position=4)]
|
||||
[string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
)
|
||||
|
||||
function Update-NugetConfig {
|
||||
param (
|
||||
[string]$filePath = "nuget.config"
|
||||
[string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
|
||||
)
|
||||
|
||||
Write-Host "Updating nuget.config file"
|
||||
@@ -35,7 +43,33 @@ function Update-NugetConfig {
|
||||
$xml.Save($filePath)
|
||||
}
|
||||
|
||||
$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
|
||||
function Read-FileWithEncoding {
|
||||
param (
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$reader = New-Object System.IO.StreamReader($Path, $true) # auto-detect encoding
|
||||
$content = $reader.ReadToEnd()
|
||||
$encoding = $reader.CurrentEncoding
|
||||
$reader.Close()
|
||||
|
||||
return [PSCustomObject]@{
|
||||
Content = $content
|
||||
Encoding = $encoding
|
||||
}
|
||||
}
|
||||
|
||||
function Write-FileWithEncoding {
|
||||
param (
|
||||
[string]$Path,
|
||||
[string]$Content,
|
||||
[System.Text.Encoding]$Encoding
|
||||
)
|
||||
|
||||
$writer = New-Object System.IO.StreamWriter($Path, $false, $Encoding)
|
||||
$writer.Write($Content)
|
||||
$writer.Close()
|
||||
}
|
||||
|
||||
# Execute nuget list and capture the output
|
||||
if ($useExperimentalVersion) {
|
||||
@@ -79,50 +113,54 @@ if ($latestVersion) {
|
||||
}
|
||||
|
||||
# Update packages.config files
|
||||
Get-ChildItem -Recurse packages.config | ForEach-Object {
|
||||
$content = Get-Content $_.FullName -Raw
|
||||
Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $_.FullName -Value $content
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update Directory.Packages.props file
|
||||
$propsFile = "Directory.Packages.props"
|
||||
$propsFile = [System.IO.Path]::Combine($rootPath,"Directory.Packages.props")
|
||||
if (Test-Path $propsFile) {
|
||||
$content = Get-Content $propsFile -Raw
|
||||
$file = Read-FileWithEncoding -Path $propsFile
|
||||
$content = $file.Content
|
||||
if ($content -match '<PackageVersion Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="' + $WinAppSDKVersion + '" />'
|
||||
$oldVersionString = '<PackageVersion Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*" />'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $propsFile -Value $content
|
||||
Write-FileWithEncoding -Path $propsFile -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $propsFile
|
||||
}
|
||||
}
|
||||
|
||||
# Update .vcxproj files
|
||||
Get-ChildItem -Recurse *.vcxproj | ForEach-Object {
|
||||
$content = Get-Content $_.FullName -Raw
|
||||
Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match '\\Microsoft.WindowsAppSDK.') {
|
||||
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion + '\'
|
||||
$oldVersionString = '\\Microsoft.WindowsAppSDK.[-.0-9a-zA-Z]*\\'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $_.FullName -Value $content
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
# Update .csproj files
|
||||
Get-ChildItem -Recurse *.csproj | ForEach-Object {
|
||||
$content = Get-Content $_.FullName -Raw
|
||||
Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
|
||||
$file = Read-FileWithEncoding -Path $_.FullName
|
||||
$content = $file.Content
|
||||
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
|
||||
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
|
||||
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
|
||||
$content = $content -replace $oldVersionString, $newVersionString
|
||||
Set-Content -Path $_.FullName -Value $content
|
||||
Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
|
||||
Write-Host "Modified " $_.FullName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"notificationAliases": ["powertoys@microsoft.com"],
|
||||
"instanceUrl": "https://microsoft.visualstudio.com",
|
||||
"projectName": "OS",
|
||||
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys"
|
||||
"areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\PowerToys"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.6
|
||||
default: 1.7
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -148,5 +148,7 @@ extends:
|
||||
parameters:
|
||||
versionNumber: ${{ parameters.versionNumber }}
|
||||
includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }}
|
||||
${{ if ne(parameters.publishSymbolsToPublic, true) }}:
|
||||
symbolExpiryTime: 10 # For private builds, expire symbols within 10 days. The default is 100 years.
|
||||
subscription: $(SymbolPublishingServiceConnection)
|
||||
symbolProject: $(SymbolPublishingProject)
|
||||
|
||||
@@ -17,6 +17,7 @@ steps:
|
||||
arguments: >
|
||||
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
|
||||
- script: echo $(WinAppSDKVersion)
|
||||
displayName: 'Display WinAppSDK Version Found'
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.120-preview" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
|
||||
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->
|
||||
@@ -55,7 +55,7 @@
|
||||
-->
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
|
||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
|
||||
|
||||
38
NOTICE.md
38
NOTICE.md
@@ -75,6 +75,40 @@ OTHER DEALINGS IN THE SOFTWARE.
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
```
|
||||
|
||||
## Utility: Command Palette Built-in Extensions
|
||||
|
||||
### Calculator
|
||||
|
||||
#### Mages
|
||||
|
||||
We use the Mages NuGet package for calculating the result of expression.
|
||||
|
||||
**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages)
|
||||
|
||||
```
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 - 2025 Florian Rappl
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
## Utility: File Explorer Add-ins
|
||||
|
||||
### Monaco Editor
|
||||
@@ -1438,8 +1472,8 @@ SOFTWARE.
|
||||
- Microsoft.Windows.CsWin32 0.2.46-beta
|
||||
- Microsoft.Windows.CsWinRT 2.2.0
|
||||
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
|
||||
- Microsoft.WindowsAppSDK 1.6.250205002
|
||||
- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview
|
||||
- Microsoft.WindowsAppSDK 1.7.250401001
|
||||
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
|
||||
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
|
||||
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
|
||||
- ModernWpfUI 0.9.4
|
||||
|
||||
@@ -79,10 +79,7 @@
|
||||
<ComponentGroupRef Id="ToolComponentGroup" />
|
||||
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
|
||||
<ComponentGroupRef Id="WorkspacesComponentGroup" />
|
||||
|
||||
<?if $(var.CIBuild) = "true" ?>
|
||||
<ComponentGroupRef Id="CmdPalComponentGroup" />
|
||||
<?endif?>
|
||||
<ComponentGroupRef Id="CmdPalComponentGroup" />
|
||||
</Feature>
|
||||
|
||||
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Some items may be set in Directory.Build.props in root -->
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project=".\Common.Dotnet.PrepareGeneratedFolder.targets" />
|
||||
|
||||
<PropertyGroup>
|
||||
<WindowsSdkPackageVersion>10.0.22621.57</WindowsSdkPackageVersion>
|
||||
<TargetFramework>net9.0-windows10.0.22621.0</TargetFramework>
|
||||
|
||||
16
src/Common.Dotnet.PrepareGeneratedFolder.targets
Normal file
16
src/Common.Dotnet.PrepareGeneratedFolder.targets
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<Target Name="EnsureGeneratedBaseFolder" BeforeTargets="XamlPreCompile">
|
||||
<PropertyGroup>
|
||||
<!-- Only create the base 'generated' folder -->
|
||||
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Create 'generated' folder if missing -->
|
||||
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
|
||||
|
||||
<!-- Optional logging for debugging -->
|
||||
<Message Text="Ensured: $(GeneratedBasePath)" Importance="Low" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -141,6 +141,40 @@ namespace ManagedCommon
|
||||
return lab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a Oklab color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> to convert</param>
|
||||
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
|
||||
public static (double Lightness, double ChromaticityA, double ChromaticityB) ConvertToOklabColor(Color color)
|
||||
{
|
||||
var linear = ConvertSRGBToLinearRGB(color.R / 255d, color.G / 255d, color.B / 255d);
|
||||
var oklab = GetOklabColorFromLinearRGB(linear.R, linear.G, linear.B);
|
||||
return oklab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a Oklch color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> to convert</param>
|
||||
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
|
||||
public static (double Lightness, double Chroma, double Hue) ConvertToOklchColor(Color color)
|
||||
{
|
||||
var oklab = ConvertToOklabColor(color);
|
||||
var oklch = GetOklchColorFromOklab(oklab.Lightness, oklab.ChromaticityA, oklab.ChromaticityB);
|
||||
|
||||
return oklch;
|
||||
}
|
||||
|
||||
public static (double R, double G, double B) ConvertSRGBToLinearRGB(double r, double g, double b)
|
||||
{
|
||||
// inverse companding, gamma correction must be undone
|
||||
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
|
||||
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
|
||||
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
|
||||
return (rLinear, gLinear, bLinear);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a CIE XYZ color (XYZ)
|
||||
/// The constants of the formula matches this Wikipedia page, but at a higher precision:
|
||||
@@ -156,10 +190,7 @@ namespace ManagedCommon
|
||||
double g = color.G / 255d;
|
||||
double b = color.B / 255d;
|
||||
|
||||
// inverse companding, gamma correction must be undone
|
||||
double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
|
||||
double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
|
||||
double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
|
||||
(double rLinear, double gLinear, double bLinear) = ConvertSRGBToLinearRGB(r, g, b);
|
||||
|
||||
return (
|
||||
(rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429),
|
||||
@@ -210,6 +241,63 @@ namespace ManagedCommon
|
||||
return (l, a, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a linear RGB color <see cref="double"/> to an Oklab color.
|
||||
/// The constants of this formula come from https://github.com/Evercoder/culori/blob/2bedb8f0507116e75f844a705d0b45cf279b15d0/src/oklab/convertLrgbToOklab.js
|
||||
/// and the implementation is based on https://bottosson.github.io/posts/oklab/
|
||||
/// </summary>
|
||||
/// <param name="r">Linear R value</param>
|
||||
/// <param name="g">Linear G value</param>
|
||||
/// <param name="b">Linear B value</param>
|
||||
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
|
||||
private static (double Lightness, double ChromaticityA, double ChromaticityB)
|
||||
GetOklabColorFromLinearRGB(double r, double g, double b)
|
||||
{
|
||||
double l = (0.41222147079999993 * r) + (0.5363325363 * g) + (0.0514459929 * b);
|
||||
double m = (0.2119034981999999 * r) + (0.6806995450999999 * g) + (0.1073969566 * b);
|
||||
double s = (0.08830246189999998 * r) + (0.2817188376 * g) + (0.6299787005000002 * b);
|
||||
|
||||
double l_ = Math.Cbrt(l);
|
||||
double m_ = Math.Cbrt(m);
|
||||
double s_ = Math.Cbrt(s);
|
||||
|
||||
return (
|
||||
(0.2104542553 * l_) + (0.793617785 * m_) - (0.0040720468 * s_),
|
||||
(1.9779984951 * l_) - (2.428592205 * m_) + (0.4505937099 * s_),
|
||||
(0.0259040371 * l_) + (0.7827717662 * m_) - (0.808675766 * s_)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an Oklab color <see cref="double"/> from Cartesian form to its polar form Oklch
|
||||
/// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
|
||||
/// </summary>
|
||||
/// <param name="lightness">The <see cref="lightness"/></param>
|
||||
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
|
||||
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
|
||||
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
|
||||
private static (double Lightness, double Chroma, double Hue)
|
||||
GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
|
||||
{
|
||||
return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a color in Cartesian form (Lab) to its polar form (LCh)
|
||||
/// </summary>
|
||||
/// <param name="lightness">The <see cref="lightness"/></param>
|
||||
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
|
||||
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
|
||||
/// <returns>The lightness, chroma, and hue angle</returns>
|
||||
private static (double Lightness, double Chroma, double Hue)
|
||||
GetLCHColorFromLAB(double lightness, double chromaticity_a, double chromaticity_b)
|
||||
{
|
||||
// Lab to LCh transformation
|
||||
double chroma = Math.Sqrt(Math.Pow(chromaticity_a, 2) + Math.Pow(chromaticity_b, 2));
|
||||
double hue = Math.Round(chroma, 3) == 0 ? 0.0 : ((Math.Atan2(chromaticity_b, chromaticity_a) * 180d / Math.PI) + 360d) % 360d;
|
||||
return (lightness, chroma, hue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a natural color (hue, whiteness, blackness)
|
||||
/// </summary>
|
||||
@@ -276,12 +364,17 @@ namespace ManagedCommon
|
||||
{ "Br", 'p' }, // brightness percent
|
||||
{ "In", 'p' }, // intensity percent
|
||||
{ "Ll", 'p' }, // lightness (HSL) percent
|
||||
{ "Lc", 'p' }, // lightness(CIELAB)percent
|
||||
{ "Va", 'p' }, // value percent
|
||||
{ "Wh", 'p' }, // whiteness percent
|
||||
{ "Bn", 'p' }, // blackness percent
|
||||
{ "Ca", 'p' }, // chromaticityA percent
|
||||
{ "Cb", 'p' }, // chromaticityB percent
|
||||
{ "Lc", 'p' }, // lightness (CIE) percent
|
||||
{ "Ca", 'p' }, // chromaticityA (CIELAB) percent
|
||||
{ "Cb", 'p' }, // chromaticityB (CIELAB) percent
|
||||
{ "Lo", 'p' }, // lightness (Oklab/Oklch) percent
|
||||
{ "Oa", 'p' }, // chromaticityA (Oklab) percent
|
||||
{ "Ob", 'p' }, // chromaticityB (Oklab) percent
|
||||
{ "Oc", 'p' }, // chroma (Oklch) percent
|
||||
{ "Oh", 'p' }, // hue angle (Oklch) percent
|
||||
{ "Xv", 'i' }, // X value int
|
||||
{ "Yv", 'i' }, // Y value int
|
||||
{ "Zv", 'i' }, // Z value int
|
||||
@@ -424,6 +517,10 @@ namespace ManagedCommon
|
||||
var (lightnessC, _, _) = ConvertToCIELABColor(color);
|
||||
lightnessC = Math.Round(lightnessC, 2);
|
||||
return lightnessC.ToString(CultureInfo.InvariantCulture);
|
||||
case "Lo":
|
||||
var (lightnessO, _, _) = ConvertToOklabColor(color);
|
||||
lightnessO = Math.Round(lightnessO, 2);
|
||||
return lightnessO.ToString(CultureInfo.InvariantCulture);
|
||||
case "Wh":
|
||||
var (_, whiteness, _) = ConvertToHWBColor(color);
|
||||
whiteness = Math.Round(whiteness * 100);
|
||||
@@ -440,6 +537,22 @@ namespace ManagedCommon
|
||||
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
|
||||
chromaticityB = Math.Round(chromaticityB, 2);
|
||||
return chromaticityB.ToString(CultureInfo.InvariantCulture);
|
||||
case "Oa":
|
||||
var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
|
||||
chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
|
||||
return chromaticityAOklab.ToString(CultureInfo.InvariantCulture);
|
||||
case "Ob":
|
||||
var (_, _, chromaticityBOklab) = ConvertToOklabColor(color);
|
||||
chromaticityBOklab = Math.Round(chromaticityBOklab, 2);
|
||||
return chromaticityBOklab.ToString(CultureInfo.InvariantCulture);
|
||||
case "Oc":
|
||||
var (_, chromaOklch, _) = ConvertToOklchColor(color);
|
||||
chromaOklch = Math.Round(chromaOklch, 2);
|
||||
return chromaOklch.ToString(CultureInfo.InvariantCulture);
|
||||
case "Oh":
|
||||
var (_, _, hueOklch) = ConvertToOklchColor(color);
|
||||
hueOklch = Math.Round(hueOklch, 2);
|
||||
return hueOklch.ToString(CultureInfo.InvariantCulture);
|
||||
case "Xv":
|
||||
var (x, _, _) = ConvertToCIEXYZColor(color);
|
||||
x = Math.Round(x * 100, 4);
|
||||
@@ -495,8 +608,10 @@ namespace ManagedCommon
|
||||
case "HSI": return "hsi(%Hu, %Si%, %In%)";
|
||||
case "HWB": return "hwb(%Hu, %Wh%, %Bn%)";
|
||||
case "NCol": return "%Hn, %Wh%, %Bn%";
|
||||
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
|
||||
case "CIEXYZ": return "XYZ(%Xv, %Yv, %Zv)";
|
||||
case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
|
||||
case "Oklab": return "oklab(%Lo, %Oa, %Ob)";
|
||||
case "Oklch": return "oklch(%Lo, %Oc, %Oh)";
|
||||
case "VEC4": return "(%Reff, %Grff, %Blff, 1f)";
|
||||
case "Decimal": return "%Dv";
|
||||
case "HEX Int": return "0xFF%ReX%GrX%BlX";
|
||||
|
||||
51
src/common/ManagedCommon/IdRecoveryHelper.cs
Normal file
51
src/common/ManagedCommon/IdRecoveryHelper.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
public static class IdRecoveryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Fixes invalid IDs in the given list by assigning unique values.
|
||||
/// It ensures that all IDs are non-empty and unique, correcting any duplicates or empty IDs.
|
||||
/// </summary>
|
||||
/// <param name="items">The list of items that may contain invalid IDs.</param>
|
||||
public static void RecoverInvalidIds<T>(IEnumerable<T> items)
|
||||
where T : class, IHasId
|
||||
{
|
||||
var idSet = new HashSet<int>();
|
||||
int newId = 0;
|
||||
var sortedItems = items.OrderBy(i => i.Id).ToList(); // Sort items by ID for consistent processing
|
||||
|
||||
// Iterate through the list and fix invalid IDs
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
// If the ID is invalid or already exists in the set (duplicate), assign a new unique ID
|
||||
if (!idSet.Add(item.Id))
|
||||
{
|
||||
// Find the next available unique ID
|
||||
while (idSet.Contains(newId))
|
||||
{
|
||||
newId++;
|
||||
}
|
||||
|
||||
item.Id = newId;
|
||||
idSet.Add(newId); // Add the newly assigned ID to the set
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface IHasId
|
||||
{
|
||||
int Id { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -301,6 +301,7 @@ namespace package
|
||||
if (!std::filesystem::exists(directoryPath))
|
||||
{
|
||||
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
|
||||
|
||||
@@ -18,10 +18,19 @@ public static class OcrHelpers
|
||||
{
|
||||
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
|
||||
{
|
||||
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
|
||||
var ocrLanguage = GetOCRLanguage();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
|
||||
OcrEngine ocrEngine;
|
||||
if (ocrLanguage is not null)
|
||||
{
|
||||
ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine from specified language");
|
||||
}
|
||||
else
|
||||
{
|
||||
ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages() ?? throw new InvalidOperationException("Unable to create OCR engine from user profile language");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
<ProjectPriFileName>PowerToys.EnvironmentVariablesUILib.pri</ProjectPriFileName>
|
||||
<GenerateLibraryLayout>true</GenerateLibraryLayout>
|
||||
<IsPackable>true</IsPackable>
|
||||
<!-- The default generated file path exceeds the length limit 260 on the build agent. Using a shorter path as a workaround. -->
|
||||
<CompilerGeneratedFilesOutputPath>$(ProjectDir)obj\g</CompilerGeneratedFilesOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -56,7 +54,4 @@
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="EnsureCompilerGeneratedFilesOutputPathExists" BeforeTargets="XamlPreCompile">
|
||||
<MakeDir Directories="$(CompilerGeneratedFilesOutputPath)" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
@@ -141,7 +141,7 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
@@ -153,7 +153,7 @@
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Web.WebView2.1.0.2903.40\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.6.250205002\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.7.250401001\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -4,5 +4,5 @@
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22621.2428" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.6.250205002" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.7.250401001" targetFramework="native" />
|
||||
</packages>
|
||||
@@ -121,6 +121,22 @@ namespace AppLauncher
|
||||
// packaged apps: try launching first by AppUserModel.ID
|
||||
// usage example: elevated Terminal
|
||||
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName);
|
||||
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
|
||||
if (res.isOk())
|
||||
{
|
||||
launched = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
|
||||
}
|
||||
}
|
||||
|
||||
// win32 app with appUserModelId:
|
||||
// usage example: steam games
|
||||
if (!launched && !app.appUserModelId.empty())
|
||||
{
|
||||
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
|
||||
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "pch.h"
|
||||
#include "AppUtils.h"
|
||||
#include "SteamHelper.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
#include <propvarutil.h>
|
||||
@@ -34,6 +35,8 @@ namespace Utils
|
||||
|
||||
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
|
||||
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
|
||||
|
||||
constexpr const wchar_t* SteamUrlProtocol = L"steam:";
|
||||
}
|
||||
|
||||
AppList IterateAppsFolder()
|
||||
@@ -138,6 +141,34 @@ namespace Utils
|
||||
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
|
||||
{
|
||||
data.installPath = propVariantString.m_pData;
|
||||
|
||||
if (!data.installPath.empty())
|
||||
{
|
||||
const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
|
||||
|
||||
if (isSteamProtocol)
|
||||
{
|
||||
Logger::info(L"Found steam game: protocol path: {}", data.installPath);
|
||||
data.protocolPath = data.installPath;
|
||||
|
||||
try
|
||||
{
|
||||
auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath);
|
||||
auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId);
|
||||
|
||||
if (gameFolder)
|
||||
{
|
||||
data.installPath = gameFolder->gameInstallationPath;
|
||||
Logger::info(L"Found steam game: physical path: {}", data.installPath);
|
||||
}
|
||||
}
|
||||
catch (std::exception ex)
|
||||
{
|
||||
Logger::error(L"Failed to get installPath for game {}", data.installPath);
|
||||
Logger::error("Error: {}", ex.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,5 +428,10 @@ namespace Utils
|
||||
{
|
||||
return installPath.ends_with(NonLocalizable::ChromeFilename);
|
||||
}
|
||||
|
||||
bool AppData::IsSteamGame() const
|
||||
{
|
||||
return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ namespace Utils
|
||||
std::wstring packageFullName;
|
||||
std::wstring appUserModelId;
|
||||
std::wstring pwaAppId;
|
||||
std::wstring protocolPath;
|
||||
bool canLaunchElevated = false;
|
||||
|
||||
bool IsEdge() const;
|
||||
bool IsChrome() const;
|
||||
bool IsSteamGame() const;
|
||||
};
|
||||
|
||||
using AppList = std::vector<AppData>;
|
||||
|
||||
171
src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
Normal file
171
src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "pch.h"
|
||||
#include "SteamHelper.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <filesystem>
|
||||
#include <regex>
|
||||
#include <string>
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
|
||||
static std::wstring Utf8ToWide(const std::string& utf8)
|
||||
{
|
||||
if (utf8.empty())
|
||||
return L"";
|
||||
|
||||
int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
|
||||
if (size <= 0)
|
||||
return L"";
|
||||
|
||||
std::wstring wide(size, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), wide.data(), size);
|
||||
return wide;
|
||||
}
|
||||
|
||||
namespace Steam
|
||||
{
|
||||
using namespace std;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::optional<std::wstring> GetSteamExePathFromRegistry()
|
||||
{
|
||||
static std::optional<std::wstring> cachedPath;
|
||||
if (cachedPath.has_value())
|
||||
{
|
||||
return cachedPath;
|
||||
}
|
||||
|
||||
const std::vector<HKEY> roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS };
|
||||
const std::vector<std::wstring> subKeys = {
|
||||
L"steam\\shell\\open\\command",
|
||||
L"Software\\Classes\\steam\\shell\\open\\command",
|
||||
};
|
||||
|
||||
for (HKEY root : roots)
|
||||
{
|
||||
for (const auto& subKey : subKeys)
|
||||
{
|
||||
HKEY hKey;
|
||||
if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
|
||||
{
|
||||
wchar_t value[512];
|
||||
DWORD size = sizeof(value);
|
||||
DWORD type = 0;
|
||||
|
||||
if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast<LPBYTE>(value), &size) == ERROR_SUCCESS &&
|
||||
(type == REG_SZ || type == REG_EXPAND_SZ))
|
||||
{
|
||||
std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim");
|
||||
std::wcmatch match;
|
||||
if (std::regex_search(value, match, exeRegex) && match.size() > 1)
|
||||
{
|
||||
RegCloseKey(hKey);
|
||||
cachedPath = match[1].str();
|
||||
return cachedPath;
|
||||
}
|
||||
}
|
||||
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cachedPath = std::nullopt;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
static fs::path GetSteamBasePath()
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return fs::path(*steamFolderOpt).parent_path() / L"steamapps";
|
||||
}
|
||||
|
||||
static fs::path GetAcfFilePath(const std::wstring& gameId)
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf");
|
||||
}
|
||||
|
||||
static fs::path GetGameInstallPath(const std::wstring& gameFolderName)
|
||||
{
|
||||
auto steamFolderOpt = GetSteamExePathFromRegistry();
|
||||
if (!steamFolderOpt)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return GetSteamBasePath() / L"common" / gameFolderName;
|
||||
}
|
||||
|
||||
static unordered_map<wstring, wstring> ParseAcfFile(const fs::path& acfPath)
|
||||
{
|
||||
unordered_map<wstring, wstring> result;
|
||||
|
||||
ifstream file(acfPath);
|
||||
if (!file.is_open())
|
||||
return result;
|
||||
|
||||
string line;
|
||||
while (getline(file, line))
|
||||
{
|
||||
smatch matches;
|
||||
static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim");
|
||||
|
||||
if (regex_search(line, matches, pattern) && matches.size() == 3)
|
||||
{
|
||||
wstring key = Utf8ToWide(matches[1].str());
|
||||
wstring value = Utf8ToWide(matches[2].str());
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<Steam::SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId)
|
||||
{
|
||||
fs::path acfPath = Steam::GetAcfFilePath(gameId);
|
||||
|
||||
if (!fs::exists(acfPath))
|
||||
return nullptr;
|
||||
|
||||
auto kv = ParseAcfFile(acfPath);
|
||||
if (kv.empty() || kv.find(L"installdir") == kv.end())
|
||||
return nullptr;
|
||||
|
||||
fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]);
|
||||
if (!fs::exists(gamePath))
|
||||
return nullptr;
|
||||
|
||||
auto game = std::make_unique<Steam::SteamGame>();
|
||||
game->gameId = gameId;
|
||||
game->gameInstallationPath = gamePath.wstring();
|
||||
return game;
|
||||
}
|
||||
|
||||
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath)
|
||||
{
|
||||
const std::wstring steamGamePrefix = L"steam://rungameid/";
|
||||
|
||||
if (urlPath.rfind(steamGamePrefix, 0) == 0)
|
||||
{
|
||||
return urlPath.substr(steamGamePrefix.length());
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
24
src/modules/Workspaces/WorkspacesLib/SteamHelper.h
Normal file
24
src/modules/Workspaces/WorkspacesLib/SteamHelper.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
namespace NonLocalizable
|
||||
{
|
||||
const std::wstring AcfFileNameTemplate = L"appmanifest_<gameid>.acfs";
|
||||
}
|
||||
|
||||
namespace Steam
|
||||
{
|
||||
struct SteamGame
|
||||
{
|
||||
std::wstring gameId;
|
||||
std::wstring gameInstallationPath;
|
||||
};
|
||||
|
||||
std::unique_ptr<SteamGame> GetSteamGameInfoFromAcfFile(const std::wstring& gameId);
|
||||
|
||||
std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath);
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="PwaHelper.h" />
|
||||
<ClInclude Include="Result.h" />
|
||||
<ClInclude Include="SteamHelper.h" />
|
||||
<ClInclude Include="StringUtils.h" />
|
||||
<ClInclude Include="utils.h" />
|
||||
<ClInclude Include="WbemHelper.h" />
|
||||
@@ -57,6 +58,7 @@
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="PwaHelper.cpp" />
|
||||
<ClCompile Include="SteamGameHelper.cpp" />
|
||||
<ClCompile Include="two_way_pipe_message_ipc.cpp" />
|
||||
<ClCompile Include="WbemHelper.cpp" />
|
||||
<ClCompile Include="WorkspacesData.cpp" />
|
||||
|
||||
@@ -53,6 +53,9 @@
|
||||
<ClInclude Include="StringUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SteamHelper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
@@ -88,6 +91,9 @@
|
||||
<ClCompile Include="WbemHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SteamGameHelper.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -71,6 +71,8 @@ namespace SnapshotUtils
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::info("Try to get window app:{}", reinterpret_cast<void*>(window));
|
||||
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
|
||||
@@ -118,10 +120,19 @@ namespace SnapshotUtils
|
||||
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
|
||||
if (!data.has_value() || data->name.empty())
|
||||
{
|
||||
Logger::info(L"Installed app not found: {}", processPath);
|
||||
Logger::info(L"Installed app not found:{},{}", reinterpret_cast<void*>(window), processPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
|
||||
{
|
||||
// Only care about steam games if it has no thick frame to remain consistent with
|
||||
// the behavior as before.
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger::info(L"Found app for window:{},{}", reinterpret_cast<void*>(window), processPath);
|
||||
|
||||
auto appData = data.value();
|
||||
|
||||
bool isEdge = appData.IsEdge();
|
||||
|
||||
@@ -200,6 +200,14 @@ std::optional<WindowWithDistance> WindowArranger::GetNearestWindow(const Workspa
|
||||
}
|
||||
|
||||
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
|
||||
|
||||
if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
|
||||
{
|
||||
// Only care about steam games if it has no thick frame to remain consistent with
|
||||
// the behavior as before.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!data.has_value())
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -9,10 +9,12 @@ namespace WindowFilter
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
|
||||
bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
|
||||
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
|
||||
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
|
||||
if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
|
||||
|
||||
Logger::info("Style for window: {}, {:#x}", reinterpret_cast<void*>(window), style);
|
||||
|
||||
if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons))
|
||||
{
|
||||
// popup windows we want to snap: e.g. Calculator, Telegram
|
||||
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.
|
||||
|
||||
@@ -121,4 +121,11 @@ namespace WindowUtils
|
||||
|
||||
return std::wstring(title);
|
||||
}
|
||||
|
||||
|
||||
inline bool HasThickFrame(HWND window)
|
||||
{
|
||||
auto style = GetWindowLong(window, GWL_STYLE);
|
||||
return WindowUtils::HasStyle(style, WS_THICKFRAME);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<Project DefaultTargets="Build"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Import Project="..\Microsoft.CmdPal.UI\CmdPal.pre.props" Condition="Exists('..\Microsoft.CmdPal.UI\CmdPal.pre.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
@@ -49,13 +50,20 @@
|
||||
</ItemGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>
|
||||
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
|
||||
%(PreprocessorDefinitions);
|
||||
</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'">
|
||||
IS_DEV_BRANDING;%(PreprocessorDefinitions)
|
||||
</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <interface/powertoy_module_interface.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/logger_helper.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
@@ -10,10 +11,11 @@
|
||||
#include <common/utils/resources.h>
|
||||
#include <common/utils/package.h>
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <common/interop/shared_constants.h>
|
||||
#include <Psapi.h>
|
||||
#include <TlHelp32.h>
|
||||
#include <common/utils/winapi_error.h>
|
||||
#include <thread>
|
||||
|
||||
HINSTANCE g_hInst_cmdPal = 0;
|
||||
|
||||
@@ -37,8 +39,6 @@ BOOL APIENTRY DllMain(HMODULE hInstance,
|
||||
class CmdPal : public PowertoyModuleIface
|
||||
{
|
||||
private:
|
||||
bool m_enabled = false;
|
||||
|
||||
std::wstring app_name;
|
||||
|
||||
//contains the non localized key of the powertoy
|
||||
@@ -46,7 +46,10 @@ private:
|
||||
|
||||
HANDLE m_hTerminateEvent;
|
||||
|
||||
void LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
|
||||
// Track if this is the first call to enable
|
||||
bool firstEnableCall = true;
|
||||
|
||||
static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
|
||||
{
|
||||
std::wstring dir = std::filesystem::path(appPath).parent_path();
|
||||
|
||||
@@ -54,6 +57,10 @@ private:
|
||||
sei.cbSize = sizeof(SHELLEXECUTEINFO);
|
||||
sei.hwnd = nullptr;
|
||||
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
|
||||
if (silentFail)
|
||||
{
|
||||
sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
|
||||
}
|
||||
sei.lpVerb = elevated ? L"runas" : L"open";
|
||||
sei.lpFile = appPath.c_str();
|
||||
sei.lpParameters = commandLineArgs.c_str();
|
||||
@@ -64,7 +71,11 @@ private:
|
||||
{
|
||||
std::wstring error = get_last_error_or_default(GetLastError());
|
||||
Logger::error(L"Failed to launch process. {}", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_launched.store(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<DWORD> GetProcessesIdByName(const std::wstring& processName)
|
||||
@@ -122,6 +133,9 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
static std::atomic<bool> m_enabled;
|
||||
static std::atomic<bool> m_launched;
|
||||
|
||||
CmdPal()
|
||||
{
|
||||
app_name = L"CmdPal";
|
||||
@@ -133,10 +147,7 @@ public:
|
||||
|
||||
~CmdPal()
|
||||
{
|
||||
if (m_enabled)
|
||||
{
|
||||
}
|
||||
m_enabled = false;
|
||||
CmdPal::m_enabled.store(false);
|
||||
}
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
@@ -203,15 +214,18 @@ public:
|
||||
{
|
||||
Logger::trace("CmdPal::enable()");
|
||||
|
||||
m_enabled = true;
|
||||
CmdPal::m_enabled.store(true);
|
||||
|
||||
try
|
||||
{
|
||||
std::wstring packageName = L"Microsoft.CommandPalette";
|
||||
#ifdef _DEBUG
|
||||
packageName = L"Microsoft.CommandPalette.Dev";
|
||||
std::wstring packageName = L"Microsoft.CommandPalette";
|
||||
std::wstring launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App";
|
||||
#ifdef IS_DEV_BRANDING
|
||||
packageName = L"Microsoft.CommandPalette.Dev";
|
||||
launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App";
|
||||
#endif
|
||||
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
||||
|
||||
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger::info(L"CmdPal not installed. Installing...");
|
||||
|
||||
@@ -238,19 +252,34 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
|
||||
errorMessage += e.what();
|
||||
Logger::error(errorMessage);
|
||||
catch (std::exception& e)
|
||||
{
|
||||
std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
|
||||
errorMessage += e.what();
|
||||
Logger::error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
#if _DEBUG
|
||||
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
|
||||
#else
|
||||
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
|
||||
#endif
|
||||
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
||||
{
|
||||
Logger::error("Cmdpal is not registered, quit..");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!firstEnableCall)
|
||||
{
|
||||
Logger::trace("Not first attempt, try to launch");
|
||||
LaunchApp(launchPath, L"RunFromPT", false /*no elevated*/, false /*error pop up*/);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If first time enable, do retry launch.
|
||||
Logger::trace("First attempt, try to launch");
|
||||
std::thread launchThread(&CmdPal::RetryLaunch, launchPath);
|
||||
launchThread.detach();
|
||||
}
|
||||
|
||||
firstEnableCall = false;
|
||||
}
|
||||
|
||||
virtual void disable()
|
||||
@@ -258,7 +287,44 @@ public:
|
||||
Logger::trace("CmdPal::disable()");
|
||||
TerminateCmdPal();
|
||||
|
||||
m_enabled = false;
|
||||
CmdPal::m_enabled.store(false);
|
||||
}
|
||||
|
||||
static void RetryLaunch(std::wstring path)
|
||||
{
|
||||
const int base_delay_milliseconds = 1000;
|
||||
int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
|
||||
int retry = 0;
|
||||
do
|
||||
{
|
||||
auto launch_result = LaunchApp(path, L"RunFromPT", false, retry < max_retry);
|
||||
if (launch_result)
|
||||
{
|
||||
Logger::info(L"CmdPal launched successfully after {} retries.", retry);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
|
||||
}
|
||||
|
||||
// When we got max retry, we don't need to wait for the next retry.
|
||||
if (retry < max_retry)
|
||||
{
|
||||
int delay = base_delay_milliseconds * (1 << (retry));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
|
||||
}
|
||||
++retry;
|
||||
} while (retry <= max_retry && m_enabled.load() && !m_launched.load());
|
||||
|
||||
if (!m_enabled.load() || m_launched.load())
|
||||
{
|
||||
Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"CmdPal launch failed after {} attempts.", retry);
|
||||
}
|
||||
}
|
||||
|
||||
virtual bool on_hotkey(size_t) override
|
||||
@@ -273,11 +339,14 @@ public:
|
||||
|
||||
virtual bool is_enabled() override
|
||||
{
|
||||
return m_enabled;
|
||||
return CmdPal::m_enabled.load();
|
||||
}
|
||||
};
|
||||
|
||||
std::atomic<bool> CmdPal::m_enabled{ false };
|
||||
std::atomic<bool> CmdPal::m_launched{ false };
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
{
|
||||
return new CmdPal();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
|
||||
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.250205002" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250401001" />
|
||||
<PackageVersion Include="Shmuelie.WinRTServer" Version="2.1.1" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.3" />
|
||||
|
||||
@@ -49,7 +49,7 @@ public partial class AppStateModel : ObservableObject
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, _deserializerOptions);
|
||||
var loaded = JsonSerializer.Deserialize<AppStateModel>(jsonContent, JsonSerializationContext.Default.AppStateModel);
|
||||
|
||||
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
|
||||
|
||||
@@ -73,7 +73,7 @@ public partial class AppStateModel : ObservableObject
|
||||
try
|
||||
{
|
||||
// Serialize the main dictionary to JSON and save it to the file
|
||||
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
|
||||
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel);
|
||||
|
||||
// Is it valid JSON?
|
||||
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
|
||||
@@ -89,7 +89,7 @@ public partial class AppStateModel : ObservableObject
|
||||
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
|
||||
}
|
||||
|
||||
var serialized = savedSettings.ToJsonString(_serializerOptions);
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
@@ -122,18 +122,19 @@ public partial class AppStateModel : ObservableObject
|
||||
return Path.Combine(directory, "state.json");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
|
||||
// private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||
// {
|
||||
// WriteIndented = true,
|
||||
// Converters = { new JsonStringEnumConverter() },
|
||||
// };
|
||||
|
||||
private static readonly JsonSerializerOptions _deserializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
IncludeFields = true,
|
||||
AllowTrailingCommas = true,
|
||||
PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
};
|
||||
// private static readonly JsonSerializerOptions _deserializerOptions = new()
|
||||
// {
|
||||
// PropertyNameCaseInsensitive = true,
|
||||
// IncludeFields = true,
|
||||
// AllowTrailingCommas = true,
|
||||
// PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
|
||||
// ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
// };
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
@@ -25,6 +25,8 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
field = value;
|
||||
SetSelectedItem(value);
|
||||
|
||||
OnPropertyChanged(nameof(SelectedItem));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +49,9 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
public partial PageViewModel? CurrentPage { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<CommandContextItemViewModel> ContextCommands { get; set; } = [];
|
||||
public partial ObservableCollection<ContextMenuStackViewModel> ContextMenuStack { get; set; } = [];
|
||||
|
||||
public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
|
||||
|
||||
public CommandBarViewModel()
|
||||
{
|
||||
@@ -100,35 +104,97 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
if (SelectedItem.MoreCommands.Count() > 1)
|
||||
{
|
||||
ShouldShowContextMenu = true;
|
||||
ContextCommands = [.. SelectedItem.AllCommands];
|
||||
|
||||
ContextMenuStack.Clear();
|
||||
ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
}
|
||||
else
|
||||
{
|
||||
ShouldShowContextMenu = false;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
OnPropertyChanged(nameof(ShouldShowContextMenu));
|
||||
}
|
||||
|
||||
// InvokeItemCommand is what this will be in Xaml due to source generator
|
||||
// this comes in when an item in the list is tapped
|
||||
[RelayCommand]
|
||||
private void InvokeItem(CommandContextItemViewModel item) =>
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item.Command.Model, item.Model));
|
||||
// [RelayCommand]
|
||||
public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
|
||||
PerformCommand(item);
|
||||
|
||||
// this comes in when the primary button is tapped
|
||||
public void InvokePrimaryCommand()
|
||||
{
|
||||
if (PrimaryCommand != null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
|
||||
}
|
||||
PerformCommand(SecondaryCommand);
|
||||
}
|
||||
|
||||
// this comes in when the secondary button is tapped
|
||||
public void InvokeSecondaryCommand()
|
||||
{
|
||||
if (SecondaryCommand != null)
|
||||
PerformCommand(SecondaryCommand);
|
||||
}
|
||||
|
||||
public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||
{
|
||||
var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
|
||||
return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
|
||||
}
|
||||
|
||||
private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
|
||||
{
|
||||
if (command == null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
|
||||
return ContextKeybindingResult.Unhandled;
|
||||
}
|
||||
|
||||
if (command.HasMoreCommands)
|
||||
{
|
||||
ContextMenuStack.Add(new ContextMenuStackViewModel(command));
|
||||
OnPropertyChanging(nameof(ContextMenu));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
return ContextKeybindingResult.KeepOpen;
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(command.Command.Model, command.Model));
|
||||
return ContextKeybindingResult.Hide;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanPopContextStack()
|
||||
{
|
||||
return ContextMenuStack.Count > 1;
|
||||
}
|
||||
|
||||
public void PopContextStack()
|
||||
{
|
||||
if (ContextMenuStack.Count > 1)
|
||||
{
|
||||
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
|
||||
}
|
||||
|
||||
OnPropertyChanging(nameof(ContextMenu));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
}
|
||||
|
||||
public void ClearContextStack()
|
||||
{
|
||||
while (ContextMenuStack.Count > 1)
|
||||
{
|
||||
ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
|
||||
}
|
||||
|
||||
OnPropertyChanging(nameof(ContextMenu));
|
||||
OnPropertyChanged(nameof(ContextMenu));
|
||||
}
|
||||
}
|
||||
|
||||
public enum ContextKeybindingResult
|
||||
{
|
||||
Unhandled,
|
||||
Hide,
|
||||
KeepOpen,
|
||||
}
|
||||
|
||||
@@ -9,12 +9,16 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context)
|
||||
{
|
||||
private readonly KeyChord nullKeyChord = new(0, 0, 0);
|
||||
|
||||
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
|
||||
|
||||
public bool IsCritical { get; private set; }
|
||||
|
||||
public KeyChord? RequestedShortcut { get; private set; }
|
||||
|
||||
public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord);
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
if (IsInitialized)
|
||||
@@ -31,6 +35,9 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
||||
}
|
||||
|
||||
IsCritical = contextItem.IsCritical;
|
||||
|
||||
// I actually don't think this will ever actually be null, because
|
||||
// KeyChord is a struct, which isn't nullable in WinRT
|
||||
if (contextItem.RequestedShortcut != null)
|
||||
{
|
||||
RequestedShortcut = new(
|
||||
|
||||
@@ -48,7 +48,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public List<CommandContextItemViewModel> MoreCommands { get; private set; } = [];
|
||||
|
||||
IEnumerable<CommandContextItemViewModel> ICommandBarContext.MoreCommands => MoreCommands;
|
||||
IEnumerable<CommandContextItemViewModel> IContextMenuContext.MoreCommands => MoreCommands;
|
||||
|
||||
public bool HasMoreCommands => MoreCommands.Count > 0;
|
||||
|
||||
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
// use Initialize straight up
|
||||
MoreCommands.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.InitializeProperties();
|
||||
contextItem.SlowInitializeProperties();
|
||||
});
|
||||
|
||||
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
|
||||
if (!string.IsNullOrEmpty(model.Command.Name))
|
||||
{
|
||||
_itemTitle = Name,
|
||||
Subtitle = Subtitle,
|
||||
Command = Command,
|
||||
_defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
|
||||
{
|
||||
_itemTitle = Name,
|
||||
Subtitle = Subtitle,
|
||||
Command = Command,
|
||||
|
||||
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
||||
};
|
||||
// TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
|
||||
};
|
||||
|
||||
// Only set the icon on the context item for us if our command didn't
|
||||
// have its own icon
|
||||
if (!Command.HasIcon)
|
||||
{
|
||||
_defaultCommandContextItem._listItemIcon = _listItemIcon;
|
||||
// Only set the icon on the context item for us if our command didn't
|
||||
// have its own icon
|
||||
if (!Command.HasIcon)
|
||||
{
|
||||
_defaultCommandContextItem._listItemIcon = _listItemIcon;
|
||||
}
|
||||
}
|
||||
|
||||
Initialized |= InitializedState.SelectionInitialized;
|
||||
|
||||
@@ -13,12 +13,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
|
||||
{
|
||||
public CreatedExtensionForm(string name, string displayName, string path)
|
||||
{
|
||||
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
|
||||
TemplateJson = CardTemplate;
|
||||
DataJson = $$"""
|
||||
{
|
||||
"name": {{JsonSerializer.Serialize(name)}},
|
||||
"directory": {{JsonSerializer.Serialize(path)}},
|
||||
"displayName": {{JsonSerializer.Serialize(displayName)}}
|
||||
"name": {{serializeString(name)}},
|
||||
"directory": {{serializeString(path)}},
|
||||
"displayName": {{serializeString(displayName)}}
|
||||
}
|
||||
""";
|
||||
_name = name;
|
||||
@@ -28,13 +29,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
|
||||
|
||||
public override ICommandResult SubmitForm(string inputs, string data)
|
||||
{
|
||||
JsonObject? dataInput = JsonNode.Parse(data)?.AsObject();
|
||||
var dataInput = JsonNode.Parse(data)?.AsObject();
|
||||
if (dataInput == null)
|
||||
{
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
|
||||
var verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
|
||||
return verb switch
|
||||
{
|
||||
"sln" => OpenSolution(),
|
||||
@@ -47,7 +48,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
|
||||
private ICommandResult OpenSolution()
|
||||
{
|
||||
string[] parts = [_path, _name, $"{_name}.sln"];
|
||||
string pathToSolution = Path.Combine(parts);
|
||||
var pathToSolution = Path.Combine(parts);
|
||||
ShellHelpers.OpenInShell(pathToSolution);
|
||||
return CommandResult.Hide();
|
||||
}
|
||||
@@ -55,7 +56,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
|
||||
private ICommandResult OpenDirectory()
|
||||
{
|
||||
string[] parts = [_path, _name];
|
||||
string pathToDir = Path.Combine(parts);
|
||||
var pathToDir = Path.Combine(parts);
|
||||
ShellHelpers.OpenInShell(pathToDir);
|
||||
return CommandResult.Hide();
|
||||
}
|
||||
|
||||
@@ -194,9 +194,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
|
||||
Directory.Delete(tempDir, true);
|
||||
}
|
||||
|
||||
private string FormatJsonString(string str)
|
||||
{
|
||||
private string FormatJsonString(string str) =>
|
||||
|
||||
// Escape the string for JSON
|
||||
return JsonSerializer.Serialize(str);
|
||||
}
|
||||
JsonSerializer.Serialize(str, JsonSerializationContext.Default.String);
|
||||
}
|
||||
|
||||
@@ -51,15 +51,16 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
// If we fail to parse the card JSON, then display _our own card_
|
||||
// with the exception
|
||||
AdaptiveCardTemplate template = new(ErrorCardJson);
|
||||
var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
|
||||
|
||||
// todo: we could probably stick Card.Errors in there too
|
||||
var dataJson = $$"""
|
||||
{
|
||||
"error_message": {{JsonSerializer.Serialize(e.Message)}},
|
||||
"error_stack": {{JsonSerializer.Serialize(e.StackTrace)}},
|
||||
"inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}},
|
||||
"template_json": {{JsonSerializer.Serialize(TemplateJson)}},
|
||||
"data_json": {{JsonSerializer.Serialize(DataJson)}}
|
||||
"error_message": {{serializeString(e.Message)}},
|
||||
"error_stack": {{serializeString(e.StackTrace)}},
|
||||
"inner_exception": {{serializeString(e.InnerException?.Message)}},
|
||||
"template_json": {{serializeString(TemplateJson)}},
|
||||
"data_json": {{serializeString(DataJson)}}
|
||||
}
|
||||
""";
|
||||
var cardJson = template.Expand(dataJson);
|
||||
|
||||
@@ -167,7 +167,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
|
||||
|
||||
Commands.ForEach(contextItem =>
|
||||
{
|
||||
contextItem.InitializeProperties();
|
||||
contextItem.SlowInitializeProperties();
|
||||
});
|
||||
}
|
||||
else
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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 Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class ContextMenuStackViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<CommandContextItemViewModel> FilteredItems { get; set; }
|
||||
|
||||
private readonly IContextMenuContext _context;
|
||||
private string _lastSearchText = string.Empty;
|
||||
|
||||
// private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
|
||||
public ContextMenuStackViewModel(IContextMenuContext context)
|
||||
{
|
||||
_context = context;
|
||||
FilteredItems = [.. context.AllCommands];
|
||||
}
|
||||
|
||||
public void SetSearchText(string searchText)
|
||||
{
|
||||
if (searchText == _lastSearchText)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastSearchText = searchText;
|
||||
|
||||
var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
|
||||
if (string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, commands);
|
||||
return;
|
||||
}
|
||||
|
||||
var newResults = ListHelpers.FilterList<CommandContextItemViewModel>(commands, searchText, ScoreContextCommand);
|
||||
ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
|
||||
}
|
||||
|
||||
private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
|
||||
{
|
||||
if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(item.Title))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
|
||||
|
||||
var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
|
||||
|
||||
return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
|
||||
}
|
||||
|
||||
public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
|
||||
{
|
||||
var keybindings = _context.Keybindings();
|
||||
if (keybindings != null)
|
||||
{
|
||||
// Does the pressed key match any of the keybindings?
|
||||
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
|
||||
if (keybindings.TryGetValue(pressedKeyChord, out var item))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -434,7 +434,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
break;
|
||||
case nameof(EmptyContent):
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext);
|
||||
EmptyContent.InitializeProperties();
|
||||
EmptyContent.SlowInitializeProperties();
|
||||
break;
|
||||
case nameof(IsLoading):
|
||||
UpdateEmptyContent();
|
||||
@@ -452,6 +452,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(EmptyContent));
|
||||
|
||||
DoOnUiThread(
|
||||
() =>
|
||||
{
|
||||
|
||||
@@ -51,6 +51,12 @@ public record PerformCommandMessage
|
||||
Context = context.Unsafe;
|
||||
}
|
||||
|
||||
public PerformCommandMessage(CommandContextItemViewModel contextCommand)
|
||||
{
|
||||
Command = contextCommand.Command.Model;
|
||||
Context = contextCommand.Model.Unsafe;
|
||||
}
|
||||
|
||||
public PerformCommandMessage(ConfirmResultViewModel vm)
|
||||
{
|
||||
Command = vm.PrimaryCommand.Model;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
|
||||
{
|
||||
public bool Handled { get; set; }
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
|
||||
@@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
|
||||
{
|
||||
}
|
||||
|
||||
// Represents everything the command bar needs to know about to show command
|
||||
// buttons at the bottom.
|
||||
//
|
||||
// This is implemented by both ListItemViewModel and ContentPageViewModel,
|
||||
// the two things with sub-commands.
|
||||
public interface ICommandBarContext : INotifyPropertyChanged
|
||||
public interface IContextMenuContext : INotifyPropertyChanged
|
||||
{
|
||||
public IEnumerable<CommandContextItemViewModel> MoreCommands { get; }
|
||||
|
||||
public bool HasMoreCommands { get; }
|
||||
|
||||
public List<CommandContextItemViewModel> AllCommands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mapping of key -> command item for this particular item's
|
||||
/// MoreCommands. (This won't include the primary Command, but it will
|
||||
/// include the secondary one). This map can be used to quickly check if a
|
||||
/// shortcut key was pressed
|
||||
/// </summary>
|
||||
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
|
||||
/// that have a shortcut key set.</returns>
|
||||
public Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
|
||||
{
|
||||
return MoreCommands
|
||||
.Where(c => c.HasRequestedShortcut)
|
||||
.ToDictionary(
|
||||
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
|
||||
c => c);
|
||||
}
|
||||
}
|
||||
|
||||
// Represents everything the command bar needs to know about to show command
|
||||
// buttons at the bottom.
|
||||
//
|
||||
// This is implemented by both ListItemViewModel and ContentPageViewModel,
|
||||
// the two things with sub-commands.
|
||||
public interface ICommandBarContext : IContextMenuContext
|
||||
{
|
||||
public string SecondaryCommandName { get; }
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand { get; }
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand { get; }
|
||||
|
||||
public List<CommandContextItemViewModel> AllCommands { get; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -67,4 +69,15 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Just mark it as AOT compatible. Do not publish with AOT now. We need fully test before we really publish it as AOT enabled-->
|
||||
<!--<PropertyGroup>
|
||||
<SelfContained>true</SelfContained>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
--><!-- <DisableRuntimeMarshalling>true</DisableRuntimeMarshalling> --><!--
|
||||
<PublishAot>true</PublishAot>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
</PropertyGroup>-->
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -11,7 +11,7 @@ using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
public class ExtensionService : IExtensionService, IDisposable
|
||||
public partial class ExtensionService : IExtensionService, IDisposable
|
||||
{
|
||||
public event TypedEventHandler<IExtensionService, IEnumerable<IExtensionWrapper>>? OnExtensionAdded;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Windows.Win32;
|
||||
using Windows.Win32.System.Com;
|
||||
using WinRT;
|
||||
|
||||
// [assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
|
||||
public class ExtensionWrapper : IExtensionWrapper
|
||||
@@ -113,25 +114,36 @@ public class ExtensionWrapper : IExtensionWrapper
|
||||
// -2147467262: E_NOINTERFACE
|
||||
// -2147024893: E_PATH_NOT_FOUND
|
||||
var guid = typeof(IExtension).GUID;
|
||||
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
|
||||
|
||||
if (hr.Value == -2147024893)
|
||||
unsafe
|
||||
{
|
||||
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
|
||||
var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
|
||||
|
||||
// We don't really need to throw this exception.
|
||||
// We'll just return out nothing.
|
||||
return;
|
||||
if (hr.Value == -2147024893)
|
||||
{
|
||||
Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
|
||||
|
||||
// We don't really need to throw this exception.
|
||||
// We'll just return out nothing.
|
||||
return;
|
||||
}
|
||||
|
||||
extensionPtr = Marshal.GetIUnknownForObject((nint)extensionObj);
|
||||
if (hr < 0)
|
||||
{
|
||||
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
|
||||
// extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
|
||||
extensionPtr = (nint)extensionObj;
|
||||
if (hr < 0)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
|
||||
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
|
||||
}
|
||||
|
||||
extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
|
||||
if (hr < 0)
|
||||
{
|
||||
Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
|
||||
Marshal.ThrowExceptionForHR(hr);
|
||||
}
|
||||
|
||||
_extensionObject = MarshalInterface<IExtension>.FromAbi(extensionPtr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://aka.ms/CsWin32.schema.json",
|
||||
"allowMarshaling": false
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
|
||||
//// Run on background thread from ListPage.xaml.cs
|
||||
[RelayCommand]
|
||||
private Task<bool> InitializeAsync()
|
||||
internal Task<bool> InitializeAsync()
|
||||
{
|
||||
// TODO: We may want a SemaphoreSlim lock here.
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
public partial class RecentCommandsManager : ObservableObject
|
||||
{
|
||||
[JsonInclude]
|
||||
private List<HistoryItem> History { get; set; } = [];
|
||||
internal List<HistoryItem> History { get; set; } = [];
|
||||
|
||||
public RecentCommandsManager()
|
||||
{
|
||||
|
||||
@@ -40,6 +40,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public bool ShowSystemTrayIcon { get; set; } = true;
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen { get; set; } = true;
|
||||
|
||||
public Dictionary<string, ProviderSettings> ProviderSettings { get; set; } = [];
|
||||
|
||||
public Dictionary<string, CommandAlias> Aliases { get; set; } = [];
|
||||
@@ -91,7 +93,7 @@ public partial class SettingsModel : ObservableObject
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, _deserializerOptions);
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
|
||||
|
||||
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
|
||||
|
||||
@@ -115,7 +117,7 @@ public partial class SettingsModel : ObservableObject
|
||||
try
|
||||
{
|
||||
// Serialize the main dictionary to JSON and save it to the file
|
||||
var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
|
||||
var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
|
||||
|
||||
// Is it valid JSON?
|
||||
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
|
||||
@@ -131,7 +133,7 @@ public partial class SettingsModel : ObservableObject
|
||||
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
|
||||
}
|
||||
|
||||
var serialized = savedSettings.ToJsonString(_serializerOptions);
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
// TODO: Instead of just raising the event here, we should
|
||||
@@ -164,19 +166,34 @@ public partial class SettingsModel : ObservableObject
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
// [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
|
||||
// private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||
// {
|
||||
// WriteIndented = true,
|
||||
// Converters = { new JsonStringEnumConverter() },
|
||||
// };
|
||||
// private static readonly JsonSerializerOptions _deserializerOptions = new()
|
||||
// {
|
||||
// PropertyNameCaseInsensitive = true,
|
||||
// IncludeFields = true,
|
||||
// Converters = { new JsonStringEnumConverter() },
|
||||
// AllowTrailingCommas = true,
|
||||
// };
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions _deserializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
IncludeFields = true,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
[JsonSerializable(typeof(float))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(HistoryItem))]
|
||||
[JsonSerializable(typeof(SettingsModel))]
|
||||
[JsonSerializable(typeof(AppStateModel))]
|
||||
[JsonSerializable(typeof(List<HistoryItem>), TypeInfoPropertyName = "HistoryList")]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
|
||||
internal sealed partial class JsonSerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
public enum MonitorBehavior
|
||||
|
||||
@@ -108,6 +108,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnoreShortcutWhenFullscreen
|
||||
{
|
||||
get => _settings.IgnoreShortcutWhenFullscreen;
|
||||
set
|
||||
{
|
||||
_settings.IgnoreShortcutWhenFullscreen = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
|
||||
|
||||
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
|
||||
|
||||
@@ -109,9 +109,12 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
|
||||
// TODO GH #239 switch back when using the new MD text block
|
||||
// _ = _queue.EnqueueAsync(() =>
|
||||
_ = Task.Factory.StartNew(
|
||||
() =>
|
||||
async () =>
|
||||
{
|
||||
var result = (bool)viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
|
||||
// bool f = await viewModel.InitializeCommand.ExecutionTask.;
|
||||
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
|
||||
// var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault<bool?>()!;
|
||||
var result = await viewModel.InitializeAsync();
|
||||
|
||||
CurrentPage = viewModel; // result ? viewModel : null;
|
||||
////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<!-- Template for actions in the mode actions dropdown button -->
|
||||
<DataTemplate x:Key="ContextMenuViewModelTemplate" x:DataType="viewmodels:CommandContextItemViewModel">
|
||||
<Grid>
|
||||
<Grid AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -53,37 +53,16 @@
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Title, Mode=OneWay}" />
|
||||
<!--<TextBlock
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="16,0,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />-->
|
||||
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<animations:ImplicitAnimationSet x:Name="ShowAnimations">
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.340" />
|
||||
<animations:ScaleAnimation
|
||||
From="0.85"
|
||||
To="1"
|
||||
Duration="0:0:0.350" />
|
||||
</animations:ImplicitAnimationSet>
|
||||
<animations:ImplicitAnimationSet x:Name="HideAnimations">
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="0:0:0.240" />
|
||||
<animations:ScaleAnimation
|
||||
From="1"
|
||||
To="0.85"
|
||||
Duration="0:0:0.350" />
|
||||
</animations:ImplicitAnimationSet>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
@@ -92,9 +71,9 @@
|
||||
Padding="4"
|
||||
ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid
|
||||
@@ -135,9 +114,6 @@
|
||||
<Button
|
||||
x:Name="SettingsIconButton"
|
||||
x:Uid="SettingsButton"
|
||||
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
|
||||
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="SettingsIcon_Tapped"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
@@ -157,6 +133,8 @@
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
@@ -167,10 +145,8 @@
|
||||
<Button
|
||||
x:Name="PrimaryButton"
|
||||
Padding="6,4,4,4"
|
||||
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
|
||||
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="PrimaryButton_Tapped"
|
||||
@@ -198,10 +174,8 @@
|
||||
<Button
|
||||
x:Name="SecondaryButton"
|
||||
Padding="6,4,4,4"
|
||||
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
|
||||
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="SecondaryButton_Tapped"
|
||||
Visibility="{x:Bind ViewModel.HasSecondaryCommand, Mode=OneWay}">
|
||||
@@ -245,35 +219,48 @@
|
||||
x:Name="MoreCommandsButton"
|
||||
x:Uid="MoreCommandsButton"
|
||||
Padding="4"
|
||||
animations:Implicit.HideAnimations="{StaticResource HideAnimations}"
|
||||
animations:Implicit.ShowAnimations="{StaticResource ShowAnimations}"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
Content="{ui:FontIcon Glyph=,
|
||||
FontSize=16}"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
ToolTipService.ToolTip="Ctrl+k"
|
||||
ToolTipService.ToolTip="Ctrl+K"
|
||||
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="TopEdgeAlignedRight">
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
Margin="-16,-12,-16,-12"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,7,12,7" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
<Flyout
|
||||
Closing="Flyout_Closing"
|
||||
Opened="Flyout_Opened"
|
||||
Placement="TopEdgeAlignedRight">
|
||||
<StackPanel>
|
||||
|
||||
<ListView
|
||||
x:Name="CommandsDropdown"
|
||||
MinWidth="248"
|
||||
Margin="-16,-12,-16,-12"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="CommandsDropdown_ItemClick"
|
||||
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
|
||||
ItemsSource="{x:Bind ViewModel.ContextMenu.FilteredItems, Mode=OneWay}"
|
||||
KeyDown="CommandsDropdown_KeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="Padding" Value="12,7,12,7" />
|
||||
</Style>
|
||||
</ListView.ItemContainerStyle>
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
|
||||
<TextBox
|
||||
x:Name="ContextFilterBox"
|
||||
x:Uid="ContextFilterBox"
|
||||
Margin="-12,12,-12,-12"
|
||||
KeyDown="ContextFilterBox_KeyDown"
|
||||
PreviewKeyDown="ContextFilterBox_PreviewKeyDown"
|
||||
TextChanged="ContextFilterBox_TextChanged" />
|
||||
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
@@ -6,18 +6,22 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Views;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Windows.System;
|
||||
using Windows.UI.Core;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class CommandBar : UserControl,
|
||||
IRecipient<OpenContextMenuMessage>,
|
||||
IRecipient<TryCommandKeybindingMessage>,
|
||||
ICurrentPageAware
|
||||
{
|
||||
public CommandBarViewModel ViewModel { get; set; } = new();
|
||||
public CommandBarViewModel ViewModel { get; } = new();
|
||||
|
||||
public PageViewModel? CurrentPageViewModel
|
||||
{
|
||||
@@ -35,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
public void Receive(OpenContextMenuMessage message)
|
||||
@@ -49,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
};
|
||||
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
CommandsDropdown.Focus(FocusState.Programmatic);
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
public void Receive(TryCommandKeybindingMessage msg)
|
||||
{
|
||||
if (!ViewModel.ShouldShowContextMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
|
||||
|
||||
if (result == ContextKeybindingResult.Hide)
|
||||
{
|
||||
msg.Handled = true;
|
||||
}
|
||||
else if (result == ContextKeybindingResult.KeepOpen)
|
||||
{
|
||||
if (!MoreCommandsButton.Flyout.IsOpen)
|
||||
{
|
||||
var options = new FlyoutShowOptions
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
};
|
||||
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
|
||||
}
|
||||
|
||||
UpdateUiForStackChange();
|
||||
|
||||
msg.Handled = true;
|
||||
}
|
||||
else if (result == ContextKeybindingResult.Unhandled)
|
||||
{
|
||||
msg.Handled = false;
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
|
||||
@@ -85,8 +125,160 @@ public sealed partial class CommandBar : UserControl,
|
||||
{
|
||||
if (e.ClickedItem is CommandContextItemViewModel item)
|
||||
{
|
||||
ViewModel?.InvokeItemCommand.Execute(item);
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
|
||||
{
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CommandsDropdown_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Handled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
|
||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
|
||||
|
||||
var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
|
||||
if (result == ContextKeybindingResult.Hide)
|
||||
{
|
||||
e.Handled = true;
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
else if (result == ContextKeybindingResult.KeepOpen)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (result == ContextKeybindingResult.Unhandled)
|
||||
{
|
||||
e.Handled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Flyout_Opened(object sender, object e)
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
|
||||
{
|
||||
ViewModel?.ClearContextStack();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
if (prop == nameof(ViewModel.ContextMenu))
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
|
||||
|
||||
if (CommandsDropdown.SelectedIndex == -1)
|
||||
{
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
|
||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
|
||||
|
||||
if (e.Key == VirtualKey.Enter)
|
||||
{
|
||||
if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
|
||||
{
|
||||
if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
|
||||
{
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
else if (e.Key == VirtualKey.Escape ||
|
||||
(e.Key == VirtualKey.Left && altPressed))
|
||||
{
|
||||
if (ViewModel.CanPopContextStack())
|
||||
{
|
||||
ViewModel.PopContextStack();
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
else
|
||||
{
|
||||
MoreCommandsButton.Flyout.Hide();
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
CommandsDropdown_KeyDown(sender, e);
|
||||
}
|
||||
|
||||
private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.Up)
|
||||
{
|
||||
// navigate previous
|
||||
if (CommandsDropdown.SelectedIndex > 0)
|
||||
{
|
||||
CommandsDropdown.SelectedIndex--;
|
||||
}
|
||||
else
|
||||
{
|
||||
CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == VirtualKey.Down)
|
||||
{
|
||||
// navigate next
|
||||
if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
|
||||
{
|
||||
CommandsDropdown.SelectedIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateUiForStackChange()
|
||||
{
|
||||
ContextFilterBox.Text = string.Empty;
|
||||
ViewModel.ContextMenu?.SetSearchText(string.Empty);
|
||||
CommandsDropdown.SelectedIndex = 0;
|
||||
ContextFilterBox.Focus(FocusState.Programmatic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,9 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
|
||||
|
||||
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
|
||||
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
|
||||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
|
||||
if (ctrlPressed && e.Key == VirtualKey.Enter)
|
||||
{
|
||||
// ctrl+enter
|
||||
@@ -164,6 +166,16 @@ public sealed partial class SearchBar : UserControl,
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
}
|
||||
|
||||
if (!e.Handled)
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
}
|
||||
}
|
||||
|
||||
private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
@@ -281,5 +293,5 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
public void Receive(GoHomeMessage message) => ClearSearch();
|
||||
|
||||
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="AutomationProperties.AutomationControlType" Value="Custom" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Padding" Value="{ThemeResource TagPadding}" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
@@ -56,6 +57,7 @@
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
AutomationProperties.Name="{TemplateBinding Text}"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
|
||||
@@ -28,21 +28,22 @@
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="viewmodels:TagViewModel">
|
||||
<ItemContainer>
|
||||
<cpcontrols:Tag
|
||||
BackgroundColor="{x:Bind Background, Mode=OneWay}"
|
||||
FontSize="12"
|
||||
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
|
||||
Icon="{x:Bind Icon, Mode=OneWay}"
|
||||
Text="{x:Bind Text, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</ItemContainer>
|
||||
<cpcontrols:Tag
|
||||
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
|
||||
BackgroundColor="{x:Bind Background, Mode=OneWay}"
|
||||
FontSize="12"
|
||||
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
|
||||
Icon="{x:Bind Icon, Mode=OneWay}"
|
||||
Text="{x:Bind Text, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
|
||||
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="viewmodels:ListItemViewModel">
|
||||
|
||||
<Grid Padding="0,12,0,12" ColumnSpacing="12">
|
||||
<Grid
|
||||
Padding="0,12,0,12"
|
||||
AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
|
||||
ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" />
|
||||
@@ -55,6 +56,7 @@
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="4,0,4,0"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
@@ -136,14 +138,15 @@
|
||||
</controls:Case>
|
||||
<controls:Case Value="True">
|
||||
<StackPanel
|
||||
Margin="24"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="56"
|
||||
Height="56"
|
||||
Width="48"
|
||||
Height="48"
|
||||
Margin="8"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.EmptyContent.Icon, Mode=OneWay}"
|
||||
@@ -152,11 +155,15 @@
|
||||
Margin="0,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}" />
|
||||
Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}" />
|
||||
Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
|
||||
@@ -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.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
internal static class NativeMethods
|
||||
{
|
||||
[DllImport("shell32.dll")]
|
||||
public static extern int SHQueryUserNotificationState(out UserNotificationState state);
|
||||
}
|
||||
|
||||
internal enum UserNotificationState : int
|
||||
{
|
||||
QUNS_NOT_PRESENT = 1,
|
||||
QUNS_BUSY,
|
||||
QUNS_RUNNING_D3D_FULL_SCREEN,
|
||||
QUNS_PRESENTATION_MODE,
|
||||
QUNS_ACCEPTS_NOTIFICATIONS,
|
||||
QUNS_QUIET_TIME,
|
||||
QUNS_APP,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
internal sealed partial class WindowHelper
|
||||
{
|
||||
public static bool IsWindowFullscreen()
|
||||
{
|
||||
UserNotificationState state;
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
||||
if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) == null)
|
||||
{
|
||||
if (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
|
||||
state == UserNotificationState.QUNS_BUSY ||
|
||||
state == UserNotificationState.QUNS_PRESENTATION_MODE)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public sealed partial class MainWindow : Window,
|
||||
private readonly WNDPROC? _hotkeyWndProc;
|
||||
private readonly WNDPROC? _originalWndProc;
|
||||
private readonly List<TopLevelHotkey> _hotkeys = [];
|
||||
private bool _ignoreHotKeyWhenFullScreen = true;
|
||||
|
||||
// Stylistically, window messages are WM_*
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
@@ -157,6 +158,8 @@ public sealed partial class MainWindow : Window,
|
||||
SetupHotkey(settings);
|
||||
SetupTrayIcon(settings.ShowSystemTrayIcon);
|
||||
|
||||
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
|
||||
|
||||
// This will prevent our window from appearing in alt+tab or the taskbar.
|
||||
// You'll _need_ to use the hotkey to summon it.
|
||||
AppWindow.IsShownInSwitchers = System.Diagnostics.Debugger.IsAttached;
|
||||
@@ -504,6 +507,15 @@ public sealed partial class MainWindow : Window,
|
||||
var hotkeyIndex = (int)wParam.Value;
|
||||
if (hotkeyIndex < _hotkeys.Count)
|
||||
{
|
||||
if (_ignoreHotKeyWhenFullScreen)
|
||||
{
|
||||
// If we're in full screen mode, ignore the hotkey
|
||||
if (WindowHelper.IsWindowFullscreen())
|
||||
{
|
||||
return (LRESULT)IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
var hotkey = _hotkeys[hotkeyIndex];
|
||||
HandleSummon(hotkey.CommandId);
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="viewModels:TagViewModel">
|
||||
<cpcontrols:Tag
|
||||
HorizontalAlignment="Left"
|
||||
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
|
||||
BackgroundColor="{x:Bind Background, Mode=OneWay}"
|
||||
FontSize="12"
|
||||
ForegroundColor="{x:Bind Foreground, Mode=OneWay}"
|
||||
@@ -147,27 +148,36 @@
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
EasingType="Cubic"
|
||||
From="0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.340" />
|
||||
Duration="0:0:0.187" />
|
||||
<animations:ScaleAnimation
|
||||
EasingMode="EaseIn"
|
||||
EasingType="Cubic"
|
||||
From="0.5"
|
||||
To="1"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.187" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Cubic"
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="0:0:0.240" />
|
||||
Duration="0:0:0.187" />
|
||||
<animations:ScaleAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Cubic"
|
||||
From="1"
|
||||
To="0.5"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.187" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</Image>
|
||||
<Button
|
||||
x:Name="BackButton"
|
||||
x:Uid="BackButton"
|
||||
Margin="4,0,4,0"
|
||||
Padding="4"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -178,35 +188,42 @@
|
||||
FontSize="16"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tapped="BackButton_Tapped"
|
||||
ToolTipService.ToolTip="Back"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
EasingType="Cubic"
|
||||
From="0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.340" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:ScaleAnimation
|
||||
From="0.5"
|
||||
To="1"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:TranslationAnimation
|
||||
From="16,0,0"
|
||||
To="0,0,0"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.333" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Cubic"
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="0:0:0.340" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:ScaleAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Cubic"
|
||||
From="1"
|
||||
To="0.5"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:TranslationAnimation
|
||||
EasingMode="EaseOut"
|
||||
EasingType="Cubic"
|
||||
From="0,0,0"
|
||||
To="16,0,0"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.187" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</Button>
|
||||
<cpcontrols:IconBox
|
||||
@@ -223,29 +240,29 @@
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.450" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:ScaleAnimation
|
||||
From="0.8"
|
||||
To="1"
|
||||
Duration="0:0:0.500" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:TranslationAnimation
|
||||
From="8,0,0"
|
||||
To="0,0,0"
|
||||
Duration="0:0:0.400" />
|
||||
Duration="0:0:0.187" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="0:0:0.340" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:ScaleAnimation
|
||||
From="1"
|
||||
To="0.8"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.333" />
|
||||
<animations:TranslationAnimation
|
||||
From="0,0,0"
|
||||
To="8,0,0"
|
||||
Duration="0:0:0.350" />
|
||||
Duration="0:0:0.187" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</cpcontrols:IconBox>
|
||||
</StackPanel>
|
||||
@@ -272,13 +289,13 @@
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.340" />
|
||||
Duration="0:0:0.333" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="0:0:0.340" />
|
||||
Duration="0:0:0.333" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
</ProgressBar>
|
||||
|
||||
@@ -313,21 +330,21 @@
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
To="1.0"
|
||||
Duration="0:0:0.270" />
|
||||
Duration="0:0:0.187" />
|
||||
<animations:TranslationAnimation
|
||||
From="24,0,0"
|
||||
To="0,0,0"
|
||||
Duration="0:0:0.280" />
|
||||
Duration="0:0:0.187" />
|
||||
</animations:Implicit.ShowAnimations>
|
||||
<animations:Implicit.HideAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="1.0"
|
||||
To="0"
|
||||
Duration="0:0:0.180" />
|
||||
Duration="0:0:0.187" />
|
||||
<animations:TranslationAnimation
|
||||
From="0,0,0"
|
||||
To="24,0,0"
|
||||
Duration="0:0:0.220" />
|
||||
Duration="0:0:0.187" />
|
||||
</animations:Implicit.HideAnimations>
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
@@ -339,10 +356,10 @@
|
||||
|
||||
<cpcontrols:IconBox
|
||||
x:Name="HeroImageBorder"
|
||||
MinWidth="64"
|
||||
MinHeight="64"
|
||||
MaxHeight="96"
|
||||
Width="64"
|
||||
Margin="16,8,16,16"
|
||||
HorizontalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
|
||||
Visibility="{x:Bind HasHeroImage, Mode=OneWay}" />
|
||||
@@ -350,8 +367,10 @@
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.Details.Title, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" />
|
||||
|
||||
|
||||
@@ -427,8 +427,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
|
||||
_settingsWindow.Activate();
|
||||
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.IgnoreShortcutWhenFullscreen, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
@@ -328,6 +328,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_LowLevelHook_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Try this if there are issues with the shortcut (Command Palette might not get focus when triggered from an elevated window)</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Ignore shortcut in fullscreen mode</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_GoHome_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Go home when activated</value>
|
||||
</data>
|
||||
@@ -379,9 +385,6 @@ 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="MoreCommandsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>More commands</value>
|
||||
</data>
|
||||
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open Command Palette settings</value>
|
||||
</data>
|
||||
@@ -391,10 +394,22 @@ 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="ContextFilterBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search commands...</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Show system tray icon</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_ShowSystemTrayIcon_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Choose if Command Palette is visible in the system tray</value>
|
||||
</data>
|
||||
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
</data>
|
||||
<data name="BackButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
</data>
|
||||
<data name="MoreCommandsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>More</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -3,7 +3,7 @@
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<PropertyGroup>
|
||||
<PathToRoot>..\..\..\..\</PathToRoot>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.6.250205002</WasdkNuget>
|
||||
<WasdkNuget>$(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250401001</WasdkNuget>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('$(WasdkNuget)\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
|
||||
@@ -34,7 +34,7 @@ internal sealed partial class AddBookmarkForm : FormContent
|
||||
"style": "text",
|
||||
"id": "name",
|
||||
"label": "{{Resources.bookmarks_form_name_label}}",
|
||||
"value": {{JsonSerializer.Serialize(name)}},
|
||||
"value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
|
||||
},
|
||||
@@ -42,7 +42,7 @@ internal sealed partial class AddBookmarkForm : FormContent
|
||||
"type": "Input.Text",
|
||||
"style": "text",
|
||||
"id": "bookmark",
|
||||
"value": {{JsonSerializer.Serialize(url)}},
|
||||
"value": {{JsonSerializer.Serialize(url, BookmarkSerializationContext.Default.String)}},
|
||||
"label": "{{Resources.bookmarks_form_bookmark_label}}",
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
[JsonSerializable(typeof(float))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(BookmarkData))]
|
||||
[JsonSerializable(typeof(Bookmarks))]
|
||||
[JsonSerializable(typeof(List<BookmarkData>), TypeInfoPropertyName = "BookmarkList")]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
||||
internal sealed partial class BookmarkSerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public sealed class Bookmarks
|
||||
|
||||
if (!string.IsNullOrEmpty(jsonStringReading))
|
||||
{
|
||||
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, _jsonOptions) ?? new Bookmarks();
|
||||
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class Bookmarks
|
||||
|
||||
public static void WriteToFile(string path, Bookmarks data)
|
||||
{
|
||||
var jsonString = JsonSerializer.Serialize(data, _jsonOptions);
|
||||
var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks);
|
||||
|
||||
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -16,7 +17,7 @@
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
@@ -24,7 +25,7 @@
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\Bookmark.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
@@ -39,5 +40,5 @@
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -2,176 +2,34 @@
|
||||
// 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.Data;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.CmdPal.Ext.Calc.Pages;
|
||||
using Microsoft.CmdPal.Ext.Calc.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc;
|
||||
|
||||
public partial class CalculatorCommandProvider : CommandProvider
|
||||
{
|
||||
private readonly ListItem _listItem = new(new CalculatorListPage()) { Subtitle = Resources.calculator_top_level_subtitle };
|
||||
private readonly FallbackCalculatorItem _fallback = new();
|
||||
private readonly ListItem _listItem = new(new CalculatorListPage(settings))
|
||||
{
|
||||
Subtitle = Resources.calculator_top_level_subtitle,
|
||||
MoreCommands = [new CommandContextItem(settings.Settings.SettingsPage)],
|
||||
};
|
||||
|
||||
private readonly FallbackCalculatorItem _fallback = new(settings);
|
||||
private static SettingsManager settings = new();
|
||||
|
||||
public CalculatorCommandProvider()
|
||||
{
|
||||
Id = "Calculator";
|
||||
DisplayName = Resources.calculator_display_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
|
||||
Icon = CalculatorIcons.ProviderIcon;
|
||||
Settings = settings.Settings;
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [_listItem];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() => [_fallback];
|
||||
}
|
||||
|
||||
// The calculator page is a dynamic list page
|
||||
// * The first command is where we display the results. Title=result, Subtitle=query
|
||||
// - The default command is `SaveCommand`.
|
||||
// - When you save, insert into list at spot 1
|
||||
// - change SearchText to the result
|
||||
// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard
|
||||
// * The rest of the items are previously saved results
|
||||
// - Command is a CopyCommand
|
||||
// - Each item also sets the TextToSuggest to the result
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
public sealed partial class CalculatorListPage : DynamicListPage
|
||||
{
|
||||
private readonly List<ListItem> _items = [];
|
||||
private readonly SaveCommand _saveCommand = new();
|
||||
private readonly CopyTextCommand _copyContextCommand;
|
||||
private readonly CommandContextItem _copyContextMenuItem;
|
||||
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.calculator_error);
|
||||
|
||||
public CalculatorListPage()
|
||||
{
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
|
||||
Name = Resources.calculator_title;
|
||||
PlaceholderText = Resources.calculator_placeholder_text;
|
||||
Id = "com.microsoft.cmdpal.calculator";
|
||||
|
||||
_copyContextCommand = new CopyTextCommand(string.Empty);
|
||||
_copyContextMenuItem = new CommandContextItem(_copyContextCommand);
|
||||
|
||||
_items.Add(new(_saveCommand) { Icon = new IconInfo("\uE94E") });
|
||||
|
||||
UpdateSearchText(string.Empty, string.Empty);
|
||||
|
||||
_saveCommand.SaveRequested += HandleSave;
|
||||
}
|
||||
|
||||
private void HandleSave(object sender, object args)
|
||||
{
|
||||
var lastResult = _items[0].Title;
|
||||
if (!string.IsNullOrEmpty(lastResult))
|
||||
{
|
||||
var li = new ListItem(new CopyTextCommand(lastResult))
|
||||
{
|
||||
Title = _items[0].Title,
|
||||
Subtitle = _items[0].Subtitle,
|
||||
TextToSuggest = lastResult,
|
||||
};
|
||||
_items.Insert(1, li);
|
||||
_items[0].Subtitle = string.Empty;
|
||||
SearchText = lastResult;
|
||||
this.RaiseItemsChanged(this._items.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
var firstItem = _items[0];
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
{
|
||||
firstItem.Title = Resources.calculator_placeholder_text;
|
||||
firstItem.Subtitle = string.Empty;
|
||||
firstItem.MoreCommands = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
_copyContextCommand.Text = ParseQuery(newSearch, out var result) ? result : string.Empty;
|
||||
firstItem.Title = result;
|
||||
firstItem.Subtitle = newSearch;
|
||||
firstItem.MoreCommands = [_copyContextMenuItem];
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool ParseQuery(string equation, out string result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resultNumber = new DataTable().Compute(equation, null);
|
||||
result = resultNumber.ToString() ?? string.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result = string.Format(CultureInfo.CurrentCulture, ErrorMessage, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items.ToArray();
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
public sealed partial class SaveCommand : InvokableCommand
|
||||
{
|
||||
public event TypedEventHandler<object, object> SaveRequested;
|
||||
|
||||
public SaveCommand()
|
||||
{
|
||||
Name = Resources.calculator_save_command_name;
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
SaveRequested?.Invoke(this, this);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
internal sealed partial class FallbackCalculatorItem : FallbackCommandItem
|
||||
{
|
||||
private readonly CopyTextCommand _copyCommand = new(string.Empty);
|
||||
private static readonly IconInfo _cachedIcon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
|
||||
|
||||
public FallbackCalculatorItem()
|
||||
: base(new NoOpCommand(), Resources.calculator_title)
|
||||
{
|
||||
Command = _copyCommand;
|
||||
_copyCommand.Name = string.Empty;
|
||||
Title = string.Empty;
|
||||
Subtitle = Resources.calculator_placeholder_text;
|
||||
Icon = _cachedIcon;
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
if (CalculatorListPage.ParseQuery(query, out var result))
|
||||
{
|
||||
_copyCommand.Text = result;
|
||||
_copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name;
|
||||
Title = result;
|
||||
|
||||
// we have to make the subtitle the equation,
|
||||
// so that we will still string match the original query
|
||||
// Otherwise, something like 1+2 will have a title of "3" and not match
|
||||
Subtitle = query;
|
||||
}
|
||||
else
|
||||
{
|
||||
_copyCommand.Text = string.Empty;
|
||||
_copyCommand.Name = string.Empty;
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public static class BracketHelper
|
||||
{
|
||||
public static bool IsBracketComplete(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var valueTuples = query
|
||||
.Select(BracketTrail)
|
||||
.Where(r => r != default);
|
||||
|
||||
var trailTest = new Stack<TrailType>();
|
||||
|
||||
foreach (var (direction, type) in valueTuples)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case TrailDirection.Open:
|
||||
trailTest.Push(type);
|
||||
break;
|
||||
case TrailDirection.Close:
|
||||
// Try to get item out of stack
|
||||
if (!trailTest.TryPop(out var popped))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type != popped)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
default:
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Can't process value (Parameter direction: {direction})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trailTest.Count == 0;
|
||||
}
|
||||
|
||||
private static (TrailDirection Direction, TrailType Type) BracketTrail(char @char)
|
||||
{
|
||||
switch (@char)
|
||||
{
|
||||
case '(':
|
||||
return (TrailDirection.Open, TrailType.Round);
|
||||
case ')':
|
||||
return (TrailDirection.Close, TrailType.Round);
|
||||
case '[':
|
||||
return (TrailDirection.Open, TrailType.Bracket);
|
||||
case ']':
|
||||
return (TrailDirection.Close, TrailType.Bracket);
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private enum TrailDirection
|
||||
{
|
||||
None,
|
||||
Open,
|
||||
Close,
|
||||
}
|
||||
|
||||
private enum TrailType
|
||||
{
|
||||
None,
|
||||
Bracket,
|
||||
Round,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using Mages.Core;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public static class CalculateEngine
|
||||
{
|
||||
private static readonly Engine _magesEngine = new Engine(new Configuration
|
||||
{
|
||||
Scope = new Dictionary<string, object>
|
||||
{
|
||||
{ "e", Math.E }, // e is not contained in the default mages engine
|
||||
},
|
||||
});
|
||||
|
||||
public const int RoundingDigits = 10;
|
||||
|
||||
public enum TrigMode
|
||||
{
|
||||
Radians,
|
||||
Degrees,
|
||||
Gradians,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interpret
|
||||
/// </summary>
|
||||
/// <param name="cultureInfo">Use CultureInfo.CurrentCulture if something is user facing</param>
|
||||
public static CalculateResult Interpret(SettingsManager settings, string input, CultureInfo cultureInfo, out string error)
|
||||
{
|
||||
error = default;
|
||||
|
||||
if (!CalculateHelper.InputValid(input))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
// check for division by zero
|
||||
// We check if the string contains a slash followed by space (optional) and zero. Whereas the zero must not be followed by a dot, comma, 'b', 'o' or 'x' as these indicate a number with decimal digits or a binary/octal/hexadecimal value respectively. The zero must also not be followed by other digits.
|
||||
if (new Regex("\\/\\s*0(?!(?:[,\\.0-9]|[box]0*[1-9a-f]))", RegexOptions.IgnoreCase).Match(input).Success)
|
||||
{
|
||||
error = Properties.Resources.calculator_division_by_zero;
|
||||
return default;
|
||||
}
|
||||
|
||||
// mages has quirky log representation
|
||||
// mage has log == ln vs log10
|
||||
input = input.
|
||||
Replace("log(", "log10(", true, CultureInfo.CurrentCulture).
|
||||
Replace("ln(", "log(", true, CultureInfo.CurrentCulture);
|
||||
|
||||
input = CalculateHelper.FixHumanMultiplicationExpressions(input);
|
||||
|
||||
// Get the user selected trigonometry unit
|
||||
TrigMode trigMode = settings.TrigUnit;
|
||||
|
||||
// Modify trig functions depending on angle unit setting
|
||||
input = CalculateHelper.UpdateTrigFunctions(input, trigMode);
|
||||
|
||||
// Expand conversions between trig units
|
||||
input = CalculateHelper.ExpandTrigConversions(input, trigMode);
|
||||
|
||||
var result = _magesEngine.Interpret(input);
|
||||
|
||||
// This could happen for some incorrect queries, like pi(2)
|
||||
if (result == null)
|
||||
{
|
||||
error = Properties.Resources.calculator_expression_not_complete;
|
||||
return default;
|
||||
}
|
||||
|
||||
result = TransformResult(result);
|
||||
if (result is string)
|
||||
{
|
||||
error = result as string;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(result?.ToString()))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var decimalResult = Convert.ToDecimal(result, cultureInfo);
|
||||
var roundedResult = Round(decimalResult);
|
||||
|
||||
return new CalculateResult()
|
||||
{
|
||||
Result = decimalResult,
|
||||
RoundedResult = roundedResult,
|
||||
};
|
||||
}
|
||||
|
||||
public static decimal Round(decimal value)
|
||||
{
|
||||
return Math.Round(value, RoundingDigits, MidpointRounding.AwayFromZero);
|
||||
}
|
||||
|
||||
private static dynamic TransformResult(object result)
|
||||
{
|
||||
if (result.ToString() == "NaN")
|
||||
{
|
||||
return Properties.Resources.calculator_not_a_number;
|
||||
}
|
||||
|
||||
if (result is Function)
|
||||
{
|
||||
return Properties.Resources.calculator_expression_not_complete;
|
||||
}
|
||||
|
||||
if (result is double[,])
|
||||
{
|
||||
// '[10,10]' is interpreted as array by mages engine
|
||||
return Properties.Resources.calculator_double_array_returned;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// 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.RegularExpressions;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public static class CalculateHelper
|
||||
{
|
||||
private static readonly Regex RegValidExpressChar = new Regex(
|
||||
@"^(" +
|
||||
@"%|" +
|
||||
@"ceil\s*\(|floor\s*\(|exp\s*\(|max\s*\(|min\s*\(|abs\s*\(|log(?:2|10)?\s*\(|ln\s*\(|sqrt\s*\(|pow\s*\(|" +
|
||||
@"factorial\s*\(|sign\s*\(|round\s*\(|rand\s*\(\)|randi\s*\([^\)]|" +
|
||||
@"sin\s*\(|cos\s*\(|tan\s*\(|arcsin\s*\(|arccos\s*\(|arctan\s*\(|" +
|
||||
@"sinh\s*\(|cosh\s*\(|tanh\s*\(|arsinh\s*\(|arcosh\s*\(|artanh\s*\(|" +
|
||||
@"rad\s*\(|deg\s*\(|grad\s*\(|" + /* trigonometry unit conversion macros */
|
||||
@"pi|" +
|
||||
@"==|~=|&&|\|\||" +
|
||||
@"((-?(\d+(\.\d*)?)|-?(\.\d+))[Ee](-?\d+))|" + /* expression from CheckScientificNotation between parenthesis */
|
||||
@"e|[0-9]|0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
|
||||
@")+$",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private const string DegToRad = "(pi / 180) * ";
|
||||
private const string DegToGrad = "(10 / 9) * ";
|
||||
private const string GradToRad = "(pi / 200) * ";
|
||||
private const string GradToDeg = "(9 / 10) * ";
|
||||
private const string RadToDeg = "(180 / pi) * ";
|
||||
private const string RadToGrad = "(200 / pi) * ";
|
||||
|
||||
public static bool InputValid(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
throw new ArgumentNullException(paramName: nameof(input));
|
||||
}
|
||||
|
||||
if (!RegValidExpressChar.IsMatch(input))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BracketHelper.IsBracketComplete(input))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the input ends with a binary operator then it is not a valid input to mages and the Interpret function would throw an exception. Because we expect here that the user has not finished typing we block those inputs.
|
||||
var trimmedInput = input.TrimEnd();
|
||||
if (trimmedInput.EndsWith('+') || trimmedInput.EndsWith('-') || trimmedInput.EndsWith('*') || trimmedInput.EndsWith('|') || trimmedInput.EndsWith('\\') || trimmedInput.EndsWith('^') || trimmedInput.EndsWith('=') || trimmedInput.EndsWith('&') || trimmedInput.EndsWith('/') || trimmedInput.EndsWith('%'))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string FixHumanMultiplicationExpressions(string input)
|
||||
{
|
||||
var output = CheckScientificNotation(input);
|
||||
output = CheckNumberOrConstantThenParenthesisExpr(output);
|
||||
output = CheckNumberOrConstantThenFunc(output);
|
||||
output = CheckParenthesisExprThenFunc(output);
|
||||
output = CheckParenthesisExprThenParenthesisExpr(output);
|
||||
output = CheckNumberThenConstant(output);
|
||||
output = CheckConstantThenConstant(output);
|
||||
return output;
|
||||
}
|
||||
|
||||
private static string CheckScientificNotation(string input)
|
||||
{
|
||||
/**
|
||||
* NOTE: By the time the expression gets to us, it's already in English format.
|
||||
*
|
||||
* Regex explanation:
|
||||
* (-?(\d+({0}\d*)?)|-?({0}\d+)): Used to capture one of two types:
|
||||
* -?(\d+({0}\d*)?): Captures a decimal number starting with a number (e.g. "-1.23")
|
||||
* -?({0}\d+): Captures a decimal number without leading number (e.g. ".23")
|
||||
* e: Captures 'e' or 'E'
|
||||
* (-?\d+): Captures an integer number (e.g. "-1" or "23")
|
||||
*/
|
||||
var p = @"(-?(\d+(\.\d*)?)|-?(\.\d+))e(-?\d+)";
|
||||
return Regex.Replace(input, p, "($1 * 10^($5))", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
/*
|
||||
* num (exp)
|
||||
* const (exp)
|
||||
*/
|
||||
private static string CheckNumberOrConstantThenParenthesisExpr(string input)
|
||||
{
|
||||
var output = input;
|
||||
do
|
||||
{
|
||||
input = output;
|
||||
output = Regex.Replace(input, @"(\d+|pi|e)\s*(\()", m =>
|
||||
{
|
||||
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
|
||||
{
|
||||
return m.Value;
|
||||
}
|
||||
|
||||
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
|
||||
});
|
||||
}
|
||||
while (output != input);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* num func
|
||||
* const func
|
||||
*/
|
||||
private static string CheckNumberOrConstantThenFunc(string input)
|
||||
{
|
||||
var output = input;
|
||||
do
|
||||
{
|
||||
input = output;
|
||||
output = Regex.Replace(input, @"(\d+|pi|e)\s*([a-zA-Z]+[0-9]*\s*\()", m =>
|
||||
{
|
||||
if (input[m.Index] == 'e' && input[m.Index + 1] == 'x' && input[m.Index + 2] == 'p')
|
||||
{
|
||||
return m.Value;
|
||||
}
|
||||
|
||||
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
|
||||
{
|
||||
return m.Value;
|
||||
}
|
||||
|
||||
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
|
||||
});
|
||||
}
|
||||
while (output != input);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* (exp) func
|
||||
* func func
|
||||
*/
|
||||
private static string CheckParenthesisExprThenFunc(string input)
|
||||
{
|
||||
var p = @"(\))\s*([a-zA-Z]+[0-9]*\s*\()";
|
||||
var r = "$1 * $2";
|
||||
return Regex.Replace(input, p, r);
|
||||
}
|
||||
|
||||
/*
|
||||
* (exp) (exp)
|
||||
* func (exp)
|
||||
*/
|
||||
private static string CheckParenthesisExprThenParenthesisExpr(string input)
|
||||
{
|
||||
var p = @"(\))\s*(\()";
|
||||
var r = "$1 * $2";
|
||||
return Regex.Replace(input, p, r);
|
||||
}
|
||||
|
||||
/*
|
||||
* num const
|
||||
*/
|
||||
private static string CheckNumberThenConstant(string input)
|
||||
{
|
||||
var output = input;
|
||||
do
|
||||
{
|
||||
input = output;
|
||||
output = Regex.Replace(input, @"(\d+)\s*(pi|e)", m =>
|
||||
{
|
||||
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
|
||||
{
|
||||
return m.Value;
|
||||
}
|
||||
|
||||
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
|
||||
});
|
||||
}
|
||||
while (output != input);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/*
|
||||
* const const
|
||||
*/
|
||||
private static string CheckConstantThenConstant(string input)
|
||||
{
|
||||
var output = input;
|
||||
do
|
||||
{
|
||||
input = output;
|
||||
output = Regex.Replace(input, @"(pi|e)\s*(pi|e)", m =>
|
||||
{
|
||||
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
|
||||
{
|
||||
return m.Value;
|
||||
}
|
||||
|
||||
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
|
||||
});
|
||||
}
|
||||
while (output != input);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Gets the index of the closing bracket of a function
|
||||
private static int FindClosingBracketIndex(string input, int start)
|
||||
{
|
||||
var bracketCount = 0; // Set count to zero
|
||||
for (var i = start; i < input.Length; i++)
|
||||
{
|
||||
if (input[i] == '(')
|
||||
{
|
||||
bracketCount++;
|
||||
}
|
||||
else if (input[i] == ')')
|
||||
{
|
||||
bracketCount--;
|
||||
if (bracketCount == 0)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // Unmatched brackets
|
||||
}
|
||||
|
||||
private static string ModifyTrigFunction(string input, string function, string modification)
|
||||
{
|
||||
// Get the RegEx pattern to match, depending on whether the function is inverse or normal
|
||||
var pattern = function.StartsWith("arc", StringComparison.Ordinal) ? string.Empty : @"(?<!c)";
|
||||
pattern += $@"{function}\s*\(";
|
||||
|
||||
var index = 0; // Index for match to ensure that the same match is not found twice
|
||||
|
||||
Regex regex = new Regex(pattern);
|
||||
Match match;
|
||||
|
||||
while ((match = regex.Match(input, index)).Success)
|
||||
{
|
||||
index = match.Index + match.Groups[0].Length + modification.Length; // Get the next index to look from for further matches
|
||||
|
||||
var endIndex = FindClosingBracketIndex(input, match.Index + match.Groups[0].Length - 1); // Find the index of the closing bracket of the function
|
||||
|
||||
// If no valid bracket index was found, try the next match
|
||||
if (endIndex == -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var argument = input.Substring(match.Index + match.Groups[0].Length, endIndex - (match.Index + match.Groups[0].Length)); // Extract the argument between the brackets
|
||||
var replaced = function.StartsWith("arc", StringComparison.Ordinal) ? $"{modification}({match.Groups[0].Value}{argument}))" : $"{match.Groups[0].Value}{modification}({argument}))"; // The string to substitute in, handles differing formats of inverse functions
|
||||
|
||||
input = input.Remove(match.Index, endIndex - match.Index + 1); // Remove the match from the input
|
||||
input = input.Insert(match.Index, replaced); // Substitute with the new string
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
public static string UpdateTrigFunctions(string input, CalculateEngine.TrigMode mode)
|
||||
{
|
||||
var modifiedInput = input;
|
||||
if (mode == CalculateEngine.TrigMode.Degrees)
|
||||
{
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "sin", DegToRad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "cos", DegToRad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "tan", DegToRad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "arcsin", RadToDeg);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "arccos", RadToDeg);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "arctan", RadToDeg);
|
||||
}
|
||||
else if (mode == CalculateEngine.TrigMode.Gradians)
|
||||
{
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "sin", GradToRad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "cos", GradToRad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "tan", GradToRad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "arcsin", RadToGrad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "arccos", RadToGrad);
|
||||
modifiedInput = ModifyTrigFunction(modifiedInput, "arctan", RadToGrad);
|
||||
}
|
||||
|
||||
return modifiedInput;
|
||||
}
|
||||
|
||||
private static string ModifyMathFunction(string input, string function, string modification)
|
||||
{
|
||||
// Create the pattern to match the function, opening bracket, and any spaces in between
|
||||
var pattern = $@"{function}\s*\(";
|
||||
return Regex.Replace(input, pattern, modification + "(");
|
||||
}
|
||||
|
||||
public static string ExpandTrigConversions(string input, CalculateEngine.TrigMode mode)
|
||||
{
|
||||
var modifiedInput = input;
|
||||
|
||||
// Expand "rad", "deg" and "grad" to their respective conversions for the current trig unit
|
||||
if (mode == CalculateEngine.TrigMode.Radians)
|
||||
{
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "deg", DegToRad);
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "grad", GradToRad);
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "rad", string.Empty);
|
||||
}
|
||||
else if (mode == CalculateEngine.TrigMode.Degrees)
|
||||
{
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "deg", string.Empty);
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "grad", GradToDeg);
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "rad", RadToDeg);
|
||||
}
|
||||
else if (mode == CalculateEngine.TrigMode.Gradians)
|
||||
{
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "deg", DegToGrad);
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "grad", string.Empty);
|
||||
modifiedInput = ModifyMathFunction(modifiedInput, "rad", RadToGrad);
|
||||
}
|
||||
|
||||
return modifiedInput;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public struct CalculateResult : IEquatable<CalculateResult>
|
||||
{
|
||||
public decimal? Result { get; set; }
|
||||
|
||||
public decimal? RoundedResult { get; set; }
|
||||
|
||||
public bool Equals(CalculateResult other)
|
||||
{
|
||||
return Result == other.Result && RoundedResult == other.RoundedResult;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is CalculateResult other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Result, RoundedResult);
|
||||
}
|
||||
|
||||
public static bool operator ==(CalculateResult left, CalculateResult right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(CalculateResult left, CalculateResult right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
@@ -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.Ext.Calc.Helper;
|
||||
|
||||
public static class CalculatorIcons
|
||||
{
|
||||
public static IconInfo ResultIcon => new("\uE94E");
|
||||
|
||||
public static IconInfo SaveIcon => new("\uE74E");
|
||||
|
||||
public static IconInfo ErrorIcon => new("\uE783");
|
||||
|
||||
public static IconInfo ProviderIcon => IconHelpers.FromRelativePath("Assets\\Calculator.svg");
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
internal static class ErrorHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Method to handles errors while calculating
|
||||
/// </summary>
|
||||
/// <param name="isFallbackSearch">Bool to indicate if it is a fallback query.</param>
|
||||
/// <param name="queryInput">User input as string including the action keyword.</param>
|
||||
/// <param name="errorMessage">Error message if applicable.</param>
|
||||
/// <param name="exception">Exception if applicable.</param>
|
||||
/// <returns>List of results to show. Either an error message or an empty list.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if <paramref name="errorMessage"/> and <paramref name="exception"/> are both filled with their default values.</exception>
|
||||
internal static ListItem OnError(bool isFallbackSearch, string queryInput, string errorMessage, Exception exception = default)
|
||||
{
|
||||
string userMessage;
|
||||
|
||||
if (errorMessage != default)
|
||||
{
|
||||
Logger.LogError($"Failed to calculate <{queryInput}>: {errorMessage}");
|
||||
userMessage = errorMessage;
|
||||
}
|
||||
else if (exception != default)
|
||||
{
|
||||
Logger.LogError($"Exception when query for <{queryInput}>", exception);
|
||||
userMessage = exception.Message;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("The arguments error and exception have default values. One of them has to be filled with valid error data (error message/exception)!");
|
||||
}
|
||||
|
||||
return isFallbackSearch ? null : CreateErrorResult(userMessage);
|
||||
}
|
||||
|
||||
private static ListItem CreateErrorResult(string errorMessage)
|
||||
{
|
||||
return new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = Properties.Resources.calculator_calculation_failed_title,
|
||||
Subtitle = errorMessage,
|
||||
Icon = CalculatorIcons.ErrorIcon,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to convert all numbers in a text from one culture format to another.
|
||||
/// </summary>
|
||||
public class NumberTranslator
|
||||
{
|
||||
private readonly CultureInfo sourceCulture;
|
||||
private readonly CultureInfo targetCulture;
|
||||
private readonly Regex splitRegexForSource;
|
||||
private readonly Regex splitRegexForTarget;
|
||||
|
||||
private NumberTranslator(CultureInfo sourceCulture, CultureInfo targetCulture)
|
||||
{
|
||||
this.sourceCulture = sourceCulture;
|
||||
this.targetCulture = targetCulture;
|
||||
|
||||
splitRegexForSource = GetSplitRegex(this.sourceCulture);
|
||||
splitRegexForTarget = GetSplitRegex(this.targetCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="NumberTranslator"/>.
|
||||
/// </summary>
|
||||
/// <param name="sourceCulture">source culture</param>
|
||||
/// <param name="targetCulture">target culture</param>
|
||||
/// <returns>Number translator for target culture</returns>
|
||||
public static NumberTranslator Create(CultureInfo sourceCulture, CultureInfo targetCulture)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sourceCulture);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(targetCulture);
|
||||
|
||||
return new NumberTranslator(sourceCulture, targetCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translate from source to target culture.
|
||||
/// </summary>
|
||||
/// <param name="input">input string to translate</param>
|
||||
/// <returns>translated string</returns>
|
||||
public string Translate(string input)
|
||||
{
|
||||
return Translate(input, sourceCulture, targetCulture, splitRegexForSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translate from target to source culture.
|
||||
/// </summary>
|
||||
/// <param name="input">input string to translate back to source culture</param>
|
||||
/// <returns>source culture string</returns>
|
||||
public string TranslateBack(string input)
|
||||
{
|
||||
return Translate(input, targetCulture, sourceCulture, splitRegexForTarget);
|
||||
}
|
||||
|
||||
private static string Translate(string input, CultureInfo cultureFrom, CultureInfo cultureTo, Regex splitRegex)
|
||||
{
|
||||
var outputBuilder = new StringBuilder();
|
||||
var hexRegex = new Regex(@"(?:(0x[\da-fA-F]+))");
|
||||
|
||||
var hexTokens = hexRegex.Split(input);
|
||||
|
||||
foreach (var hexToken in hexTokens)
|
||||
{
|
||||
if (hexToken.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
// Mages engine has issues processing large hex number (larger than 7 hex digits + 0x prefix = 9 characters). So we convert it to decimal and pass it to the engine.
|
||||
if (hexToken.Length > 9)
|
||||
{
|
||||
try
|
||||
{
|
||||
var num = Convert.ToInt64(hexToken, 16);
|
||||
var numStr = num.ToString(cultureFrom);
|
||||
outputBuilder.Append(numStr);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
outputBuilder.Append(hexToken);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
outputBuilder.Append(hexToken);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var tokens = splitRegex.Split(hexToken);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
var leadingZeroCount = 0;
|
||||
|
||||
// Count leading zero characters.
|
||||
foreach (var c in token)
|
||||
{
|
||||
if (c != '0')
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
leadingZeroCount++;
|
||||
}
|
||||
|
||||
// number is all zero characters. no need to add zero characters at the end.
|
||||
if (token.Length == leadingZeroCount)
|
||||
{
|
||||
leadingZeroCount = 0;
|
||||
}
|
||||
|
||||
decimal number;
|
||||
|
||||
outputBuilder.Append(
|
||||
decimal.TryParse(token, NumberStyles.Number, cultureFrom, out number)
|
||||
? (new string('0', leadingZeroCount) + number.ToString(cultureTo))
|
||||
: token.Replace(cultureFrom.TextInfo.ListSeparator, cultureTo.TextInfo.ListSeparator));
|
||||
}
|
||||
}
|
||||
|
||||
return outputBuilder.ToString();
|
||||
}
|
||||
|
||||
private static Regex GetSplitRegex(CultureInfo culture)
|
||||
{
|
||||
var splitPattern = $"((?:\\d|{Regex.Escape(culture.NumberFormat.NumberDecimalSeparator)}";
|
||||
if (!string.IsNullOrEmpty(culture.NumberFormat.NumberGroupSeparator))
|
||||
{
|
||||
splitPattern += $"|{Regex.Escape(culture.NumberFormat.NumberGroupSeparator)}";
|
||||
}
|
||||
|
||||
splitPattern += ")+)";
|
||||
return new Regex(splitPattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public static partial class QueryHelper
|
||||
{
|
||||
public static ListItem Query(string query, SettingsManager settings, bool isFallbackSearch, TypedEventHandler<object, object> handleSave = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
if (!isFallbackSearch)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handleSave);
|
||||
}
|
||||
|
||||
CultureInfo inputCulture = settings.InputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
|
||||
CultureInfo outputCulture = settings.OutputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
|
||||
|
||||
// Happens if the user has only typed the action key so far
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
NumberTranslator translator = NumberTranslator.Create(inputCulture, new CultureInfo("en-US"));
|
||||
var input = translator.Translate(query.Normalize(NormalizationForm.FormKC));
|
||||
|
||||
if (!CalculateHelper.InputValid(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Using CurrentUICulture since this is user facing
|
||||
var result = CalculateEngine.Interpret(settings, input, outputCulture, out var errorMessage);
|
||||
|
||||
// This could happen for some incorrect queries, like pi(2)
|
||||
if (result.Equals(default(CalculateResult)))
|
||||
{
|
||||
// If errorMessage is not default then do error handling
|
||||
return errorMessage == default ? null : ErrorHandler.OnError(isFallbackSearch, query, errorMessage);
|
||||
}
|
||||
|
||||
if (isFallbackSearch)
|
||||
{
|
||||
// Fallback search
|
||||
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query);
|
||||
}
|
||||
|
||||
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query, handleSave);
|
||||
}
|
||||
catch (Mages.Core.ParseException)
|
||||
{
|
||||
// Invalid input
|
||||
return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_expression_not_complete);
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
// Result to big to convert to decimal
|
||||
return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_not_covert_to_decimal);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Any other crash occurred
|
||||
// We want to keep the process alive if any the mages library throws any exceptions.
|
||||
return ErrorHandler.OnError(isFallbackSearch, query, default, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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.Globalization;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public static class ResultHelper
|
||||
{
|
||||
public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, TypedEventHandler<object, object> handleSave)
|
||||
{
|
||||
// Return null when the expression is not a valid calculator query.
|
||||
if (roundedResult == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = roundedResult?.ToString(outputCulture);
|
||||
|
||||
// Create a SaveCommand and subscribe to the SaveRequested event
|
||||
// This can append the result to the history list.
|
||||
var saveCommand = new SaveCommand(result);
|
||||
saveCommand.SaveRequested += handleSave;
|
||||
|
||||
var copyCommandItem = CreateResult(roundedResult, inputCulture, outputCulture, query);
|
||||
|
||||
return new ListItem(saveCommand)
|
||||
{
|
||||
// Using CurrentCulture since this is user facing
|
||||
Icon = CalculatorIcons.ResultIcon,
|
||||
Title = result,
|
||||
Subtitle = query,
|
||||
TextToSuggest = result,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(copyCommandItem.Command)
|
||||
{
|
||||
Icon = copyCommandItem.Icon,
|
||||
Title = copyCommandItem.Title,
|
||||
Subtitle = copyCommandItem.Subtitle,
|
||||
},
|
||||
..copyCommandItem.MoreCommands,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query)
|
||||
{
|
||||
// Return null when the expression is not a valid calculator query.
|
||||
if (roundedResult == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var decimalResult = roundedResult?.ToString(outputCulture);
|
||||
|
||||
List<CommandContextItem> context = [];
|
||||
|
||||
if (decimal.IsInteger((decimal)roundedResult))
|
||||
{
|
||||
var i = decimal.ToInt64((decimal)roundedResult);
|
||||
try
|
||||
{
|
||||
var hexResult = "0x" + i.ToString("X", outputCulture);
|
||||
context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex })
|
||||
{
|
||||
Title = hexResult,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error parsing hex format", ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var binaryResult = "0b" + i.ToString("B", outputCulture);
|
||||
context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary })
|
||||
{
|
||||
Title = binaryResult,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error parsing binary format", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return new ListItem(new CopyTextCommand(decimalResult))
|
||||
{
|
||||
// Using CurrentCulture since this is user facing
|
||||
Title = decimalResult,
|
||||
Subtitle = query,
|
||||
TextToSuggest = decimalResult,
|
||||
MoreCommands = context.ToArray(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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.CmdPal.Ext.Calc.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public sealed partial class SaveCommand : InvokableCommand
|
||||
{
|
||||
private readonly string _result;
|
||||
|
||||
public event TypedEventHandler<object, object> SaveRequested;
|
||||
|
||||
public SaveCommand(string result)
|
||||
{
|
||||
Name = Resources.calculator_save_command_name;
|
||||
Icon = CalculatorIcons.SaveIcon;
|
||||
_result = result;
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
SaveRequested?.Invoke(this, this);
|
||||
ClipboardHelper.SetText(_result);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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.IO;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
|
||||
public class SettingsManager : JsonSettingsManager
|
||||
{
|
||||
private static readonly string _namespace = "calculator";
|
||||
|
||||
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
|
||||
|
||||
private static readonly List<ChoiceSetSetting.Choice> _trigUnitChoices = new()
|
||||
{
|
||||
new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_radians, "0"),
|
||||
new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_degrees, "1"),
|
||||
new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_gradians, "2"),
|
||||
};
|
||||
|
||||
private readonly ChoiceSetSetting _trigUnit = new(
|
||||
Namespaced(nameof(TrigUnit)),
|
||||
Properties.Resources.calculator_settings_trig_unit_mode,
|
||||
Properties.Resources.calculator_settings_trig_unit_mode_description,
|
||||
_trigUnitChoices);
|
||||
|
||||
private readonly ToggleSetting _inputUseEnNumberFormat = new(
|
||||
Namespaced(nameof(InputUseEnglishFormat)),
|
||||
Properties.Resources.calculator_settings_in_en_format,
|
||||
Properties.Resources.calculator_settings_in_en_format_description,
|
||||
false);
|
||||
|
||||
private readonly ToggleSetting _outputUseEnNumberFormat = new(
|
||||
Namespaced(nameof(OutputUseEnglishFormat)),
|
||||
Properties.Resources.calculator_settings_out_en_format,
|
||||
Properties.Resources.calculator_settings_out_en_format_description,
|
||||
false);
|
||||
|
||||
public CalculateEngine.TrigMode TrigUnit
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_trigUnit.Value == null || string.IsNullOrEmpty(_trigUnit.Value))
|
||||
{
|
||||
return CalculateEngine.TrigMode.Radians;
|
||||
}
|
||||
|
||||
var success = int.TryParse(_trigUnit.Value, out var result);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return CalculateEngine.TrigMode.Radians;
|
||||
}
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case 0:
|
||||
return CalculateEngine.TrigMode.Radians;
|
||||
case 1:
|
||||
return CalculateEngine.TrigMode.Degrees;
|
||||
case 2:
|
||||
return CalculateEngine.TrigMode.Gradians;
|
||||
default:
|
||||
return CalculateEngine.TrigMode.Radians;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool InputUseEnglishFormat => _inputUseEnNumberFormat.Value;
|
||||
|
||||
public bool OutputUseEnglishFormat => _outputUseEnNumberFormat.Value;
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
|
||||
Settings.Add(_trigUnit);
|
||||
Settings.Add(_inputUseEnNumberFormat);
|
||||
Settings.Add(_outputUseEnNumberFormat);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Calc</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
@@ -9,9 +10,14 @@
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Ext.Calc.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Mages" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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.Threading;
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.CmdPal.Ext.Calc.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Pages;
|
||||
|
||||
// The calculator page is a dynamic list page
|
||||
// * The first command is where we display the results. Title=result, Subtitle=query
|
||||
// - The default command is `SaveCommand`.
|
||||
// - When you save, insert into list at spot 1
|
||||
// - change SearchText to the result
|
||||
// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard
|
||||
// * The rest of the items are previously saved results
|
||||
// - Command is a CopyCommand
|
||||
// - Each item also sets the TextToSuggest to the result
|
||||
public sealed partial class CalculatorListPage : DynamicListPage
|
||||
{
|
||||
private readonly Lock _resultsLock = new();
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly List<ListItem> _items = [];
|
||||
private readonly List<ListItem> history = [];
|
||||
private readonly ListItem _emptyItem;
|
||||
|
||||
// This is the text that saved when the user click the result.
|
||||
// We need to avoid the double calculation. This may cause some wierd behaviors.
|
||||
private string skipQuerySearchText = string.Empty;
|
||||
|
||||
public CalculatorListPage(SettingsManager settings)
|
||||
{
|
||||
_settingsManager = settings;
|
||||
Icon = CalculatorIcons.ProviderIcon;
|
||||
Name = Resources.calculator_title;
|
||||
PlaceholderText = Resources.calculator_placeholder_text;
|
||||
Id = "com.microsoft.cmdpal.calculator";
|
||||
|
||||
_emptyItem = new ListItem(new NoOpCommand())
|
||||
{
|
||||
Title = Resources.calculator_placeholder_text,
|
||||
Icon = CalculatorIcons.ResultIcon,
|
||||
};
|
||||
EmptyContent = new CommandItem(new NoOpCommand())
|
||||
{
|
||||
Icon = CalculatorIcons.ProviderIcon,
|
||||
Title = Resources.calculator_placeholder_text,
|
||||
};
|
||||
|
||||
UpdateSearchText(string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
if (oldSearch == newSearch)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(skipQuerySearchText) && newSearch == skipQuerySearchText)
|
||||
{
|
||||
// only skip once.
|
||||
skipQuerySearchText = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
skipQuerySearchText = string.Empty;
|
||||
|
||||
_emptyItem.Subtitle = newSearch;
|
||||
|
||||
var result = QueryHelper.Query(newSearch, _settingsManager, false, HandleSave);
|
||||
UpdateResult(result);
|
||||
}
|
||||
|
||||
private void UpdateResult(ListItem result)
|
||||
{
|
||||
lock (_resultsLock)
|
||||
{
|
||||
this._items.Clear();
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
this._items.Add(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
_items.Add(_emptyItem);
|
||||
}
|
||||
|
||||
this._items.AddRange(history);
|
||||
}
|
||||
|
||||
RaiseItemsChanged(this._items.Count);
|
||||
}
|
||||
|
||||
private void HandleSave(object sender, object args)
|
||||
{
|
||||
var lastResult = _items[0].Title;
|
||||
if (!string.IsNullOrEmpty(lastResult))
|
||||
{
|
||||
var li = new ListItem(new CopyTextCommand(lastResult))
|
||||
{
|
||||
Title = _items[0].Title,
|
||||
Subtitle = _items[0].Subtitle,
|
||||
TextToSuggest = lastResult,
|
||||
};
|
||||
|
||||
history.Insert(0, li);
|
||||
_items.Insert(1, li);
|
||||
|
||||
// Why we need to clean the query record? Removed, but if necessary, please move it back.
|
||||
// _items[0].Subtitle = string.Empty;
|
||||
|
||||
// this change will call the UpdateSearchText again.
|
||||
// We need to avoid it.
|
||||
skipQuerySearchText = lastResult;
|
||||
SearchText = lastResult;
|
||||
this.RaiseItemsChanged(this._items.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items.ToArray();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Calc.Helper;
|
||||
using Microsoft.CmdPal.Ext.Calc.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc.Pages;
|
||||
|
||||
public sealed partial class FallbackCalculatorItem : FallbackCommandItem
|
||||
{
|
||||
private readonly CopyTextCommand _copyCommand = new(string.Empty);
|
||||
private readonly SettingsManager _settings;
|
||||
|
||||
public FallbackCalculatorItem(SettingsManager settings)
|
||||
: base(new NoOpCommand(), Resources.calculator_title)
|
||||
{
|
||||
Command = _copyCommand;
|
||||
_copyCommand.Name = string.Empty;
|
||||
Title = string.Empty;
|
||||
Subtitle = Resources.calculator_placeholder_text;
|
||||
Icon = CalculatorIcons.ProviderIcon;
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
var result = QueryHelper.Query(query, _settings, true, null);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
_copyCommand.Text = string.Empty;
|
||||
_copyCommand.Name = string.Empty;
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
MoreCommands = [];
|
||||
return;
|
||||
}
|
||||
|
||||
_copyCommand.Text = result.Title;
|
||||
_copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name;
|
||||
Title = result.Title;
|
||||
|
||||
// we have to make the subtitle the equation,
|
||||
// so that we will still string match the original query
|
||||
// Otherwise, something like 1+2 will have a title of "3" and not match
|
||||
Subtitle = query;
|
||||
|
||||
MoreCommands = result.MoreCommands;
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to calculate the input.
|
||||
/// </summary>
|
||||
public static string calculator_calculation_failed_title {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_calculation_failed_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy binary.
|
||||
/// </summary>
|
||||
public static string calculator_copy_binary {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_copy_binary", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy.
|
||||
/// </summary>
|
||||
@@ -69,6 +87,15 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy hexadecimal.
|
||||
/// </summary>
|
||||
public static string calculator_copy_hex {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_copy_hex", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Calculator.
|
||||
/// </summary>
|
||||
@@ -78,6 +105,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Expression contains division by zero.
|
||||
/// </summary>
|
||||
public static string calculator_division_by_zero {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_division_by_zero", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unsupported use of square brackets.
|
||||
/// </summary>
|
||||
public static string calculator_double_array_returned {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_double_array_returned", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error: {0}.
|
||||
/// </summary>
|
||||
@@ -87,6 +132,33 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Expression wrong or incomplete.
|
||||
/// </summary>
|
||||
public static string calculator_expression_not_complete {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_expression_not_complete", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Calculation result is not a valid number (NaN).
|
||||
/// </summary>
|
||||
public static string calculator_not_a_number {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_not_a_number", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Result value was either too large or too small for a decimal number.
|
||||
/// </summary>
|
||||
public static string calculator_not_covert_to_decimal {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_not_covert_to_decimal", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Type an equation....
|
||||
/// </summary>
|
||||
@@ -105,6 +177,105 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use English (United States) number format for input.
|
||||
/// </summary>
|
||||
public static string calculator_settings_in_en_format {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_in_en_format", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ignores your system setting and expects numbers in the format '{0}'..
|
||||
/// </summary>
|
||||
public static string calculator_settings_in_en_format_description {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_in_en_format_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Use English (United States) number format for output.
|
||||
/// </summary>
|
||||
public static string calculator_settings_out_en_format {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_out_en_format", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Ignores your system setting and returns numbers in the format '{0}'..
|
||||
/// </summary>
|
||||
public static string calculator_settings_out_en_format_description {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_out_en_format_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Replace input if query ends with '='.
|
||||
/// </summary>
|
||||
public static string calculator_settings_replace_input {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_replace_input", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to When using direct activation, appending '=' to the expression will replace the input with the calculated result (e.g. '=5*3-2=' will change the query to '=13')..
|
||||
/// </summary>
|
||||
public static string calculator_settings_replace_input_description {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_replace_input_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Degrees.
|
||||
/// </summary>
|
||||
public static string calculator_settings_trig_unit_degrees {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_trig_unit_degrees", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Gradians.
|
||||
/// </summary>
|
||||
public static string calculator_settings_trig_unit_gradians {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_trig_unit_gradians", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Trigonometry Unit.
|
||||
/// </summary>
|
||||
public static string calculator_settings_trig_unit_mode {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_trig_unit_mode", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Specifies the angle unit to use for trigonometry operations.
|
||||
/// </summary>
|
||||
public static string calculator_settings_trig_unit_mode_description {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_trig_unit_mode_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Radians.
|
||||
/// </summary>
|
||||
public static string calculator_settings_trig_unit_radians {
|
||||
get {
|
||||
return ResourceManager.GetString("calculator_settings_trig_unit_radians", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Calculator.
|
||||
/// </summary>
|
||||
|
||||
@@ -140,4 +140,63 @@
|
||||
<data name="calculator_copy_command_name" xml:space="preserve">
|
||||
<value>Copy</value>
|
||||
</data>
|
||||
<data name="calculator_calculation_failed_title" xml:space="preserve">
|
||||
<value>Failed to calculate the input</value>
|
||||
</data>
|
||||
<data name="calculator_division_by_zero" xml:space="preserve">
|
||||
<value>Expression contains division by zero</value>
|
||||
</data>
|
||||
<data name="calculator_expression_not_complete" xml:space="preserve">
|
||||
<value>Expression wrong or incomplete</value>
|
||||
</data>
|
||||
<data name="calculator_not_a_number" xml:space="preserve">
|
||||
<value>Calculation result is not a valid number (NaN)</value>
|
||||
</data>
|
||||
<data name="calculator_double_array_returned" xml:space="preserve">
|
||||
<value>Unsupported use of square brackets</value>
|
||||
</data>
|
||||
<data name="calculator_settings_trig_unit_gradians" xml:space="preserve">
|
||||
<value>Gradians</value>
|
||||
</data>
|
||||
<data name="calculator_settings_trig_unit_degrees" xml:space="preserve">
|
||||
<value>Degrees</value>
|
||||
</data>
|
||||
<data name="calculator_settings_trig_unit_radians" xml:space="preserve">
|
||||
<value>Radians</value>
|
||||
</data>
|
||||
<data name="calculator_settings_trig_unit_mode" xml:space="preserve">
|
||||
<value>Trigonometry Unit</value>
|
||||
</data>
|
||||
<data name="calculator_settings_trig_unit_mode_description" xml:space="preserve">
|
||||
<value>Specifies the angle unit to use for trigonometry operations</value>
|
||||
</data>
|
||||
<data name="calculator_settings_out_en_format" xml:space="preserve">
|
||||
<value>Use English (United States) number format for output</value>
|
||||
</data>
|
||||
<data name="calculator_settings_out_en_format_description" xml:space="preserve">
|
||||
<value>Ignores your system setting and returns numbers in the format '{0}'.</value>
|
||||
<comment>{0} is a placeholder and will be replaced in code.</comment>
|
||||
</data>
|
||||
<data name="calculator_settings_in_en_format" xml:space="preserve">
|
||||
<value>Use English (United States) number format for input</value>
|
||||
</data>
|
||||
<data name="calculator_settings_in_en_format_description" xml:space="preserve">
|
||||
<value>Ignores your system setting and expects numbers in the format '{0}'.</value>
|
||||
<comment>{0} is a placeholder and will be replaced in code.</comment>
|
||||
</data>
|
||||
<data name="calculator_settings_replace_input" xml:space="preserve">
|
||||
<value>Replace input if query ends with '='</value>
|
||||
</data>
|
||||
<data name="calculator_settings_replace_input_description" xml:space="preserve">
|
||||
<value>When using direct activation, appending '=' to the expression will replace the input with the calculated result (e.g. '=5*3-2=' will change the query to '=13').</value>
|
||||
</data>
|
||||
<data name="calculator_not_covert_to_decimal" xml:space="preserve">
|
||||
<value>Result value was either too large or too small for a decimal number</value>
|
||||
</data>
|
||||
<data name="calculator_copy_hex" xml:space="preserve">
|
||||
<value>Copy hexadecimal</value>
|
||||
</data>
|
||||
<data name="calculator_copy_binary" xml:space="preserve">
|
||||
<value>Copy binary</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -17,7 +17,7 @@ internal static class NativeMethods
|
||||
internal INPUTTYPE type;
|
||||
internal InputUnion data;
|
||||
|
||||
internal static int Size => Marshal.SizeOf(typeof(INPUT));
|
||||
internal static int Size => Marshal.SizeOf<INPUT>();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.ClipboardHistory</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Registry</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Shell</RootNamespace>
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.System.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell;
|
||||
namespace Microsoft.CmdPal.Ext.System;
|
||||
|
||||
public sealed partial class EmptyRecycleBinCommand : InvokableCommand
|
||||
{
|
||||
|
||||
@@ -2,7 +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 Microsoft.CmdPal.Ext.Shell;
|
||||
using Microsoft.CmdPal.Ext.System.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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 Microsoft.CmdPal.Ext.System.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.System;
|
||||
|
||||
internal sealed partial class FallbackSystemCommandItem : FallbackCommandItem
|
||||
{
|
||||
public FallbackSystemCommandItem(SettingsManager settings)
|
||||
: base(new NoOpCommand(), Resources.Microsoft_plugin_ext_fallback_display_title)
|
||||
{
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
|
||||
var isBootedInUefiMode = Win32Helpers.GetSystemFirmwareType() == FirmwareType.Uefi;
|
||||
var hideEmptyRB = settings.HideEmptyRecycleBin;
|
||||
var confirmSystemCommands = settings.ShowDialogToConfirmCommand;
|
||||
var showSuccessOnEmptyRB = settings.ShowSuccessMessageAfterEmptyingRecycleBin;
|
||||
|
||||
systemCommands = Commands.GetSystemCommands(isBootedInUefiMode, hideEmptyRB, confirmSystemCommands, showSuccessOnEmptyRB);
|
||||
}
|
||||
|
||||
private readonly List<IListItem> systemCommands;
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
IListItem? result = null;
|
||||
var resultScore = 0;
|
||||
|
||||
// find the max score for the query
|
||||
foreach (var command in systemCommands)
|
||||
{
|
||||
var title = command.Title;
|
||||
var subTitle = command.Subtitle;
|
||||
var titleScore = StringMatcher.FuzzySearch(query, title).Score;
|
||||
var subTitleScore = StringMatcher.FuzzySearch(query, subTitle).Score;
|
||||
|
||||
var maxScore = Math.Max(titleScore, subTitleScore);
|
||||
if (maxScore > resultScore)
|
||||
{
|
||||
resultScore = maxScore;
|
||||
result = command;
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Title = result.Title;
|
||||
Subtitle = result.Subtitle;
|
||||
Icon = result.Icon;
|
||||
Command = result.Command;
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ internal static class Commands
|
||||
{
|
||||
results.AddRange(new[]
|
||||
{
|
||||
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_empty, "explorer.exe", "shell:RecycleBinFolder"))
|
||||
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder"))
|
||||
{
|
||||
Title = Resources.Microsoft_plugin_sys_RecycleBinOpen,
|
||||
Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description,
|
||||
@@ -102,7 +102,7 @@ internal static class Commands
|
||||
else
|
||||
{
|
||||
results.Add(
|
||||
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_empty, "explorer.exe", "shell:RecycleBinFolder"))
|
||||
new ListItem(new OpenInShellCommand(Resources.Microsoft_plugin_command_name_open, "explorer.exe", "shell:RecycleBinFolder"))
|
||||
{
|
||||
Title = Resources.Microsoft_plugin_sys_RecycleBin,
|
||||
Subtitle = Resources.Microsoft_plugin_sys_RecycleBin_description,
|
||||
|
||||
@@ -311,14 +311,14 @@ internal sealed class NetworkConnectionProperties
|
||||
{
|
||||
switch (property)
|
||||
{
|
||||
case string:
|
||||
return string.IsNullOrWhiteSpace(property) ? string.Empty : $"\n\n{title}{property}";
|
||||
case string str:
|
||||
return string.IsNullOrWhiteSpace(str) ? string.Empty : $"\n\n{title}{str}";
|
||||
case List<string> listString:
|
||||
return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
|
||||
return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listString)}";
|
||||
case List<IPAddress> listIP:
|
||||
return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
|
||||
return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listIP)}";
|
||||
case IPAddressCollection collectionIP:
|
||||
return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
|
||||
return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", collectionIP)}";
|
||||
case null:
|
||||
return string.Empty;
|
||||
default:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.System</RootNamespace>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user