mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-30 17:07:23 +01:00
Compare commits
68 Commits
leilzh/fuz
...
dev/snickl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c77ce6ebe2 | ||
|
|
49cdcc5e30 | ||
|
|
680e8f87e9 | ||
|
|
8a07b7b560 | ||
|
|
f0a23ceaeb | ||
|
|
f2373cf259 | ||
|
|
cfdcf91625 | ||
|
|
2f678d1fb3 | ||
|
|
f49625210c | ||
|
|
602eef8830 | ||
|
|
13a6287dea | ||
|
|
6cb852077a | ||
|
|
cdc5f073f0 | ||
|
|
0427a7a7b0 | ||
|
|
2d0d12f06c | ||
|
|
06e5db6ff0 | ||
|
|
1a097ae09c | ||
|
|
980ca46cdb | ||
|
|
6a1999d601 | ||
|
|
5655c61794 | ||
|
|
371b7f0868 | ||
|
|
b6bcc92eb4 | ||
|
|
2ac464279a | ||
|
|
8ce198a47b | ||
|
|
15ef9189ba | ||
|
|
2c555e2c2b | ||
|
|
83817700e1 | ||
|
|
7e92a9a5e9 | ||
|
|
fe067def65 | ||
|
|
6b9c99c2f6 | ||
|
|
ba6af794ac | ||
|
|
9cb99be4e9 | ||
|
|
ad974bd679 | ||
|
|
49e5bbb5f0 | ||
|
|
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
|
||||
23
.github/actions/spell-check/expect.txt
vendored
23
.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
|
||||
@@ -312,6 +315,7 @@ debugbreak
|
||||
declatory
|
||||
decryptor
|
||||
Dedup
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
DEFAULTCOLOR
|
||||
DEFAULTFLAGS
|
||||
@@ -323,6 +327,7 @@ DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
DEFPUSHBUTTON
|
||||
DEFT
|
||||
deinitialization
|
||||
DELA
|
||||
DELETEDKEYIMAGE
|
||||
@@ -343,6 +348,7 @@ devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
devpal
|
||||
DFX
|
||||
DIALOGEX
|
||||
digicert
|
||||
dimm
|
||||
@@ -523,6 +529,7 @@ FZE
|
||||
gacutil
|
||||
Gaeilge
|
||||
Gaidhlig
|
||||
gameid
|
||||
GC'ed
|
||||
GCLP
|
||||
gdi
|
||||
@@ -713,6 +720,7 @@ INPUTSINK
|
||||
INPUTTYPE
|
||||
INSTALLDESKTOPSHORTCUT
|
||||
INSTALLDIR
|
||||
installdir
|
||||
INSTALLFOLDER
|
||||
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
|
||||
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
|
||||
@@ -1007,6 +1015,7 @@ netsh
|
||||
newcolor
|
||||
NEWDIALOGSTYLE
|
||||
NEWFILE
|
||||
NEWFILEHEADER
|
||||
newitem
|
||||
newpath
|
||||
newplus
|
||||
@@ -1040,6 +1049,7 @@ NOINHERITLAYOUT
|
||||
NOINTERFACE
|
||||
NOINVERT
|
||||
NOLINKINFO
|
||||
nologo
|
||||
NOMCX
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
@@ -1272,6 +1282,7 @@ pstm
|
||||
PStr
|
||||
pstream
|
||||
pstrm
|
||||
pswd
|
||||
PSYSTEM
|
||||
psz
|
||||
ptb
|
||||
@@ -1302,6 +1313,7 @@ QUNS
|
||||
QXZ
|
||||
RAII
|
||||
RAlt
|
||||
Rappl
|
||||
randi
|
||||
Rasterization
|
||||
Rasterize
|
||||
@@ -1417,6 +1429,7 @@ searchterm
|
||||
SEARCHUI
|
||||
SECONDARYDISPLAY
|
||||
secpol
|
||||
securestring
|
||||
SEEMASKINVOKEIDLIST
|
||||
SELCHANGE
|
||||
SENDCHANGE
|
||||
@@ -1570,6 +1583,7 @@ stdcpp
|
||||
stdcpplatest
|
||||
STDMETHODCALLTYPE
|
||||
STDMETHODIMP
|
||||
steamapps
|
||||
STGC
|
||||
STGM
|
||||
STGMEDIUM
|
||||
@@ -1651,6 +1665,7 @@ telephon
|
||||
templatenamespace
|
||||
testprocess
|
||||
TEXCOORD
|
||||
TEXTBOXNEWLINE
|
||||
TEXTEXTRACTOR
|
||||
TEXTINCLUDE
|
||||
tfopen
|
||||
@@ -1971,3 +1986,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.PowerToys.Telemetry" version="2.0.2" />
|
||||
<package id="Microsoft.PowerToys.Telemetry" version="2.0.3" />
|
||||
</packages>
|
||||
|
||||
@@ -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\\DFX-Developer Fundamentals and Experiences\\DEFT\\PowerToys"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.6
|
||||
default: 1.7
|
||||
- name: useExperimentalVersion
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -112,10 +112,12 @@ extends:
|
||||
# Prepare the localizations and telemetry config before the release build
|
||||
- template: .pipelines/v2/templates/steps-fetch-and-prepare-localizations.yml@self
|
||||
|
||||
- script: |
|
||||
call nuget.exe restore -configFile .pipelines/release-nuget.config -PackagesDirectory . .pipelines/packages.config || exit /b 1
|
||||
move /Y "Microsoft.PowerToys.Telemetry.2.0.2\build\include\TraceLoggingDefines.h" "src\common\Telemetry\TraceLoggingDefines.h" || exit /b 1
|
||||
move /Y "Microsoft.PowerToys.Telemetry.2.0.2\build\include\TelemetryBase.cs" "src\common\Telemetry\TelemetryBase.cs" || exit /b 1
|
||||
- pwsh: |-
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandUseErrorActionPreference = $true
|
||||
& nuget.exe restore -configFile .pipelines/release-nuget.config -PackagesDirectory . .pipelines/packages.config
|
||||
Move-Item -Force -Verbose "Microsoft.PowerToys.Telemetry.*\build\include\TraceLoggingDefines.h" "src\common\Telemetry\TraceLoggingDefines.h"
|
||||
Move-Item -Force -Verbose "Microsoft.PowerToys.Telemetry.*\build\include\TelemetryBase.cs" "src\common\Telemetry\TelemetryBase.cs"
|
||||
displayName: Emplace telemetry files
|
||||
|
||||
- stage: Build_SDK
|
||||
@@ -148,5 +150,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)
|
||||
|
||||
@@ -344,6 +344,11 @@ jobs:
|
||||
flattenFolders: True
|
||||
OverWrite: True
|
||||
|
||||
# Check if all projects (located in src sub-folder) import common props
|
||||
- pwsh: |-
|
||||
& '.pipelines/verifyCommonProps.ps1' -sourceDir '$(build.sourcesdirectory)\src'
|
||||
displayName: Audit shared common props for CSharp projects in src sub-folder
|
||||
|
||||
# Check if deps.json files don't reference different dll versions.
|
||||
- pwsh: |-
|
||||
& '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\$(BuildPlatform)\$(BuildConfiguration)'
|
||||
|
||||
@@ -17,6 +17,7 @@ steps:
|
||||
arguments: >
|
||||
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
|
||||
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
|
||||
-rootPath "$(build.sourcesdirectory)"
|
||||
|
||||
- script: echo $(WinAppSDKVersion)
|
||||
displayName: 'Display WinAppSDK Version Found'
|
||||
|
||||
54
.pipelines/verifyCommonProps.ps1
Normal file
54
.pipelines/verifyCommonProps.ps1
Normal file
@@ -0,0 +1,54 @@
|
||||
[CmdletBinding()]
|
||||
Param(
|
||||
[Parameter(Mandatory = $True, Position = 1)]
|
||||
[string]$sourceDir
|
||||
)
|
||||
|
||||
# scan all csharp project in the source directory
|
||||
function Get-CSharpProjects {
|
||||
param (
|
||||
[string]$path
|
||||
)
|
||||
|
||||
# Get all .csproj files under the specified path
|
||||
return Get-ChildItem -Path $path -Recurse -Filter *.csproj | Select-Object -ExpandProperty FullName
|
||||
}
|
||||
|
||||
# Check if the project file imports 'Common.Dotnet.CsWinRT.props'
|
||||
function Test-ImportSharedCsWinRTProps {
|
||||
param (
|
||||
[string]$filePath
|
||||
)
|
||||
|
||||
# Load the XML content of the .csproj file
|
||||
[xml]$csprojContent = Get-Content -Path $filePath
|
||||
|
||||
|
||||
# Check if the Import element with Project attribute containing 'Common.Dotnet.CsWinRT.props' exists
|
||||
return $csprojContent.Project.Import | Where-Object { $null -ne $_.Project -and $_.Project.EndsWith('Common.Dotnet.CsWinRT.props') }
|
||||
}
|
||||
|
||||
# Call the function with the provided source directory
|
||||
$csprojFilesArray = Get-CSharpProjects -path $sourceDir
|
||||
|
||||
$hasInvalidCsProj = $false
|
||||
|
||||
# Enumerate the array of file paths and call Validate-ImportSharedCsWinRTProps for each file
|
||||
foreach ($csprojFile in $csprojFilesArray) {
|
||||
# Skip if the file ends with 'TemplateCmdPalExtension.csproj'
|
||||
if ($csprojFile -like '*TemplateCmdPalExtension.csproj') {
|
||||
continue
|
||||
}
|
||||
|
||||
$importExists = Test-ImportSharedCsWinRTProps -filePath $csprojFile
|
||||
if (!$importExists) {
|
||||
Write-Output "$csprojFile need to import 'Common.Dotnet.CsWinRT.props'."
|
||||
$hasInvalidCsProj = $true
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasInvalidCsProj) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -31,22 +31,22 @@
|
||||
<!-- Including MessagePack to force version, since it's used by StreamJsonRpc but contains vulnerabilities. After StreamJsonRpc updates the version of MessagePack, we can upgrade StreamJsonRpc instead. -->
|
||||
<PackageVersion Include="MessagePack" Version="3.1.3" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.5" />
|
||||
<!-- Including Microsoft.Bcl.AsyncInterfaces to force version, since it's used by Microsoft.SemanticKernel. -->
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.15.0" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<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.Windows.Compatibility" Version="9.0.4" />
|
||||
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.5" />
|
||||
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
|
||||
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.5" />
|
||||
<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" />
|
||||
@@ -72,28 +72,28 @@
|
||||
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<!-- Package System.CodeDom 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.Management but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.4" />
|
||||
<PackageVersion Include="System.CodeDom" Version="9.0.5" />
|
||||
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.4" />
|
||||
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Data.OleDb" Version="9.0.5" />
|
||||
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
|
||||
<PackageVersion Include="System.Data.SqlClient" Version="4.8.6" />
|
||||
<!-- Package System.Diagnostics.EventLog 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.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.5" />
|
||||
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="9.0.5" />
|
||||
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
|
||||
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.4" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.4" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="9.0.5" />
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.5" />
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.56.0" />
|
||||
|
||||
80
NOTICE.md
80
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
|
||||
@@ -1419,27 +1453,27 @@ SOFTWARE.
|
||||
- Mages 3.0.0
|
||||
- Markdig.Signed 0.34.0
|
||||
- MessagePack 3.1.3
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.4
|
||||
- Microsoft.Bcl.AsyncInterfaces 9.0.5
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
||||
- Microsoft.Data.Sqlite 9.0.4
|
||||
- Microsoft.Data.Sqlite 9.0.5
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.DotNet.ILCompiler (A)
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.4
|
||||
- Microsoft.Extensions.Hosting 9.0.4
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.4
|
||||
- Microsoft.Extensions.Logging 9.0.4
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.4
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.5
|
||||
- Microsoft.Extensions.Hosting 9.0.5
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.5
|
||||
- Microsoft.Extensions.Logging 9.0.5
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.5
|
||||
- Microsoft.NET.ILLink.Tasks (A)
|
||||
- Microsoft.SemanticKernel 1.15.0
|
||||
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
|
||||
- Microsoft.Web.WebView2 1.0.2903.40
|
||||
- Microsoft.Win32.SystemEvents 9.0.4
|
||||
- Microsoft.Windows.Compatibility 9.0.4
|
||||
- Microsoft.Win32.SystemEvents 9.0.5
|
||||
- Microsoft.Windows.Compatibility 9.0.5
|
||||
- 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
|
||||
@@ -1453,25 +1487,25 @@ SOFTWARE.
|
||||
- SharpCompress 0.37.2
|
||||
- StreamJsonRpc 2.21.69
|
||||
- StyleCop.Analyzers 1.2.0-beta.556
|
||||
- System.CodeDom 9.0.4
|
||||
- System.CodeDom 9.0.5
|
||||
- System.CommandLine 2.0.0-beta4.22272.1
|
||||
- System.ComponentModel.Composition 9.0.4
|
||||
- System.Configuration.ConfigurationManager 9.0.4
|
||||
- System.Data.OleDb 9.0.4
|
||||
- System.ComponentModel.Composition 9.0.5
|
||||
- System.Configuration.ConfigurationManager 9.0.5
|
||||
- System.Data.OleDb 9.0.5
|
||||
- System.Data.SqlClient 4.8.6
|
||||
- System.Diagnostics.EventLog 9.0.4
|
||||
- System.Diagnostics.PerformanceCounter 9.0.4
|
||||
- System.Drawing.Common 9.0.4
|
||||
- System.Diagnostics.EventLog 9.0.5
|
||||
- System.Diagnostics.PerformanceCounter 9.0.5
|
||||
- System.Drawing.Common 9.0.5
|
||||
- System.IO.Abstractions 22.0.13
|
||||
- System.IO.Abstractions.TestingHelpers 22.0.13
|
||||
- System.Management 9.0.4
|
||||
- System.Management 9.0.5
|
||||
- System.Net.Http 4.3.4
|
||||
- System.Private.Uri 4.3.2
|
||||
- System.Reactive 6.0.1
|
||||
- System.Runtime.Caching 9.0.4
|
||||
- System.ServiceProcess.ServiceController 9.0.4
|
||||
- System.Text.Encoding.CodePages 9.0.4
|
||||
- System.Text.Json 9.0.4
|
||||
- System.Runtime.Caching 9.0.5
|
||||
- System.ServiceProcess.ServiceController 9.0.5
|
||||
- System.Text.Encoding.CodePages 9.0.5
|
||||
- System.Text.Json 9.0.5
|
||||
- System.Text.RegularExpressions 4.3.1
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 5.56.0
|
||||
|
||||
@@ -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>
|
||||
|
||||
10
src/Common.Dotnet.FuzzTest.props
Normal file
10
src/Common.Dotnet.FuzzTest.props
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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">
|
||||
<!-- OneFuzz does not currently support testing with .NET 9.
|
||||
As a temporary workaround, create a .NET 8 project and use file links
|
||||
to include the code that needs testing. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "leilzh@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "leilzh@microsoft.com",
|
||||
|
||||
@@ -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,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.FuzzTest.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
@@ -58,7 +58,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
@@ -99,7 +99,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
@@ -140,7 +140,7 @@
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SALT",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
|
||||
@@ -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,17 @@ 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"x-cmdpal://background";
|
||||
#ifdef IS_DEV_BRANDING
|
||||
packageName = L"Microsoft.CommandPalette.Dev";
|
||||
#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 +251,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"", 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 +286,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"", 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 +338,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();
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,15 @@
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.1.0" />
|
||||
<PackageVersion Include="Microsoft.CommandPalette.Extensions" Version="0.2.0" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0-preview.24508.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
|
||||
<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" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.5" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -14,11 +14,12 @@ namespace TemplateCmdPalExtension;
|
||||
public class Program
|
||||
{
|
||||
[MTAThread]
|
||||
public static async Task Main(string[] args)
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
|
||||
{
|
||||
await using global::Shmuelie.WinRTServer.ComServer server = new();
|
||||
global::Shmuelie.WinRTServer.ComServer server = new();
|
||||
|
||||
ManualResetEvent extensionDisposedEvent = new(false);
|
||||
|
||||
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
|
||||
@@ -31,6 +32,8 @@ public class Program
|
||||
// This will make the main thread wait until the event is signalled by the extension class.
|
||||
// Since we have single instance of the extension object, we exit as soon as it is disposed.
|
||||
extensionDisposedEvent.WaitOne();
|
||||
server.Stop();
|
||||
server.UnsafeDispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Contracts;
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
T Read<T>(string folderPath, string fileName);
|
||||
|
||||
void Save<T>(string folderPath, string fileName, T content);
|
||||
|
||||
void Delete(string folderPath, string fileName);
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// 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.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Contracts;
|
||||
|
||||
public interface ILocalSettingsService
|
||||
{
|
||||
Task<bool> HasSettingAsync(string key);
|
||||
|
||||
Task<T?> ReadSettingAsync<T>(string key);
|
||||
|
||||
Task SaveSettingAsync<T>(string key, T value);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// 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.Common.Services;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension class implementing extension methods for <see cref="Application"/>.
|
||||
/// </summary>
|
||||
public static class ApplicationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Get registered services at the application level from anywhere in the
|
||||
/// application.
|
||||
///
|
||||
/// Note:
|
||||
/// https://learn.microsoft.com/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current
|
||||
/// "Application is a singleton that implements the static Current property
|
||||
/// to provide shared access to the Application instance for the current
|
||||
/// application. The singleton pattern ensures that state managed by
|
||||
/// Application, including shared resources and properties, is available
|
||||
/// from a single, shared location."
|
||||
///
|
||||
/// Example of usage:
|
||||
/// <code>
|
||||
/// Application.Current.GetService<T>()
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Service type.</typeparam>
|
||||
/// <param name="application">Current application.</param>
|
||||
/// <returns>Service reference.</returns>
|
||||
public static T GetService<T>(this Application application)
|
||||
where T : class
|
||||
{
|
||||
return (application as IApp)!.GetService<T>();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Extensions;
|
||||
|
||||
public static class IHostExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="ActivatorUtilities.CreateInstance(IServiceProvider, Type, object[])"/>
|
||||
/// </summary>
|
||||
public static T CreateInstance<T>(this IHost host, params object[] parameters)
|
||||
{
|
||||
return ActivatorUtilities.CreateInstance<T>(host.Services, parameters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service object for the specified type, or throws an exception
|
||||
/// if type was not registered.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Service type</typeparam>
|
||||
/// <param name="host">Host object</param>
|
||||
/// <returns>Service object</returns>
|
||||
/// <exception cref="ArgumentException">Throw an exception if the specified
|
||||
/// type is not registered</exception>
|
||||
public static T GetService<T>(this IHost host)
|
||||
where T : class
|
||||
{
|
||||
if (host.Services.GetService(typeof(T)) is not T service)
|
||||
{
|
||||
throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
|
||||
}
|
||||
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
// 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.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static class Json
|
||||
{
|
||||
public static async Task<T> ToObjectAsync<T>(string value)
|
||||
{
|
||||
if (typeof(T) == typeof(bool))
|
||||
{
|
||||
return (T)(object)bool.Parse(value);
|
||||
}
|
||||
|
||||
await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value));
|
||||
return (await JsonSerializer.DeserializeAsync<T>(stream))!;
|
||||
}
|
||||
|
||||
public static async Task<string> StringifyAsync<T>(T value)
|
||||
{
|
||||
if (typeof(T) == typeof(bool))
|
||||
{
|
||||
return value!.ToString()!.ToLowerInvariant();
|
||||
}
|
||||
|
||||
await using var stream = new MemoryStream();
|
||||
await JsonSerializer.SerializeAsync(stream, value);
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static class NativeEventWaiter
|
||||
public static partial class NativeEventWaiter
|
||||
{
|
||||
public static void WaitForEventLoop(string eventName, Action callback)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ using Windows.Win32.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
public static class RuntimeHelper
|
||||
public static partial class RuntimeHelper
|
||||
{
|
||||
public static bool IsMSIX
|
||||
{
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Messages;
|
||||
|
||||
public record HideWindowMessage()
|
||||
public partial record HideWindowMessage()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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.Common</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Models;
|
||||
|
||||
public class LocalSettingsOptions
|
||||
{
|
||||
public string? ApplicationDataFolder
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string? LocalSettingsFile
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,7 @@
|
||||
EnableWindow
|
||||
CoCreateInstance
|
||||
FileOpenDialog
|
||||
FileSaveDialog
|
||||
IFileOpenDialog
|
||||
IFileSaveDialog
|
||||
SHCreateItemFromParsingName
|
||||
GetCurrentPackageFullName
|
||||
SetWindowLong
|
||||
GetWindowLong
|
||||
WINDOW_EX_STYLE
|
||||
SHLoadIndirectString
|
||||
StrFormatByteSizeEx
|
||||
SFBS_FLAGS
|
||||
MAX_PATH
|
||||
GetDpiForWindow
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
// 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.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.CmdPal.Common.Contracts;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
public class FileService : IFileService
|
||||
{
|
||||
private static readonly Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
||||
|
||||
#pragma warning disable CS8603 // Possible null reference return.
|
||||
public T Read<T>(string folderPath, string fileName)
|
||||
{
|
||||
var path = Path.Combine(folderPath, fileName);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
using var fileStream = File.OpenText(path);
|
||||
return JsonSerializer.Deserialize<T>(fileStream.BaseStream);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
#pragma warning restore CS8603 // Possible null reference return.
|
||||
|
||||
public void Save<T>(string folderPath, string fileName, T content)
|
||||
{
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
|
||||
var fileContent = JsonSerializer.Serialize(content);
|
||||
File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, _encoding);
|
||||
}
|
||||
|
||||
public void Delete(string folderPath, string fileName)
|
||||
{
|
||||
if (fileName != null && File.Exists(Path.Combine(folderPath, fileName)))
|
||||
{
|
||||
File.Delete(Path.Combine(folderPath, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for the current application singleton object exposing the API
|
||||
/// that can be accessed from anywhere in the application.
|
||||
/// </summary>
|
||||
public interface IApp
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets services registered at the application level.
|
||||
/// </summary>
|
||||
public T GetService<T>()
|
||||
where T : class;
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Common.Contracts;
|
||||
using Microsoft.CmdPal.Common.Helpers;
|
||||
using Microsoft.CmdPal.Common.Models;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Services;
|
||||
|
||||
public class LocalSettingsService : ILocalSettingsService
|
||||
{
|
||||
// TODO! for now, we're hardcoding the path as effectively:
|
||||
// %localappdata%\CmdPal\LocalSettings.json
|
||||
private const string DefaultApplicationDataFolder = "CmdPal";
|
||||
private const string DefaultLocalSettingsFile = "LocalSettings.json";
|
||||
|
||||
private readonly IFileService _fileService;
|
||||
private readonly LocalSettingsOptions _options;
|
||||
|
||||
private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
private readonly string _applicationDataFolder;
|
||||
private readonly string _localSettingsFile;
|
||||
|
||||
private readonly bool _isMsix;
|
||||
|
||||
private Dictionary<string, object> _settings;
|
||||
private bool _isInitialized;
|
||||
|
||||
public LocalSettingsService(IFileService fileService, IOptions<LocalSettingsOptions> options)
|
||||
{
|
||||
_isMsix = false; // RuntimeHelper.IsMSIX;
|
||||
|
||||
_fileService = fileService;
|
||||
_options = options.Value;
|
||||
|
||||
_applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? DefaultApplicationDataFolder);
|
||||
_localSettingsFile = _options.LocalSettingsFile ?? DefaultLocalSettingsFile;
|
||||
|
||||
_settings = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_settings = await Task.Run(() => _fileService.Read<Dictionary<string, object>>(_applicationDataFolder, _localSettingsFile)) ?? new Dictionary<string, object>();
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> HasSettingAsync(string key)
|
||||
{
|
||||
if (_isMsix)
|
||||
{
|
||||
return ApplicationData.Current.LocalSettings.Values.ContainsKey(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
if (_settings != null)
|
||||
{
|
||||
return _settings.ContainsKey(key);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<T?> ReadSettingAsync<T>(string key)
|
||||
{
|
||||
if (_isMsix)
|
||||
{
|
||||
if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj))
|
||||
{
|
||||
return await Json.ToObjectAsync<T>((string)obj);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
if (_settings != null && _settings.TryGetValue(key, out var obj))
|
||||
{
|
||||
var s = obj.ToString();
|
||||
|
||||
if (s != null)
|
||||
{
|
||||
return await Json.ToObjectAsync<T>(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public async Task SaveSettingAsync<T>(string key, T value)
|
||||
{
|
||||
if (_isMsix)
|
||||
{
|
||||
ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!);
|
||||
}
|
||||
else
|
||||
{
|
||||
await InitializeAsync();
|
||||
|
||||
_settings[key] = await Json.StringifyAsync(value!);
|
||||
|
||||
await Task.Run(() => _fileService.Save(_applicationDataFolder, _localSettingsFile, _settings));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
// };
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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(PrimaryCommand);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -66,8 +66,10 @@ public sealed class CommandProviderWrapper
|
||||
DisplayName = provider.DisplayName;
|
||||
Icon = new(provider.Icon);
|
||||
Icon.InitializeProperties();
|
||||
|
||||
// Note: explicitly not InitializeProperties()ing the settings here. If
|
||||
// we do that, then we'd regress GH #38321
|
||||
Settings = new(provider.Settings, this, _taskScheduler);
|
||||
Settings.InitializeProperties();
|
||||
|
||||
Logger.LogDebug($"Initialized command provider {ProviderId}");
|
||||
}
|
||||
@@ -151,9 +153,11 @@ public sealed class CommandProviderWrapper
|
||||
Icon = new(model.Icon);
|
||||
Icon.InitializeProperties();
|
||||
|
||||
// Note: explicitly not InitializeProperties()ing the settings here. If
|
||||
// we do that, then we'd regress GH #38321
|
||||
Settings = new(model.Settings, this, _taskScheduler);
|
||||
Settings.InitializeProperties();
|
||||
|
||||
// We do need to explicitly initialize commands though
|
||||
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
|
||||
|
||||
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
|
||||
@@ -194,21 +198,6 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
/* This is a View/ExtensionHost piece
|
||||
* public void AllowSetForeground(bool allow)
|
||||
{
|
||||
if (!IsExtension)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var iextn = extensionWrapper?.GetExtensionObject();
|
||||
unsafe
|
||||
{
|
||||
PInvoke.CoAllowSetForegroundWindow(iextn);
|
||||
}
|
||||
}*/
|
||||
|
||||
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
|
||||
|
||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||
|
||||
@@ -2,18 +2,25 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
|
||||
public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
|
||||
{
|
||||
private readonly ExtensionObject<ICommandSettings> _model = new(_unsafeSettings);
|
||||
|
||||
public ContentPageViewModel? SettingsPage { get; private set; }
|
||||
|
||||
public void InitializeProperties()
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
public bool HasSettings =>
|
||||
_model.Unsafe != null && // We have a settings model AND
|
||||
(!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page
|
||||
|
||||
private void UnsafeInitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
@@ -27,4 +34,27 @@ public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings,
|
||||
SettingsPage.InitializeProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public void SafeInitializeProperties()
|
||||
{
|
||||
try
|
||||
{
|
||||
UnsafeInitializeProperties();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load settings page", ex: ex);
|
||||
}
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
public void DoOnUiThread(Action action)
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
mainThread);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text.Json;
|
||||
using AdaptiveCards.ObjectModel.WinUI3;
|
||||
using AdaptiveCards.Templating;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -28,42 +29,67 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
|
||||
public AdaptiveCardParseResult? Card { get; private set; }
|
||||
|
||||
private static string Serialize(string? s) =>
|
||||
JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
|
||||
|
||||
private static bool TryBuildCard(
|
||||
string templateJson,
|
||||
string dataJson,
|
||||
out AdaptiveCardParseResult? card,
|
||||
out Exception? error)
|
||||
{
|
||||
card = null;
|
||||
error = null;
|
||||
|
||||
try
|
||||
{
|
||||
var template = new AdaptiveCardTemplate(templateJson);
|
||||
var cardJson = template.Expand(dataJson);
|
||||
card = AdaptiveCard.FromJsonString(cardJson);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error building card from template: {Message}", ex.Message);
|
||||
error = ex;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var model = _formModel.Unsafe;
|
||||
if (model == null)
|
||||
if (model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TemplateJson = model.TemplateJson;
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
TemplateJson = model.TemplateJson;
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
|
||||
AdaptiveCardTemplate template = new(TemplateJson);
|
||||
var cardJson = template.Expand(DataJson);
|
||||
Card = AdaptiveCard.FromJsonString(cardJson);
|
||||
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
|
||||
{
|
||||
Card = builtCard;
|
||||
UpdateProperty(nameof(Card));
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// If we fail to parse the card JSON, then display _our own card_
|
||||
// with the exception
|
||||
AdaptiveCardTemplate template = new(ErrorCardJson);
|
||||
|
||||
// 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)}}
|
||||
}
|
||||
""";
|
||||
var cardJson = template.Expand(dataJson);
|
||||
Card = AdaptiveCard.FromJsonString(cardJson);
|
||||
var errorPayload = $$"""
|
||||
{
|
||||
"error_message": {{Serialize(renderingError!.Message)}},
|
||||
"error_stack": {{Serialize(renderingError.StackTrace)}},
|
||||
"inner_exception": {{Serialize(renderingError.InnerException?.Message)}},
|
||||
"template_json": {{Serialize(TemplateJson)}},
|
||||
"data_json": {{Serialize(DataJson)}}
|
||||
}
|
||||
""";
|
||||
|
||||
if (TryBuildCard(ErrorCardJson, errorPayload, out var errorCard, out var _))
|
||||
{
|
||||
Card = errorCard;
|
||||
UpdateProperty(nameof(Card));
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
private Task? _initializeItemsTask;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
private ListItemViewModel? _lastSelectedItem;
|
||||
|
||||
public override bool IsInitialized
|
||||
{
|
||||
get => base.IsInitialized; protected set
|
||||
@@ -328,7 +330,24 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void UpdateSelectedItem(ListItemViewModel item)
|
||||
private void UpdateSelectedItem(ListItemViewModel? item)
|
||||
{
|
||||
if (_lastSelectedItem != null)
|
||||
{
|
||||
_lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged;
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
SetSelectedItem(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSelectedItem();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetSelectedItem(ListItemViewModel item)
|
||||
{
|
||||
if (!item.SafeSlowInit())
|
||||
{
|
||||
@@ -355,6 +374,60 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
TextToSuggest = item.TextToSuggest;
|
||||
});
|
||||
|
||||
_lastSelectedItem = item;
|
||||
_lastSelectedItem.PropertyChanged += SelectedItemPropertyChanged;
|
||||
}
|
||||
|
||||
private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
var item = _lastSelectedItem;
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// already on the UI thread here
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(item.Command):
|
||||
case nameof(item.SecondaryCommand):
|
||||
case nameof(item.AllCommands):
|
||||
case nameof(item.Name):
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
|
||||
break;
|
||||
case nameof(item.Details):
|
||||
if (ShowDetails && item.HasDetails)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));
|
||||
}
|
||||
else
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
}
|
||||
|
||||
break;
|
||||
case nameof(item.TextToSuggest):
|
||||
TextToSuggest = item.TextToSuggest;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearSelectedItem()
|
||||
{
|
||||
// GH #322:
|
||||
// For inexplicable reasons, if you try updating the command bar and
|
||||
// the details on the same UI thread tick as updating the list, we'll
|
||||
// explode
|
||||
DoOnUiThread(
|
||||
() =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
|
||||
|
||||
WeakReferenceMessenger.Default.Send<HideDetailsMessage>();
|
||||
|
||||
TextToSuggest = string.Empty;
|
||||
});
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
@@ -434,7 +507,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 +525,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.
|
||||
|
||||
@@ -182,6 +182,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
var updateProperty = true;
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(Name):
|
||||
@@ -198,9 +199,21 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
case nameof(Icon):
|
||||
this.Icon = new(model.Icon);
|
||||
break;
|
||||
default:
|
||||
updateProperty = false;
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
// GH #38829: If we always UpdateProperty here, then there's a possible
|
||||
// race condition, where we raise the PropertyChanged(SearchText)
|
||||
// before the subclass actually retrieves the new SearchText from the
|
||||
// model. In that race situation, if the UI thread handles the
|
||||
// PropertyChanged before ListViewModel fetches the SearchText, it'll
|
||||
// think that the old search text is the _new_ value.
|
||||
if (updateProperty)
|
||||
{
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
public new void ShowException(Exception ex, string? extensionHint = null)
|
||||
|
||||
@@ -18,6 +18,8 @@ public partial class ProviderSettingsViewModel(
|
||||
IServiceProvider _serviceProvider) : ObservableObject
|
||||
{
|
||||
private readonly SettingsModel _settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||
private readonly Lock _initializeSettingsLock = new();
|
||||
private Task? _initializeSettingsTask;
|
||||
|
||||
public string DisplayName => _provider.DisplayName;
|
||||
|
||||
@@ -34,6 +36,9 @@ public partial class ProviderSettingsViewModel(
|
||||
|
||||
public IconInfoViewModel Icon => _provider.Icon;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool LoadingSettings { get; set; } = _provider.Settings?.HasSettings ?? false;
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _providerSettings.IsEnabled;
|
||||
@@ -56,15 +61,60 @@ public partial class ProviderSettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether returns true if we have a settings page
|
||||
/// that's initialized, or we are still working on initializing that
|
||||
/// settings page. If we don't have a settings object, or that settings
|
||||
/// object doesn't have a settings page, then we'll return false.
|
||||
/// </summary>
|
||||
public bool HasSettings
|
||||
{
|
||||
OnPropertyChanged(nameof(ExtensionSubtext));
|
||||
OnPropertyChanged(nameof(TopLevelCommands));
|
||||
get
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_provider.Settings.Initialized)
|
||||
{
|
||||
return _provider.Settings.HasSettings;
|
||||
}
|
||||
|
||||
// settings still need to be loaded.
|
||||
return LoadingSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasSettings => _provider.Settings != null && _provider.Settings.SettingsPage != null;
|
||||
/// <summary>
|
||||
/// Gets will return the settings page, if we have one, and have initialized it.
|
||||
/// If we haven't initialized it, this will kick off a thread to start
|
||||
/// initializing it.
|
||||
/// </summary>
|
||||
public ContentPageViewModel? SettingsPage
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public ContentPageViewModel? SettingsPage => HasSettings ? _provider?.Settings?.SettingsPage : null;
|
||||
if (_provider.Settings.Initialized)
|
||||
{
|
||||
LoadingSettings = false;
|
||||
return _provider.Settings.SettingsPage;
|
||||
}
|
||||
|
||||
// Don't load the settings if we're already working on it
|
||||
lock (_initializeSettingsLock)
|
||||
{
|
||||
_initializeSettingsTask ??= Task.Run(InitializeSettingsPage);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[field: AllowNull]
|
||||
public List<TopLevelViewModel> TopLevelCommands
|
||||
@@ -90,4 +140,30 @@ public partial class ProviderSettingsViewModel(
|
||||
}
|
||||
|
||||
private void Save() => SettingsModel.SaveSettings(_settings);
|
||||
|
||||
private void InitializeSettingsPage()
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_provider.Settings.SafeInitializeProperties();
|
||||
_provider.Settings.DoOnUiThread(() =>
|
||||
{
|
||||
// Changing these properties will try to update XAML, and that has
|
||||
// to be handled on the UI thread, so we need to raise them on the
|
||||
// UI thread
|
||||
LoadingSettings = false;
|
||||
OnPropertyChanged(nameof(HasSettings));
|
||||
OnPropertyChanged(nameof(LoadingSettings));
|
||||
OnPropertyChanged(nameof(SettingsPage));
|
||||
});
|
||||
}
|
||||
|
||||
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
|
||||
{
|
||||
OnPropertyChanged(nameof(ExtensionSubtext));
|
||||
OnPropertyChanged(nameof(TopLevelCommands));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
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;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -44,6 +45,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
public async Task<bool> LoadBuiltinsAsync()
|
||||
{
|
||||
var s = new Stopwatch();
|
||||
s.Start();
|
||||
|
||||
_builtInCommands.Clear();
|
||||
|
||||
// Load built-In commands first. These are all in-proc, and
|
||||
@@ -53,53 +57,48 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
{
|
||||
CommandProviderWrapper wrapper = new(provider, _taskScheduler);
|
||||
_builtInCommands.Add(wrapper);
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.Stop();
|
||||
|
||||
Logger.LogDebug($"Loading built-ins took {s.ElapsedMilliseconds}ms");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// May be called from a background thread
|
||||
private async Task LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
|
||||
await commandProvider.LoadTopLevelCommands(_serviceProvider, weakSelf);
|
||||
|
||||
var settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||
var makeAndAdd = (ICommandItem? i, bool fallback) =>
|
||||
|
||||
List<TopLevelViewModel> commands = [];
|
||||
|
||||
foreach (var item in commandProvider.TopLevelItems)
|
||||
{
|
||||
var commandItemViewModel = new CommandItemViewModel(new(i), weakSelf);
|
||||
var topLevelViewModel = new TopLevelViewModel(commandItemViewModel, fallback, commandProvider.ExtensionHost, commandProvider.ProviderId, settings, _serviceProvider);
|
||||
commands.Add(item);
|
||||
}
|
||||
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
TopLevelCommands.Add(topLevelViewModel);
|
||||
}
|
||||
};
|
||||
|
||||
await Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var item in commandProvider.TopLevelItems)
|
||||
{
|
||||
TopLevelCommands.Add(item);
|
||||
}
|
||||
|
||||
foreach (var item in commandProvider.FallbackItems)
|
||||
{
|
||||
TopLevelCommands.Add(item);
|
||||
}
|
||||
}
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
_taskScheduler);
|
||||
foreach (var item in commandProvider.FallbackItems)
|
||||
{
|
||||
commands.Add(item);
|
||||
}
|
||||
|
||||
commandProvider.CommandsChanged -= CommandProvider_CommandsChanged;
|
||||
commandProvider.CommandsChanged += CommandProvider_CommandsChanged;
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
// By all accounts, we're already on a background thread (the COM call
|
||||
@@ -239,25 +238,71 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions)
|
||||
{
|
||||
// TODO This most definitely needs a lock
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
Logger.LogDebug($"Starting {extension.PackageFullName}");
|
||||
try
|
||||
{
|
||||
// start it ...
|
||||
await extension.StartExtensionAsync();
|
||||
var timer = new Stopwatch();
|
||||
timer.Start();
|
||||
|
||||
// ... and fetch the command provider from it.
|
||||
CommandProviderWrapper wrapper = new(extension, _taskScheduler);
|
||||
_extensionCommandProviders.Add(wrapper);
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
catch (Exception ex)
|
||||
// Start all extensions in parallel
|
||||
var startTasks = extensions.Select(StartExtensionWithTimeoutAsync);
|
||||
|
||||
// Wait for all extensions to start
|
||||
var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper != null).Select(w => w!).ToList();
|
||||
|
||||
foreach (var wrapper in wrappers)
|
||||
{
|
||||
_extensionCommandProviders.Add(wrapper!);
|
||||
}
|
||||
|
||||
// Load the commands from the providers in parallel
|
||||
var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync);
|
||||
|
||||
var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results != null).Select(r => r!).ToList();
|
||||
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var commands in commandSets)
|
||||
{
|
||||
Logger.LogError(ex.ToString());
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
|
||||
}
|
||||
|
||||
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
|
||||
{
|
||||
Logger.LogDebug($"Starting {extension.PackageFullName}");
|
||||
try
|
||||
{
|
||||
await extension.StartExtensionAsync().WaitAsync(TimeSpan.FromSeconds(10));
|
||||
return new CommandProviderWrapper(extension, _taskScheduler);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to start extension {extension.PackageFullName}: {ex}");
|
||||
return null; // Return null for failed extensions
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await LoadTopLevelCommandsFromProvider(wrapper!).WaitAsync(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Logger.LogError($"Loading commands from {wrapper!.ExtensionHost?.Extension?.PackageFullName} timed out");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load commands for extension {wrapper!.ExtensionHost?.Extension?.PackageFullName}: {ex}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)
|
||||
|
||||
@@ -73,26 +73,12 @@ public partial class App : Application
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
AppWindow = new MainWindow();
|
||||
|
||||
var cmdArgs = Environment.GetCommandLineArgs();
|
||||
|
||||
var runFromPT = false;
|
||||
foreach (var arg in cmdArgs)
|
||||
{
|
||||
if (arg == "RunFromPT")
|
||||
{
|
||||
runFromPT = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!runFromPT)
|
||||
{
|
||||
AppWindow.Activate();
|
||||
}
|
||||
var activatedEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
((MainWindow)AppWindow).HandleLaunch(activatedEventArgs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
@@ -13,4 +12,9 @@ namespace Microsoft.CmdPal.UI.Events;
|
||||
public class BeginInvoke : EventBase, IEvent
|
||||
{
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
|
||||
public BeginInvoke()
|
||||
{
|
||||
EventName = "CmdPal_BeginInvoke";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
@@ -13,4 +12,9 @@ namespace Microsoft.CmdPal.UI.Events;
|
||||
public class ColdLaunch : EventBase, IEvent
|
||||
{
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
|
||||
public ColdLaunch()
|
||||
{
|
||||
EventName = "CmdPal_ColdLaunch";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
@@ -17,6 +16,8 @@ public class OpenPage : EventBase, IEvent
|
||||
public OpenPage(int pageDepth)
|
||||
{
|
||||
PageDepth = pageDepth;
|
||||
|
||||
EventName = "CmdPal_OpenPage";
|
||||
}
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.Tracing;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
@@ -13,4 +12,9 @@ namespace Microsoft.CmdPal.UI.Events;
|
||||
public class ReactivateInstance : EventBase, IEvent
|
||||
{
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
|
||||
public ReactivateInstance()
|
||||
{
|
||||
EventName = "CmdPal_ReactivateInstance";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -124,14 +124,12 @@ public sealed partial class ListPage : Page,
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS is too aggressive at pruning methods bound in XAML")]
|
||||
private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (ItemsList.SelectedItem is ListItemViewModel item)
|
||||
var vm = ViewModel;
|
||||
var li = ItemsList.SelectedItem as ListItemViewModel;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
var vm = ViewModel;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
vm?.UpdateSelectedItemCommand.Execute(item);
|
||||
});
|
||||
}
|
||||
vm?.UpdateSelectedItemCommand.Execute(li);
|
||||
});
|
||||
|
||||
// There's mysterious behavior here, where the selection seemingly
|
||||
// changes to _nothing_ when we're backspacing to a single character.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Window
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.CmdPal.UI.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
@@ -6,8 +6,13 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:pages="using:Microsoft.CmdPal.UI.Pages"
|
||||
xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Width="800"
|
||||
Height="480"
|
||||
MinWidth="320"
|
||||
MinHeight="240"
|
||||
Activated="MainWindow_Activated"
|
||||
Closed="MainWindow_Closed"
|
||||
mc:Ignorable="d">
|
||||
<pages:ShellPage x:Name="RootShellPage" />
|
||||
</Window>
|
||||
</winuiex:WindowEx>
|
||||
|
||||
@@ -19,21 +19,25 @@ using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Windows.ApplicationModel.Activation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
using Windows.UI.WindowManagement;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
using WinUIEx;
|
||||
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
public sealed partial class MainWindow : Window,
|
||||
public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<DismissMessage>,
|
||||
IRecipient<ShowWindowMessage>,
|
||||
IRecipient<HideWindowMessage>,
|
||||
@@ -43,6 +47,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
|
||||
@@ -81,7 +86,6 @@ public sealed partial class MainWindow : Window,
|
||||
|
||||
this.SetIcon();
|
||||
AppWindow.Title = RS_.GetString("AppName");
|
||||
AppWindow.Resize(new SizeInt32 { Width = 1000, Height = 620 });
|
||||
PositionCentered();
|
||||
SetAcrylic();
|
||||
|
||||
@@ -157,6 +161,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;
|
||||
@@ -229,6 +235,16 @@ public sealed partial class MainWindow : Window,
|
||||
PositionCentered(display);
|
||||
|
||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
|
||||
|
||||
// instead of showing the window, uncloak it from DWM
|
||||
// This will make it visible to the user, without the animation or frames for
|
||||
// loading XAML with composition
|
||||
unsafe
|
||||
{
|
||||
BOOL value = false;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
|
||||
}
|
||||
|
||||
PInvoke.SetForegroundWindow(hwnd);
|
||||
PInvoke.SetActiveWindow(hwnd);
|
||||
}
|
||||
@@ -286,7 +302,7 @@ public sealed partial class MainWindow : Window,
|
||||
ShowHwnd(message.Hwnd, settings.SummonOn);
|
||||
}
|
||||
|
||||
public void Receive(HideWindowMessage message) => PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
public void Receive(HideWindowMessage message) => HideWindow();
|
||||
|
||||
public void Receive(QuitMessage message) =>
|
||||
|
||||
@@ -294,7 +310,21 @@ public sealed partial class MainWindow : Window,
|
||||
DispatcherQueue.TryEnqueue(() => Close());
|
||||
|
||||
public void Receive(DismissMessage message) =>
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
HideWindow();
|
||||
|
||||
private void HideWindow()
|
||||
{
|
||||
// Hide our window
|
||||
|
||||
// Instead of hiding the window, cloak it from DWM
|
||||
// This will make it invisible to the user, such that we can show it again
|
||||
// by uncloaking it, which avoids an unnecessary "flicker in" that XAML does
|
||||
unsafe
|
||||
{
|
||||
BOOL value = true;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &value, (uint)sizeof(BOOL));
|
||||
}
|
||||
}
|
||||
|
||||
internal void MainWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
@@ -383,7 +413,9 @@ public sealed partial class MainWindow : Window,
|
||||
return;
|
||||
}
|
||||
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
// This will DWM cloak our window:
|
||||
HideWindow();
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
|
||||
}
|
||||
|
||||
@@ -393,6 +425,40 @@ public sealed partial class MainWindow : Window,
|
||||
}
|
||||
}
|
||||
|
||||
public void HandleLaunch(AppActivationArguments? activatedEventArgs)
|
||||
{
|
||||
if (activatedEventArgs == null)
|
||||
{
|
||||
Summon(string.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activatedEventArgs.Kind == Microsoft.Windows.AppLifecycle.ExtendedActivationKind.Protocol)
|
||||
{
|
||||
if (activatedEventArgs.Data is IProtocolActivatedEventArgs protocolArgs)
|
||||
{
|
||||
if (protocolArgs.Uri.ToString() is string uri)
|
||||
{
|
||||
// was the URI "x-cmdpal://background" ?
|
||||
if (uri.StartsWith("x-cmdpal://background", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// we're running, we don't want to activate our window. bail
|
||||
return;
|
||||
}
|
||||
else if (uri.StartsWith("x-cmdpal://settings", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Activate();
|
||||
}
|
||||
|
||||
public void Summon(string commandId) =>
|
||||
|
||||
// The actual showing and hiding of the window will be done by the
|
||||
@@ -401,11 +467,6 @@ public sealed partial class MainWindow : Window,
|
||||
// know till the message is being handled.
|
||||
WeakReferenceMessenger.Default.Send<HotkeySummonMessage>(new(commandId, _hwnd));
|
||||
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private const uint DOT_KEY = 0xBE;
|
||||
private const uint WM_HOTKEY = 0x0312;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
|
||||
private void UnregisterHotkeys()
|
||||
{
|
||||
_keyboardListener.ClearHotkeys();
|
||||
@@ -476,10 +537,24 @@ public sealed partial class MainWindow : Window,
|
||||
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
||||
|
||||
var isVisible = this.Visible;
|
||||
unsafe
|
||||
{
|
||||
// We need to check if our window is cloaked or not. A cloaked window is still
|
||||
// technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
|
||||
// (these are all separate states)
|
||||
long attr = 0;
|
||||
PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
|
||||
if (attr == 1 /* DWM_CLOAKED_APP */)
|
||||
{
|
||||
isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Note to future us: the wParam will have the index of the hotkey we registered.
|
||||
// We can use that in the future to differentiate the hotkeys we've pressed
|
||||
// so that we can bind hotkeys to individual commands
|
||||
if (!this.Visible || !isRootHotkey)
|
||||
if (!isVisible || !isRootHotkey)
|
||||
{
|
||||
Activate();
|
||||
|
||||
@@ -487,7 +562,16 @@ public sealed partial class MainWindow : Window,
|
||||
}
|
||||
else if (isRootHotkey)
|
||||
{
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
// If there's a debugger attached...
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
{
|
||||
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
|
||||
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
return;
|
||||
}
|
||||
|
||||
HideWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -499,29 +583,25 @@ public sealed partial class MainWindow : Window,
|
||||
{
|
||||
switch (uMsg)
|
||||
{
|
||||
case WM_HOTKEY:
|
||||
// Prevent the window from maximizing when double-clicking the title bar area
|
||||
case PInvoke.WM_NCLBUTTONDBLCLK:
|
||||
return (LRESULT)IntPtr.Zero;
|
||||
case PInvoke.WM_HOTKEY:
|
||||
{
|
||||
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);
|
||||
|
||||
// var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
|
||||
|
||||
// // Note to future us: the wParam will have the index of the hotkey we registered.
|
||||
// // We can use that in the future to differentiate the hotkeys we've pressed
|
||||
// // so that we can bind hotkeys to individual commands
|
||||
// if (!this.Visible || !isRootHotkey)
|
||||
// {
|
||||
// Activate();
|
||||
|
||||
// Summon(hotkey.CommandId);
|
||||
// }
|
||||
// else if (isRootHotkey)
|
||||
// {
|
||||
// PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
// }
|
||||
}
|
||||
|
||||
return (LRESULT)IntPtr.Zero;
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
|
||||
|
||||
<PackageReference Include="WinUIEx" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user