diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f9389b8d91..1a85de1e06 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,6 +7,13 @@ body: - type: markdown attributes: value: Please make sure to [search for existing issues](https://github.com/microsoft/PowerToys/issues) before filing a new one! +- type: markdown + attributes: + value: | + We are aware of the following high-volume issues and are actively working on them. Please check if your issue is one of these before filing a new bug report: + * **PowerToys Run crash related to "Desktop composition is disabled"**: This may appear as `COMException: 0x80263001`. For more details, see issue [#31226](https://github.com/microsoft/PowerToys/issues/31226). + * **PowerToys Run crash with `COMException (0xD0000701)`**: For more details, see issue [#30769](https://github.com/microsoft/PowerToys/issues/30769). + * **PowerToys Run crash with a "Cyclic reference" error**: This `System.InvalidOperationException` is detailed in issue [#36451](https://github.com/microsoft/PowerToys/issues/36451). - id: version type: input attributes: diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index 756c450534..c655bb1b55 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -95,6 +95,7 @@ OTP Yubi Yubico Perplexity +Groq svgl # KEYS @@ -321,3 +322,16 @@ REGSTR # Misc Win32 APIs and PInvokes INVOKEIDLIST + +# PowerRename metadata pattern abbreviations (used in tests and regex patterns) +DDDD +FFF +HHH +riday +YYY + +# GitHub issue/PR commands +azp +feedbackhub +needinfo +reportbug diff --git a/.github/actions/spell-check/excludes.txt b/.github/actions/spell-check/excludes.txt index c6f1225788..551c248923 100644 --- a/.github/actions/spell-check/excludes.txt +++ b/.github/actions/spell-check/excludes.txt @@ -105,6 +105,7 @@ ^src/common/notifications/BackgroundActivatorDLL/cpp\.hint$ ^src/common/sysinternals/Eula/ ^src/modules/cmdpal/doc/initial-sdk-spec/list-elements-mock-002\.pdn$ +^src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleMarkdownImagesPage\.cs$ ^src/modules/colorPicker/ColorPickerUI/Shaders/GridShader\.cso$ ^src/modules/launcher/Plugins/Microsoft\.PowerToys\.Run\.Plugin\.TimeDate/Properties/ ^src/modules/MouseUtils/MouseJumpUI/MainForm\.resx$ diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index cb1ddb8cc4..d4be728886 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -2,8 +2,8 @@ AAAAs abcdefghjkmnpqrstuvxyz abgr ABlocked -ABOUTBOX ABORTIFHUNG +ABOUTBOX Abug Acceleratorkeys ACCEPTFILES @@ -22,6 +22,7 @@ ADate ADDSTRING ADDUNDORECORD ADifferent +adjacents ADMINS adml admx @@ -34,6 +35,7 @@ AFX AGGREGATABLE AHK AHybrid +AIUI akv ALarger ALIGNRIGHT @@ -54,6 +56,7 @@ ANull AOC aocfnapldcnfbofgmbbllojgocaelgdd AOklab +aot APARTMENTTHREADED APeriod apicontract @@ -63,6 +66,7 @@ APIIs Apm APPBARDATA APPEXECLINK +appext APPLICATIONFRAMEHOST appmanifest APPMODEL @@ -94,10 +98,10 @@ ASSOCSTR ASYNCWINDOWPLACEMENT ASYNCWINDOWPOS atl -ATX ATRIOX +ATX aumid -Authenticode +authenticode AUTOBUDDY AUTOCHECKBOX AUTOHIDE @@ -111,10 +115,13 @@ AValid AWAYMODE azcliversion azman +azureaiinference +azureinference +azureopenai +backticks bbwe BCIE bck -backticks BESTEFFORT bezelled bhid @@ -135,7 +142,7 @@ bla BLACKFRAME BLENDFUNCTION Blockquotes -Blt +blt BLURBEHIND BLURREGION bmi @@ -143,6 +150,7 @@ BNumber BODGY BOklab BOOTSTRAPPERINSTALLFOLDER +Bootstrappers BOTTOMALIGN boxmodel BPBF @@ -176,7 +184,10 @@ Canvascustomlayout CAPTUREBLT CAPTURECHANGED CARETBLINKING +Carlseibert CAtl +caub +CBN cch CCHDEVICENAME CCHFORMNAME @@ -195,7 +206,6 @@ changecursor CHILDACTIVATE CHILDWINDOW CHOOSEFONT -CIBUILD cidl CIELCh cim @@ -211,6 +221,7 @@ clientside CLIPBOARDUPDATE CLIPCHILDREN CLIPSIBLINGS +CLITo closesocket clp CLSCTX @@ -231,6 +242,7 @@ CODENAME codereview Codespaces Coen +cognitiveservices COINIT colid colorconv @@ -245,12 +257,12 @@ cominterop commandnotfound commandpalette compmgmt +COMPOSITIONDISABLED COMPOSITIONFULL CONFIGW CONFLICTINGMODIFIERKEY CONFLICTINGMODIFIERSHORTCUT CONOUT -coreclr constexpr contentdialog contentfiles @@ -262,6 +274,7 @@ copiedcolorrepresentation coppied copyable COPYPEN +coreclr COREWINDOW Corpor cotaskmem @@ -270,9 +283,9 @@ countof covrun cpcontrols cph -cppcoreguidelines cplusplus CPower +cppcoreguidelines cpptools cppvsdbg cppwinrt @@ -281,6 +294,7 @@ CREATEPROCESS CREATESCHEDULEDTASK CREATESTRUCT CREATEWINDOWFAILED +creativecommons CRECT CRH critsec @@ -300,6 +314,7 @@ CURRENTDIR CURSORINFO cursorpos CURSORSHOWING +CURSORWRAP customaction CUSTOMACTIONTEST CUSTOMFORMATPLACEHOLDER @@ -315,7 +330,6 @@ CYSCREEN CYSMICON CYVIRTUALSCREEN Czechia -cziplib Dac dacl DAffine @@ -340,7 +354,6 @@ debugbreak decryptor Dedup Deduplicator -Deeplink DEFAULTBOOTSTRAPPERINSTALLFOLDER DEFAULTCOLOR DEFAULTFLAGS @@ -387,7 +400,6 @@ DISPLAYFREQUENCY displayname DISPLAYORIENTATION divyan -djwsxzxb Dlg DLGFRAME DLGMODALFRAME @@ -398,6 +410,8 @@ DNLEN DONOTROUND DONTVALIDATEPATH dotnet +downsampled +downsampling downscale DPICHANGED DPIs @@ -413,7 +427,7 @@ DROPFILES DSTINVERT DString DSVG -DTo +dto DUMMYUNIONNAME dutil DVASPECT @@ -447,7 +461,6 @@ EDITKEYBOARD EDITSHORTCUTS EDITTEXT EFile -ekus eku emojis ENABLEDELAYEDEXPANSION @@ -516,6 +529,7 @@ FARPROC fdx fesf FFFF +Figma FILEEXPLORER fileexploreraddons fileexplorerpreview @@ -543,6 +557,7 @@ flac flyouts FMask fmtid +FNumber FOF FOFX FOLDERID @@ -553,6 +568,7 @@ FORCEMINIMIZE FORMATDLGORD formatetc FORPARSING +foundrylocal FRAMECHANGED frm FROMTOUCH @@ -571,15 +587,16 @@ gdi gdiplus GDIPVER GDISCALED +geolocator GETCLIENTAREAANIMATION GETCURSEL GETDESKWALLPAPER GETDLGCODE GETDPISCALEDSIZE getfilesiginforedist -geolocator GETHOTKEY GETICON +GETLBTEXT GETMINMAXINFO GETNONCLIENTMETRICS GETPROPERTYSTOREFLAGS @@ -587,16 +604,19 @@ GETSCREENSAVERRUNNING GETSECKEY GETSTICKYKEYS GETTEXTLENGTH -gitmodules GHND +gitmodules GMEM GNumber +googleai +googlegemini gpedit gpo GPOCA gpp gpu gradians +grctlext Gridcustomlayout GSM gtm @@ -694,6 +714,7 @@ HTCLIENT hthumbnail HTOUCHINPUT HTTRANSPARENT +hutchinsoniana HVal HValue Hvci @@ -708,9 +729,9 @@ HWNDPARENT HWNDPREV hyjiacan IAI -icf ICONERROR ICONLOCATION +icf IDCANCEL IDD idk @@ -724,9 +745,11 @@ ietf IEXPLORE IFACEMETHOD IFACEMETHODIMP +ifd IGNOREUNKNOWN IGo iid +IIM Iindex Ijwhost ILD @@ -750,7 +773,8 @@ INITDIALOG INITGUID INITTOLOGFONTSTRUCT INLINEPREFIX -Inlines +inlines +Inno INPC inproc INPUTHARDWARE @@ -767,7 +791,6 @@ INSTALLFOLDERTOPREVIOUSINSTALLFOLDER INSTALLLOCATION INSTALLMESSAGE INSTALLPROPERTY -installscopeperuser INSTALLSTARTMENUSHORTCUT INSTALLSTATE Inste @@ -780,6 +803,7 @@ invokecommand ipcmanager IPREVIEW ipreviewhandlervisualssetfont +IPTC irow irprops isbi @@ -823,15 +847,14 @@ keyvault KILLFOCUS killrunner kmph -ksa kvp Kybd LARGEICON lastcodeanalysissucceeded LASTEXITCODE LAYOUTRTL -LCh lbl +LCh lcid LCIDTo lcl @@ -847,10 +870,10 @@ LExit lhwnd LIBFUZZER LIBID +lightswitch LIMITSIZE LIMITTEXT lindex -lightswitch linkid LINKOVERLAY LINQTo @@ -861,6 +884,7 @@ LLKH llkhf LMEM LMENU +lng LOADFROMFILE LOBYTE localappdata @@ -870,16 +894,14 @@ LOCATIONCHANGE LOCKTYPE LOGFONT LOGFONTW -logon LOGMSG +logon LOGPIXELSX LOGPIXELSY -lng lon longdate LONGNAMES lowlevel -lquadrant LOWORD lparam LPBITMAPINFOHEADER @@ -913,6 +935,7 @@ lpv LPW lpwcx lpwndpl +lquadrant LReader LRESULT LSTATUS @@ -939,6 +962,7 @@ MAKELONG MAKELPARAM makepri MAKEWPARAM +Malware manifestdependency MAPPEDTOSAMEKEY MAPTOSAMESHORTCUT @@ -961,6 +985,7 @@ MENUITEMINFO MENUITEMINFOW MERGECOPY MERGEPAINT +metadatamatters Metadatas metafile mfc @@ -1007,9 +1032,6 @@ mousepointer mouseutils MOVESIZEEND MOVESIZESTART -muxx -muxxc -muxxh MRM MRT mru @@ -1029,6 +1051,7 @@ msiexec MSIFASTINSTALL MSIHANDLE MSIRESTARTMANAGERCONTROL +MSIs msixbundle MSIXCA MSLLHOOKSTRUCT @@ -1037,16 +1060,21 @@ msrc msstore msvcp MT +mstsc MTND MULTIPLEUSE multizone muxc +muxx +muxxc +muxxh MVPs mvvm MVVMTK MWBEx MYICON NAMECHANGE +Notavailable namespaceanddescendants nao NCACTIVATE @@ -1151,6 +1179,7 @@ nowarn NOZORDER NPH npmjs +NPU NResize NTAPI ntdll @@ -1158,8 +1187,8 @@ ntfs NTSTATUS NTSYSAPI NULLCURSOR -nullref nullonfailure +nullref numberbox nwc ocr @@ -1175,13 +1204,18 @@ oldpath oldtheme oleaut OLECHAR +ollama onebranch +onnx OOBEUI openas opencode OPENFILENAME +openrdp opensource openxmlformats +ollama +onnx OPTIMIZEFORINVOKE ORPHANEDDIALOGTITLE ORSCANS @@ -1254,6 +1288,7 @@ pguid phbm phbmp phicon +Photoshop phwnd pici pidl @@ -1372,6 +1407,7 @@ QDC qit QITAB QITABENT +QNN qoi Quarternary QUERYENDSESSION @@ -1381,8 +1417,8 @@ quickaccent QUNS RAII RAlt -RAquadrant randi +RAquadrant rasterization Rasterize RAWINPUTDEVICE @@ -1392,6 +1428,8 @@ RAWPATH rbhid rclsid RCZOOMIT +remotedesktop +rdp RDW READMODE READOBJECTS @@ -1409,9 +1447,7 @@ regfile REGISTERCLASSFAILED REGISTRYHEADER REGISTRYPREVIEWEXT -registryroot regkey -regroot regsvr REINSTALLMODE releaseblog @@ -1464,7 +1500,6 @@ rstringalpha rstringdigit rtb RTLREADING -rtm runas rundll rungameid @@ -1478,6 +1513,7 @@ sacl safeprojectname SAMEKEYPREVIOUSLYMAPPED SAMESHORTCUTPREVIOUSLYMAPPED +samsung sancov SAVEFAILED scanled @@ -1520,8 +1556,8 @@ SETRULES SETSCREENSAVEACTIVE SETSTICKYKEYS SETTEXT -settingscard SETTINGCHANGE +settingscard SETTINGSCHANGED settingsheader settingshotkeycontrol @@ -1596,6 +1632,7 @@ SKIPOWNPROCESS sku SLGP sln +slnx SMALLICON smartphone smileys @@ -1666,6 +1703,7 @@ stringtable stringval Strm strret +STRSAFE stscanf sttngs Stubless @@ -1677,7 +1715,6 @@ sublang SUBMODULEUPDATE subresource Superbar -suntimes sut svchost SVGIn @@ -1711,7 +1748,6 @@ SYSTEMMODAL SYSTEMTIME TARG TARGETAPPHEADER -TARGETDIR targetentrypoint TARGETHEADER targetver @@ -1741,10 +1777,10 @@ textextractor TEXTINCLUDE tfopen tgz +THEMECHANGED themeresources THH THICKFRAME -THEMECHANGED THISCOMPONENT throughs TILEDWINDOW @@ -1816,6 +1852,7 @@ UNCPRIORITY UNDNAME UNICODETEXT unins +Uninstaller uninstalls Uniquifies unitconverter @@ -1832,6 +1869,7 @@ UPDATENOW UPDATEREGISTRY updown UPGRADINGPRODUCTCODE +upscaling Uptool urld Usb @@ -1913,6 +1951,7 @@ Wca WCE wcex WClass +WCRAPI wcsicmp wcsncpy wcsnicmp @@ -1930,6 +1969,7 @@ wgpocpl WHEREID wic wifi +wikimedia wikipedia WIL winapi @@ -1943,6 +1983,7 @@ WINDOWPLACEMENT WINDOWPOSCHANGED WINDOWPOSCHANGING WINDOWSBUILDNUMBER +windowsml windowssearch windowssettings WINDOWSTYLES @@ -1958,6 +1999,7 @@ Winhook WINL winlogon winmd +winml WINNT winres winrt @@ -2020,18 +2062,21 @@ WTSAT Wubi WUX Wwanpp +xap XAxis XButton xclip xcopy XDeployment xdf +XDimension XDocument XElement xfd XFile XIncrement XLoc +xmp XNamespace Xoshiro XPels @@ -2048,6 +2093,7 @@ XVIRTUALSCREEN xxxxxx YAxis ycombinator +YDimension YIncrement yinle yinyue @@ -2055,8 +2101,8 @@ YPels YPos YResolution YSpeed -YTimer YStr +YTimer YVIRTUALSCREEN ZEROINIT zonability diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt index cb303a10ad..181d728e84 100644 --- a/.github/actions/spell-check/patterns.txt +++ b/.github/actions/spell-check/patterns.txt @@ -253,7 +253,7 @@ _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING # hit-count: 1 file-count: 1 # Amazon -\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|)[^"'\s]+ +\bamazon\.com/[-\w]+/(?:dp/[0-9A-Z]+|) # hit-count: 3 file-count: 3 # imgur diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 04f9cfaaeb..560b44b5a4 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,6 +5,7 @@ ## PR Checklist - [ ] Closes: #xxx + - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 0db3dc6595..be07e7facc 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -21,6 +21,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: 'Dependency Review' uses: actions/dependency-review-action@v4 \ No newline at end of file diff --git a/.github/workflows/manual-batch-issue-deduplication.yml b/.github/workflows/manual-batch-issue-deduplication.yml index d02dc2e282..616e2244f0 100644 --- a/.github/workflows/manual-batch-issue-deduplication.yml +++ b/.github/workflows/manual-batch-issue-deduplication.yml @@ -27,7 +27,7 @@ jobs: issue: ${{ fromJson(github.event.inputs.issue_numbers) }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Run GenAI Issue Deduplicator uses: pelikhan/action-genai-issue-dedup@v0 diff --git a/.github/workflows/msstore-submissions.yml b/.github/workflows/msstore-submissions.yml index a44dafb199..36dfc4d785 100644 --- a/.github/workflows/msstore-submissions.yml +++ b/.github/workflows/msstore-submissions.yml @@ -40,7 +40,7 @@ jobs: echo powerToysInstallerArm64Url=$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("arm64"))][0].browser_download_url') >> $GITHUB_OUTPUT - name: Setup .NET 9.0 - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '9.0.x' diff --git a/.gitignore b/.gitignore index ed3f80a4ec..1318abc22c 100644 --- a/.gitignore +++ b/.gitignore @@ -349,10 +349,7 @@ src/common/Telemetry/*.etl /src/modules/powerrename/ui/RCb24464 # Generated installer file for Monaco source files. -/installer/PowerToysSetup/MonacoSRC.wxs -/installer/PowerToysSetup/DscResources.wxs /installer/PowerToysSetupVNext/MonacoSRC.wxs -/installer/PowerToysSetupVNext/DscResources.wxs # MSBuildCache /MSBuildCacheLogs/ diff --git a/.gitmodules b/.gitmodules index 1601291341..f878c1a9e3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "deps/expected-lite"] path = deps/expected-lite url = https://github.com/martinmoene/expected-lite.git -[submodule "deps/cziplib"] - path = deps/cziplib - url = https://github.com/kuba--/zip.git diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index d99fabf0eb..83289fa102 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -5,7 +5,6 @@ { "MatchedPath": [ "*.resources.dll", - "WinUI3Apps\\Assets\\Settings\\Scripts\\*.ps1", "PowerToys.ActionRunner.exe", @@ -27,6 +26,7 @@ "PowerToys.GPOWrapper.dll", "PowerToys.GPOWrapperProjection.dll", "PowerToys.AllExperiments.dll", + "LanguageModelProvider.dll", "Common.Search.dll", @@ -181,6 +181,7 @@ "PowerToys.MousePointerCrosshairs.dll", "PowerToys.MouseJumpUI.dll", "PowerToys.MouseJumpUI.exe", + "PowerToys.CursorWrap.dll", "PowerToys.MouseWithoutBorders.dll", "PowerToys.MouseWithoutBorders.exe", @@ -290,6 +291,7 @@ "Mono.Cecil.Rocks.dll", "Newtonsoft.Json.dll", "CommunityToolkit.WinUI.Controls.TitleBar.dll", + "CommunityToolkit.WinUI.Controls.OpacityMaskView.dll", "NLog.dll", "HtmlAgilityPack.dll", @@ -346,6 +348,8 @@ "Testably.Abstractions.FileSystem.Interface.dll", "WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll", "ColorCode.Core.dll", + "Microsoft.SemanticKernel.Connectors.Ollama.dll", + "OllamaSharp.dll", "UnitsNet.dll", "UtfUnknown.dll", diff --git a/.pipelines/ESRPSigning_installer.json b/.pipelines/ESRPSigning_installer.json deleted file mode 100644 index c9e505d3a2..0000000000 --- a/.pipelines/ESRPSigning_installer.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "Version": "1.0.0", - "UseMinimatch": false, - "SignBatches": [ - { - "MatchedPath": [ - "PowerToysSetupCustomActionsVNext.dll", - "SilentFilesInUseBAFunction.dll", - "PowerToys*Setup-*.exe", - "PowerToys*Setup-*.msi" - ], - "SigningInfo": { - "Operations": [ - { - "KeyCode": "CP-230012", - "OperationSetCode": "SigntoolSign", - "Parameters": [ - { - "parameterName": "OpusName", - "parameterValue": "Microsoft" - }, - { - "parameterName": "OpusInfo", - "parameterValue": "http://www.microsoft.com" - }, - { - "parameterName": "FileDigest", - "parameterValue": "/fd \"SHA256\"" - }, - { - "parameterName": "PageHash", - "parameterValue": "/NPH" - }, - { - "parameterName": "TimeStamp", - "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - } - ], - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-230012", - "OperationSetCode": "SigntoolVerify", - "Parameters": [], - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] - } - } - ] -} diff --git a/.pipelines/generateDscManifests.ps1 b/.pipelines/generateDscManifests.ps1 index 109610e62e..e0a2f463af 100644 --- a/.pipelines/generateDscManifests.ps1 +++ b/.pipelines/generateDscManifests.ps1 @@ -65,21 +65,28 @@ if (-not (Test-Path $outputDir)) { New-Item -Path $outputDir -ItemType Directory -Force | Out-Null } -Write-Host "DSC manifests will be generated to: '$outputDir'" +# DSC v3 manifests go to DSCModules subfolder +$dscOutputDir = Join-Path $outputDir 'DSCModules' +if (-not (Test-Path $dscOutputDir)) { + Write-Host "Creating DSCModules subfolder at '$dscOutputDir'." + New-Item -Path $dscOutputDir -ItemType Directory -Force | Out-Null +} -Write-Host "Cleaning previously generated DSC manifest files from '$outputDir'." -Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force +Write-Host "DSC manifests will be generated to: '$dscOutputDir'" -$arguments = @('manifest', '--resource', 'settings', '--outputDir', $outputDir) +Write-Host "Cleaning previously generated DSC manifest files from '$dscOutputDir'." +Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction SilentlyContinue | Remove-Item -Force + +$arguments = @('manifest', '--resource', 'settings', '--outputDir', $dscOutputDir) Write-Host "Invoking DSC manifest generator: '$exePath' $($arguments -join ' ')" & $exePath @arguments if ($LASTEXITCODE -ne 0) { throw "PowerToys.DSC.exe exited with code $LASTEXITCODE" } -$generatedFiles = Get-ChildItem -Path $outputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop +$generatedFiles = Get-ChildItem -Path $dscOutputDir -Filter 'microsoft.powertoys.*.settings.dsc.resource.json' -ErrorAction Stop if ($generatedFiles.Count -eq 0) { - throw "No DSC manifest files were generated in '$outputDir'." + throw "No DSC manifest files were generated in '$dscOutputDir'." } Write-Host "Generated $($generatedFiles.Count) DSC manifest file(s):" diff --git a/.pipelines/packages.config b/.pipelines/packages.config index c4bca409f9..1e9b92d3b7 100644 --- a/.pipelines/packages.config +++ b/.pipelines/packages.config @@ -1,4 +1,4 @@ - + diff --git a/.pipelines/v2/ci.yml b/.pipelines/v2/ci.yml index 6b0105a38a..297c268757 100644 --- a/.pipelines/v2/ci.yml +++ b/.pipelines/v2/ci.yml @@ -32,7 +32,7 @@ parameters: - name: enableMsBuildCaching type: boolean displayName: "Enable MSBuild Caching" - default: true + default: false - name: runTests type: boolean displayName: "Run Tests" diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml index e8cc7d5ed8..71f80f574b 100644 --- a/.pipelines/v2/release.yml +++ b/.pipelines/v2/release.yml @@ -52,8 +52,6 @@ extends: name: SHINE-INT-S ${{ if eq(parameters.useVSPreview, true) }}: demands: ImageOverride -equals SHINE-VS17-Preview - ${{ else }}: - image: SHINE-VS17-Latest os: windows sdl: tsa: @@ -75,7 +73,6 @@ extends: name: SHINE-INT-L demands: # Our INT agents have a large disk mounted at P:\ - - WorkFolder -equals P:\_work - ${{ if eq(parameters.useVSPreview, true) }}: - ImageOverride -equals SHINE-VS17-Preview os: windows @@ -126,7 +123,6 @@ extends: parameters: pool: name: SHINE-INT-L - image: SHINE-VS17-Latest os: windows official: true codeSign: true diff --git a/.pipelines/v2/templates/job-build-project.yml b/.pipelines/v2/templates/job-build-project.yml index 2ffd0c7f62..4ce0c0e7c0 100644 --- a/.pipelines/v2/templates/job-build-project.yml +++ b/.pipelines/v2/templates/job-build-project.yml @@ -111,6 +111,7 @@ jobs: ${{ else }}: OutputBuildPlatform: ${{ platform }} variables: + NUGET_PACKAGES: 'C:\NuGetPackages' # Some of our build steps cache these here... and it was apparently part of the global environment MakeAppxPath: 'C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x86\MakeAppx.exe' # Azure DevOps abhors a vacuum # If these are blank, expansion will fail later on... which will result in direct substitution of the variable *names* @@ -139,6 +140,10 @@ jobs: - output: pipelineArtifact artifactName: $(JobOutputArtifactName) targetPath: $(Build.ArtifactStagingDirectory) + - output: pipelineArtifact + artifactName: $(JobOutputArtifactName)-failure-$(System.JobAttempt) + targetPath: $(LogOutputDirectory) + condition: or(failed(), canceled()) steps: - checkout: self clean: true @@ -187,14 +192,14 @@ jobs: displayName: Verify XAML formatting - pwsh: |- - & '.pipelines/verifyNugetPackages.ps1' -solution '$(build.sourcesdirectory)\PowerToys.sln' - displayName: Verify Nuget package versions for PowerToys.sln + & '.pipelines/verifyNugetPackages.ps1' -solution '$(build.sourcesdirectory)\PowerToys.slnx' + displayName: Verify Nuget package versions for PowerToys.slnx - pwsh: |- - & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\PowerToys.sln' + & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\PowerToys.slnx' & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\tools\BugReportTool\BugReportTool.sln' & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\tools\StylesReportTool\StylesReportTool.sln' - & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\installer\PowerToysSetup.sln' + & '.pipelines/verifyArm64Configuration.ps1' -solution '$(build.sourcesdirectory)\installer\PowerToysSetup.slnx' displayName: Verify ARM64 configurations - ${{ if eq(parameters.enablePackageCaching, true) }}: @@ -247,7 +252,7 @@ jobs: ${{ else }}: displayName: Build PowerToys main project inputs: - solution: 'PowerToys.sln' + solution: 'PowerToys.slnx' vsVersion: 17.0 msbuildArgs: >- -restore -graph @@ -266,6 +271,26 @@ jobs: env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: VSBuild@1 + displayName: Generate DSC artifacts for ARM64 + condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64')) + inputs: + solution: PowerToys.slnx + vsVersion: 17.0 + msbuildArgs: >- + -restore + /p:Configuration=$(BuildConfiguration) + /p:Platform=x64 + /t:DSC\PowerToys_Settings_DSC_Schema_Generator + /bl:$(LogOutputDirectory)\build-dsc-generator.binlog + ${{ parameters.additionalBuildOptions }} + $(MSBuildCacheParameters) + $(RestoreAdditionalProjectSourcesArg) + platform: x64 + configuration: $(BuildConfiguration) + msbuildArchitecture: x64 + maximumCpuCount: true + # Build PowerToys.DSC.exe for ARM64 (x64 uses existing binary from previous build) - task: VSBuild@1 displayName: Build PowerToys.DSC.exe (x64 for generating manifests) @@ -375,7 +400,7 @@ jobs: ### HACK: On ARM64 builds, building an app with Windows App SDK copies the x64 WebView2 dll instead of the ARM64 one. This task makes sure the right dll is used. - task: CopyFiles@2 displayName: HACK Copy core WebView2 ARM64 dll to output directory - condition: eq(variables['BuildPlatform'],'arm64') + condition: and(succeeded(), eq(variables['BuildPlatform'], 'arm64')) inputs: contents: packages/Microsoft.Web.WebView2.1.0.2903.40/runtimes/win-ARM64/native_uap/Microsoft.Web.WebView2.Core.dll targetFolder: $(Build.SourcesDirectory)/ARM64/Release/WinUI3Apps/ @@ -414,11 +439,11 @@ jobs: inputs: testResultsFormat: VSTest testResultsFiles: '**/*.trx' - condition: ne(variables['BuildPlatform'],'arm64') + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) # Native dlls - task: VSTest@2 - condition: ne(variables['BuildPlatform'],'arm64') # No arm64 agents to run the tests. + condition: and(succeeded(), ne(variables['BuildPlatform'], 'arm64')) # No arm64 agents to run the tests. displayName: 'Native Tests' inputs: platform: '$(BuildPlatform)' @@ -512,14 +537,6 @@ jobs: versionNumber: ${{ parameters.versionNumber }} additionalBuildOptions: ${{ parameters.additionalBuildOptions }} - - template: steps-build-installer-vnext.yml - parameters: - codeSign: ${{ parameters.codeSign }} - signingIdentity: ${{ parameters.signingIdentity }} - versionNumber: ${{ parameters.versionNumber }} - additionalBuildOptions: ${{ parameters.additionalBuildOptions }} - buildUserInstaller: true # NOTE: This is the distinction between the above and below rules - # This saves ~1GiB per architecture. We won't need these later. # Removes: # - All .pdb files from any static libs .libs (which were only used during linking) diff --git a/.pipelines/v2/templates/job-build-ui-tests.yml b/.pipelines/v2/templates/job-build-ui-tests.yml index b9fad16d44..342750d51c 100644 --- a/.pipelines/v2/templates/job-build-ui-tests.yml +++ b/.pipelines/v2/templates/job-build-ui-tests.yml @@ -74,7 +74,7 @@ jobs: command: restore feedsToUse: config configPath: nuget.config - restoreSolution: PowerToys.sln + restoreSolution: PowerToys.slnx restoreDirectory: '$(Build.SourcesDirectory)\packages' # Build all UI test projects if no specific modules are specified @@ -129,4 +129,4 @@ jobs: - publish: $(JobOutputDirectory) artifact: $(JobOutputArtifactName) displayName: Publish UI Test artifacts - condition: always() \ No newline at end of file + condition: always() diff --git a/.pipelines/v2/templates/steps-build-installer-vnext.yml b/.pipelines/v2/templates/steps-build-installer-vnext.yml index 882df8696a..bf467ed5d9 100644 --- a/.pipelines/v2/templates/steps-build-installer-vnext.yml +++ b/.pipelines/v2/templates/steps-build-installer-vnext.yml @@ -2,9 +2,6 @@ parameters: - name: versionNumber type: string default: "0.0.1" - - name: buildUserInstaller - type: boolean - default: false - name: codeSign type: boolean default: false @@ -25,43 +22,26 @@ steps: arguments: 'install --global wix --version 5.0.2' - pwsh: |- - & git clean -xfd -e *exe -- .\installer\ - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Clean installer to reduce cross-contamination - - - pwsh: |- - # Determine whether this is a per-user build - $IsPerUser = $${{ parameters.buildUserInstaller }} - - # Build slug used to locate the artifacts - $InstallerBuildSlug = if ($IsPerUser) { 'UserSetup' } else { 'MachineSetup' } - - # VNext bundle folder; base name intentionally omits the VNext suffix - $InstallerFolder = 'PowerToysSetupVNext' - if ($IsPerUser) { - $InstallerBasename = "PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)" - } - else { - $InstallerBasename = "PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)" - } - - # Export variables for downstream steps - Write-Host "##vso[task.setvariable variable=InstallerBuildSlug]$InstallerBuildSlug" - Write-Host "##vso[task.setvariable variable=InstallerRelativePath]$(BuildPlatform)\$(BuildConfiguration)\$InstallerBuildSlug" - Write-Host "##vso[task.setvariable variable=InstallerBasename]$InstallerBasename" - Write-Host "##vso[task.setvariable variable=InstallerFolder]$InstallerFolder" - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Prepare Installer variables + Write-Host "##vso[task.setvariable variable=InstallerMachineRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\MachineSetup" + Write-Host "##vso[task.setvariable variable=InstallerUserRoot]installer\PowerToysSetupVNext\$(BuildPlatform)\$(BuildConfiguration)\UserSetup" + Write-Host "##vso[task.setvariable variable=InstallerMachineBasename]PowerToysSetup-${{ parameters.versionNumber }}-$(BuildPlatform)" + Write-Host "##vso[task.setvariable variable=InstallerUserBasename]PowerToysUserSetup-${{ parameters.versionNumber }}-$(BuildPlatform)" + displayName: Prepare Installer variables # This dll needs to be built and signed before building the MSI. + # The Custom Actions project contains a pre-build event that prepares the .wxs files + # by filling them out with all our components. We pass RunBuildEvents=true to force + # that logic to run. - task: VSBuild@1 - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build PowerToysSetupCustomActionsVNext + displayName: Build Shared Support DLLs inputs: - solution: "**/installer/PowerToysSetup.sln" + solution: "**/installer/PowerToysSetup.slnx" vsVersion: 17.0 msbuildArgs: >- - /t:PowerToysSetupCustomActionsVNext - /p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true + /t:PowerToysSetupCustomActionsVNext;SilentFilesInUseBAFunction + /p:RunBuildEvents=true;RestorePackagesConfig=true;CIBuild=true -restore -graph - /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-actions.binlog + /bl:$(LogOutputDirectory)\installer-actions.binlog ${{ parameters.additionalBuildOptions }} platform: $(BuildPlatform) configuration: $(BuildConfiguration) @@ -70,28 +50,53 @@ steps: maximumCpuCount: true - ${{ if eq(parameters.codeSign, true) }}: - - template: steps-esrp-signing.yml + - template: steps-esrp-sign-files-authenticode.yml parameters: - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign PowerToysSetupCustomActionsVNext + displayName: Sign Shared Support DLLs signingIdentity: ${{ parameters.signingIdentity }} - inputs: - FolderPath: 'installer/PowerToysSetupCustomActionsVNext/$(InstallerRelativePath)' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + folder: 'installer' + pattern: |- + **/PowerToysSetupCustomActionsVNext.dll + **/SilentFilesInUseBAFunction.dll ## INSTALLER START #### MSI BUILDING AND SIGNING + # + # The MSI build contains code that reverts the .wxs files to their in-tree versions. + # This is only supposed to happen during local builds. Since this build system is + # supposed to run side by side--machine and then user--we do NOT want to destroy + # the .wxs files. Therefore, we pass RunBuildEvents=false to suppress all of that + # logic. + # + # We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built. + # We only pass -restore on the first one because the second run should already have all + # of the dependencies. - task: VSBuild@1 - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext MSI + displayName: 💻 Build VNext MSI inputs: - solution: "**/installer/PowerToysSetup.sln" + solution: "**/installer/PowerToysSetup.slnx" vsVersion: 17.0 msbuildArgs: >- -restore /t:PowerToysInstallerVNext - /p:RunBuildEvents=false;PerUser=${{parameters.buildUserInstaller}};BuildProjectReferences=false;CIBuild=true - /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-msi.binlog + /p:RunBuildEvents=false;PerUser=false;BuildProjectReferences=false;CIBuild=true + /bl:$(LogOutputDirectory)\installer-machine-msi.binlog + ${{ parameters.additionalBuildOptions }} + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: false # don't undo our hard work above by deleting the CustomActions dll + msbuildArchitecture: x64 + maximumCpuCount: true + + - task: VSBuild@1 + displayName: 👤 Build VNext MSI + inputs: + solution: "**/installer/PowerToysSetup.slnx" + vsVersion: 17.0 + msbuildArgs: >- + /t:PowerToysInstallerVNext + /p:RunBuildEvents=false;PerUser=true;BuildProjectReferences=false;CIBuild=true + /bl:$(LogOutputDirectory)\installer-user-msi.binlog ${{ parameters.additionalBuildOptions }} platform: $(BuildPlatform) configuration: $(BuildConfiguration) @@ -100,77 +105,66 @@ steps: maximumCpuCount: true - script: |- - wix msi decompile installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).msi -x $(build.sourcesdirectory)\extractedMsi - dir $(build.sourcesdirectory)\extractedMsi - displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract and verify MSI" + wix msi decompile $(InstallerMachineRoot)\$(InstallerMachineBasename).msi -x $(build.sourcesdirectory)\extractedMachineMsi + wix msi decompile $(InstallerUserRoot)\$(InstallerUserBasename).msi -x $(build.sourcesdirectory)\extractedUserMsi + dir $(build.sourcesdirectory)\extractedMachineMsi + dir $(build.sourcesdirectory)\extractedUserMsi + displayName: "WiX5: Extract and verify MSIs" # Check if deps.json files don't reference different dll versions. - pwsh: |- - & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMsi\File' - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Audit deps.json in MSI extracted files + & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File' + & '.pipelines/verifyDepsJsonLibraryVersions.ps1' -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File' + displayName: Audit deps.json in MSI extracted files - ${{ if eq(parameters.codeSign, true) }}: - pwsh: |- - & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\File' - & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMsi\Binary' - git clean -xfd ./extractedMsi - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Verify all binaries are signed and versioned + & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\File' + & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedMachineMsi\Binary' + & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\File' + & .pipelines/versionAndSignCheck.ps1 -targetDir '$(build.sourcesdirectory)\extractedUserMsi\Binary' + git clean -xfd ./extractedMachineMsi ./extractedUserMsi + displayName: Verify all binaries are signed and versioned - - template: steps-esrp-signing.yml + - template: steps-esrp-sign-files-authenticode.yml parameters: - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign VNext MSI + displayName: Sign VNext MSIs signingIdentity: ${{ parameters.signingIdentity }} - inputs: - FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + folder: 'installer' + pattern: '**/PowerToys*Setup-*.msi' #### END MSI - #### BUILDING AND SIGNING SilentFilesInUseBAFunction DLL - - task: VSBuild@1 - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build SilentFilesInUseBAFunction - inputs: - solution: "**/installer/PowerToysSetup.sln" - vsVersion: 17.0 - msbuildArgs: >- - /t:SilentFilesInUseBAFunction - /p:RunBuildEvents=true;PerUser=${{parameters.buildUserInstaller}};RestorePackagesConfig=true;CIBuild=true - -restore -graph - /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-SilentFilesInUseBAFunction.binlog - ${{ parameters.additionalBuildOptions }} - platform: $(BuildPlatform) - configuration: $(BuildConfiguration) - clean: false # don't undo our hard work above by deleting the msi - msbuildArchitecture: x64 - maximumCpuCount: true - - - ${{ if eq(parameters.codeSign, true) }}: - - template: steps-esrp-signing.yml - parameters: - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign SilentFilesInUseBAFunction - signingIdentity: ${{ parameters.signingIdentity }} - inputs: - FolderPath: 'installer/$(BuildPlatform)/$(BuildConfiguration)' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' - - #### END BUILDING AND SIGNING SilentFilesInUseBAFunction DLL - #### BOOTSTRAP BUILDING AND SIGNING + # We pass BuildProjectReferences=false so that it does not recompile the DLLs we just built. + # We only pass -restore on the first one because the second run should already have all + # of the dependencies. - task: VSBuild@1 - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Build VNext Bootstrapper + displayName: 💻 Build VNext Bootstrapper inputs: - solution: "**/installer/PowerToysSetup.sln" + solution: "**/installer/PowerToysSetup.slnx" vsVersion: 17.0 msbuildArgs: >- -restore /t:PowerToysBootstrapperVNext - /p:PerUser=${{parameters.buildUserInstaller}};CIBuild=true - /bl:$(LogOutputDirectory)\installer-$(InstallerBuildSlug)-bootstrapper.binlog - -restore -graph + /p:PerUser=false;BuildProjectReferences=false;CIBuild=true + /bl:$(LogOutputDirectory)\installer-machine-bootstrapper.binlog + ${{ parameters.additionalBuildOptions }} + platform: $(BuildPlatform) + configuration: $(BuildConfiguration) + clean: false # don't undo our hard work above by deleting the MSI nor SilentFilesInUseBAFunction + msbuildArchitecture: x64 + maximumCpuCount: true + + - task: VSBuild@1 + displayName: 👤 Build VNext Bootstrapper + inputs: + solution: "**/installer/PowerToysSetup.slnx" + vsVersion: 17.0 + msbuildArgs: >- + /t:PowerToysBootstrapperVNext + /p:PerUser=true;BuildProjectReferences=false;CIBuild=true + /bl:$(LogOutputDirectory)\installer-user-bootstrapper.binlog ${{ parameters.additionalBuildOptions }} platform: $(BuildPlatform) configuration: $(BuildConfiguration) @@ -181,54 +175,41 @@ steps: # The entirety of bundle unpacking/re-packing is unnecessary if we are not code signing it. - ${{ if eq(parameters.codeSign, true) }}: - script: |- - wix burn detach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe - displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Extract Engine from Bundle" + wix burn detach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe + wix burn detach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe + displayName: "WiX5: Extract Engines from Bundles" - - template: steps-esrp-signing.yml + - template: steps-esrp-sign-files-authenticode.yml parameters: - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign WiX Engine + displayName: Sign WiX Engines signingIdentity: ${{ parameters.signingIdentity }} - inputs: - FolderPath: "installer" - Pattern: engine.exe - signConfigType: inlineSignParams - inlineOperation: | - [ - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolSign", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "http://www.microsoft.com", - "FileDigest": "/fd \"SHA256\"", - "PageHash": "/NPH", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - }, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-230012", - "OperationCode": "SigntoolVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] + folder: "installer" + pattern: '*-engine.exe' - script: |- - wix burn reattach installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe -engine installer\engine.exe -o installer\$(InstallerFolder)\$(InstallerRelativePath)\$(InstallerBasename).exe - displayName: "${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} WiX5: Reattach Engine to Bundle" + wix burn reattach $(InstallerMachineRoot)\$(InstallerMachineBasename).exe -engine installer\machine-engine.exe -o $(InstallerMachineRoot)\$(InstallerMachineBasename).exe + wix burn reattach $(InstallerUserRoot)\$(InstallerUserBasename).exe -engine installer\user-engine.exe -o $(InstallerUserRoot)\$(InstallerUserBasename).exe + displayName: "WiX5: Reattach Engines to Bundles" - - template: steps-esrp-signing.yml + - pwsh: |- + & wix burn extract -oba installer\ba\m "$(InstallerMachineRoot)\$(InstallerMachineBasename).exe" + & wix burn extract -oba installer\ba\u "$(InstallerUserRoot)\$(InstallerUserBasename).exe" + Get-ChildItem installer\ba -Recurse -Include *.exe,*.dll | Get-AuthenticodeSignature | ForEach-Object { + If ($_.Status -Ne "Valid") { + Write-Error $_.StatusMessage + } Else { + Write-Host $_.StatusMessage + } + } + & git clean -fdx installer\ba + displayName: "WiX5: Verify Bootstrapper content is signed" + + - template: steps-esrp-sign-files-authenticode.yml parameters: - displayName: ${{replace(replace(parameters.buildUserInstaller,'True','👤'),'False','💻')}} Sign Final Bootstrapper + displayName: Sign Final Bootstrappers signingIdentity: ${{ parameters.signingIdentity }} - inputs: - FolderPath: 'installer/$(InstallerFolder)/$(InstallerRelativePath)' - signType: batchSigning - batchSignPolicyFile: '$(build.sourcesdirectory)\.pipelines\ESRPSigning_installer.json' - ciPolicyFile: '$(build.sourcesdirectory)\.pipelines\CIPolicy.xml' + folder: 'installer' + pattern: '**/PowerToys*Setup-*.exe' #### END BOOTSTRAP ## END INSTALLER diff --git a/.pipelines/v2/templates/steps-esrp-sign-files-authenticode.yml b/.pipelines/v2/templates/steps-esrp-sign-files-authenticode.yml new file mode 100644 index 0000000000..5b9bbd2fce --- /dev/null +++ b/.pipelines/v2/templates/steps-esrp-sign-files-authenticode.yml @@ -0,0 +1,45 @@ +parameters: + - name: displayName + type: string + default: Sign Specific Files + - name: folder + type: string + - name: pattern + type: string + - name: signingIdentity + type: object + default: {} + +steps: + - template: steps-esrp-signing.yml + parameters: + displayName: ${{ parameters.displayName }} + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: ${{ parameters.folder }} + Pattern: ${{ parameters.pattern }} + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: |- + [ + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "PageHash": "/NPH", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-230012", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] diff --git a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml index 9c59312844..cdb28b572c 100644 --- a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml +++ b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml @@ -54,4 +54,13 @@ steps: feedsToUse: 'config' nugetConfigPath: '$(build.sourcesdirectory)\nuget.config' restoreSolution: '$(build.sourcesdirectory)\**\*.sln' - includeNuGetOrg: false \ No newline at end of file + includeNuGetOrg: false + +- task: NuGetCommand@2 + displayName: 'Restore NuGet packages (slnx)' + inputs: + command: 'restore' + feedsToUse: 'config' + nugetConfigPath: '$(build.sourcesdirectory)\nuget.config' + restoreSolution: '$(build.sourcesdirectory)\**\*.slnx' + includeNuGetOrg: false diff --git a/.vscode/launch.json b/.vscode/launch.json index b2d2bca9ac..940ff302de 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -38,6 +38,17 @@ "env": {}, "console": "internalConsole", "stopAtEntry": false - } + }, + { + "name": "Run AdvancedPaste (managed, no build, ARCH configurable)", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}\\${input:arch}\\Debug\\WinUI3Apps\\PowerToys.AdvancedPaste.exe", + "args": [], + "cwd": "${workspaceFolder}", + "env": {}, + "console": "internalConsole", + "stopAtEntry": false + }, ] } \ No newline at end of file diff --git a/Cpp.Build.props b/Cpp.Build.props index 5a4538f940..f146a4d770 100644 --- a/Cpp.Build.props +++ b/Cpp.Build.props @@ -26,6 +26,7 @@ true $(MsbuildThisFileDirectory)\CppRuleSet.ruleset + $(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(CAExcludePath) @@ -34,7 +35,7 @@ arm64 false true - $(MSBuildThisFileFullPath)\..\deps\;$(MSBuildThisFileFullPath)\..\packages\;$(ExternalIncludePath) + $(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(ExternalIncludePath) Guard diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index 56a2eb9eee..8aba94f12f 100644 --- a/DATA_AND_PRIVACY.md +++ b/DATA_AND_PRIVACY.md @@ -147,6 +147,18 @@ _If you want to find diagnostic data events in the source code, these two links Microsoft.PowerToys.AdvancedPasteSemanticKernelFormatEvent Triggered when Advanced Paste leverages the Semantic Kernel. + + Microsoft.PowerToys.AdvancedPasteSemanticKernelErrorEvent + Occurs when the Semantic Kernel workflow encounters an error. + + + Microsoft.PowerToys.AdvancedPasteEndpointUsageEvent + Logs the AI provider, model, and processing duration for each endpoint call. + + + Microsoft.PowerToys.AdvancedPasteCustomActionErrorEvent + Records provider, model, and status details when a custom action fails. + ### Always on Top @@ -231,6 +243,10 @@ _If you want to find diagnostic data events in the source code, these two links Event Name Description + + Microsoft.PowerToys.CmdNotFound_EnableCmdNotFound + Triggered when Command Not Found is enabled or disabled. + Microsoft.PowerToys.CmdNotFoundInstallEvent Triggered when a Command Not Found is installed. @@ -245,6 +261,62 @@ _If you want to find diagnostic data events in the source code, these two links +### Command Palette + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Event NameDescription
Microsoft.PowerToys.CmdPal_BeginInvokeTriggered when the Command Palette is launched by the user.
Microsoft.PowerToys.CmdPal_ColdLaunchOccurs when Command Palette starts for the first time (cold start).
Microsoft.PowerToys.CmdPal_OpenPageTriggered when a page is opened within the Command Palette, tracking navigation depth.
Microsoft.PowerToys.CmdPal_OpenUriOccurs when a URI is opened through the Command Palette, including whether it's a web URL.
Microsoft.PowerToys.CmdPal_ReactivateInstanceTriggered when an existing Command Palette instance is reactivated.
Microsoft.PowerToys.CmdPal_RunCommandLogs when a command is executed through the Command Palette, including admin elevation status.
Microsoft.PowerToys.CmdPal_RunQueryTriggered when a search query is performed, including result count and duration.
Microsoft.PowerToys.CmdPalDismissedOnEscOccurs when the Command Palette is dismissed by pressing the Escape key.
Microsoft.PowerToys.CmdPalDismissedOnLostFocusTriggered when the Command Palette is dismissed due to losing focus.
Microsoft.PowerToys.CmdPalHotkeySummonedLogs when the Command Palette is summoned via hotkey, distinguishing between global and context-specific hotkeys.
Microsoft.PowerToys.CmdPalInvokeResultRecords the result type of a Command Palette invocation.
Microsoft.PowerToys.CmdPalProcessStartedTriggered when the Command Palette process is started.
+ ### Crop And Lock @@ -723,6 +795,10 @@ _If you want to find diagnostic data events in the source code, these two links + + + + @@ -731,6 +807,10 @@ _If you want to find diagnostic data events in the source code, these two links + + + + @@ -916,12 +996,8 @@ _If you want to find diagnostic data events in the source code, these two links - - - - - - + + diff --git a/Directory.Packages.props b/Directory.Packages.props index e19421ff79..89d2284b34 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,4 +1,4 @@ - + true true @@ -7,9 +7,9 @@ + - @@ -35,23 +35,33 @@ - + - + - - - - - - + + + + + + + + + + + + + + + + - + - + - + @@ -74,7 +84,7 @@ - + @@ -84,28 +94,29 @@ - + - - - + + + - + - - + + + - + - - - - + + + + @@ -128,4 +139,4 @@ - + \ No newline at end of file diff --git a/NOTICE.md b/NOTICE.md index 51cb2cb79b..23efb64864 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1526,10 +1526,10 @@ SOFTWARE. - AdaptiveCards.Rendering.WinUI3 - AdaptiveCards.Templating - Appium.WebDriver -- Azure.AI.OpenAI - CoenM.ImageSharp.ImageHash - CommunityToolkit.Common - CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock +- CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView - CommunityToolkit.Mvvm - CommunityToolkit.WinUI.Animations - CommunityToolkit.WinUI.Collections diff --git a/PowerToys.sln b/PowerToys.sln deleted file mode 100644 index f4f2e1bd0f..0000000000 --- a/PowerToys.sln +++ /dev/null @@ -1,3352 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.32014.148 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "runner", "src\runner\runner.vcxproj", "{9412D5C6-2CF2-4FC2-A601-B55508EA9B27}" - ProjectSection(ProjectDependencies) = postProject - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73} = {031AC72E-FA28-4AB7-B690-6F7B9C28AA73} - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC} = {08E71C67-6A7E-4CA1-B04E-2FB336410BAC} - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B} = {0B43679E-EDFA-4DA0-AD30-F4628B308B1B} - {0B593A6C-4143-4337-860E-DB5710FB87DB} = {0B593A6C-4143-4337-860E-DB5710FB87DB} - {17DA04DF-E393-4397-9CF0-84DABE11032E} = {17DA04DF-E393-4397-9CF0-84DABE11032E} - {217DF501-135C-4E38-BFC8-99D4821032EA} = {217DF501-135C-4E38-BFC8-99D4821032EA} - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34} = {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34} - {38177D56-6AD1-4ADF-88C9-2843A7932166} = {38177D56-6AD1-4ADF-88C9-2843A7932166} - {48804216-2A0E-4168-A6D8-9CD068D14227} = {48804216-2A0E-4168-A6D8-9CD068D14227} - {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {5CCC8468-DEC8-4D36-99D4-5C891BEBD481} - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A} = {5E7360A8-D048-4ED3-8F09-0BFD64C5529A} - {655C9AF2-18D3-4DA6-80E4-85504A7722BA} = {655C9AF2-18D3-4DA6-80E4-85504A7722BA} - {69E1EE8D-143A-4060-9129-4658ACF14AAF} = {69E1EE8D-143A-4060-9129-4658ACF14AAF} - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB} = {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB} - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9} = {89F34AF7-1C34-4A72-AA6E-534BCF972BD9} - {AF2349B8-E5B6-4004-9502-687C1C7730B1} = {AF2349B8-E5B6-4004-9502-687C1C7730B1} - {B25AC7A5-FB9F-4789-B392-D5C85E948670} = {B25AC7A5-FB9F-4789-B392-D5C85E948670} - {BA58206B-1493-4C75-BFEA-A85768A1E156} = {BA58206B-1493-4C75-BFEA-A85768A1E156} - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} = {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D} - {D940E07F-532C-4FF3-883F-790DA014F19A} = {D940E07F-532C-4FF3-883F-790DA014F19A} - {DA425894-6E13-404F-8DCB-78584EC0557A} = {DA425894-6E13-404F-8DCB-78584EC0557A} - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D} = {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D} - {E364F67B-BB12-4E91-B639-355866EBCD8B} = {E364F67B-BB12-4E91-B639-355866EBCD8B} - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{4574FDD0-F61D-4376-98BF-E5A1262C11EC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "interface", "interface", "{3BB8493E-D18E-4485-A320-CB40F90F55AE}" - ProjectSection(SolutionItems) = preProject - src\modules\interface\powertoy_module_interface.h = src\modules\interface\powertoy_module_interface.h - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fancyzones", "fancyzones", "{D1D6BC88-09AE-4FB4-AD24-5DED46A791DD}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZonesLib", "src\modules\fancyzones\FancyZonesLib\FancyZonesLib.vcxproj", "{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZones.UnitTests", "src\modules\fancyzones\FancyZonesTests\UnitTests\UnitTests.vcxproj", "{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}" - ProjectSection(ProjectDependencies) = postProject - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "common", "common", "{1AFB6476-670D-4E80-A464-657E01DFF482}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Common.Lib.UnitTests", "src\common\UnitTests-CommonLib\UnitTests-CommonLib.vcxproj", "{1A066C63-64B3-45F8-92FE-664E1CCE8077}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PackageIdentity", "src\PackageIdentity\PackageIdentity.vcxproj", "{E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditor", "src\modules\fancyzones\editor\FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "powerrename", "powerrename", "{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameExt", "src\modules\powerrename\dll\PowerRenameExt.vcxproj", "{B25AC7A5-FB9F-4789-B392-D5C85E948670}" - ProjectSection(ProjectDependencies) = postProject - {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameLib", "src\modules\powerrename\lib\PowerRenameLib.vcxproj", "{51920F1F-C28C-4ADF-8660-4238766796C2}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameTest", "src\modules\powerrename\testapp\PowerRenameTest.vcxproj", "{A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.UnitTests", "src\modules\powerrename\unittests\PowerRenameLibUnitTests.vcxproj", "{2151F984-E006-4A9F-92EF-C6DDE3DC8413}" - ProjectSection(ProjectDependencies) = postProject - {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} - {B25AC7A5-FB9F-4789-B392-D5C85E948670} = {B25AC7A5-FB9F-4789-B392-D5C85E948670} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ModuleTemplateCompileTest", "tools\project_template\ModuleTemplate\ModuleTemplateCompileTest.vcxproj", "{64A80062-4D8B-4229-8A38-DFA1D7497749}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManager", "src\modules\keyboardmanager\dll\KeyboardManager.vcxproj", "{89F34AF7-1C34-4A72-AA6E-534BCF972BD9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "imageresizer", "imageresizer", "{6C7F47CC-2151-44A3-A546-41C70025132C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageResizerUI", "src\modules\imageresizer\ui\ImageResizerUI.csproj", "{2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImageResizerExt", "src\modules\imageresizer\dll\ImageResizerExt.vcxproj", "{0B43679E-EDFA-4DA0-AD30-F4628B308B1B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageResizer.UnitTests", "src\modules\imageresizer\tests\ImageResizer.UnitTests.csproj", "{E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.ActionRunner", "src\ActionRunner\ActionRunner.vcxproj", "{D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}" - ProjectSection(ProjectDependencies) = postProject - {17DA04DF-E393-4397-9CF0-84DABE11032E} = {17DA04DF-E393-4397-9CF0-84DABE11032E} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ApplicationUpdate", "src\common\updating\updating.vcxproj", "{17DA04DF-E393-4397-9CF0-84DABE11032E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "keyboardmanager", "keyboardmanager", "{38BDB927-829B-4C65-9CD9-93FB05D66D65}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerCommon", "src\modules\keyboardmanager\common\KeyboardManagerCommon.vcxproj", "{8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "launcher", "launcher", "{C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wox.Infrastructure", "src\modules\launcher\Wox.Infrastructure\Wox.Infrastructure.csproj", "{4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wox.Plugin", "src\modules\launcher\Wox.Plugin\Wox.Plugin.csproj", "{8451ECDD-2EA4-4966-BB0A-7BBC40138E80}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wox.Test", "src\modules\launcher\Wox.Test\Wox.Test.csproj", "{FF742965-9A80-41A5-B042-D6C7D3A21708}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{4AFC9975-2456-4C70-94A4-84073C1CED93}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.Calculator", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.csproj", "{59BD9891-3837-438A-958D-ADC7F91F6F7E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.VSCodeWorkspaces", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.csproj", "{4D971245-7A70-41D5-BAA0-DDB5684CAF51}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.WindowWalker", "src\modules\launcher\Plugins\Microsoft.Plugin.WindowWalker\Microsoft.Plugin.WindowWalker.csproj", "{74F1B9ED-F59C-4FE7-B473-7B453E30837E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Program", "src\modules\launcher\Plugins\Microsoft.Plugin.Program\Microsoft.Plugin.Program.csproj", "{FDB3555B-58EF-4AE6-B5F1-904719637AB4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Shell", "src\modules\launcher\Plugins\Microsoft.Plugin.Shell\Microsoft.Plugin.Shell.csproj", "{C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Indexer", "src\modules\launcher\Plugins\Microsoft.Plugin.Indexer\Microsoft.Plugin.Indexer.csproj", "{F8B870EB-D5F5-45BA-9CF7-A5C459818820}" - ProjectSection(ProjectDependencies) = postProject - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80} = {8451ECDD-2EA4-4966-BB0A-7BBC40138E80} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Launcher", "src\modules\launcher\Microsoft.Launcher\Microsoft.Launcher.vcxproj", "{E364F67B-BB12-4E91-B639-355866EBCD8B}" - ProjectSection(ProjectDependencies) = postProject - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2} = {F97E5003-F263-4D4A-A964-0F1F3C82DEF2} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerLauncher", "src\modules\launcher\PowerLauncher\PowerLauncher.csproj", "{F97E5003-F263-4D4A-A964-0F1F3C82DEF2}" - ProjectSection(ProjectDependencies) = postProject - {03276A39-D4E9-417C-8FFD-200B0EE5E871} = {03276A39-D4E9-417C-8FFD-200B0EE5E871} - {0351ADA4-0C32-4652-9BA0-41F7B602372B} = {0351ADA4-0C32-4652-9BA0-41F7B602372B} - {4BABF3FE-3451-42FD-873F-3C332E18DCEF} = {4BABF3FE-3451-42FD-873F-3C332E18DCEF} - {4D971245-7A70-41D5-BAA0-DDB5684CAF51} = {4D971245-7A70-41D5-BAA0-DDB5684CAF51} - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D} = {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D} - {5043CECE-E6A7-4867-9CBE-02D27D83747A} = {5043CECE-E6A7-4867-9CBE-02D27D83747A} - {59BD9891-3837-438A-958D-ADC7F91F6F7E} = {59BD9891-3837-438A-958D-ADC7F91F6F7E} - {5A1DB2F0-0715-4B3B-98E6-79BC41540045} = {5A1DB2F0-0715-4B3B-98E6-79BC41540045} - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF} = {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF} - {74F1B9ED-F59C-4FE7-B473-7B453E30837E} = {74F1B9ED-F59C-4FE7-B473-7B453E30837E} - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} = {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} - {9F94B303-5E21-4364-9362-64426F8DB932} = {9F94B303-5E21-4364-9362-64426F8DB932} - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85} = {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85} - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4} = {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4} - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} - {D095BE44-1F2E-463E-A494-121892A75EA2} = {D095BE44-1F2E-463E-A494-121892A75EA2} - {F8B870EB-D5F5-45BA-9CF7-A5C459818820} = {F8B870EB-D5F5-45BA-9CF7-A5C459818820} - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B} = {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B} - {FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {FDB3555B-58EF-4AE6-B5F1-904719637AB4} - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "previewpane", "previewpane", "{2F305555-C296-497E-AC20-5FA1B237996A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PreviewHandlerCommon", "src\modules\previewpane\Common\PreviewHandlerCommon.csproj", "{AF2349B8-E5B6-4004-9502-687C1C7730B1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarkdownPreviewHandler", "src\modules\previewpane\MarkdownPreviewHandler\MarkdownPreviewHandler.csproj", "{6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.MarkdownPreviewHandler.UnitTests", "src\modules\previewpane\UnitTests-MarkdownPreviewHandler\Preview.MarkdownPreviewHandler.UnitTests.csproj", "{A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SvgPreviewHandler", "src\modules\previewpane\SvgPreviewHandler\SvgPreviewHandler.csproj", "{DA425894-6E13-404F-8DCB-78584EC0557A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.SvgPreviewHandler.UnitTests", "src\modules\previewpane\UnitTests-SvgPreviewHandler\Preview.SvgPreviewHandler.UnitTests.csproj", "{060D75DA-2D1C-48E6-A4A1-6F0718B64661}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.PreviewHandlerCommon.UnitTests", "src\modules\previewpane\UnitTests-PreviewHandlerCommon\Preview.PreviewHandlerCommon.UnitTests.csproj", "{748417CA-F17E-487F-9411-CAFB6D3F4877}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "powerpreview", "src\modules\previewpane\powerpreview\powerpreview.vcxproj", "{217DF501-135C-4E38-BFC8-99D4821032EA}" - ProjectSection(ProjectDependencies) = postProject - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} = {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "settings-ui", "settings-ui", "{C3081D9A-1586-441A-B5F4-ED815B3719C1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4981CCD1-4CD9-4A49-B240-00AA46493FF8}" - ProjectSection(SolutionItems) = preProject - src\.editorconfig = src\.editorconfig - .vsconfig = .vsconfig - src\Common.Dotnet.AotCompatibility.props = src\Common.Dotnet.AotCompatibility.props - src\Common.Dotnet.CsWinRT.props = src\Common.Dotnet.CsWinRT.props - src\Common.SelfContained.props = src\Common.SelfContained.props - Cpp.Build.props = Cpp.Build.props - Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets - Directory.Packages.props = Directory.Packages.props - src\Monaco.props = src\Monaco.props - src\Solution.props = src\Solution.props - src\Version.props = src\Version.props - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Settings.UI.Library", "src\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj", "{B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Folder", "src\modules\launcher\Plugins\Microsoft.Plugin.Folder\Microsoft.Plugin.Folder.csproj", "{787B8AA6-CA93-4C84-96FE-DF31110AD1C4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerLauncher.Telemetry", "src\modules\launcher\PowerLauncher.Telemetry\PowerLauncher.Telemetry.csproj", "{08C8C05F-0362-41BC-818C-724572DF8B06}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedTelemetry", "src\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj", "{5D00D290-4016-4CFE-9E41-1E7C724509BA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCommon", "src\common\ManagedCommon\ManagedCommon.csproj", "{4AED67B6-55FD-486F-B917-E543DEE2CB3C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Program.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Program.UnitTests\Microsoft.Plugin.Program.UnitTests.csproj", "{42851751-CBC8-45A6-97F5-7A0753F7B4D1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.SvgThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-SvgThumbnailProvider\Preview.SvgThumbnailProvider.UnitTests.csproj", "{1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SvgThumbnailProvider", "src\modules\previewpane\SvgThumbnailProvider\SvgThumbnailProvider.csproj", "{8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ColorPicker", "src\modules\colorPicker\ColorPicker\ColorPicker.vcxproj", "{655C9AF2-18D3-4DA6-80E4-85504A7722BA}" - ProjectSection(ProjectDependencies) = postProject - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD} = {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ColorPickerUI", "src\modules\colorPicker\ColorPickerUI\ColorPickerUI.csproj", "{BA58206B-1493-4C75-BFEA-A85768A1E156}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "colorpicker", "colorpicker", "{1D78B84B-CA39-406C-98F4-71F7EC266CC0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Uri", "src\modules\launcher\Plugins\Microsoft.Plugin.Uri\Microsoft.Plugin.Uri.csproj", "{03276A39-D4E9-417C-8FFD-200B0EE5E871}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Uri.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Uri.UnitTests\Microsoft.Plugin.Uri.UnitTests.csproj", "{B81FB7B6-D30E-428F-908A-41422EFC1172}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Settings.UI.UnitTests", "src\settings-ui\Settings.UI.UnitTests\Settings.UI.UnitTests.csproj", "{0F85E674-34AE-443D-954C-8321EB8B93B1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest\Microsoft.PowerToys.Run.Plugin.Calculator.UnitTest.csproj", "{632BBE62-5421-49EA-835A-7FFA4F499BD6}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "spdlog", "src\logging\logging.vcxproj", "{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.System", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.System\Microsoft.PowerToys.Run.Plugin.System.csproj", "{FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.System.UnitTests", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.System.UnitTests\Microsoft.PowerToys.Run.Plugin.System.UnitTests.csproj", "{DA5A6FE9-0040-40CC-83CC-764AE5306590}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.Service", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Service\Microsoft.PowerToys.Run.Plugin.Service.csproj", "{0351ADA4-0C32-4652-9BA0-41F7B602372B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "logger", "src\common\logger\logger.vcxproj", "{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}" - ProjectSection(ProjectDependencies) = postProject - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F} = {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SettingsAPI", "src\common\SettingsAPI\SettingsAPI.vcxproj", "{6955446D-23F7-4023-9BB3-8657F904AF99}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Interop.UnitTests", "src\common\interop\interop-tests\Common.Interop.UnitTests.csproj", "{58736667-1027-4AD7-BFDF-7A3A6474103A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "notifications", "notifications", "{D92131D6-7610-4D60-A7DB-1C169783F83B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Notifications", "src\common\notifications\notifications.vcxproj", "{1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BackgroundActivatorDLL", "src\common\notifications\BackgroundActivatorDLL\BackgroundActivatorDLL.vcxproj", "{031AC72E-FA28-4AB7-B690-6F7B9C28AA73}" - ProjectSection(ProjectDependencies) = postProject - {0B593A6C-4143-4337-860E-DB5710FB87DB} = {0B593A6C-4143-4337-860E-DB5710FB87DB} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BackgroundActivator", "src\common\notifications\BackgroundActivator\BackgroundActivator.vcxproj", "{0B593A6C-4143-4337-860E-DB5710FB87DB}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Version", "src\common\version\version.vcxproj", "{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "interop", "interop", "{5A7818A8-109C-4E1C-850D-1A654E234B0E}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "log", "log", "{E4E03FE0-94FD-47C7-88C5-F17D0AA549D3}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "COMUtils", "src\common\COMUtils\COMUtils.vcxproj", "{7319089E-46D6-4400-BC65-E39BDF1416EE}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Display", "src\common\Display\Display.vcxproj", "{CABA8DFB-823B-4BF2-93AC-3F31984150D9}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Themes", "src\common\Themes\Themes.vcxproj", "{98537082-0FDB-40DE-ABD8-0DC5A4269BAB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643-4663-475E-B329-03F0C9918D48}" - ProjectSection(SolutionItems) = preProject - src\common\utils\appMutex.h = src\common\utils\appMutex.h - src\common\utils\color.h = src\common\utils\color.h - src\common\utils\com_object_factory.h = src\common\utils\com_object_factory.h - src\common\utils\elevation.h = src\common\utils\elevation.h - src\common\utils\EventLocker.h = src\common\utils\EventLocker.h - src\common\utils\EventWaiter.h = src\common\utils\EventWaiter.h - src\common\utils\excluded_apps.h = src\common\utils\excluded_apps.h - src\common\utils\exec.h = src\common\utils\exec.h - src\common\utils\game_mode.h = src\common\utils\game_mode.h - src\common\utils\gpo.h = src\common\utils\gpo.h - src\common\utils\HDropIterator.h = src\common\utils\HDropIterator.h - src\common\utils\HttpClient.h = src\common\utils\HttpClient.h - src\common\utils\json.h = src\common\utils\json.h - src\common\utils\language_helper.h = src\common\utils\language_helper.h - src\common\utils\logger_helper.h = src\common\utils\logger_helper.h - src\common\utils\modulesRegistry.h = src\common\utils\modulesRegistry.h - src\common\utils\MsiUtils.h = src\common\utils\MsiUtils.h - src\common\utils\MsWindowsSettings.h = src\common\utils\MsWindowsSettings.h - src\common\utils\OnThreadExecutor.h = src\common\utils\OnThreadExecutor.h - src\common\utils\os-detect.h = src\common\utils\os-detect.h - src\common\utils\package.h = src\common\utils\package.h - src\common\utils\ProcessWaiter.h = src\common\utils\ProcessWaiter.h - src\common\utils\process_path.h = src\common\utils\process_path.h - src\common\utils\registry.h = src\common\utils\registry.h - src\common\utils\resources.h = src\common\utils\resources.h - src\common\utils\serialized.h = src\common\utils\serialized.h - src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h - src\common\utils\string_utils.h = src\common\utils\string_utils.h - src\common\utils\timeutil.h = src\common\utils\timeutil.h - src\common\utils\UnhandledExceptionHandler.h = src\common\utils\UnhandledExceptionHandler.h - src\common\utils\winapi_error.h = src\common\utils\winapi_error.h - src\common\utils\window.h = src\common\utils\window.h - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Telemetry", "Telemetry", "{8F62026A-294B-41C6-8839-87463613F216}" - ProjectSection(SolutionItems) = preProject - src\common\Telemetry\ProjectTelemetry.h = src\common\Telemetry\ProjectTelemetry.h - src\common\Telemetry\TelemetryBase.cs = src\common\Telemetry\TelemetryBase.cs - src\common\Telemetry\TraceBase.h = src\common\Telemetry\TraceBase.h - src\common\Telemetry\TraceLoggingDefines.h = src\common\Telemetry\TraceLoggingDefines.h - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.UI", "src\common\Common.UI\Common.UI.csproj", "{C3A17DCA-217B-462C-BB0C-BE086AF80081}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PdfPreviewHandler", "src\modules\previewpane\PdfPreviewHandler\PdfPreviewHandler.csproj", "{69E1EE8D-143A-4060-9129-4658ACF14AAF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.PdfPreviewHandler.UnitTests", "src\modules\previewpane\UnitTests-PdfPreviewHandler\Preview.PdfPreviewHandler.UnitTests.csproj", "{ECC20689-002A-4354-95A6-B58DF089C6FF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.Registry", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Registry\Microsoft.PowerToys.Run.Plugin.Registry.csproj", "{4BABF3FE-3451-42FD-873F-3C332E18DCEF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.Registry.UnitTests", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.Registry.UnitTest\Microsoft.PowerToys.Run.Plugin.Registry.UnitTests.csproj", "{0648DF05-5DDA-4BE1-B5F2-584926EBDB65}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEngine", "src\modules\keyboardmanager\KeyboardManagerEngine\KeyboardManagerEngine.vcxproj", "{BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEngineLibrary", "src\modules\keyboardmanager\KeyboardManagerEngineLibrary\KeyboardManagerEngineLibrary.vcxproj", "{E496B7FC-1E99-4BAB-849B-0E8367040B02}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManager.Engine.UnitTests", "src\modules\keyboardmanager\KeyboardManagerEngineTest\KeyboardManagerEngineTest.vcxproj", "{7F4B3A60-BC27-45A7-8000-68B0B6EA7466}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditor", "src\modules\keyboardmanager\KeyboardManagerEditor\KeyboardManagerEditor.vcxproj", "{8DF78B53-200E-451F-9328-01EB907193AE}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorLibrary", "src\modules\keyboardmanager\KeyboardManagerEditorLibrary\KeyboardManagerEditorLibrary.vcxproj", "{23D2070D-E4AD-4ADD-85A7-083D9C76AD49}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManager.Editor.UnitTests", "src\modules\keyboardmanager\KeyboardManagerEditorTest\KeyboardManagerEditorTest.vcxproj", "{62173D9A-6724-4C00-A1C8-FB646480A9EC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "awake", "awake", "{127F38E0-40AA-4594-B955-5616BF206882}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AwakeModuleInterface", "src\modules\awake\AwakeModuleInterface\AwakeModuleInterface.vcxproj", "{5E7360A8-D048-4ED3-8F09-0BFD64C5529A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Awake", "src\modules\awake\Awake\Awake.csproj", "{D940E07F-532C-4FF3-883F-790DA014F19A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.csproj", "{BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.UnitConverter.UnitTest", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest\Community.PowerToys.Run.Plugin.UnitConverter.UnitTest.csproj", "{3E424AD2-19E5-4AE6-B833-F53963EB5FC1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "shortcutguide", "shortcutguide", "{106CBECA-0701-4FC3-838C-9DF816A19AE2}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ShortcutGuideModuleInterface", "src\modules\ShortcutGuide\ShortcutGuideModuleInterface\ShortcutGuideModuleInterface.vcxproj", "{2D604C07-51FC-46BB-9EB7-75AECC7F5E81}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ShortcutGuide", "src\modules\ShortcutGuide\ShortcutGuide\ShortcutGuide.vcxproj", "{2EDB3EB4-FA92-4BFF-B2D8-566584837231}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZonesModuleInterface", "src\modules\fancyzones\FancyZonesModuleInterface\FancyZonesModuleInterface.vcxproj", "{48804216-2A0E-4168-A6D8-9CD068D14227}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FancyZones", "src\modules\fancyzones\FancyZones\FancyZones.vcxproj", "{FF1D7936-842A-4BBB-8BEA-E9FE796DE700}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.Update", "src\Update\PowerToys.Update.vcxproj", "{44CE9AE1-4390-42C5-BACC-0FD6B40AA203}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.WindowsSettings", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PdfThumbnailProvider", "src\modules\previewpane\PdfThumbnailProvider\PdfThumbnailProvider.csproj", "{11491FD8-F921-48BF-880C-7FEA185B80A1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.PdfThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-PdfThumbnailProvider\Preview.PdfThumbnailProvider.UnitTests.csproj", "{F40C3397-1834-4530-B2D9-8F8B8456BCDF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.WindowsTerminal", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.csproj", "{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.WindowsTerminal.UnitTests", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.UnitTests\Microsoft.Plugin.WindowsTerminal.UnitTests.csproj", "{4ED320BC-BA04-4D42-8D15-CBE62151F08B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MouseUtils", "MouseUtils", "{322566EF-20DC-43A6-B9F8-616AF942579A}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FindMyMouse", "src\modules\MouseUtils\FindMyMouse\FindMyMouse.vcxproj", "{E94FD11C-0591-456F-899F-EFC0CA548336}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MouseHighlighter", "src\modules\MouseUtils\MouseHighlighter\MouseHighlighter.vcxproj", "{782A61BE-9D85-4081-B35C-1CCC9DCC1E88}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GcodeThumbnailProvider", "src\modules\previewpane\GcodeThumbnailProvider\GcodeThumbnailProvider.csproj", "{809AA252-E17A-4FA2-B0A1-0450976B763F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.GcodeThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-GcodeThumbnailProvider\Preview.GcodeThumbnailProvider.UnitTests.csproj", "{133281D8-1BCE-4D07-B31E-796612A9609E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GcodePreviewHandler", "src\modules\previewpane\GcodePreviewHandler\GcodePreviewHandler.csproj", "{805306FF-A562-4415-8DEF-E493BDC45918}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.GcodePreviewHandler.UnitTests", "src\modules\previewpane\UnitTests-GcodePreviewHandler\Preview.GcodePreviewHandler.UnitTests.csproj", "{FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AlwaysOnTop", "AlwaysOnTop", "{60CD2D4F-C3B9-4897-9821-FCA5098B41CE}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AlwaysOnTop", "src\modules\alwaysontop\AlwaysOnTop\AlwaysOnTop.vcxproj", "{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AlwaysOnTopModuleInterface", "src\modules\alwaysontop\AlwaysOnTopModuleInterface\AlwaysOnTopModuleInterface.vcxproj", "{48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.WebSearch", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.WebSearch\Community.PowerToys.Run.Plugin.WebSearch.csproj", "{9F94B303-5E21-4364-9362-64426F8DB932}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MousePointerCrosshairs", "src\modules\MouseUtils\MousePointerCrosshairs\MousePointerCrosshairs.vcxproj", "{EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StlThumbnailProvider", "src\modules\previewpane\StlThumbnailProvider\StlThumbnailProvider.csproj", "{F7C8C0F1-5431-4347-89D0-8E5354F93CF2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.StlThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-StlThumbnailProvider\Preview.StlThumbnailProvider.UnitTests.csproj", "{F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonacoPreviewHandler", "src\modules\previewpane\MonacoPreviewHandler\MonacoPreviewHandler.csproj", "{04B193D7-3E21-46B8-A958-89B63A8A69DE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.TimeDate", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.csproj", "{5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.TimeDate.UnitTests", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.TimeDate.UnitTests\Microsoft.PowerToys.Run.Plugin.TimeDate.UnitTests.csproj", "{FD464B4C-2F68-4D06-91E7-4208146C41F5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.WindowWalker.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.WindowWalker.UnitTests\Microsoft.Plugin.WindowWalker.UnitTests.csproj", "{8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.Settings", "src\settings-ui\Settings.UI\PowerToys.Settings.csproj", "{020A7474-3601-4160-A159-D7B70B77B15F}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameUI", "src\modules\powerrename\PowerRenameUILib\PowerRenameUI.vcxproj", "{27718999-C175-450A-861C-89F911E16A88}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameContextMenu", "src\modules\powerrename\PowerRenameContextMenu\PowerRenameContextMenu.vcxproj", "{1DBBB112-4BB1-444B-8EBB-E66555C76BA6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.OneNote", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.csproj", "{5A1DB2F0-0715-4B3B-98E6-79BC41540045}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImageResizerContextMenu", "src\modules\imageresizer\ImageResizerContextMenu\ImageResizerContextMenu.vcxproj", "{93B72A06-C8BD-484F-A6F7-C9F280B150BF}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ImageResizerLib", "src\modules\imageresizer\ImageResizerLib\ImageResizerLib.vcxproj", "{18B3DB45-4FFE-4D01-97D6-5223FEEE1853}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerAccent", "PowerAccent", "{0F14491C-6369-4C45-AAA8-135814E66E6B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerAccentModuleInterface", "src\modules\poweraccent\PowerAccentModuleInterface\PowerAccentModuleInterface.vcxproj", "{34A354C5-23C7-4343-916C-C52DAF4FC39D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerAccent.Core", "src\modules\poweraccent\PowerAccent.Core\PowerAccent.Core.csproj", "{3264DF53-C805-4B0C-867C-FCEAF7AEF762}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerAccent.UI", "src\modules\poweraccent\PowerAccent.UI\PowerAccent.UI.csproj", "{31CAD28E-778A-441C-85BC-40AB3EAA2A10}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PowerOCR", "PowerOCR", "{A50C70A6-2DA0-4027-B90E-B1A40755A8A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerOCR", "src\modules\PowerOCR\PowerOCR\PowerOCR.csproj", "{25C91A4E-BA4E-467A-85CD-8B62545BF674}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerOCRModuleInterface", "src\modules\PowerOCR\PowerOCRModuleInterface\PowerOCRModuleInterface.vcxproj", "{6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B1234567-1234-1234-1234-123456789ABC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.History", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.History\Microsoft.PowerToys.Run.Plugin.History.csproj", "{212AD910-8488-4036-BE20-326931B75FB2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MeasureTool", "MeasureTool", "{7AC943C9-52E8-44CF-9083-744D8049667B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.MeasureToolCore", "src\modules\MeasureTool\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj", "{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}" - ProjectSection(ProjectDependencies) = postProject - {6955446D-23F7-4023-9BB3-8657F904AF99} = {6955446D-23F7-4023-9BB3-8657F904AF99} - {CABA8DFB-823B-4BF2-93AC-3F31984150D9} = {CABA8DFB-823B-4BF2-93AC-3F31984150D9} - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} = {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MeasureToolModuleInterface", "src\modules\MeasureTool\MeasureToolModuleInterface\MeasureToolModuleInterface.vcxproj", "{92C39820-9F84-4529-BC7D-22AAE514D63B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeasureToolUI", "src\modules\MeasureTool\MeasureToolUI\MeasureToolUI.csproj", "{515554D1-D004-4F7F-A107-2211FC0F6B2C}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerAccentKeyboardService", "src\modules\poweraccent\PowerAccentKeyboardService\PowerAccentKeyboardService.vcxproj", "{C97D9A5D-206C-454E-997E-009E227D7F02}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostsUILib", "src\modules\Hosts\HostsUILib\HostsUILib.csproj", "{31D1C81D-765F-4446-AA62-E743F6325049}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostsEditor.UnitTests", "src\modules\Hosts\Hosts.Tests\HostsEditor.UnitTests.csproj", "{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "HostsModuleInterface", "src\modules\Hosts\HostsModuleInterface\HostsModuleInterface.vcxproj", "{B41B888C-7DB8-4747-B262-4062E05A230D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FileLocksmith", "FileLocksmith", "{AB82E5DD-C32D-4F28-9746-2C780846188E}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FileLocksmithExt", "src\modules\FileLocksmith\FileLocksmithExt\FileLocksmithExt.vcxproj", "{57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLocksmithUI", "src\modules\FileLocksmith\FileLocksmithUI\FileLocksmithUI.csproj", "{E69B044A-2F8A-45AA-AD0B-256C59421807}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.FileLocksmithLib.Interop", "src\modules\FileLocksmith\FileLocksmithLibInterop\FileLocksmithLibInterop.vcxproj", "{C604B37E-9D0E-4484-8778-E8B31B0E1B3A}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GPOWrapper", "src\common\GPOWrapper\GPOWrapper.vcxproj", "{E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GPOWrapperProjection", "src\common\GPOWrapperProjection\GPOWrapperProjection.csproj", "{00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Peek", "Peek", "{17B4FA70-001E-4D33-BBBB-0D142DBC2E20}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Peek", "src\modules\peek\peek\peek.vcxproj", "{A1425B53-3D61-4679-8623-E64A0D3D0A48}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.UI", "src\modules\peek\Peek.UI\Peek.UI.csproj", "{9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.Common", "src\modules\peek\Peek.Common\Peek.Common.csproj", "{17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.FilePreviewer", "src\modules\peek\Peek.FilePreviewer\Peek.FilePreviewer.csproj", "{AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Peek.UITests", "src\modules\peek\Peek.UITests\Peek.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MarkdownPreviewHandlerCpp", "src\modules\previewpane\MarkdownPreviewHandlerCpp\MarkdownPreviewHandlerCpp.vcxproj", "{ED9A1AC6-AEB0-4569-A6E9-E1696182B545}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GcodePreviewHandlerCpp", "src\modules\previewpane\GcodePreviewHandlerCpp\GcodePreviewHandlerCpp.vcxproj", "{5A5DD09D-723A-44D3-8F2B-293584C3D731}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MonacoPreviewHandlerCpp", "src\modules\previewpane\MonacoPreviewHandlerCpp\MonacoPreviewHandlerCpp.vcxproj", "{B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PdfPreviewHandlerCpp", "src\modules\previewpane\PdfPreviewHandlerCpp\PdfPreviewHandlerCpp.vcxproj", "{54F7C616-FD41-4E62-BFF9-015686914F4D}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SvgPreviewHandlerCpp", "src\modules\previewpane\SvgPreviewHandlerCpp\SvgPreviewHandlerCpp.vcxproj", "{143F13E3-D2E3-4D83-B035-356612D99956}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GcodeThumbnailProviderCpp", "src\modules\previewpane\GcodeThumbnailProviderCpp\GcodeThumbnailProviderCpp.vcxproj", "{56CC2F10-6E41-453D-BE16-C593A5E58482}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PdfThumbnailProviderCpp", "src\modules\previewpane\PdfThumbnailProviderCpp\PdfThumbnailProviderCpp.vcxproj", "{CA5518ED-0458-4B09-8F53-4122B9888655}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StlThumbnailProviderCpp", "src\modules\previewpane\StlThumbnailProviderCpp\StlThumbnailProviderCpp.vcxproj", "{D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SvgThumbnailProviderCpp", "src\modules\previewpane\SvgThumbnailProviderCpp\SvgThumbnailProviderCpp.vcxproj", "{2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MouseWithoutBorders", "MouseWithoutBorders", "{B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MouseWithoutBordersModuleInterface", "src\modules\MouseWithoutBorders\ModuleInterface\MouseWithoutBordersModuleInterface.vcxproj", "{2833C9C6-AB32-4048-A5C7-A70898337B57}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseWithoutBorders", "src\modules\MouseWithoutBorders\App\MouseWithoutBorders.csproj", "{50B82783-242F-42D2-BC03-B3430BF01354}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseWithoutBordersService", "src\modules\MouseWithoutBorders\App\Service\MouseWithoutBordersService.csproj", "{B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseWithoutBordersHelper", "src\modules\MouseWithoutBorders\App\Helper\MouseWithoutBordersHelper.csproj", "{A663E672-B26D-4EC0-BEAB-FE2E424AC46F}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MouseJump", "src\modules\MouseUtils\MouseJump\MouseJump.vcxproj", "{8A08D663-4995-40E3-B42C-3F910625F284}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseJump.Common", "src\modules\MouseUtils\MouseJump.Common\MouseJump.Common.csproj", "{923DF87C-CA99-4D1C-B1D2-959174E95BFA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseJump.Common.UnitTests", "src\modules\MouseUtils\MouseJump.Common.UnitTests\MouseJump.Common.UnitTests.csproj", "{D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseJumpUI", "src\modules\MouseUtils\MouseJumpUI\MouseJumpUI.csproj", "{D962A009-834F-4EEC-AABB-430DF8F98E39}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AdvancedPaste", "AdvancedPaste", "{9873BA05-4C41-4819-9283-CF45D795431B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AdvancedPasteModuleInterface", "src\modules\AdvancedPaste\AdvancedPasteModuleInterface\AdvancedPasteModuleInterface.vcxproj", "{FC373B24-3293-453C-AAF5-CF2909DCEE6A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AllExperiments", "src\common\AllExperiments\AllExperiments.csproj", "{9CE59ED5-7087-4353-88EB-788038A73CEC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreviewUILib", "src\modules\registrypreview\RegistryPreviewUILib\RegistryPreviewUILib.csproj", "{FD86C06A-FB54-4D5E-9831-1CDADF60D45F}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegistryPreviewExt", "src\modules\registrypreview\RegistryPreviewExt\RegistryPreviewExt.vcxproj", "{697C6AF9-0A48-49A9-866C-67DA12384015}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilePreviewCommon", "src\common\FilePreviewCommon\FilePreviewCommon.csproj", "{9EBAA524-0EDA-470B-95D4-39383285CBB2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.PowerToys", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.csproj", "{500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.ValueGenerator", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.csproj", "{D095BE44-1F2E-463E-A494-121892A75EA2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests\Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests.csproj", "{90F9FA90-2C20-4004-96E6-F3B78151F5A5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CropAndLock", "CropAndLock", "{3B227528-4BA6-4CAF-B44A-A10C78A64849}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLock", "src\modules\CropAndLock\CropAndLock\CropAndLock.vcxproj", "{F5E1146E-B7B3-4E11-85FD-270A500BD78C}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLockModuleInterface", "src\modules\CropAndLock\CropAndLockModuleInterface\CropAndLockModuleInterface.vcxproj", "{3157FA75-86CF-4EE2-8F62-C43F776493C6}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmdNotFound", "cmdNotFound", "{4C0D0746-BE5B-49EE-BD5D-A7811628AE8B}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvironmentVariables", "EnvironmentVariables", "{538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariablesUILib", "src\modules\EnvironmentVariables\EnvironmentVariablesUILib\EnvironmentVariablesUILib.csproj", "{51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EnvironmentVariablesModuleInterface", "src\modules\EnvironmentVariables\EnvironmentVariablesModuleInterface\EnvironmentVariablesModuleInterface.vcxproj", "{B9420661-B0E4-4241-ABD4-4A27A1F64250}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QoiThumbnailProviderCpp", "src\modules\previewpane\QoiThumbnailProviderCpp\QoiThumbnailProviderCpp.vcxproj", "{CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QoiThumbnailProvider", "src\modules\previewpane\QoiThumbnailProvider\QoiThumbnailProvider.csproj", "{D949EC7D-48A9-4279-95D5-078E7FD1F048}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QoiPreviewHandlerCpp", "src\modules\previewpane\QoiPreviewHandlerCpp\QoiPreviewHandlerCpp.vcxproj", "{3BAF9C81-A194-4925-A035-5E24A5D1E542}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QoiPreviewHandler", "src\modules\previewpane\QoiPreviewHandler\QoiPreviewHandler.csproj", "{6B04803D-B418-4833-A67E-B0FC966636A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.QoiPreviewHandler.UnitTests", "src\modules\previewpane\UnitTests-QoiPreviewHandler\Preview.QoiPreviewHandler.UnitTests.csproj", "{3940AD4D-F748-4BE4-9083-85769CD553EF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Preview.QoiThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-QoiThumbnailProvider\Preview.QoiThumbnailProvider.UnitTests.csproj", "{F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdNotFoundModuleInterface", "src\modules\cmdNotFound\CmdNotFoundModuleInterface\CmdNotFoundModuleInterface.vcxproj", "{0014D652-901F-4456-8D65-06FC5F997FB0}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FileLocksmithContextMenu", "src\modules\FileLocksmith\FileLocksmithContextMenu\FileLocksmithContextMenu.vcxproj", "{799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FileLocksmithLib", "src\modules\FileLocksmith\FileLocksmithLib\FileLocksmithLib.vcxproj", "{9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedPaste", "src\modules\AdvancedPaste\AdvancedPaste\AdvancedPaste.csproj", "{C32D254F-7597-4CBE-BF74-D922D81CDF29}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts", "src\modules\Hosts\Hosts\Hosts.csproj", "{02DD46D3-F761-47D9-8894-2D6DA0124650}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modules\registrypreview\RegistryPreview\RegistryPreview.csproj", "{8E23E173-7127-4A5F-9F93-3049F2B68047}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditorCommon", "src\modules\fancyzones\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj", "{C0974915-8A1D-4BF0-977B-9587D3807AB7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DSC", "DSC", "{557C4636-D7E1-4838-A504-7D19B725EE95}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.Settings.DSC.Schema.Generator", "src\dsc\PowerToys.Settings.DSC.Schema.Generator\PowerToys.Settings.DSC.Schema.Generator.csproj", "{1D6893CB-BC0C-46A8-A76C-9728706CA51A}" - ProjectSection(ProjectDependencies) = postProject - {020A7474-3601-4160-A159-D7B70B77B15F} = {020A7474-3601-4160-A159-D7B70B77B15F} - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension", "src\modules\NewPlus\NewShellExtensionContextMenu\NewShellExtensionContextMenu.vcxproj", "{8ACB33D9-C95B-47D4-8363-9731EE0930A0}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "New+", "New+", "{CA716AE6-FE5C-40AC-BB8F-2C87912687AC}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.Interop", "src\common\interop\PowerToys.Interop.vcxproj", "{F055103B-F80B-4D0C-BF48-057C55620033}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workspaces", "Workspaces", "{A2221D7E-55E7-4BEA-90D1-4F162D670BBF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workspaces-common", "workspaces-common", "{BE126CBB-AE12-406A-9837-A05ACFCA57A7}" - ProjectSection(SolutionItems) = preProject - src\modules\Workspaces\workspaces-common\GuidUtils.h = src\modules\Workspaces\workspaces-common\GuidUtils.h - src\modules\Workspaces\workspaces-common\InvokePoint.h = src\modules\Workspaces\workspaces-common\InvokePoint.h - src\modules\Workspaces\workspaces-common\MonitorEnumerator.h = src\modules\Workspaces\workspaces-common\MonitorEnumerator.h - src\modules\Workspaces\workspaces-common\MonitorUtils.h = src\modules\Workspaces\workspaces-common\MonitorUtils.h - src\modules\Workspaces\workspaces-common\VirtualDesktop.h = src\modules\Workspaces\workspaces-common\VirtualDesktop.h - src\modules\Workspaces\workspaces-common\WindowEnumerator.h = src\modules\Workspaces\workspaces-common\WindowEnumerator.h - src\modules\Workspaces\workspaces-common\WindowFilter.h = src\modules\Workspaces\workspaces-common\WindowFilter.h - src\modules\Workspaces\workspaces-common\WindowUtils.h = src\modules\Workspaces\workspaces-common\WindowUtils.h - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowProperties", "WindowProperties", "{14CB58B7-D280-4A7A-95DE-4B2DF14EA000}" - ProjectSection(SolutionItems) = preProject - src\modules\Workspaces\WindowProperties\WorkspacesWindowPropertyUtils.h = src\modules\Workspaces\WindowProperties\WorkspacesWindowPropertyUtils.h - EndProjectSection -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLib", "src\modules\Workspaces\WorkspacesLib\WorkspacesLib.vcxproj", "{B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Workspaces.Lib.UnitTests", "src\modules\Workspaces\WorkspacesLib.UnitTests\WorkspacesLibUnitTests.vcxproj", "{A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesLauncherUI", "src\modules\Workspaces\WorkspacesLauncherUI\WorkspacesLauncherUI.csproj", "{9C53CC25-0623-4569-95BC-B05410675EE3}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesModuleInterface", "src\modules\Workspaces\WorkspacesModuleInterface\WorkspacesModuleInterface.vcxproj", "{45285DF2-9742-4ECA-9AC9-58951FC26489}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesSnapshotTool", "src\modules\Workspaces\WorkspacesSnapshotTool\WorkspacesSnapshotTool.vcxproj", "{3D63307B-9D27-44FD-B033-B26F39245B85}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkspacesEditor", "src\modules\Workspaces\WorkspacesEditor\WorkspacesEditor.csproj", "{367D7543-7DBA-4381-99F1-BF6142A996C4}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesLauncher", "src\modules\Workspaces\WorkspacesLauncher\WorkspacesLauncher.vcxproj", "{2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WorkspacesWindowArranger", "src\modules\Workspaces\WorkspacesWindowArranger\WorkspacesWindowArranger.vcxproj", "{37D07516-4185-43A4-924F-3C7A5D95ECF6}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EtwTrace", "src\common\Telemetry\EtwTrace\EtwTrace.vcxproj", "{8F021B46-362B-485C-BFBA-CCF83E820CBD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseWithoutBorders.UnitTests", "src\modules\MouseWithoutBorders\MouseWithoutBorders.UnitTests\MouseWithoutBorders.UnitTests.csproj", "{66614C26-314C-4B91-9071-76133422CFEF}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommandPalette", "CommandPalette", "{3846508C-77EB-4034-A702-F8BB263C4F79}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Built-in Extensions", "Built-in Extensions", "{ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Apps", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj", "{6CE438DF-C245-4997-A360-0A0939E4BA34}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Bookmarks", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj", "{E09AA983-C755-474F-83D6-A5CDF528C070}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Calc", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj", "{6D56B64D-FF1F-488F-AFED-9B9854A5D399}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Registry", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj", "{92EC89E4-9972-453A-8A1A-3A9E230C146A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsServices", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsServices\Microsoft.CmdPal.Ext.WindowsServices.csproj", "{51939B4F-1F62-4BFF-A6A2-C08646E5BE95}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsSettings", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsSettings\Microsoft.CmdPal.Ext.WindowsSettings.csproj", "{D1160404-D3D1-497A-883A-4059C07C2273}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowsTerminal", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowsTerminal\Microsoft.CmdPal.Ext.WindowsTerminal.csproj", "{40F6D69D-E321-400F-A767-5628C7AE453D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extension SDK", "Extension SDK", "{F3D09629-59A2-4924-A4B9-D6BFAA2C1B49}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.CommandPalette.Extensions", "src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions\Microsoft.CommandPalette.Extensions.vcxproj", "{305DD37E-C85D-4B08-AAFE-7381FA890463}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CommandPalette.Extensions.Toolkit", "src\modules\cmdpal\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj", "{CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Core.Common", "src\modules\cmdpal\Core\Microsoft.CmdPal.Core.Common\Microsoft.CmdPal.Core.Common.csproj", "{14E62033-58D0-4A7D-8990-52F50A08BBBD}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.UI", "src\modules\cmdpal\Microsoft.Terminal.UI\Microsoft.Terminal.UI.vcxproj", "{6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample Extensions", "Sample Extensions", "{071E18A4-A530-46B8-AB7D-B862EE55E24E}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProcessMonitorExtension", "src\modules\cmdpal\ext\ProcessMonitorExtension\ProcessMonitorExtension.csproj", "{C846F7A7-792A-47D9-B0CB-417C900EE03D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SamplePagesExtension", "src\modules\cmdpal\ext\SamplePagesExtension\SamplePagesExtension.csproj", "{C831231F-891C-4572-9694-45062534B42A}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UI", "UI", "{7520A2FE-00A2-49B8-83ED-DB216E874C04}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI", "src\modules\cmdpal\Microsoft.CmdPal.UI\Microsoft.CmdPal.UI.csproj", "{8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.ViewModels", "src\modules\cmdpal\Microsoft.CmdPal.UI.ViewModels\Microsoft.CmdPal.UI.ViewModels.csproj", "{C66020D1-CB10-4CF7-8715-84C97FD5E5E2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.ClipboardHistory", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj", "{79775343-7A3D-445D-9104-3DD5B2893DF9}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalModuleInterface", "src\modules\cmdpal\CmdPalModuleInterface\CmdPalModuleInterface.vcxproj", "{0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesCsharpLibrary", "src\modules\Workspaces\WorkspacesCsharpLibrary\WorkspacesCsharpLibrary.csproj", "{89D0E199-B17A-418C-B2F8-7375B6708357}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension.win10", "src\modules\NewPlus\NewShellExtensionContextMenu.win10\NewPlus.ShellExtension.win10.vcxproj", "{0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Indexer", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj", "{453CBB73-A3CB-4D0B-8D24-6940B86FE21D}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.Shell", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj", "{C0CE3B5E-16D3-495D-B335-CA791B660162}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.Ext.WindowWalker", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WindowWalker\Microsoft.CmdPal.Ext.WindowWalker.csproj", "{3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj", "{605E914B-7232-4789-AF46-BF5D3DDFC14E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WinGet", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.WinGet\Microsoft.CmdPal.Ext.WinGet.csproj", "{E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ZoomIt", "ZoomIt", "{DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomIt", "src\modules\ZoomIt\ZoomIt\ZoomIt.vcxproj", "{0A84F764-3A88-44CD-AA96-41BDBD48627B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItModuleInterface", "src\modules\ZoomIt\ZoomItModuleInterface\ZoomItModuleInterface.vcxproj", "{E4585179-2AC1-4D5F-A3FF-CFC5392F694C}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZoomItSettingsInterop", "src\modules\ZoomIt\ZoomItSettingsInterop\ZoomItSettingsInterop.vcxproj", "{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj", "{DCC6BD67-17BB-47AA-B507-FB0FE43A7449}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UITestAutomation", "src\common\UITestAutomation\UITestAutomation.csproj", "{A558C25D-2007-498E-8B6F-43405AFAE9E2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyboardManagerEditorUI", "src\modules\keyboardmanager\KeyboardManagerEditorUI\KeyboardManagerEditorUI.csproj", "{08F9155D-B6DC-46E5-9C83-AF60B655898B}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorLibraryWrapper", "src\modules\keyboardmanager\KeyboardManagerEditorLibraryWrapper\KeyboardManagerEditorLibraryWrapper.vcxproj", "{4382A954-179A-4078-92AF-715187DFFF50}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostsEditor.FuzzTests", "src\modules\Hosts\Hosts.FuzzTests\HostsEditor.FuzzTests.csproj", "{EBED240C-8702-452D-B764-6DB9DA9179AF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostsEditor.UITests", "src\modules\Hosts\Hosts.UITests\HostsEditor.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests", "src\modules\registrypreview\RegistryPreview.FuzzTests\RegistryPreview.FuzzTests.csproj", "{5702B3CC-8575-48D5-83D8-15BB42269CD3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj", "{64B88F02-CD88-4ED8-9624-989A800230F9}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "src\modules\cmdpal\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj", "{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.FuzzTests", "src\modules\powerrename\PowerRename.FuzzingTest\PowerRename.FuzzingTest.vcxproj", "{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BgcodePreviewHandler", "src\modules\previewpane\BgcodePreviewHandler\BgcodePreviewHandler.csproj", "{9E0CBC06-F29A-4810-B93C-97D53863B95E}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BgcodePreviewHandlerCpp", "src\modules\previewpane\BgcodePreviewHandlerCpp\BgcodePreviewHandlerCpp.vcxproj", "{F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BgcodeThumbnailProviderCpp", "src\modules\previewpane\BgcodeThumbnailProviderCpp\BgcodeThumbnailProviderCpp.vcxproj", "{47B0678C-806B-4FE1-9F50-46BA88989532}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BgcodeThumbnailProvider", "src\modules\previewpane\BgcodeThumbnailProvider\BgcodeThumbnailProvider.csproj", "{9BC1C986-1E97-4D07-A7B1-CE226C239EFA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview.BgcodePreviewHandler.UnitTests", "src\modules\previewpane\UnitTests-BgcodePreviewHandler\Preview.BgcodePreviewHandler.UnitTests.csproj", "{99CA1509-FB73-456E-AFAF-AB89C017BD72}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Preview.BgcodeThumbnailProvider.UnitTests", "src\modules\previewpane\UnitTests-BgcodeThumbnailProvider\Preview.BgcodeThumbnailProvider.UnitTests.csproj", "{61CBF221-9452-4934-B685-146285E080D7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Search", "src\common\Common.Search\Common.Search.csproj", "{38F187B2-6638-5A40-072F-DBE5E54070A0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Settings.UI.XamlIndexBuilder", "src\settings-ui\Settings.UI.XamlIndexBuilder\Settings.UI.XamlIndexBuilder.csproj", "{DA0744BC-E822-680E-9CEB-D0FBA903A8EE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MouseUtils.UITests", "src\modules\MouseUtils\MouseUtils.UITests\MouseUtils.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Workspaces.Editor.UITests", "src\modules\Workspaces\WorkspacesEditorUITest\Workspaces.Editor.UITests.csproj", "{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CalculatorEngineCommon", "src\common\CalculatorEngineCommon\CalculatorEngineCommon.vcxproj", "{2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCsWin32", "src\common\ManagedCsWin32\ManagedCsWin32.csproj", "{14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerRename.UITests", "src\modules\powerrename\PowerRenameUITest\PowerRename.UITests.csproj", "{9D3F3793-EFE3-4525-8782-238015DABA62}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Core.ViewModels", "src\modules\cmdpal\Core\Microsoft.CmdPal.Core.ViewModels\Microsoft.CmdPal.Core.ViewModels.csproj", "{24133F7F-C1D1-DE04-EFA8-F5D5467FE027}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{0E556541-6A45-42CB-AE49-EE5A9BE05E7C}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{27D9CB3A-46D1-402C-9273-F88CB8AC42F7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B9617A31-0F0A-4397-851D-BF2FBEE32D7F}" - ProjectSection(SolutionItems) = preProject - src\modules\launcher\Plugins\Microsoft.Plugin.Folder.UnitTests\Microsoft.Plugin.Folder.UnitTests.csproj = src\modules\launcher\Plugins\Microsoft.Plugin.Folder.UnitTests\Microsoft.Plugin.Folder.UnitTests.csproj - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1C48CD47-D610-463A-A53C-AF82DD6C47E7}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D9BD324E-1D80-44AA-8E7B-73EB00944434}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{8EF25507-2575-4ADE-BF7E-D23376903AB8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerOCR.UITests", "src\modules\PowerOCR\PowerOCR-UITests\PowerOCR.UITests.csproj", "{070AC093-C9F2-20AD-0BCD-F318FC2761EA}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{2C318EC3-BA86-4372-B1BC-DB0F33C208B2}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BFFB607F-7C78-434B-86B9-DA4C8196A1B5}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{66E1534A-1587-42B2-912F-45C994D32904}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E885E71F-0B34-4A03-B13B-20F4E05E90BB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{264B412F-DB8B-4CF8-A74B-96998B183045}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3527BF37-DFC5-4309-A032-29278CA21328}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{68328142-5B31-4715-BCBB-7B6345EE0971}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.FuzzTests", "src\modules\AdvancedPaste\AdvancedPaste.FuzzTests\AdvancedPaste.FuzzTests.csproj", "{4122388B-59E4-CDB0-0338-EA6881DF86F0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.UnitTests", "src\modules\AdvancedPaste\AdvancedPaste.UnitTests\AdvancedPaste.UnitTests.csproj", "{988C9FAF-5AEC-EB15-578D-FED0DF52BF55}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UITests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UITests\Microsoft.CmdPal.UITests.csproj", "{6748A29D-DA6A-033A-825B-752295FF6AA0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZones.FuzzTests", "src\modules\fancyzones\FancyZones.FuzzTests\FancyZones.FuzzTests.csproj", "{6EABCF9A-6526-441F-932F-658B1DC3E403}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZones.UITests", "src\modules\fancyzones\FancyZones.UITests\FancyZones.UITests.csproj", "{69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZonesEditor.UITests", "src\modules\fancyzones\FancyZonesEditor.UITests\FancyZonesEditor.UITests.csproj", "{9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZonesEditor.UnitTests", "src\modules\fancyzones\FancyZonesEditor.UnitTests\FancyZonesEditor.UnitTests.csproj", "{806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPickerUI.UnitTests", "src\modules\colorPicker\ColorPickerUI.UnitTests\ColorPickerUI.UnitTests.csproj", "{F93C2817-C846-4259-84D8-B39A6B57C8DE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{8131151D-B0E9-4E18-84A5-E5F946C4480A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Calc.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Calc.UnitTests\Microsoft.CmdPal.Ext.Calc.UnitTests.csproj", "{E816D7AC-4688-4ECB-97CC-3D8E798F3825}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Registry.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Registry.UnitTests\Microsoft.CmdPal.Ext.Registry.UnitTests.csproj", "{E816D7AD-4688-4ECB-97CC-3D8E798F3826}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.System.UnitTests\Microsoft.CmdPal.Ext.System.UnitTests.csproj", "{E816D7AE-4688-4ECB-97CC-3D8E798F3827}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.TimeDate.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.TimeDate.UnitTests\Microsoft.CmdPal.Ext.TimeDate.UnitTests.csproj", "{E816D7AF-4688-4ECB-97CC-3D8E798F3828}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WindowWalker.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.WindowWalker.UnitTests\Microsoft.CmdPal.Ext.WindowWalker.UnitTests.csproj", "{E816D7B0-4688-4ECB-97CC-3D8E798F3829}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LightSwitch", "LightSwitch", "{5B201255-53C8-490B-A34F-01F05D48A477}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LightSwitchModuleInterface", "src\modules\LightSwitch\LightSwitchModuleInterface\LightSwitchModuleInterface.vcxproj", "{38177D56-6AD1-4ADF-88C9-2843A7932166}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LightSwitchService", "src\modules\LightSwitch\LightSwitchService\LightSwitchService.vcxproj", "{08E71C67-6A7E-4CA1-B04E-2FB336410BAC}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{E11826E1-76DF-42AC-985C-164CC2EE57A1}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenRuler.UITests", "src\modules\MeasureTool\Tests\ScreenRuler.UITests\ScreenRuler.UITests.csproj", "{66C069F8-C548-4CA6-8CDE-239104D68E88}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "v3", "v3", "{9605B84E-FAC4-477B-B9EC-0753177EE6A8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC", "src\dsc\v3\PowerToys.DSC\PowerToys.DSC.csproj", "{94CDC147-6137-45E9-AEDE-17FF809607C0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerToys.DSC.UnitTests", "src\dsc\v3\PowerToys.DSC.UnitTests\PowerToys.DSC.UnitTests.csproj", "{A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.WebSearch.UnitTests\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj", "{E816D7B2-4688-4ECB-97CC-3D8E798F3831}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3DCCD936-D085-4869-A1DE-CA6A64152C94}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\modules\LightSwitch\Tests\LightSwitch.UITests\LightSwitch.UITests.csproj", "{F5333ED7-06D8-4AB3-953A-36D63F08CB6F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.UI.ViewModels.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.UI.ViewModels.UnitTests\Microsoft.CmdPal.UI.ViewModels.UnitTests.csproj", "{A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM64 = Debug|ARM64 - Debug|x64 = Debug|x64 - Release|ARM64 = Release|ARM64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Debug|ARM64.Build.0 = Debug|ARM64 - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Debug|x64.ActiveCfg = Debug|x64 - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Debug|x64.Build.0 = Debug|x64 - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Release|ARM64.ActiveCfg = Release|ARM64 - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Release|ARM64.Build.0 = Release|ARM64 - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Release|x64.ActiveCfg = Release|x64 - {9412D5C6-2CF2-4FC2-A601-B55508EA9B27}.Release|x64.Build.0 = Release|x64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Debug|ARM64.Build.0 = Debug|ARM64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Debug|x64.ActiveCfg = Debug|x64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Debug|x64.Build.0 = Debug|x64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Release|ARM64.ActiveCfg = Release|ARM64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Release|ARM64.Build.0 = Release|ARM64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Release|x64.ActiveCfg = Release|x64 - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}.Release|x64.Build.0 = Release|x64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Debug|ARM64.Build.0 = Debug|ARM64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Debug|x64.ActiveCfg = Debug|x64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Debug|x64.Build.0 = Debug|x64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Release|ARM64.ActiveCfg = Release|ARM64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Release|ARM64.Build.0 = Release|ARM64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Release|x64.ActiveCfg = Release|x64 - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}.Release|x64.Build.0 = Release|x64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Debug|ARM64.Build.0 = Debug|ARM64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Debug|x64.ActiveCfg = Debug|x64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Debug|x64.Build.0 = Debug|x64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|ARM64.ActiveCfg = Release|ARM64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|ARM64.Build.0 = Release|ARM64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.ActiveCfg = Release|x64 - {1A066C63-64B3-45F8-92FE-664E1CCE8077}.Release|x64.Build.0 = Release|x64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|ARM64.Build.0 = Debug|ARM64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|x64.ActiveCfg = Debug|x64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Debug|x64.Build.0 = Debug|x64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|ARM64.ActiveCfg = Release|ARM64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|ARM64.Build.0 = Release|ARM64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|x64.ActiveCfg = Release|x64 - {E2A5A82E-1E5B-4C8D-9A4F-2B1A8F9E5C3D}.Release|x64.Build.0 = Release|x64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|ARM64.Build.0 = Debug|ARM64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.ActiveCfg = Debug|x64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|x64.Build.0 = Debug|x64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|ARM64.ActiveCfg = Release|ARM64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|ARM64.Build.0 = Release|ARM64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.ActiveCfg = Release|x64 - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.Build.0 = Release|x64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|ARM64.Build.0 = Debug|ARM64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x64.ActiveCfg = Debug|x64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x64.Build.0 = Debug|x64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|ARM64.ActiveCfg = Release|ARM64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|ARM64.Build.0 = Release|ARM64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|x64.ActiveCfg = Release|x64 - {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Release|x64.Build.0 = Release|x64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|ARM64.Build.0 = Debug|ARM64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|x64.ActiveCfg = Debug|x64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Debug|x64.Build.0 = Debug|x64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|ARM64.ActiveCfg = Release|ARM64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|ARM64.Build.0 = Release|ARM64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|x64.ActiveCfg = Release|x64 - {51920F1F-C28C-4ADF-8660-4238766796C2}.Release|x64.Build.0 = Release|x64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|ARM64.Build.0 = Debug|ARM64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|x64.ActiveCfg = Debug|x64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Debug|x64.Build.0 = Debug|x64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|ARM64.ActiveCfg = Release|ARM64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|ARM64.Build.0 = Release|ARM64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|x64.ActiveCfg = Release|x64 - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2}.Release|x64.Build.0 = Release|x64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|ARM64.Build.0 = Debug|ARM64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|x64.ActiveCfg = Debug|x64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Debug|x64.Build.0 = Debug|x64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|ARM64.ActiveCfg = Release|ARM64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|ARM64.Build.0 = Release|ARM64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|x64.ActiveCfg = Release|x64 - {2151F984-E006-4A9F-92EF-C6DDE3DC8413}.Release|x64.Build.0 = Release|x64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Debug|ARM64.Build.0 = Debug|ARM64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Debug|x64.ActiveCfg = Debug|x64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Debug|x64.Build.0 = Debug|x64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|ARM64.ActiveCfg = Release|ARM64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|ARM64.Build.0 = Release|ARM64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.ActiveCfg = Release|x64 - {64A80062-4D8B-4229-8A38-DFA1D7497749}.Release|x64.Build.0 = Release|x64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|ARM64.Build.0 = Debug|ARM64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.ActiveCfg = Debug|x64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Debug|x64.Build.0 = Debug|x64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Release|ARM64.ActiveCfg = Release|ARM64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Release|ARM64.Build.0 = Release|ARM64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Release|x64.ActiveCfg = Release|x64 - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9}.Release|x64.Build.0 = Release|x64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Debug|ARM64.Build.0 = Debug|ARM64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Debug|x64.ActiveCfg = Debug|x64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Debug|x64.Build.0 = Debug|x64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Release|ARM64.ActiveCfg = Release|ARM64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Release|ARM64.Build.0 = Release|ARM64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Release|x64.ActiveCfg = Release|x64 - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}.Release|x64.Build.0 = Release|x64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Debug|ARM64.Build.0 = Debug|ARM64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Debug|x64.ActiveCfg = Debug|x64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Debug|x64.Build.0 = Debug|x64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Release|ARM64.ActiveCfg = Release|ARM64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Release|ARM64.Build.0 = Release|ARM64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Release|x64.ActiveCfg = Release|x64 - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B}.Release|x64.Build.0 = Release|x64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Debug|ARM64.Build.0 = Debug|ARM64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Debug|x64.ActiveCfg = Debug|x64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Debug|x64.Build.0 = Debug|x64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Release|ARM64.ActiveCfg = Release|ARM64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Release|ARM64.Build.0 = Release|ARM64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Release|x64.ActiveCfg = Release|x64 - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}.Release|x64.Build.0 = Release|x64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Debug|ARM64.Build.0 = Debug|ARM64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Debug|x64.ActiveCfg = Debug|x64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Debug|x64.Build.0 = Debug|x64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Release|ARM64.ActiveCfg = Release|ARM64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Release|ARM64.Build.0 = Release|ARM64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Release|x64.ActiveCfg = Release|x64 - {D29DDD63-E2CF-4657-9FD5-2AEDE4257E5D}.Release|x64.Build.0 = Release|x64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Debug|ARM64.Build.0 = Debug|ARM64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Debug|x64.ActiveCfg = Debug|x64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Debug|x64.Build.0 = Debug|x64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Release|ARM64.ActiveCfg = Release|ARM64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Release|ARM64.Build.0 = Release|ARM64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Release|x64.ActiveCfg = Release|x64 - {17DA04DF-E393-4397-9CF0-84DABE11032E}.Release|x64.Build.0 = Release|x64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Debug|ARM64.Build.0 = Debug|ARM64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Debug|x64.ActiveCfg = Debug|x64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Debug|x64.Build.0 = Debug|x64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Release|ARM64.ActiveCfg = Release|ARM64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Release|ARM64.Build.0 = Release|ARM64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Release|x64.ActiveCfg = Release|x64 - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}.Release|x64.Build.0 = Release|x64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|ARM64.Build.0 = Debug|ARM64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|x64.ActiveCfg = Debug|x64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Debug|x64.Build.0 = Debug|x64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Release|ARM64.ActiveCfg = Release|ARM64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Release|ARM64.Build.0 = Release|ARM64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Release|x64.ActiveCfg = Release|x64 - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3}.Release|x64.Build.0 = Release|x64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Debug|ARM64.Build.0 = Debug|ARM64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Debug|x64.ActiveCfg = Debug|x64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Debug|x64.Build.0 = Debug|x64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|ARM64.ActiveCfg = Release|ARM64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|ARM64.Build.0 = Release|ARM64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x64.ActiveCfg = Release|x64 - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80}.Release|x64.Build.0 = Release|x64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|ARM64.Build.0 = Debug|ARM64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|x64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|x64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Release|ARM64.ActiveCfg = Release|ARM64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Release|ARM64.Build.0 = Release|ARM64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Release|x64.ActiveCfg = Release|x64 - {FF742965-9A80-41A5-B042-D6C7D3A21708}.Release|x64.Build.0 = Release|x64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Debug|ARM64.Build.0 = Debug|ARM64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Debug|x64.ActiveCfg = Debug|x64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Debug|x64.Build.0 = Debug|x64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Release|ARM64.ActiveCfg = Release|ARM64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Release|ARM64.Build.0 = Release|ARM64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Release|x64.ActiveCfg = Release|x64 - {59BD9891-3837-438A-958D-ADC7F91F6F7E}.Release|x64.Build.0 = Release|x64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Debug|ARM64.Build.0 = Debug|ARM64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Debug|x64.ActiveCfg = Debug|x64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Debug|x64.Build.0 = Debug|x64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Release|ARM64.ActiveCfg = Release|ARM64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Release|ARM64.Build.0 = Release|ARM64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Release|x64.ActiveCfg = Release|x64 - {4D971245-7A70-41D5-BAA0-DDB5684CAF51}.Release|x64.Build.0 = Release|x64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Debug|ARM64.Build.0 = Debug|ARM64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Debug|x64.ActiveCfg = Debug|x64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Debug|x64.Build.0 = Debug|x64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Release|ARM64.ActiveCfg = Release|ARM64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Release|ARM64.Build.0 = Release|ARM64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Release|x64.ActiveCfg = Release|x64 - {74F1B9ED-F59C-4FE7-B473-7B453E30837E}.Release|x64.Build.0 = Release|x64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Debug|ARM64.Build.0 = Debug|ARM64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Debug|x64.ActiveCfg = Debug|x64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Debug|x64.Build.0 = Debug|x64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Release|ARM64.ActiveCfg = Release|ARM64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Release|ARM64.Build.0 = Release|ARM64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Release|x64.ActiveCfg = Release|x64 - {FDB3555B-58EF-4AE6-B5F1-904719637AB4}.Release|x64.Build.0 = Release|x64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|ARM64.Build.0 = Debug|ARM64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|x64.ActiveCfg = Debug|x64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Debug|x64.Build.0 = Debug|x64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Release|ARM64.ActiveCfg = Release|ARM64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Release|ARM64.Build.0 = Release|ARM64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Release|x64.ActiveCfg = Release|x64 - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0}.Release|x64.Build.0 = Release|x64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Debug|ARM64.Build.0 = Debug|ARM64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Debug|x64.ActiveCfg = Debug|x64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Debug|x64.Build.0 = Debug|x64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Release|ARM64.ActiveCfg = Release|ARM64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Release|ARM64.Build.0 = Release|ARM64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Release|x64.ActiveCfg = Release|x64 - {F8B870EB-D5F5-45BA-9CF7-A5C459818820}.Release|x64.Build.0 = Release|x64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Debug|ARM64.Build.0 = Debug|ARM64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Debug|x64.ActiveCfg = Debug|x64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Debug|x64.Build.0 = Debug|x64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Release|ARM64.ActiveCfg = Release|ARM64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Release|ARM64.Build.0 = Release|ARM64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Release|x64.ActiveCfg = Release|x64 - {E364F67B-BB12-4E91-B639-355866EBCD8B}.Release|x64.Build.0 = Release|x64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Debug|ARM64.Build.0 = Debug|ARM64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Debug|x64.ActiveCfg = Debug|x64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Debug|x64.Build.0 = Debug|x64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Release|ARM64.ActiveCfg = Release|ARM64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Release|ARM64.Build.0 = Release|ARM64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Release|x64.ActiveCfg = Release|x64 - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2}.Release|x64.Build.0 = Release|x64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Debug|ARM64.Build.0 = Debug|ARM64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Debug|x64.ActiveCfg = Debug|x64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Debug|x64.Build.0 = Debug|x64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Release|ARM64.ActiveCfg = Release|ARM64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Release|ARM64.Build.0 = Release|ARM64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Release|x64.ActiveCfg = Release|x64 - {AF2349B8-E5B6-4004-9502-687C1C7730B1}.Release|x64.Build.0 = Release|x64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Debug|ARM64.Build.0 = Debug|ARM64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Debug|x64.ActiveCfg = Debug|x64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Debug|x64.Build.0 = Debug|x64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Release|ARM64.ActiveCfg = Release|ARM64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Release|ARM64.Build.0 = Release|ARM64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Release|x64.ActiveCfg = Release|x64 - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB}.Release|x64.Build.0 = Release|x64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Debug|ARM64.Build.0 = Debug|ARM64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Debug|x64.ActiveCfg = Debug|x64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Debug|x64.Build.0 = Debug|x64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Release|ARM64.ActiveCfg = Release|ARM64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Release|ARM64.Build.0 = Release|ARM64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Release|x64.ActiveCfg = Release|x64 - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}.Release|x64.Build.0 = Release|x64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Debug|ARM64.Build.0 = Debug|ARM64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Debug|x64.ActiveCfg = Debug|x64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Debug|x64.Build.0 = Debug|x64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Release|ARM64.ActiveCfg = Release|ARM64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Release|ARM64.Build.0 = Release|ARM64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Release|x64.ActiveCfg = Release|x64 - {DA425894-6E13-404F-8DCB-78584EC0557A}.Release|x64.Build.0 = Release|x64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Debug|ARM64.Build.0 = Debug|ARM64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Debug|x64.ActiveCfg = Debug|x64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Debug|x64.Build.0 = Debug|x64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Release|ARM64.ActiveCfg = Release|ARM64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Release|ARM64.Build.0 = Release|ARM64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Release|x64.ActiveCfg = Release|x64 - {060D75DA-2D1C-48E6-A4A1-6F0718B64661}.Release|x64.Build.0 = Release|x64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Debug|ARM64.Build.0 = Debug|ARM64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Debug|x64.ActiveCfg = Debug|x64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Debug|x64.Build.0 = Debug|x64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Release|ARM64.ActiveCfg = Release|ARM64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Release|ARM64.Build.0 = Release|ARM64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Release|x64.ActiveCfg = Release|x64 - {748417CA-F17E-487F-9411-CAFB6D3F4877}.Release|x64.Build.0 = Release|x64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Debug|ARM64.Build.0 = Debug|ARM64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Debug|x64.ActiveCfg = Debug|x64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Debug|x64.Build.0 = Debug|x64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Release|ARM64.ActiveCfg = Release|ARM64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Release|ARM64.Build.0 = Release|ARM64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Release|x64.ActiveCfg = Release|x64 - {217DF501-135C-4E38-BFC8-99D4821032EA}.Release|x64.Build.0 = Release|x64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Debug|ARM64.Build.0 = Debug|ARM64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Debug|x64.ActiveCfg = Debug|x64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Debug|x64.Build.0 = Debug|x64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Release|ARM64.ActiveCfg = Release|ARM64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Release|ARM64.Build.0 = Release|ARM64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Release|x64.ActiveCfg = Release|x64 - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A}.Release|x64.Build.0 = Release|x64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|ARM64.Build.0 = Debug|ARM64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.ActiveCfg = Debug|x64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Debug|x64.Build.0 = Debug|x64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|ARM64.ActiveCfg = Release|ARM64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|ARM64.Build.0 = Release|ARM64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.ActiveCfg = Release|x64 - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4}.Release|x64.Build.0 = Release|x64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Debug|ARM64.Build.0 = Debug|ARM64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Debug|x64.ActiveCfg = Debug|x64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Debug|x64.Build.0 = Debug|x64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Release|ARM64.ActiveCfg = Release|ARM64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Release|ARM64.Build.0 = Release|ARM64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Release|x64.ActiveCfg = Release|x64 - {08C8C05F-0362-41BC-818C-724572DF8B06}.Release|x64.Build.0 = Release|x64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Debug|ARM64.Build.0 = Debug|ARM64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Debug|x64.ActiveCfg = Debug|x64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Debug|x64.Build.0 = Debug|x64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Release|ARM64.ActiveCfg = Release|ARM64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Release|ARM64.Build.0 = Release|ARM64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Release|x64.ActiveCfg = Release|x64 - {5D00D290-4016-4CFE-9E41-1E7C724509BA}.Release|x64.Build.0 = Release|x64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|ARM64.Build.0 = Debug|ARM64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|x64.ActiveCfg = Debug|x64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Debug|x64.Build.0 = Debug|x64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|ARM64.ActiveCfg = Release|ARM64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|ARM64.Build.0 = Release|ARM64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.ActiveCfg = Release|x64 - {4AED67B6-55FD-486F-B917-E543DEE2CB3C}.Release|x64.Build.0 = Release|x64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|ARM64.Build.0 = Debug|ARM64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.ActiveCfg = Debug|x64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Debug|x64.Build.0 = Debug|x64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|ARM64.ActiveCfg = Release|ARM64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|ARM64.Build.0 = Release|ARM64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.ActiveCfg = Release|x64 - {42851751-CBC8-45A6-97F5-7A0753F7B4D1}.Release|x64.Build.0 = Release|x64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Debug|ARM64.Build.0 = Debug|ARM64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Debug|x64.ActiveCfg = Debug|x64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Debug|x64.Build.0 = Debug|x64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Release|ARM64.ActiveCfg = Release|ARM64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Release|ARM64.Build.0 = Release|ARM64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Release|x64.ActiveCfg = Release|x64 - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}.Release|x64.Build.0 = Release|x64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Debug|ARM64.Build.0 = Debug|ARM64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Debug|x64.ActiveCfg = Debug|x64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Debug|x64.Build.0 = Debug|x64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Release|ARM64.ActiveCfg = Release|ARM64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Release|ARM64.Build.0 = Release|ARM64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Release|x64.ActiveCfg = Release|x64 - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD}.Release|x64.Build.0 = Release|x64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Debug|ARM64.Build.0 = Debug|ARM64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Debug|x64.ActiveCfg = Debug|x64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Debug|x64.Build.0 = Debug|x64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Release|ARM64.ActiveCfg = Release|ARM64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Release|ARM64.Build.0 = Release|ARM64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Release|x64.ActiveCfg = Release|x64 - {655C9AF2-18D3-4DA6-80E4-85504A7722BA}.Release|x64.Build.0 = Release|x64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|ARM64.Build.0 = Debug|ARM64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|x64.ActiveCfg = Debug|x64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Debug|x64.Build.0 = Debug|x64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|ARM64.ActiveCfg = Release|ARM64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|ARM64.Build.0 = Release|ARM64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|x64.ActiveCfg = Release|x64 - {BA58206B-1493-4C75-BFEA-A85768A1E156}.Release|x64.Build.0 = Release|x64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|ARM64.Build.0 = Debug|ARM64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|x64.ActiveCfg = Debug|x64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Debug|x64.Build.0 = Debug|x64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|ARM64.ActiveCfg = Release|ARM64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|ARM64.Build.0 = Release|ARM64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|x64.ActiveCfg = Release|x64 - {03276A39-D4E9-417C-8FFD-200B0EE5E871}.Release|x64.Build.0 = Release|x64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|ARM64.Build.0 = Debug|ARM64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|x64.ActiveCfg = Debug|x64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Debug|x64.Build.0 = Debug|x64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|ARM64.ActiveCfg = Release|ARM64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|ARM64.Build.0 = Release|ARM64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|x64.ActiveCfg = Release|x64 - {B81FB7B6-D30E-428F-908A-41422EFC1172}.Release|x64.Build.0 = Release|x64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Debug|ARM64.Build.0 = Debug|ARM64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Debug|x64.ActiveCfg = Debug|x64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Debug|x64.Build.0 = Debug|x64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Release|ARM64.ActiveCfg = Release|ARM64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Release|ARM64.Build.0 = Release|ARM64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Release|x64.ActiveCfg = Release|x64 - {0F85E674-34AE-443D-954C-8321EB8B93B1}.Release|x64.Build.0 = Release|x64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Debug|ARM64.Build.0 = Debug|ARM64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Debug|x64.ActiveCfg = Debug|x64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Debug|x64.Build.0 = Debug|x64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Release|ARM64.ActiveCfg = Release|ARM64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Release|ARM64.Build.0 = Release|ARM64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Release|x64.ActiveCfg = Release|x64 - {632BBE62-5421-49EA-835A-7FFA4F499BD6}.Release|x64.Build.0 = Release|x64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|ARM64.Build.0 = Debug|ARM64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.ActiveCfg = Debug|x64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.Build.0 = Debug|x64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|ARM64.ActiveCfg = Release|ARM64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|ARM64.Build.0 = Release|ARM64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.ActiveCfg = Release|x64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.Build.0 = Release|x64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Debug|ARM64.Build.0 = Debug|ARM64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Debug|x64.ActiveCfg = Debug|x64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Debug|x64.Build.0 = Debug|x64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Release|ARM64.ActiveCfg = Release|ARM64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Release|ARM64.Build.0 = Release|ARM64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Release|x64.ActiveCfg = Release|x64 - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B}.Release|x64.Build.0 = Release|x64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Debug|ARM64.Build.0 = Debug|ARM64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Debug|x64.ActiveCfg = Debug|x64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Debug|x64.Build.0 = Debug|x64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Release|ARM64.ActiveCfg = Release|ARM64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Release|ARM64.Build.0 = Release|ARM64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Release|x64.ActiveCfg = Release|x64 - {DA5A6FE9-0040-40CC-83CC-764AE5306590}.Release|x64.Build.0 = Release|x64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Debug|ARM64.Build.0 = Debug|ARM64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Debug|x64.ActiveCfg = Debug|x64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Debug|x64.Build.0 = Debug|x64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Release|ARM64.ActiveCfg = Release|ARM64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Release|ARM64.Build.0 = Release|ARM64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Release|x64.ActiveCfg = Release|x64 - {0351ADA4-0C32-4652-9BA0-41F7B602372B}.Release|x64.Build.0 = Release|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|ARM64.Build.0 = Debug|ARM64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.ActiveCfg = Debug|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.Build.0 = Debug|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|ARM64.ActiveCfg = Release|ARM64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|ARM64.Build.0 = Release|ARM64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.ActiveCfg = Release|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.Build.0 = Release|x64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|ARM64.Build.0 = Debug|ARM64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|x64.ActiveCfg = Debug|x64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Debug|x64.Build.0 = Debug|x64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Release|ARM64.ActiveCfg = Release|ARM64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Release|ARM64.Build.0 = Release|ARM64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Release|x64.ActiveCfg = Release|x64 - {6955446D-23F7-4023-9BB3-8657F904AF99}.Release|x64.Build.0 = Release|x64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Debug|ARM64.Build.0 = Debug|ARM64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Debug|x64.ActiveCfg = Debug|x64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Debug|x64.Build.0 = Debug|x64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Release|ARM64.ActiveCfg = Release|ARM64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Release|ARM64.Build.0 = Release|ARM64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Release|x64.ActiveCfg = Release|x64 - {58736667-1027-4AD7-BFDF-7A3A6474103A}.Release|x64.Build.0 = Release|x64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Debug|ARM64.Build.0 = Debug|ARM64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Debug|x64.ActiveCfg = Debug|x64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Debug|x64.Build.0 = Debug|x64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Release|ARM64.ActiveCfg = Release|ARM64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Release|ARM64.Build.0 = Release|ARM64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Release|x64.ActiveCfg = Release|x64 - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525}.Release|x64.Build.0 = Release|x64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Debug|ARM64.Build.0 = Debug|ARM64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Debug|x64.ActiveCfg = Debug|x64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Debug|x64.Build.0 = Debug|x64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Release|ARM64.ActiveCfg = Release|ARM64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Release|ARM64.Build.0 = Release|ARM64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Release|x64.ActiveCfg = Release|x64 - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73}.Release|x64.Build.0 = Release|x64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Debug|ARM64.Build.0 = Debug|ARM64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Debug|x64.ActiveCfg = Debug|x64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Debug|x64.Build.0 = Debug|x64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Release|ARM64.ActiveCfg = Release|ARM64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Release|ARM64.Build.0 = Release|ARM64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Release|x64.ActiveCfg = Release|x64 - {0B593A6C-4143-4337-860E-DB5710FB87DB}.Release|x64.Build.0 = Release|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|ARM64.Build.0 = Debug|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.ActiveCfg = Debug|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.Build.0 = Debug|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|ARM64.ActiveCfg = Release|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|ARM64.Build.0 = Release|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.ActiveCfg = Release|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.Build.0 = Release|x64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Debug|ARM64.Build.0 = Debug|ARM64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Debug|x64.ActiveCfg = Debug|x64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Debug|x64.Build.0 = Debug|x64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Release|ARM64.ActiveCfg = Release|ARM64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Release|ARM64.Build.0 = Release|ARM64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Release|x64.ActiveCfg = Release|x64 - {7319089E-46D6-4400-BC65-E39BDF1416EE}.Release|x64.Build.0 = Release|x64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Debug|ARM64.Build.0 = Debug|ARM64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Debug|x64.ActiveCfg = Debug|x64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Debug|x64.Build.0 = Debug|x64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Release|ARM64.ActiveCfg = Release|ARM64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Release|ARM64.Build.0 = Release|ARM64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Release|x64.ActiveCfg = Release|x64 - {CABA8DFB-823B-4BF2-93AC-3F31984150D9}.Release|x64.Build.0 = Release|x64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Debug|ARM64.Build.0 = Debug|ARM64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Debug|x64.ActiveCfg = Debug|x64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Debug|x64.Build.0 = Debug|x64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Release|ARM64.ActiveCfg = Release|ARM64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Release|ARM64.Build.0 = Release|ARM64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Release|x64.ActiveCfg = Release|x64 - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB}.Release|x64.Build.0 = Release|x64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Debug|ARM64.Build.0 = Debug|ARM64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Debug|x64.ActiveCfg = Debug|x64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Debug|x64.Build.0 = Debug|x64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Release|ARM64.ActiveCfg = Release|ARM64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Release|ARM64.Build.0 = Release|ARM64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Release|x64.ActiveCfg = Release|x64 - {C3A17DCA-217B-462C-BB0C-BE086AF80081}.Release|x64.Build.0 = Release|x64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Debug|ARM64.Build.0 = Debug|ARM64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Debug|x64.ActiveCfg = Debug|x64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Debug|x64.Build.0 = Debug|x64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Release|ARM64.ActiveCfg = Release|ARM64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Release|ARM64.Build.0 = Release|ARM64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Release|x64.ActiveCfg = Release|x64 - {69E1EE8D-143A-4060-9129-4658ACF14AAF}.Release|x64.Build.0 = Release|x64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Debug|ARM64.Build.0 = Debug|ARM64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Debug|x64.ActiveCfg = Debug|x64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Debug|x64.Build.0 = Debug|x64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Release|ARM64.ActiveCfg = Release|ARM64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Release|ARM64.Build.0 = Release|ARM64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Release|x64.ActiveCfg = Release|x64 - {ECC20689-002A-4354-95A6-B58DF089C6FF}.Release|x64.Build.0 = Release|x64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Debug|ARM64.Build.0 = Debug|ARM64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Debug|x64.ActiveCfg = Debug|x64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Debug|x64.Build.0 = Debug|x64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Release|ARM64.ActiveCfg = Release|ARM64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Release|ARM64.Build.0 = Release|ARM64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Release|x64.ActiveCfg = Release|x64 - {4BABF3FE-3451-42FD-873F-3C332E18DCEF}.Release|x64.Build.0 = Release|x64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Debug|ARM64.Build.0 = Debug|ARM64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Debug|x64.ActiveCfg = Debug|x64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Debug|x64.Build.0 = Debug|x64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Release|ARM64.ActiveCfg = Release|ARM64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Release|ARM64.Build.0 = Release|ARM64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Release|x64.ActiveCfg = Release|x64 - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65}.Release|x64.Build.0 = Release|x64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Debug|ARM64.Build.0 = Debug|ARM64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Debug|x64.ActiveCfg = Debug|x64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Debug|x64.Build.0 = Debug|x64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Release|ARM64.ActiveCfg = Release|ARM64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Release|ARM64.Build.0 = Release|ARM64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Release|x64.ActiveCfg = Release|x64 - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD}.Release|x64.Build.0 = Release|x64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Debug|ARM64.Build.0 = Debug|ARM64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Debug|x64.ActiveCfg = Debug|x64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Debug|x64.Build.0 = Debug|x64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Release|ARM64.ActiveCfg = Release|ARM64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Release|ARM64.Build.0 = Release|ARM64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Release|x64.ActiveCfg = Release|x64 - {E496B7FC-1E99-4BAB-849B-0E8367040B02}.Release|x64.Build.0 = Release|x64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Debug|ARM64.Build.0 = Debug|ARM64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Debug|x64.ActiveCfg = Debug|x64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Debug|x64.Build.0 = Debug|x64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Release|ARM64.ActiveCfg = Release|ARM64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Release|ARM64.Build.0 = Release|ARM64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Release|x64.ActiveCfg = Release|x64 - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466}.Release|x64.Build.0 = Release|x64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Debug|ARM64.Build.0 = Debug|ARM64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Debug|x64.ActiveCfg = Debug|x64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Debug|x64.Build.0 = Debug|x64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Release|ARM64.ActiveCfg = Release|ARM64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Release|ARM64.Build.0 = Release|ARM64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Release|x64.ActiveCfg = Release|x64 - {8DF78B53-200E-451F-9328-01EB907193AE}.Release|x64.Build.0 = Release|x64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Debug|ARM64.Build.0 = Debug|ARM64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Debug|x64.ActiveCfg = Debug|x64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Debug|x64.Build.0 = Debug|x64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Release|ARM64.ActiveCfg = Release|ARM64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Release|ARM64.Build.0 = Release|ARM64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Release|x64.ActiveCfg = Release|x64 - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49}.Release|x64.Build.0 = Release|x64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|ARM64.Build.0 = Debug|ARM64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x64.ActiveCfg = Debug|x64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Debug|x64.Build.0 = Debug|x64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|ARM64.ActiveCfg = Release|ARM64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|ARM64.Build.0 = Release|ARM64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.ActiveCfg = Release|x64 - {62173D9A-6724-4C00-A1C8-FB646480A9EC}.Release|x64.Build.0 = Release|x64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|ARM64.Build.0 = Debug|ARM64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|x64.ActiveCfg = Debug|x64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Debug|x64.Build.0 = Debug|x64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|ARM64.ActiveCfg = Release|ARM64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|ARM64.Build.0 = Release|ARM64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.ActiveCfg = Release|x64 - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A}.Release|x64.Build.0 = Release|x64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|ARM64.Build.0 = Debug|ARM64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.ActiveCfg = Debug|x64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Debug|x64.Build.0 = Debug|x64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|ARM64.ActiveCfg = Release|ARM64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|ARM64.Build.0 = Release|ARM64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.ActiveCfg = Release|x64 - {D940E07F-532C-4FF3-883F-790DA014F19A}.Release|x64.Build.0 = Release|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|ARM64.Build.0 = Debug|ARM64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.ActiveCfg = Debug|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Debug|x64.Build.0 = Debug|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|ARM64.ActiveCfg = Release|ARM64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|ARM64.Build.0 = Release|ARM64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.ActiveCfg = Release|x64 - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4}.Release|x64.Build.0 = Release|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|ARM64.Build.0 = Debug|ARM64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.ActiveCfg = Debug|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Debug|x64.Build.0 = Debug|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|ARM64.ActiveCfg = Release|ARM64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|ARM64.Build.0 = Release|ARM64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.ActiveCfg = Release|x64 - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1}.Release|x64.Build.0 = Release|x64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|ARM64.Build.0 = Debug|ARM64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.ActiveCfg = Debug|x64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Debug|x64.Build.0 = Debug|x64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|ARM64.ActiveCfg = Release|ARM64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|ARM64.Build.0 = Release|ARM64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|x64.ActiveCfg = Release|x64 - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81}.Release|x64.Build.0 = Release|x64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Debug|ARM64.Build.0 = Debug|ARM64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Debug|x64.ActiveCfg = Debug|x64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Debug|x64.Build.0 = Debug|x64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Release|ARM64.ActiveCfg = Release|ARM64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Release|ARM64.Build.0 = Release|ARM64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Release|x64.ActiveCfg = Release|x64 - {2EDB3EB4-FA92-4BFF-B2D8-566584837231}.Release|x64.Build.0 = Release|x64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Debug|ARM64.Build.0 = Debug|ARM64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Debug|x64.ActiveCfg = Debug|x64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Debug|x64.Build.0 = Debug|x64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Release|ARM64.ActiveCfg = Release|ARM64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Release|ARM64.Build.0 = Release|ARM64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Release|x64.ActiveCfg = Release|x64 - {48804216-2A0E-4168-A6D8-9CD068D14227}.Release|x64.Build.0 = Release|x64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Debug|ARM64.Build.0 = Debug|ARM64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Debug|x64.ActiveCfg = Debug|x64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Debug|x64.Build.0 = Debug|x64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|ARM64.ActiveCfg = Release|ARM64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|ARM64.Build.0 = Release|ARM64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|x64.ActiveCfg = Release|x64 - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700}.Release|x64.Build.0 = Release|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|ARM64.Build.0 = Debug|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|x64.ActiveCfg = Debug|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Debug|x64.Build.0 = Debug|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|ARM64.ActiveCfg = Release|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|ARM64.Build.0 = Release|ARM64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|x64.ActiveCfg = Release|x64 - {44CE9AE1-4390-42C5-BACC-0FD6B40AA203}.Release|x64.Build.0 = Release|x64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|ARM64.Build.0 = Debug|ARM64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x64.ActiveCfg = Debug|x64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Debug|x64.Build.0 = Debug|x64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|ARM64.ActiveCfg = Release|ARM64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|ARM64.Build.0 = Release|ARM64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.ActiveCfg = Release|x64 - {5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.Build.0 = Release|x64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Debug|ARM64.Build.0 = Debug|ARM64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Debug|x64.ActiveCfg = Debug|x64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Debug|x64.Build.0 = Debug|x64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Release|ARM64.ActiveCfg = Release|ARM64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Release|ARM64.Build.0 = Release|ARM64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Release|x64.ActiveCfg = Release|x64 - {11491FD8-F921-48BF-880C-7FEA185B80A1}.Release|x64.Build.0 = Release|x64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Debug|ARM64.Build.0 = Debug|ARM64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Debug|x64.ActiveCfg = Debug|x64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Debug|x64.Build.0 = Debug|x64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Release|ARM64.ActiveCfg = Release|ARM64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Release|ARM64.Build.0 = Release|ARM64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Release|x64.ActiveCfg = Release|x64 - {F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Release|x64.Build.0 = Release|x64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Debug|ARM64.Build.0 = Debug|ARM64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Debug|x64.ActiveCfg = Debug|x64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Debug|x64.Build.0 = Debug|x64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Release|ARM64.ActiveCfg = Release|ARM64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Release|ARM64.Build.0 = Release|ARM64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Release|x64.ActiveCfg = Release|x64 - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Release|x64.Build.0 = Release|x64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Debug|ARM64.Build.0 = Debug|ARM64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Debug|x64.ActiveCfg = Debug|x64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Debug|x64.Build.0 = Debug|x64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Release|ARM64.ActiveCfg = Release|ARM64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Release|ARM64.Build.0 = Release|ARM64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Release|x64.ActiveCfg = Release|x64 - {4ED320BC-BA04-4D42-8D15-CBE62151F08B}.Release|x64.Build.0 = Release|x64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Debug|ARM64.Build.0 = Debug|ARM64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Debug|x64.ActiveCfg = Debug|x64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Debug|x64.Build.0 = Debug|x64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Release|ARM64.ActiveCfg = Release|ARM64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Release|ARM64.Build.0 = Release|ARM64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Release|x64.ActiveCfg = Release|x64 - {E94FD11C-0591-456F-899F-EFC0CA548336}.Release|x64.Build.0 = Release|x64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Debug|ARM64.Build.0 = Debug|ARM64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Debug|x64.ActiveCfg = Debug|x64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Debug|x64.Build.0 = Debug|x64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Release|ARM64.ActiveCfg = Release|ARM64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Release|ARM64.Build.0 = Release|ARM64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Release|x64.ActiveCfg = Release|x64 - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88}.Release|x64.Build.0 = Release|x64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Debug|ARM64.Build.0 = Debug|ARM64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Debug|x64.ActiveCfg = Debug|x64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Debug|x64.Build.0 = Debug|x64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Release|ARM64.ActiveCfg = Release|ARM64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Release|ARM64.Build.0 = Release|ARM64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Release|x64.ActiveCfg = Release|x64 - {809AA252-E17A-4FA2-B0A1-0450976B763F}.Release|x64.Build.0 = Release|x64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Debug|ARM64.Build.0 = Debug|ARM64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Debug|x64.ActiveCfg = Debug|x64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Debug|x64.Build.0 = Debug|x64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Release|ARM64.ActiveCfg = Release|ARM64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Release|ARM64.Build.0 = Release|ARM64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Release|x64.ActiveCfg = Release|x64 - {133281D8-1BCE-4D07-B31E-796612A9609E}.Release|x64.Build.0 = Release|x64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Debug|ARM64.Build.0 = Debug|ARM64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Debug|x64.ActiveCfg = Debug|x64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Debug|x64.Build.0 = Debug|x64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Release|ARM64.ActiveCfg = Release|ARM64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Release|ARM64.Build.0 = Release|ARM64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Release|x64.ActiveCfg = Release|x64 - {805306FF-A562-4415-8DEF-E493BDC45918}.Release|x64.Build.0 = Release|x64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Debug|ARM64.Build.0 = Debug|ARM64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Debug|x64.ActiveCfg = Debug|x64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Debug|x64.Build.0 = Debug|x64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Release|ARM64.ActiveCfg = Release|ARM64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Release|ARM64.Build.0 = Release|ARM64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Release|x64.ActiveCfg = Release|x64 - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}.Release|x64.Build.0 = Release|x64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Debug|ARM64.Build.0 = Debug|ARM64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Debug|x64.ActiveCfg = Debug|x64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Debug|x64.Build.0 = Debug|x64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Release|ARM64.ActiveCfg = Release|ARM64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Release|ARM64.Build.0 = Release|ARM64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Release|x64.ActiveCfg = Release|x64 - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}.Release|x64.Build.0 = Release|x64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Debug|ARM64.Build.0 = Debug|ARM64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Debug|x64.ActiveCfg = Debug|x64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Debug|x64.Build.0 = Debug|x64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Release|ARM64.ActiveCfg = Release|ARM64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Release|ARM64.Build.0 = Release|ARM64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Release|x64.ActiveCfg = Release|x64 - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9}.Release|x64.Build.0 = Release|x64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Debug|ARM64.Build.0 = Debug|ARM64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Debug|x64.ActiveCfg = Debug|x64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Debug|x64.Build.0 = Debug|x64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Release|ARM64.ActiveCfg = Release|ARM64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Release|ARM64.Build.0 = Release|ARM64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Release|x64.ActiveCfg = Release|x64 - {9F94B303-5E21-4364-9362-64426F8DB932}.Release|x64.Build.0 = Release|x64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Debug|ARM64.Build.0 = Debug|ARM64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Debug|x64.ActiveCfg = Debug|x64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Debug|x64.Build.0 = Debug|x64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Release|ARM64.ActiveCfg = Release|ARM64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Release|ARM64.Build.0 = Release|ARM64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Release|x64.ActiveCfg = Release|x64 - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E}.Release|x64.Build.0 = Release|x64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Debug|ARM64.Build.0 = Debug|ARM64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Debug|x64.ActiveCfg = Debug|x64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Debug|x64.Build.0 = Debug|x64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Release|ARM64.ActiveCfg = Release|ARM64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Release|ARM64.Build.0 = Release|ARM64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Release|x64.ActiveCfg = Release|x64 - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2}.Release|x64.Build.0 = Release|x64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Debug|ARM64.Build.0 = Debug|ARM64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Debug|x64.ActiveCfg = Debug|x64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Debug|x64.Build.0 = Debug|x64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Release|ARM64.ActiveCfg = Release|ARM64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Release|ARM64.Build.0 = Release|ARM64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Release|x64.ActiveCfg = Release|x64 - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}.Release|x64.Build.0 = Release|x64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Debug|ARM64.Build.0 = Debug|ARM64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Debug|x64.ActiveCfg = Debug|x64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Debug|x64.Build.0 = Debug|x64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|ARM64.ActiveCfg = Release|ARM64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|ARM64.Build.0 = Release|ARM64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x64.ActiveCfg = Release|x64 - {04B193D7-3E21-46B8-A958-89B63A8A69DE}.Release|x64.Build.0 = Release|x64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Debug|ARM64.Build.0 = Debug|ARM64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Debug|x64.ActiveCfg = Debug|x64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Debug|x64.Build.0 = Debug|x64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Release|ARM64.ActiveCfg = Release|ARM64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Release|ARM64.Build.0 = Release|ARM64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Release|x64.ActiveCfg = Release|x64 - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF}.Release|x64.Build.0 = Release|x64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Debug|ARM64.Build.0 = Debug|ARM64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Debug|x64.ActiveCfg = Debug|x64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Debug|x64.Build.0 = Debug|x64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Release|ARM64.ActiveCfg = Release|ARM64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Release|ARM64.Build.0 = Release|ARM64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Release|x64.ActiveCfg = Release|x64 - {FD464B4C-2F68-4D06-91E7-4208146C41F5}.Release|x64.Build.0 = Release|x64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|ARM64.Build.0 = Debug|ARM64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|x64.ActiveCfg = Debug|x64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Debug|x64.Build.0 = Debug|x64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|ARM64.ActiveCfg = Release|ARM64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|ARM64.Build.0 = Release|ARM64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|x64.ActiveCfg = Release|x64 - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1}.Release|x64.Build.0 = Release|x64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Debug|ARM64.Build.0 = Debug|ARM64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Debug|x64.ActiveCfg = Debug|x64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Debug|x64.Build.0 = Debug|x64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Release|ARM64.ActiveCfg = Release|ARM64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Release|ARM64.Build.0 = Release|ARM64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Release|x64.ActiveCfg = Release|x64 - {020A7474-3601-4160-A159-D7B70B77B15F}.Release|x64.Build.0 = Release|x64 - {27718999-C175-450A-861C-89F911E16A88}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {27718999-C175-450A-861C-89F911E16A88}.Debug|ARM64.Build.0 = Debug|ARM64 - {27718999-C175-450A-861C-89F911E16A88}.Debug|x64.ActiveCfg = Debug|x64 - {27718999-C175-450A-861C-89F911E16A88}.Debug|x64.Build.0 = Debug|x64 - {27718999-C175-450A-861C-89F911E16A88}.Release|ARM64.ActiveCfg = Release|ARM64 - {27718999-C175-450A-861C-89F911E16A88}.Release|ARM64.Build.0 = Release|ARM64 - {27718999-C175-450A-861C-89F911E16A88}.Release|x64.ActiveCfg = Release|x64 - {27718999-C175-450A-861C-89F911E16A88}.Release|x64.Build.0 = Release|x64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Debug|ARM64.Build.0 = Debug|ARM64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Debug|x64.ActiveCfg = Debug|x64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Debug|x64.Build.0 = Debug|x64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Release|ARM64.ActiveCfg = Release|ARM64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Release|ARM64.Build.0 = Release|ARM64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Release|x64.ActiveCfg = Release|x64 - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6}.Release|x64.Build.0 = Release|x64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Debug|ARM64.Build.0 = Debug|ARM64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Debug|x64.ActiveCfg = Debug|x64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Debug|x64.Build.0 = Debug|x64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Release|ARM64.ActiveCfg = Release|ARM64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Release|ARM64.Build.0 = Release|ARM64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Release|x64.ActiveCfg = Release|x64 - {5A1DB2F0-0715-4B3B-98E6-79BC41540045}.Release|x64.Build.0 = Release|x64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Debug|ARM64.Build.0 = Debug|ARM64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Debug|x64.ActiveCfg = Debug|x64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Debug|x64.Build.0 = Debug|x64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Release|ARM64.ActiveCfg = Release|ARM64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Release|ARM64.Build.0 = Release|ARM64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Release|x64.ActiveCfg = Release|x64 - {93B72A06-C8BD-484F-A6F7-C9F280B150BF}.Release|x64.Build.0 = Release|x64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Debug|ARM64.Build.0 = Debug|ARM64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Debug|x64.ActiveCfg = Debug|x64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Debug|x64.Build.0 = Debug|x64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|ARM64.ActiveCfg = Release|ARM64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|ARM64.Build.0 = Release|ARM64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x64.ActiveCfg = Release|x64 - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853}.Release|x64.Build.0 = Release|x64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|ARM64.Build.0 = Debug|ARM64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|x64.ActiveCfg = Debug|x64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Debug|x64.Build.0 = Debug|x64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|ARM64.ActiveCfg = Release|ARM64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|ARM64.Build.0 = Release|ARM64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|x64.ActiveCfg = Release|x64 - {34A354C5-23C7-4343-916C-C52DAF4FC39D}.Release|x64.Build.0 = Release|x64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|ARM64.Build.0 = Debug|ARM64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|x64.ActiveCfg = Debug|x64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Debug|x64.Build.0 = Debug|x64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|ARM64.ActiveCfg = Release|ARM64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|ARM64.Build.0 = Release|ARM64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|x64.ActiveCfg = Release|x64 - {3264DF53-C805-4B0C-867C-FCEAF7AEF762}.Release|x64.Build.0 = Release|x64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|ARM64.Build.0 = Debug|ARM64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|x64.ActiveCfg = Debug|x64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Debug|x64.Build.0 = Debug|x64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|ARM64.ActiveCfg = Release|ARM64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|ARM64.Build.0 = Release|ARM64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|x64.ActiveCfg = Release|x64 - {31CAD28E-778A-441C-85BC-40AB3EAA2A10}.Release|x64.Build.0 = Release|x64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Debug|ARM64.Build.0 = Debug|ARM64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Debug|x64.ActiveCfg = Debug|x64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Debug|x64.Build.0 = Debug|x64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Release|ARM64.ActiveCfg = Release|ARM64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Release|ARM64.Build.0 = Release|ARM64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Release|x64.ActiveCfg = Release|x64 - {25C91A4E-BA4E-467A-85CD-8B62545BF674}.Release|x64.Build.0 = Release|x64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Debug|ARM64.Build.0 = Debug|ARM64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Debug|x64.ActiveCfg = Debug|x64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Debug|x64.Build.0 = Debug|x64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Release|ARM64.ActiveCfg = Release|ARM64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Release|ARM64.Build.0 = Release|ARM64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Release|x64.ActiveCfg = Release|x64 - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1}.Release|x64.Build.0 = Release|x64 - {212AD910-8488-4036-BE20-326931B75FB2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {212AD910-8488-4036-BE20-326931B75FB2}.Debug|ARM64.Build.0 = Debug|ARM64 - {212AD910-8488-4036-BE20-326931B75FB2}.Debug|x64.ActiveCfg = Debug|x64 - {212AD910-8488-4036-BE20-326931B75FB2}.Debug|x64.Build.0 = Debug|x64 - {212AD910-8488-4036-BE20-326931B75FB2}.Release|ARM64.ActiveCfg = Release|ARM64 - {212AD910-8488-4036-BE20-326931B75FB2}.Release|ARM64.Build.0 = Release|ARM64 - {212AD910-8488-4036-BE20-326931B75FB2}.Release|x64.ActiveCfg = Release|x64 - {212AD910-8488-4036-BE20-326931B75FB2}.Release|x64.Build.0 = Release|x64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|ARM64.Build.0 = Debug|ARM64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x64.ActiveCfg = Debug|x64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x64.Build.0 = Debug|x64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|ARM64.ActiveCfg = Release|ARM64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|ARM64.Build.0 = Release|ARM64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x64.ActiveCfg = Release|x64 - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x64.Build.0 = Release|x64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|ARM64.Build.0 = Debug|ARM64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x64.ActiveCfg = Debug|x64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x64.Build.0 = Debug|x64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|ARM64.ActiveCfg = Release|ARM64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|ARM64.Build.0 = Release|ARM64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x64.ActiveCfg = Release|x64 - {92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x64.Build.0 = Release|x64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.Build.0 = Debug|ARM64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.ActiveCfg = Debug|x64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.Build.0 = Debug|x64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.ActiveCfg = Release|ARM64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.Build.0 = Release|ARM64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.ActiveCfg = Release|x64 - {515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.Build.0 = Release|x64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Debug|ARM64.Build.0 = Debug|ARM64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Debug|x64.ActiveCfg = Debug|x64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Debug|x64.Build.0 = Debug|x64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Release|ARM64.ActiveCfg = Release|ARM64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Release|ARM64.Build.0 = Release|ARM64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x64.ActiveCfg = Release|x64 - {C97D9A5D-206C-454E-997E-009E227D7F02}.Release|x64.Build.0 = Release|x64 - {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|ARM64.Build.0 = Debug|Any CPU - {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x64.ActiveCfg = Debug|Any CPU - {31D1C81D-765F-4446-AA62-E743F6325049}.Debug|x64.Build.0 = Debug|Any CPU - {31D1C81D-765F-4446-AA62-E743F6325049}.Release|ARM64.ActiveCfg = Release|Any CPU - {31D1C81D-765F-4446-AA62-E743F6325049}.Release|ARM64.Build.0 = Release|Any CPU - {31D1C81D-765F-4446-AA62-E743F6325049}.Release|x64.ActiveCfg = Release|Any CPU - {31D1C81D-765F-4446-AA62-E743F6325049}.Release|x64.Build.0 = Release|Any CPU - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|ARM64.Build.0 = Debug|ARM64 - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x64.ActiveCfg = Debug|x64 - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Debug|x64.Build.0 = Debug|x64 - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|ARM64.ActiveCfg = Release|ARM64 - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|ARM64.Build.0 = Release|ARM64 - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x64.ActiveCfg = Release|x64 - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}.Release|x64.Build.0 = Release|x64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|ARM64.Build.0 = Debug|ARM64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x64.ActiveCfg = Debug|x64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Debug|x64.Build.0 = Debug|x64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|ARM64.ActiveCfg = Release|ARM64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|ARM64.Build.0 = Release|ARM64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x64.ActiveCfg = Release|x64 - {B41B888C-7DB8-4747-B262-4062E05A230D}.Release|x64.Build.0 = Release|x64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Debug|ARM64.Build.0 = Debug|ARM64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Debug|x64.ActiveCfg = Debug|x64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Debug|x64.Build.0 = Debug|x64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Release|ARM64.ActiveCfg = Release|ARM64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Release|ARM64.Build.0 = Release|ARM64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Release|x64.ActiveCfg = Release|x64 - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE}.Release|x64.Build.0 = Release|x64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Debug|ARM64.Build.0 = Debug|ARM64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Debug|x64.ActiveCfg = Debug|x64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Debug|x64.Build.0 = Debug|x64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Release|ARM64.ActiveCfg = Release|ARM64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Release|ARM64.Build.0 = Release|ARM64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Release|x64.ActiveCfg = Release|x64 - {E69B044A-2F8A-45AA-AD0B-256C59421807}.Release|x64.Build.0 = Release|x64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Debug|ARM64.Build.0 = Debug|ARM64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Debug|x64.ActiveCfg = Debug|x64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Debug|x64.Build.0 = Debug|x64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Release|ARM64.ActiveCfg = Release|ARM64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Release|ARM64.Build.0 = Release|ARM64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Release|x64.ActiveCfg = Release|x64 - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A}.Release|x64.Build.0 = Release|x64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Debug|ARM64.Build.0 = Debug|ARM64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Debug|x64.ActiveCfg = Debug|x64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Debug|x64.Build.0 = Debug|x64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Release|ARM64.ActiveCfg = Release|ARM64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Release|ARM64.Build.0 = Release|ARM64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Release|x64.ActiveCfg = Release|x64 - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788}.Release|x64.Build.0 = Release|x64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Debug|ARM64.Build.0 = Debug|ARM64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Debug|x64.ActiveCfg = Debug|x64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Debug|x64.Build.0 = Debug|x64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Release|ARM64.ActiveCfg = Release|ARM64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Release|ARM64.Build.0 = Release|ARM64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Release|x64.ActiveCfg = Release|x64 - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97}.Release|x64.Build.0 = Release|x64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|ARM64.Build.0 = Debug|ARM64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|x64.ActiveCfg = Debug|x64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Debug|x64.Build.0 = Debug|x64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|ARM64.ActiveCfg = Release|ARM64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|ARM64.Build.0 = Release|ARM64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|x64.ActiveCfg = Release|x64 - {A1425B53-3D61-4679-8623-E64A0D3D0A48}.Release|x64.Build.0 = Release|x64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|ARM64.Build.0 = Debug|ARM64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x64.ActiveCfg = Debug|x64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x64.Build.0 = Debug|x64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Debug|x64.Deploy.0 = Debug|x64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|ARM64.ActiveCfg = Release|ARM64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|ARM64.Build.0 = Release|ARM64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|ARM64.Deploy.0 = Release|ARM64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x64.ActiveCfg = Release|x64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x64.Build.0 = Release|x64 - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03}.Release|x64.Deploy.0 = Release|x64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|ARM64.Build.0 = Debug|ARM64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|x64.ActiveCfg = Debug|x64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Debug|x64.Build.0 = Debug|x64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|ARM64.ActiveCfg = Release|ARM64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|ARM64.Build.0 = Release|ARM64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|x64.ActiveCfg = Release|x64 - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB}.Release|x64.Build.0 = Release|x64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|ARM64.Build.0 = Debug|ARM64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|x64.ActiveCfg = Debug|x64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Debug|x64.Build.0 = Debug|x64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|ARM64.ActiveCfg = Release|ARM64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|ARM64.Build.0 = Release|ARM64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.ActiveCfg = Release|x64 - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC}.Release|x64.Build.0 = Release|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|ARM64.Build.0 = Debug|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|x64.ActiveCfg = Debug|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Debug|x64.Build.0 = Debug|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|ARM64.ActiveCfg = Release|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|ARM64.Build.0 = Release|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|x64.ActiveCfg = Release|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5}.Release|x64.Build.0 = Release|x64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|ARM64.Build.0 = Debug|ARM64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|x64.ActiveCfg = Debug|x64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Debug|x64.Build.0 = Debug|x64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Release|ARM64.ActiveCfg = Release|ARM64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Release|ARM64.Build.0 = Release|ARM64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Release|x64.ActiveCfg = Release|x64 - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545}.Release|x64.Build.0 = Release|x64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Debug|ARM64.Build.0 = Debug|ARM64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Debug|x64.ActiveCfg = Debug|x64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Debug|x64.Build.0 = Debug|x64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Release|ARM64.ActiveCfg = Release|ARM64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Release|ARM64.Build.0 = Release|ARM64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Release|x64.ActiveCfg = Release|x64 - {5A5DD09D-723A-44D3-8F2B-293584C3D731}.Release|x64.Build.0 = Release|x64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Debug|ARM64.Build.0 = Debug|ARM64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Debug|x64.ActiveCfg = Debug|x64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Debug|x64.Build.0 = Debug|x64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Release|ARM64.ActiveCfg = Release|ARM64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Release|ARM64.Build.0 = Release|ARM64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Release|x64.ActiveCfg = Release|x64 - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9}.Release|x64.Build.0 = Release|x64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Debug|ARM64.Build.0 = Debug|ARM64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Debug|x64.ActiveCfg = Debug|x64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Debug|x64.Build.0 = Debug|x64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Release|ARM64.ActiveCfg = Release|ARM64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Release|ARM64.Build.0 = Release|ARM64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Release|x64.ActiveCfg = Release|x64 - {54F7C616-FD41-4E62-BFF9-015686914F4D}.Release|x64.Build.0 = Release|x64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Debug|ARM64.Build.0 = Debug|ARM64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Debug|x64.ActiveCfg = Debug|x64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Debug|x64.Build.0 = Debug|x64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Release|ARM64.ActiveCfg = Release|ARM64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Release|ARM64.Build.0 = Release|ARM64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Release|x64.ActiveCfg = Release|x64 - {143F13E3-D2E3-4D83-B035-356612D99956}.Release|x64.Build.0 = Release|x64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Debug|ARM64.Build.0 = Debug|ARM64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Debug|x64.ActiveCfg = Debug|x64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Debug|x64.Build.0 = Debug|x64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Release|ARM64.ActiveCfg = Release|ARM64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Release|ARM64.Build.0 = Release|ARM64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Release|x64.ActiveCfg = Release|x64 - {56CC2F10-6E41-453D-BE16-C593A5E58482}.Release|x64.Build.0 = Release|x64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Debug|x64.ActiveCfg = Debug|x64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Debug|x64.Build.0 = Debug|x64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Release|ARM64.ActiveCfg = Release|ARM64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Release|ARM64.Build.0 = Release|ARM64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Release|x64.ActiveCfg = Release|x64 - {CA5518ED-0458-4B09-8F53-4122B9888655}.Release|x64.Build.0 = Release|x64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Debug|ARM64.Build.0 = Debug|ARM64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Debug|x64.ActiveCfg = Debug|x64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Debug|x64.Build.0 = Debug|x64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Release|ARM64.ActiveCfg = Release|ARM64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Release|ARM64.Build.0 = Release|ARM64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Release|x64.ActiveCfg = Release|x64 - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D}.Release|x64.Build.0 = Release|x64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Debug|ARM64.Build.0 = Debug|ARM64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Debug|x64.ActiveCfg = Debug|x64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Debug|x64.Build.0 = Debug|x64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|ARM64.ActiveCfg = Release|ARM64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|ARM64.Build.0 = Release|ARM64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x64.ActiveCfg = Release|x64 - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA}.Release|x64.Build.0 = Release|x64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Debug|ARM64.Build.0 = Debug|ARM64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Debug|x64.ActiveCfg = Debug|x64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Debug|x64.Build.0 = Debug|x64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Release|ARM64.ActiveCfg = Release|ARM64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Release|ARM64.Build.0 = Release|ARM64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Release|x64.ActiveCfg = Release|x64 - {2833C9C6-AB32-4048-A5C7-A70898337B57}.Release|x64.Build.0 = Release|x64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Debug|ARM64.Build.0 = Debug|ARM64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Debug|x64.ActiveCfg = Debug|x64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Debug|x64.Build.0 = Debug|x64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Release|ARM64.ActiveCfg = Release|ARM64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Release|ARM64.Build.0 = Release|ARM64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Release|x64.ActiveCfg = Release|x64 - {50B82783-242F-42D2-BC03-B3430BF01354}.Release|x64.Build.0 = Release|x64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Debug|ARM64.Build.0 = Debug|ARM64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Debug|x64.ActiveCfg = Debug|x64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Debug|x64.Build.0 = Debug|x64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Release|ARM64.ActiveCfg = Release|ARM64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Release|ARM64.Build.0 = Release|ARM64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Release|x64.ActiveCfg = Release|x64 - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2}.Release|x64.Build.0 = Release|x64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Debug|ARM64.Build.0 = Debug|ARM64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Debug|x64.ActiveCfg = Debug|x64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Debug|x64.Build.0 = Debug|x64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Release|ARM64.ActiveCfg = Release|ARM64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Release|ARM64.Build.0 = Release|ARM64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Release|x64.ActiveCfg = Release|x64 - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F}.Release|x64.Build.0 = Release|x64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|ARM64.Build.0 = Debug|ARM64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|x64.ActiveCfg = Debug|x64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Debug|x64.Build.0 = Debug|x64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Release|ARM64.ActiveCfg = Release|ARM64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Release|ARM64.Build.0 = Release|ARM64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Release|x64.ActiveCfg = Release|x64 - {8A08D663-4995-40E3-B42C-3F910625F284}.Release|x64.Build.0 = Release|x64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Debug|ARM64.Build.0 = Debug|ARM64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Debug|x64.ActiveCfg = Debug|x64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Debug|x64.Build.0 = Debug|x64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Release|ARM64.ActiveCfg = Release|ARM64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Release|ARM64.Build.0 = Release|ARM64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Release|x64.ActiveCfg = Release|x64 - {923DF87C-CA99-4D1C-B1D2-959174E95BFA}.Release|x64.Build.0 = Release|x64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Debug|ARM64.Build.0 = Debug|ARM64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Debug|x64.ActiveCfg = Debug|x64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Debug|x64.Build.0 = Debug|x64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Release|ARM64.ActiveCfg = Release|ARM64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Release|ARM64.Build.0 = Release|ARM64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Release|x64.ActiveCfg = Release|x64 - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77}.Release|x64.Build.0 = Release|x64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|ARM64.Build.0 = Debug|ARM64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|x64.ActiveCfg = Debug|x64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Debug|x64.Build.0 = Debug|x64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|ARM64.ActiveCfg = Release|ARM64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|ARM64.Build.0 = Release|ARM64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|x64.ActiveCfg = Release|x64 - {D962A009-834F-4EEC-AABB-430DF8F98E39}.Release|x64.Build.0 = Release|x64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Debug|ARM64.Build.0 = Debug|ARM64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Debug|x64.ActiveCfg = Debug|x64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Debug|x64.Build.0 = Debug|x64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Release|ARM64.ActiveCfg = Release|ARM64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Release|ARM64.Build.0 = Release|ARM64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Release|x64.ActiveCfg = Release|x64 - {FC373B24-3293-453C-AAF5-CF2909DCEE6A}.Release|x64.Build.0 = Release|x64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|ARM64.Build.0 = Debug|ARM64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|x64.ActiveCfg = Debug|x64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Debug|x64.Build.0 = Debug|x64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|ARM64.ActiveCfg = Release|ARM64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|ARM64.Build.0 = Release|ARM64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x64.ActiveCfg = Release|x64 - {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x64.Build.0 = Release|x64 - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|ARM64.Build.0 = Debug|Any CPU - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|x64.ActiveCfg = Debug|Any CPU - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|x64.Build.0 = Debug|Any CPU - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|ARM64.ActiveCfg = Release|Any CPU - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|ARM64.Build.0 = Release|Any CPU - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|x64.ActiveCfg = Release|Any CPU - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|x64.Build.0 = Release|Any CPU - {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|ARM64.Build.0 = Debug|ARM64 - {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|x64.ActiveCfg = Debug|x64 - {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|x64.Build.0 = Debug|x64 - {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|ARM64.ActiveCfg = Release|ARM64 - {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|ARM64.Build.0 = Release|ARM64 - {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|x64.ActiveCfg = Release|x64 - {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|x64.Build.0 = Release|x64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Debug|ARM64.Build.0 = Debug|ARM64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Debug|x64.ActiveCfg = Debug|x64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Debug|x64.Build.0 = Debug|x64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Release|ARM64.ActiveCfg = Release|ARM64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Release|ARM64.Build.0 = Release|ARM64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Release|x64.ActiveCfg = Release|x64 - {9EBAA524-0EDA-470B-95D4-39383285CBB2}.Release|x64.Build.0 = Release|x64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Debug|ARM64.Build.0 = Debug|ARM64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Debug|x64.ActiveCfg = Debug|x64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Debug|x64.Build.0 = Debug|x64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Release|ARM64.ActiveCfg = Release|ARM64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Release|ARM64.Build.0 = Release|ARM64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Release|x64.ActiveCfg = Release|x64 - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D}.Release|x64.Build.0 = Release|x64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Debug|ARM64.Build.0 = Debug|ARM64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Debug|x64.ActiveCfg = Debug|x64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Debug|x64.Build.0 = Debug|x64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Release|ARM64.ActiveCfg = Release|ARM64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Release|ARM64.Build.0 = Release|ARM64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Release|x64.ActiveCfg = Release|x64 - {D095BE44-1F2E-463E-A494-121892A75EA2}.Release|x64.Build.0 = Release|x64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Debug|ARM64.Build.0 = Debug|ARM64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Debug|x64.ActiveCfg = Debug|x64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Debug|x64.Build.0 = Debug|x64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|ARM64.ActiveCfg = Release|ARM64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|ARM64.Build.0 = Release|ARM64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x64.ActiveCfg = Release|x64 - {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x64.Build.0 = Release|x64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|ARM64.Build.0 = Debug|ARM64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x64.ActiveCfg = Debug|x64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x64.Build.0 = Debug|x64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|ARM64.ActiveCfg = Release|ARM64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|ARM64.Build.0 = Release|ARM64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x64.ActiveCfg = Release|x64 - {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x64.Build.0 = Release|x64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|ARM64.Build.0 = Debug|ARM64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x64.ActiveCfg = Debug|x64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x64.Build.0 = Debug|x64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|ARM64.ActiveCfg = Release|ARM64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|ARM64.Build.0 = Release|ARM64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x64.ActiveCfg = Release|x64 - {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x64.Build.0 = Release|x64 - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|ARM64.ActiveCfg = Debug|Any CPU - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|ARM64.Build.0 = Debug|Any CPU - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x64.ActiveCfg = Debug|Any CPU - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x64.Build.0 = Debug|Any CPU - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|ARM64.ActiveCfg = Release|Any CPU - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|ARM64.Build.0 = Release|Any CPU - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x64.ActiveCfg = Release|Any CPU - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x64.Build.0 = Release|Any CPU - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|ARM64.Build.0 = Debug|ARM64 - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x64.ActiveCfg = Debug|x64 - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x64.Build.0 = Debug|x64 - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|ARM64.ActiveCfg = Release|ARM64 - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|ARM64.Build.0 = Release|ARM64 - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x64.ActiveCfg = Release|x64 - {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x64.Build.0 = Release|x64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Debug|ARM64.Build.0 = Debug|ARM64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Debug|x64.ActiveCfg = Debug|x64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Debug|x64.Build.0 = Debug|x64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Release|ARM64.ActiveCfg = Release|ARM64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Release|ARM64.Build.0 = Release|ARM64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Release|x64.ActiveCfg = Release|x64 - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7}.Release|x64.Build.0 = Release|x64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Debug|ARM64.Build.0 = Debug|ARM64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Debug|x64.ActiveCfg = Debug|x64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Debug|x64.Build.0 = Debug|x64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Release|ARM64.ActiveCfg = Release|ARM64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Release|ARM64.Build.0 = Release|ARM64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Release|x64.ActiveCfg = Release|x64 - {D949EC7D-48A9-4279-95D5-078E7FD1F048}.Release|x64.Build.0 = Release|x64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Debug|ARM64.Build.0 = Debug|ARM64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Debug|x64.ActiveCfg = Debug|x64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Debug|x64.Build.0 = Debug|x64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Release|ARM64.ActiveCfg = Release|ARM64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Release|ARM64.Build.0 = Release|ARM64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Release|x64.ActiveCfg = Release|x64 - {3BAF9C81-A194-4925-A035-5E24A5D1E542}.Release|x64.Build.0 = Release|x64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Debug|ARM64.Build.0 = Debug|ARM64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Debug|x64.ActiveCfg = Debug|x64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Debug|x64.Build.0 = Debug|x64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Release|ARM64.ActiveCfg = Release|ARM64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Release|ARM64.Build.0 = Release|ARM64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Release|x64.ActiveCfg = Release|x64 - {6B04803D-B418-4833-A67E-B0FC966636A5}.Release|x64.Build.0 = Release|x64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Debug|ARM64.Build.0 = Debug|ARM64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Debug|x64.ActiveCfg = Debug|x64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Debug|x64.Build.0 = Debug|x64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Release|ARM64.ActiveCfg = Release|ARM64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Release|ARM64.Build.0 = Release|ARM64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Release|x64.ActiveCfg = Release|x64 - {3940AD4D-F748-4BE4-9083-85769CD553EF}.Release|x64.Build.0 = Release|x64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Debug|ARM64.Build.0 = Debug|ARM64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Debug|x64.ActiveCfg = Debug|x64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Debug|x64.Build.0 = Debug|x64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Release|ARM64.ActiveCfg = Release|ARM64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Release|ARM64.Build.0 = Release|ARM64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Release|x64.ActiveCfg = Release|x64 - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}.Release|x64.Build.0 = Release|x64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|ARM64.Build.0 = Debug|ARM64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|x64.ActiveCfg = Debug|x64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Debug|x64.Build.0 = Debug|x64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|ARM64.ActiveCfg = Release|ARM64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|ARM64.Build.0 = Release|ARM64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|x64.ActiveCfg = Release|x64 - {0014D652-901F-4456-8D65-06FC5F997FB0}.Release|x64.Build.0 = Release|x64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Debug|ARM64.Build.0 = Debug|ARM64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Debug|x64.ActiveCfg = Debug|x64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Debug|x64.Build.0 = Debug|x64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Release|ARM64.ActiveCfg = Release|ARM64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Release|ARM64.Build.0 = Release|ARM64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Release|x64.ActiveCfg = Release|x64 - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA}.Release|x64.Build.0 = Release|x64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Debug|ARM64.Build.0 = Debug|ARM64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Debug|x64.ActiveCfg = Debug|x64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Debug|x64.Build.0 = Debug|x64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Release|ARM64.ActiveCfg = Release|ARM64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Release|ARM64.Build.0 = Release|ARM64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Release|x64.ActiveCfg = Release|x64 - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F}.Release|x64.Build.0 = Release|x64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Debug|ARM64.Build.0 = Debug|ARM64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Debug|x64.ActiveCfg = Debug|x64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Debug|x64.Build.0 = Debug|x64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Debug|x64.Deploy.0 = Debug|x64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Release|ARM64.ActiveCfg = Release|ARM64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Release|ARM64.Build.0 = Release|ARM64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Release|ARM64.Deploy.0 = Release|ARM64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Release|x64.ActiveCfg = Release|x64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Release|x64.Build.0 = Release|x64 - {C32D254F-7597-4CBE-BF74-D922D81CDF29}.Release|x64.Deploy.0 = Release|x64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Debug|ARM64.Build.0 = Debug|ARM64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Debug|x64.ActiveCfg = Debug|x64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Debug|x64.Build.0 = Debug|x64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Release|ARM64.ActiveCfg = Release|ARM64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Release|ARM64.Build.0 = Release|ARM64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Release|x64.ActiveCfg = Release|x64 - {02DD46D3-F761-47D9-8894-2D6DA0124650}.Release|x64.Build.0 = Release|x64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Debug|ARM64.Build.0 = Debug|ARM64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Debug|x64.ActiveCfg = Debug|x64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Debug|x64.Build.0 = Debug|x64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|ARM64.ActiveCfg = Release|ARM64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|ARM64.Build.0 = Release|ARM64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.ActiveCfg = Release|x64 - {8E23E173-7127-4A5F-9F93-3049F2B68047}.Release|x64.Build.0 = Release|x64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|ARM64.Build.0 = Debug|ARM64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|x64.ActiveCfg = Debug|x64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Debug|x64.Build.0 = Debug|x64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Release|ARM64.ActiveCfg = Release|ARM64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Release|ARM64.Build.0 = Release|ARM64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Release|x64.ActiveCfg = Release|x64 - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8}.Release|x64.Build.0 = Release|x64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Debug|ARM64.Build.0 = Debug|ARM64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Debug|x64.ActiveCfg = Debug|x64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Debug|x64.Build.0 = Debug|x64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Release|ARM64.ActiveCfg = Release|ARM64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Release|ARM64.Build.0 = Release|ARM64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Release|x64.ActiveCfg = Release|x64 - {C0974915-8A1D-4BF0-977B-9587D3807AB7}.Release|x64.Build.0 = Release|x64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|ARM64.Build.0 = Debug|ARM64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|x64.ActiveCfg = Debug|x64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Debug|x64.Build.0 = Debug|x64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|ARM64.ActiveCfg = Release|ARM64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|ARM64.Build.0 = Release|ARM64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x64.ActiveCfg = Release|x64 - {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x64.Build.0 = Release|x64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|ARM64.Build.0 = Debug|ARM64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|x64.ActiveCfg = Debug|x64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|x64.Build.0 = Debug|x64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|ARM64.ActiveCfg = Release|ARM64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|ARM64.Build.0 = Release|ARM64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|x64.ActiveCfg = Release|x64 - {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|x64.Build.0 = Release|x64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|ARM64.Build.0 = Debug|ARM64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|x64.ActiveCfg = Debug|x64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|x64.Build.0 = Debug|x64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Release|ARM64.ActiveCfg = Release|ARM64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Release|ARM64.Build.0 = Release|ARM64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Release|x64.ActiveCfg = Release|x64 - {F055103B-F80B-4D0C-BF48-057C55620033}.Release|x64.Build.0 = Release|x64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|ARM64.Build.0 = Debug|ARM64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|x64.ActiveCfg = Debug|x64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Debug|x64.Build.0 = Debug|x64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|ARM64.ActiveCfg = Release|ARM64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|ARM64.Build.0 = Release|ARM64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|x64.ActiveCfg = Release|x64 - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332}.Release|x64.Build.0 = Release|x64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Debug|ARM64.Build.0 = Debug|ARM64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Debug|x64.ActiveCfg = Debug|x64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Debug|x64.Build.0 = Debug|x64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Release|ARM64.ActiveCfg = Release|ARM64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Release|ARM64.Build.0 = Release|ARM64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Release|x64.ActiveCfg = Release|x64 - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}.Release|x64.Build.0 = Release|x64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|ARM64.Build.0 = Debug|ARM64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|x64.ActiveCfg = Debug|x64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Debug|x64.Build.0 = Debug|x64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|ARM64.ActiveCfg = Release|ARM64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|ARM64.Build.0 = Release|ARM64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|x64.ActiveCfg = Release|x64 - {9C53CC25-0623-4569-95BC-B05410675EE3}.Release|x64.Build.0 = Release|x64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|ARM64.Build.0 = Debug|ARM64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|x64.ActiveCfg = Debug|x64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Debug|x64.Build.0 = Debug|x64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|ARM64.ActiveCfg = Release|ARM64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|ARM64.Build.0 = Release|ARM64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|x64.ActiveCfg = Release|x64 - {45285DF2-9742-4ECA-9AC9-58951FC26489}.Release|x64.Build.0 = Release|x64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|ARM64.Build.0 = Debug|ARM64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x64.ActiveCfg = Debug|x64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Debug|x64.Build.0 = Debug|x64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|ARM64.ActiveCfg = Release|ARM64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|ARM64.Build.0 = Release|ARM64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x64.ActiveCfg = Release|x64 - {3D63307B-9D27-44FD-B033-B26F39245B85}.Release|x64.Build.0 = Release|x64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|ARM64.Build.0 = Debug|ARM64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|x64.ActiveCfg = Debug|x64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Debug|x64.Build.0 = Debug|x64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|ARM64.ActiveCfg = Release|ARM64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|ARM64.Build.0 = Release|ARM64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|x64.ActiveCfg = Release|x64 - {367D7543-7DBA-4381-99F1-BF6142A996C4}.Release|x64.Build.0 = Release|x64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|ARM64.Build.0 = Debug|ARM64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x64.ActiveCfg = Debug|x64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Debug|x64.Build.0 = Debug|x64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|ARM64.ActiveCfg = Release|ARM64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|ARM64.Build.0 = Release|ARM64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.ActiveCfg = Release|x64 - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96}.Release|x64.Build.0 = Release|x64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|ARM64.Build.0 = Debug|ARM64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.ActiveCfg = Debug|x64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Debug|x64.Build.0 = Debug|x64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.ActiveCfg = Release|ARM64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|ARM64.Build.0 = Release|ARM64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.ActiveCfg = Release|x64 - {37D07516-4185-43A4-924F-3C7A5D95ECF6}.Release|x64.Build.0 = Release|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|ARM64.Build.0 = Debug|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|x64.ActiveCfg = Debug|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|x64.Build.0 = Debug|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|ARM64.ActiveCfg = Release|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|ARM64.Build.0 = Release|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|x64.ActiveCfg = Release|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|x64.Build.0 = Release|x64 - {66614C26-314C-4B91-9071-76133422CFEF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {66614C26-314C-4B91-9071-76133422CFEF}.Debug|ARM64.Build.0 = Debug|ARM64 - {66614C26-314C-4B91-9071-76133422CFEF}.Debug|x64.ActiveCfg = Debug|x64 - {66614C26-314C-4B91-9071-76133422CFEF}.Debug|x64.Build.0 = Debug|x64 - {66614C26-314C-4B91-9071-76133422CFEF}.Release|ARM64.ActiveCfg = Release|ARM64 - {66614C26-314C-4B91-9071-76133422CFEF}.Release|ARM64.Build.0 = Release|ARM64 - {66614C26-314C-4B91-9071-76133422CFEF}.Release|x64.ActiveCfg = Release|x64 - {66614C26-314C-4B91-9071-76133422CFEF}.Release|x64.Build.0 = Release|x64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|ARM64.Build.0 = Debug|ARM64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|x64.ActiveCfg = Debug|x64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Debug|x64.Build.0 = Debug|x64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|ARM64.ActiveCfg = Release|ARM64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|ARM64.Build.0 = Release|ARM64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|x64.ActiveCfg = Release|x64 - {6CE438DF-C245-4997-A360-0A0939E4BA34}.Release|x64.Build.0 = Release|x64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|ARM64.Build.0 = Debug|ARM64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|x64.ActiveCfg = Debug|x64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Debug|x64.Build.0 = Debug|x64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|ARM64.ActiveCfg = Release|ARM64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|ARM64.Build.0 = Release|ARM64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|x64.ActiveCfg = Release|x64 - {E09AA983-C755-474F-83D6-A5CDF528C070}.Release|x64.Build.0 = Release|x64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|ARM64.Build.0 = Debug|ARM64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|x64.ActiveCfg = Debug|x64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Debug|x64.Build.0 = Debug|x64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|ARM64.ActiveCfg = Release|ARM64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|ARM64.Build.0 = Release|ARM64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|x64.ActiveCfg = Release|x64 - {6D56B64D-FF1F-488F-AFED-9B9854A5D399}.Release|x64.Build.0 = Release|x64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|ARM64.Build.0 = Debug|ARM64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|x64.ActiveCfg = Debug|x64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Debug|x64.Build.0 = Debug|x64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|ARM64.ActiveCfg = Release|ARM64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|ARM64.Build.0 = Release|ARM64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|x64.ActiveCfg = Release|x64 - {92EC89E4-9972-453A-8A1A-3A9E230C146A}.Release|x64.Build.0 = Release|x64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|ARM64.Build.0 = Debug|ARM64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|x64.ActiveCfg = Debug|x64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Debug|x64.Build.0 = Debug|x64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|ARM64.ActiveCfg = Release|ARM64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|ARM64.Build.0 = Release|ARM64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|x64.ActiveCfg = Release|x64 - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95}.Release|x64.Build.0 = Release|x64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|ARM64.Build.0 = Debug|ARM64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|x64.ActiveCfg = Debug|x64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Debug|x64.Build.0 = Debug|x64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Release|ARM64.ActiveCfg = Release|ARM64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Release|ARM64.Build.0 = Release|ARM64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Release|x64.ActiveCfg = Release|x64 - {D1160404-D3D1-497A-883A-4059C07C2273}.Release|x64.Build.0 = Release|x64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|ARM64.Build.0 = Debug|ARM64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|x64.ActiveCfg = Debug|x64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Debug|x64.Build.0 = Debug|x64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|ARM64.ActiveCfg = Release|ARM64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|ARM64.Build.0 = Release|ARM64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|x64.ActiveCfg = Release|x64 - {40F6D69D-E321-400F-A767-5628C7AE453D}.Release|x64.Build.0 = Release|x64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|ARM64.Build.0 = Debug|ARM64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.ActiveCfg = Debug|x64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Debug|x64.Build.0 = Debug|x64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|ARM64.ActiveCfg = Release|ARM64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|ARM64.Build.0 = Release|ARM64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.ActiveCfg = Release|x64 - {305DD37E-C85D-4B08-AAFE-7381FA890463}.Release|x64.Build.0 = Release|x64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|x64.ActiveCfg = Debug|x64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Debug|x64.Build.0 = Debug|x64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|ARM64.ActiveCfg = Release|ARM64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|ARM64.Build.0 = Release|ARM64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|x64.ActiveCfg = Release|x64 - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24}.Release|x64.Build.0 = Release|x64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|ARM64.Build.0 = Debug|ARM64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|x64.ActiveCfg = Debug|x64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Debug|x64.Build.0 = Debug|x64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|ARM64.ActiveCfg = Release|ARM64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|ARM64.Build.0 = Release|ARM64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|x64.ActiveCfg = Release|x64 - {14E62033-58D0-4A7D-8990-52F50A08BBBD}.Release|x64.Build.0 = Release|x64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|ARM64.Build.0 = Debug|ARM64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|x64.ActiveCfg = Debug|x64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Debug|x64.Build.0 = Debug|x64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|ARM64.ActiveCfg = Release|ARM64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|ARM64.Build.0 = Release|ARM64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|x64.ActiveCfg = Release|x64 - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F}.Release|x64.Build.0 = Release|x64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|ARM64.Build.0 = Debug|ARM64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|x64.ActiveCfg = Debug|x64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|x64.Build.0 = Debug|x64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Debug|x64.Deploy.0 = Debug|x64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|ARM64.ActiveCfg = Release|ARM64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|ARM64.Build.0 = Release|ARM64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|ARM64.Deploy.0 = Release|ARM64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|x64.ActiveCfg = Release|x64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|x64.Build.0 = Release|x64 - {C846F7A7-792A-47D9-B0CB-417C900EE03D}.Release|x64.Deploy.0 = Release|x64 - {C831231F-891C-4572-9694-45062534B42A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C831231F-891C-4572-9694-45062534B42A}.Debug|ARM64.Build.0 = Debug|ARM64 - {C831231F-891C-4572-9694-45062534B42A}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {C831231F-891C-4572-9694-45062534B42A}.Debug|x64.ActiveCfg = Debug|x64 - {C831231F-891C-4572-9694-45062534B42A}.Debug|x64.Build.0 = Debug|x64 - {C831231F-891C-4572-9694-45062534B42A}.Debug|x64.Deploy.0 = Debug|x64 - {C831231F-891C-4572-9694-45062534B42A}.Release|ARM64.ActiveCfg = Release|ARM64 - {C831231F-891C-4572-9694-45062534B42A}.Release|ARM64.Build.0 = Release|ARM64 - {C831231F-891C-4572-9694-45062534B42A}.Release|ARM64.Deploy.0 = Release|ARM64 - {C831231F-891C-4572-9694-45062534B42A}.Release|x64.ActiveCfg = Release|x64 - {C831231F-891C-4572-9694-45062534B42A}.Release|x64.Build.0 = Release|x64 - {C831231F-891C-4572-9694-45062534B42A}.Release|x64.Deploy.0 = Release|x64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|ARM64.Build.0 = Debug|ARM64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|x64.ActiveCfg = Debug|x64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|x64.Build.0 = Debug|x64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Debug|x64.Deploy.0 = Debug|x64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|ARM64.ActiveCfg = Release|ARM64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|ARM64.Build.0 = Release|ARM64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|ARM64.Deploy.0 = Release|ARM64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|x64.ActiveCfg = Release|x64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|x64.Build.0 = Release|x64 - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90}.Release|x64.Deploy.0 = Release|x64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|ARM64.Build.0 = Debug|ARM64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|x64.ActiveCfg = Debug|x64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Debug|x64.Build.0 = Debug|x64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|ARM64.ActiveCfg = Release|ARM64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|ARM64.Build.0 = Release|ARM64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|x64.ActiveCfg = Release|x64 - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2}.Release|x64.Build.0 = Release|x64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|ARM64.Build.0 = Debug|ARM64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x64.ActiveCfg = Debug|x64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Debug|x64.Build.0 = Debug|x64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|ARM64.ActiveCfg = Release|ARM64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|ARM64.Build.0 = Release|ARM64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x64.ActiveCfg = Release|x64 - {79775343-7A3D-445D-9104-3DD5B2893DF9}.Release|x64.Build.0 = Release|x64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|ARM64.Build.0 = Debug|ARM64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|x64.ActiveCfg = Debug|x64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Debug|x64.Build.0 = Debug|x64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|ARM64.ActiveCfg = Release|ARM64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|ARM64.Build.0 = Release|ARM64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|x64.ActiveCfg = Release|x64 - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8}.Release|x64.Build.0 = Release|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|ARM64.Build.0 = Debug|ARM64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x64.ActiveCfg = Debug|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Debug|x64.Build.0 = Debug|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.ActiveCfg = Release|ARM64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|ARM64.Build.0 = Release|ARM64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.ActiveCfg = Release|x64 - {89D0E199-B17A-418C-B2F8-7375B6708357}.Release|x64.Build.0 = Release|x64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Debug|ARM64.Build.0 = Debug|ARM64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Debug|x64.ActiveCfg = Debug|x64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Debug|x64.Build.0 = Debug|x64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|ARM64.ActiveCfg = Release|ARM64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|ARM64.Build.0 = Release|ARM64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x64.ActiveCfg = Release|x64 - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E}.Release|x64.Build.0 = Release|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.Build.0 = Debug|ARM64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.ActiveCfg = Debug|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.Build.0 = Debug|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Debug|x64.Deploy.0 = Debug|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.ActiveCfg = Release|ARM64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.Build.0 = Release|ARM64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|ARM64.Deploy.0 = Release|ARM64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.ActiveCfg = Release|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.Build.0 = Release|x64 - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D}.Release|x64.Deploy.0 = Release|x64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|ARM64.Build.0 = Debug|ARM64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|x64.ActiveCfg = Debug|x64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Debug|x64.Build.0 = Debug|x64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|ARM64.ActiveCfg = Release|ARM64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|ARM64.Build.0 = Release|ARM64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x64.ActiveCfg = Release|x64 - {C0CE3B5E-16D3-495D-B335-CA791B660162}.Release|x64.Build.0 = Release|x64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|ARM64.Build.0 = Debug|ARM64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|x64.ActiveCfg = Debug|x64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Debug|x64.Build.0 = Debug|x64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|ARM64.ActiveCfg = Release|ARM64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|ARM64.Build.0 = Release|ARM64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|x64.ActiveCfg = Release|x64 - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C}.Release|x64.Build.0 = Release|x64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|ARM64.Build.0 = Debug|ARM64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|x64.ActiveCfg = Debug|x64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Debug|x64.Build.0 = Debug|x64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|ARM64.ActiveCfg = Release|ARM64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|ARM64.Build.0 = Release|ARM64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|x64.ActiveCfg = Release|x64 - {605E914B-7232-4789-AF46-BF5D3DDFC14E}.Release|x64.Build.0 = Release|x64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|ARM64.Build.0 = Debug|ARM64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x64.ActiveCfg = Debug|x64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x64.Build.0 = Debug|x64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Debug|x64.Deploy.0 = Debug|x64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|ARM64.ActiveCfg = Release|ARM64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|ARM64.Build.0 = Release|ARM64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|ARM64.Deploy.0 = Release|ARM64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x64.ActiveCfg = Release|x64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x64.Build.0 = Release|x64 - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9}.Release|x64.Deploy.0 = Release|x64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Debug|ARM64.Build.0 = Debug|ARM64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Debug|x64.ActiveCfg = Debug|x64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Debug|x64.Build.0 = Debug|x64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Release|ARM64.ActiveCfg = Release|ARM64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Release|ARM64.Build.0 = Release|ARM64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Release|x64.ActiveCfg = Release|x64 - {0A84F764-3A88-44CD-AA96-41BDBD48627B}.Release|x64.Build.0 = Release|x64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Debug|ARM64.Build.0 = Debug|ARM64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Debug|x64.ActiveCfg = Debug|x64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Debug|x64.Build.0 = Debug|x64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Release|ARM64.ActiveCfg = Release|ARM64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Release|ARM64.Build.0 = Release|ARM64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Release|x64.ActiveCfg = Release|x64 - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C}.Release|x64.Build.0 = Release|x64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Debug|ARM64.Build.0 = Debug|ARM64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Debug|x64.ActiveCfg = Debug|x64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Debug|x64.Build.0 = Debug|x64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|ARM64.ActiveCfg = Release|ARM64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|ARM64.Build.0 = Release|ARM64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.ActiveCfg = Release|x64 - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.Build.0 = Release|x64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|ARM64.Build.0 = Debug|ARM64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|x64.ActiveCfg = Debug|x64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|x64.Build.0 = Debug|x64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Debug|x64.Deploy.0 = Debug|x64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|ARM64.ActiveCfg = Release|ARM64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|ARM64.Build.0 = Release|ARM64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|ARM64.Deploy.0 = Release|ARM64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|x64.ActiveCfg = Release|x64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|x64.Build.0 = Release|x64 - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449}.Release|x64.Deploy.0 = Release|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.Build.0 = Debug|ARM64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.ActiveCfg = Debug|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.Build.0 = Debug|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.ActiveCfg = Release|ARM64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.Build.0 = Release|ARM64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.ActiveCfg = Release|x64 - {A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.Build.0 = Release|x64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.Build.0 = Debug|ARM64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|x64.ActiveCfg = Debug|x64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|x64.Build.0 = Debug|x64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Release|ARM64.ActiveCfg = Release|ARM64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Release|ARM64.Build.0 = Release|ARM64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Release|x64.ActiveCfg = Release|x64 - {08F9155D-B6DC-46E5-9C83-AF60B655898B}.Release|x64.Build.0 = Release|x64 - {4382A954-179A-4078-92AF-715187DFFF50}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4382A954-179A-4078-92AF-715187DFFF50}.Debug|ARM64.Build.0 = Debug|ARM64 - {4382A954-179A-4078-92AF-715187DFFF50}.Debug|x64.ActiveCfg = Debug|x64 - {4382A954-179A-4078-92AF-715187DFFF50}.Debug|x64.Build.0 = Debug|x64 - {4382A954-179A-4078-92AF-715187DFFF50}.Release|ARM64.ActiveCfg = Release|ARM64 - {4382A954-179A-4078-92AF-715187DFFF50}.Release|ARM64.Build.0 = Release|ARM64 - {4382A954-179A-4078-92AF-715187DFFF50}.Release|x64.ActiveCfg = Release|x64 - {4382A954-179A-4078-92AF-715187DFFF50}.Release|x64.Build.0 = Release|x64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Debug|ARM64.Build.0 = Debug|ARM64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Debug|x64.ActiveCfg = Debug|x64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Debug|x64.Build.0 = Debug|x64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|ARM64.ActiveCfg = Release|ARM64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|ARM64.Build.0 = Release|ARM64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|x64.ActiveCfg = Release|x64 - {EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|x64.Build.0 = Release|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|ARM64.Build.0 = Debug|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|x64.ActiveCfg = Debug|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|x64.Build.0 = Debug|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|ARM64.ActiveCfg = Release|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|ARM64.Build.0 = Release|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.ActiveCfg = Release|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.Build.0 = Release|x64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Debug|ARM64.Build.0 = Debug|ARM64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Debug|x64.ActiveCfg = Debug|x64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Debug|x64.Build.0 = Debug|x64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Release|ARM64.ActiveCfg = Release|ARM64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Release|ARM64.Build.0 = Release|ARM64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Release|x64.ActiveCfg = Release|x64 - {5702B3CC-8575-48D5-83D8-15BB42269CD3}.Release|x64.Build.0 = Release|x64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|ARM64.Build.0 = Debug|ARM64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.ActiveCfg = Debug|x64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Debug|x64.Build.0 = Debug|x64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.ActiveCfg = Release|ARM64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|ARM64.Build.0 = Release|ARM64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.ActiveCfg = Release|x64 - {64B88F02-CD88-4ED8-9624-989A800230F9}.Release|x64.Build.0 = Release|x64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|ARM64.Build.0 = Debug|ARM64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.ActiveCfg = Debug|x64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Debug|x64.Build.0 = Debug|x64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.ActiveCfg = Release|ARM64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.Build.0 = Release|ARM64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.ActiveCfg = Release|x64 - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.Build.0 = Release|x64 - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.ActiveCfg = Debug|x64 - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.Build.0 = Debug|x64 - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|ARM64.ActiveCfg = Release|ARM64 - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.ActiveCfg = Release|x64 - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.Build.0 = Release|x64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Debug|ARM64.Build.0 = Debug|ARM64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Debug|x64.ActiveCfg = Debug|x64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Debug|x64.Build.0 = Debug|x64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Release|ARM64.ActiveCfg = Release|ARM64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Release|ARM64.Build.0 = Release|ARM64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Release|x64.ActiveCfg = Release|x64 - {9E0CBC06-F29A-4810-B93C-97D53863B95E}.Release|x64.Build.0 = Release|x64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Debug|ARM64.Build.0 = Debug|ARM64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Debug|x64.ActiveCfg = Debug|x64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Debug|x64.Build.0 = Debug|x64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Release|ARM64.ActiveCfg = Release|ARM64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Release|ARM64.Build.0 = Release|ARM64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Release|x64.ActiveCfg = Release|x64 - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C}.Release|x64.Build.0 = Release|x64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Debug|ARM64.Build.0 = Debug|ARM64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Debug|x64.ActiveCfg = Debug|x64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Debug|x64.Build.0 = Debug|x64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Release|ARM64.ActiveCfg = Release|ARM64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Release|ARM64.Build.0 = Release|ARM64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Release|x64.ActiveCfg = Release|x64 - {47B0678C-806B-4FE1-9F50-46BA88989532}.Release|x64.Build.0 = Release|x64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Debug|ARM64.Build.0 = Debug|ARM64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Debug|x64.ActiveCfg = Debug|x64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Debug|x64.Build.0 = Debug|x64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Release|ARM64.ActiveCfg = Release|ARM64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Release|ARM64.Build.0 = Release|ARM64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Release|x64.ActiveCfg = Release|x64 - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA}.Release|x64.Build.0 = Release|x64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Debug|ARM64.Build.0 = Debug|ARM64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Debug|x64.ActiveCfg = Debug|x64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Debug|x64.Build.0 = Debug|x64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Release|ARM64.ActiveCfg = Release|ARM64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Release|ARM64.Build.0 = Release|ARM64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Release|x64.ActiveCfg = Release|x64 - {99CA1509-FB73-456E-AFAF-AB89C017BD72}.Release|x64.Build.0 = Release|x64 - {61CBF221-9452-4934-B685-146285E080D7}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {61CBF221-9452-4934-B685-146285E080D7}.Debug|ARM64.Build.0 = Debug|ARM64 - {61CBF221-9452-4934-B685-146285E080D7}.Debug|x64.ActiveCfg = Debug|x64 - {61CBF221-9452-4934-B685-146285E080D7}.Debug|x64.Build.0 = Debug|x64 - {61CBF221-9452-4934-B685-146285E080D7}.Release|ARM64.ActiveCfg = Release|ARM64 - {61CBF221-9452-4934-B685-146285E080D7}.Release|ARM64.Build.0 = Release|ARM64 - {61CBF221-9452-4934-B685-146285E080D7}.Release|x64.ActiveCfg = Release|x64 - {61CBF221-9452-4934-B685-146285E080D7}.Release|x64.Build.0 = Release|x64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|ARM64.Build.0 = Debug|ARM64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.ActiveCfg = Debug|x64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Debug|x64.Build.0 = Debug|x64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.ActiveCfg = Release|ARM64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|ARM64.Build.0 = Release|ARM64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.ActiveCfg = Release|x64 - {38F187B2-6638-5A40-072F-DBE5E54070A0}.Release|x64.Build.0 = Release|x64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|ARM64.Build.0 = Debug|ARM64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.ActiveCfg = Debug|x64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Debug|x64.Build.0 = Debug|x64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.ActiveCfg = Release|ARM64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|ARM64.Build.0 = Release|ARM64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.ActiveCfg = Release|x64 - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE}.Release|x64.Build.0 = Release|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|ARM64.Build.0 = Debug|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.ActiveCfg = Debug|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Debug|x64.Build.0 = Debug|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.ActiveCfg = Release|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|ARM64.Build.0 = Release|ARM64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.ActiveCfg = Release|x64 - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}.Release|x64.Build.0 = Release|x64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|ARM64.Build.0 = Debug|ARM64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.ActiveCfg = Debug|x64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Debug|x64.Build.0 = Debug|x64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.ActiveCfg = Release|ARM64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|ARM64.Build.0 = Release|ARM64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.ActiveCfg = Release|x64 - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9}.Release|x64.Build.0 = Release|x64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|ARM64.Build.0 = Debug|ARM64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|x64.ActiveCfg = Debug|x64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Debug|x64.Build.0 = Debug|x64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Release|ARM64.ActiveCfg = Release|ARM64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Release|ARM64.Build.0 = Release|ARM64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Release|x64.ActiveCfg = Release|x64 - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6}.Release|x64.Build.0 = Release|x64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Debug|ARM64.Build.0 = Debug|ARM64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Debug|x64.ActiveCfg = Debug|x64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Debug|x64.Build.0 = Debug|x64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Release|ARM64.ActiveCfg = Release|ARM64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Release|ARM64.Build.0 = Release|ARM64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Release|x64.ActiveCfg = Release|x64 - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A}.Release|x64.Build.0 = Release|x64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Debug|ARM64.Build.0 = Debug|ARM64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Debug|x64.ActiveCfg = Debug|x64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Debug|x64.Build.0 = Debug|x64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Release|ARM64.ActiveCfg = Release|ARM64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Release|ARM64.Build.0 = Release|ARM64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Release|x64.ActiveCfg = Release|x64 - {9D3F3793-EFE3-4525-8782-238015DABA62}.Release|x64.Build.0 = Release|x64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Debug|ARM64.Build.0 = Debug|ARM64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Debug|x64.ActiveCfg = Debug|x64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Debug|x64.Build.0 = Debug|x64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Release|ARM64.ActiveCfg = Release|ARM64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Release|ARM64.Build.0 = Release|ARM64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Release|x64.ActiveCfg = Release|x64 - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027}.Release|x64.Build.0 = Release|x64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Debug|ARM64.Build.0 = Debug|ARM64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Debug|x64.ActiveCfg = Debug|x64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Debug|x64.Build.0 = Debug|x64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Release|ARM64.ActiveCfg = Release|ARM64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Release|ARM64.Build.0 = Release|ARM64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Release|x64.ActiveCfg = Release|x64 - {070AC093-C9F2-20AD-0BCD-F318FC2761EA}.Release|x64.Build.0 = Release|x64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Debug|ARM64.Build.0 = Debug|ARM64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Debug|x64.ActiveCfg = Debug|x64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Debug|x64.Build.0 = Debug|x64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Release|ARM64.ActiveCfg = Release|ARM64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Release|ARM64.Build.0 = Release|ARM64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Release|x64.ActiveCfg = Release|x64 - {4122388B-59E4-CDB0-0338-EA6881DF86F0}.Release|x64.Build.0 = Release|x64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Debug|ARM64.Build.0 = Debug|ARM64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Debug|x64.ActiveCfg = Debug|x64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Debug|x64.Build.0 = Debug|x64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Release|ARM64.ActiveCfg = Release|ARM64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Release|ARM64.Build.0 = Release|ARM64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Release|x64.ActiveCfg = Release|x64 - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55}.Release|x64.Build.0 = Release|x64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Debug|ARM64.Build.0 = Debug|ARM64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Debug|x64.ActiveCfg = Debug|x64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Debug|x64.Build.0 = Debug|x64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Release|ARM64.ActiveCfg = Release|ARM64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Release|ARM64.Build.0 = Release|ARM64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Release|x64.ActiveCfg = Release|x64 - {6748A29D-DA6A-033A-825B-752295FF6AA0}.Release|x64.Build.0 = Release|x64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Debug|ARM64.Build.0 = Debug|ARM64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Debug|x64.ActiveCfg = Debug|x64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Debug|x64.Build.0 = Debug|x64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Release|ARM64.ActiveCfg = Release|ARM64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Release|ARM64.Build.0 = Release|ARM64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Release|x64.ActiveCfg = Release|x64 - {6EABCF9A-6526-441F-932F-658B1DC3E403}.Release|x64.Build.0 = Release|x64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Debug|ARM64.Build.0 = Debug|ARM64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Debug|x64.ActiveCfg = Debug|x64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Debug|x64.Build.0 = Debug|x64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Release|ARM64.ActiveCfg = Release|ARM64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Release|ARM64.Build.0 = Release|ARM64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Release|x64.ActiveCfg = Release|x64 - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15}.Release|x64.Build.0 = Release|x64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Debug|ARM64.Build.0 = Debug|ARM64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Debug|x64.ActiveCfg = Debug|x64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Debug|x64.Build.0 = Debug|x64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Release|ARM64.ActiveCfg = Release|ARM64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Release|ARM64.Build.0 = Release|ARM64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Release|x64.ActiveCfg = Release|x64 - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83}.Release|x64.Build.0 = Release|x64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Debug|ARM64.Build.0 = Debug|ARM64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Debug|x64.ActiveCfg = Debug|x64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Debug|x64.Build.0 = Debug|x64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Release|ARM64.ActiveCfg = Release|ARM64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Release|ARM64.Build.0 = Release|ARM64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Release|x64.ActiveCfg = Release|x64 - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9}.Release|x64.Build.0 = Release|x64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Debug|ARM64.Build.0 = Debug|ARM64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Debug|x64.ActiveCfg = Debug|x64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Debug|x64.Build.0 = Debug|x64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Release|ARM64.ActiveCfg = Release|ARM64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Release|ARM64.Build.0 = Release|ARM64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Release|x64.ActiveCfg = Release|x64 - {F93C2817-C846-4259-84D8-B39A6B57C8DE}.Release|x64.Build.0 = Release|x64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Debug|x64.Build.0 = Debug|x64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Release|x64.ActiveCfg = Release|x64 - {E816D7AC-4688-4ECB-97CC-3D8E798F3825}.Release|x64.Build.0 = Release|x64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Debug|x64.Build.0 = Debug|x64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|x64.ActiveCfg = Release|x64 - {E816D7AD-4688-4ECB-97CC-3D8E798F3826}.Release|x64.Build.0 = Release|x64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Debug|x64.Build.0 = Debug|x64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|x64.ActiveCfg = Release|x64 - {E816D7AE-4688-4ECB-97CC-3D8E798F3827}.Release|x64.Build.0 = Release|x64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Debug|x64.Build.0 = Debug|x64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|x64.ActiveCfg = Release|x64 - {E816D7AF-4688-4ECB-97CC-3D8E798F3828}.Release|x64.Build.0 = Release|x64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Debug|x64.Build.0 = Debug|x64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|x64.ActiveCfg = Release|x64 - {E816D7B0-4688-4ECB-97CC-3D8E798F3829}.Release|x64.Build.0 = Release|x64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|ARM64.Build.0 = Debug|ARM64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|x64.ActiveCfg = Debug|x64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Debug|x64.Build.0 = Debug|x64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.ActiveCfg = Release|ARM64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64 - {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|ARM64.Build.0 = Debug|ARM64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|x64.ActiveCfg = Debug|x64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Debug|x64.Build.0 = Debug|x64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|ARM64.ActiveCfg = Release|ARM64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|ARM64.Build.0 = Release|ARM64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|x64.ActiveCfg = Release|x64 - {38177D56-6AD1-4ADF-88C9-2843A7932166}.Release|x64.Build.0 = Release|x64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|ARM64.Build.0 = Debug|ARM64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|x64.ActiveCfg = Debug|x64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Debug|x64.Build.0 = Debug|x64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|ARM64.ActiveCfg = Release|ARM64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|ARM64.Build.0 = Release|ARM64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|x64.ActiveCfg = Release|x64 - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC}.Release|x64.Build.0 = Release|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|ARM64.Build.0 = Debug|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.ActiveCfg = Debug|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Debug|x64.Build.0 = Debug|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.ActiveCfg = Release|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|ARM64.Build.0 = Release|ARM64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.ActiveCfg = Release|x64 - {66C069F8-C548-4CA6-8CDE-239104D68E88}.Release|x64.Build.0 = Release|x64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|ARM64.Build.0 = Debug|ARM64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.ActiveCfg = Debug|x64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Debug|x64.Build.0 = Debug|x64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.ActiveCfg = Release|ARM64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|ARM64.Build.0 = Release|ARM64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.ActiveCfg = Release|x64 - {94CDC147-6137-45E9-AEDE-17FF809607C0}.Release|x64.Build.0 = Release|x64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|ARM64.Build.0 = Debug|ARM64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.ActiveCfg = Debug|x64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Debug|x64.Build.0 = Debug|x64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.ActiveCfg = Release|ARM64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|ARM64.Build.0 = Release|ARM64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.ActiveCfg = Release|x64 - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78}.Release|x64.Build.0 = Release|x64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64 - {E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64 - {E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.Build.0 = Debug|x64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.ActiveCfg = Release|x64 - {E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.Build.0 = Release|x64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.Build.0 = Debug|ARM64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.ActiveCfg = Debug|x64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.Build.0 = Debug|x64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.ActiveCfg = Release|ARM64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64 - {E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Build.0 = Debug|ARM64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|ARM64.Deploy.0 = Debug|ARM64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.ActiveCfg = Debug|x64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.Build.0 = Debug|x64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Debug|x64.Deploy.0 = Debug|x64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.ActiveCfg = Release|ARM64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.Build.0 = Release|ARM64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|ARM64.Deploy.0 = Release|ARM64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.ActiveCfg = Release|x64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Build.0 = Release|x64 - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F}.Release|x64.Deploy.0 = Release|x64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|ARM64.Build.0 = Debug|ARM64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.ActiveCfg = Debug|x64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Debug|x64.Build.0 = Debug|x64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.ActiveCfg = Release|ARM64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64 - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|ARM64.Build.0 = Debug|ARM64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.ActiveCfg = Debug|x64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Debug|x64.Build.0 = Debug|x64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.ActiveCfg = Release|ARM64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|ARM64.Build.0 = Release|ARM64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.ActiveCfg = Release|x64 - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {3BB8493E-D18E-4485-A320-CB40F90F55AE} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045} - {1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {B25AC7A5-FB9F-4789-B392-D5C85E948670} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} - {51920F1F-C28C-4ADF-8660-4238766796C2} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} - {A3935CF4-46C5-4A88-84D3-6B12E16E6BA2} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} - {2151F984-E006-4A9F-92EF-C6DDE3DC8413} = {66E1534A-1587-42B2-912F-45C994D32904} - {89F34AF7-1C34-4A72-AA6E-534BCF972BD9} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {6C7F47CC-2151-44A3-A546-41C70025132C} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {2BE46397-4DFA-414C-9BD4-41E4BBF8CB34} = {6C7F47CC-2151-44A3-A546-41C70025132C} - {0B43679E-EDFA-4DA0-AD30-F4628B308B1B} = {6C7F47CC-2151-44A3-A546-41C70025132C} - {E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8} = {0E556541-6A45-42CB-AE49-EE5A9BE05E7C} - {17DA04DF-E393-4397-9CF0-84DABE11032E} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {38BDB927-829B-4C65-9CD9-93FB05D66D65} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {8AFFA899-0B73-49EC-8C50-0FADDA57B2FC} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {4FD29318-A8AB-4D8F-AA47-60BC241B8DA3} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} - {8451ECDD-2EA4-4966-BB0A-7BBC40138E80} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} - {FF742965-9A80-41A5-B042-D6C7D3A21708} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {4AFC9975-2456-4C70-94A4-84073C1CED93} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} - {59BD9891-3837-438A-958D-ADC7F91F6F7E} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {4D971245-7A70-41D5-BAA0-DDB5684CAF51} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {74F1B9ED-F59C-4FE7-B473-7B453E30837E} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {C21BFF9C-2C99-4B5F-B7C9-A5E6DDDB37B0} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {F8B870EB-D5F5-45BA-9CF7-A5C459818820} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {E364F67B-BB12-4E91-B639-355866EBCD8B} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} - {F97E5003-F263-4D4A-A964-0F1F3C82DEF2} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} - {2F305555-C296-497E-AC20-5FA1B237996A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {AF2349B8-E5B6-4004-9502-687C1C7730B1} = {2F305555-C296-497E-AC20-5FA1B237996A} - {6A71162E-FC4C-4A2C-B90F-3CF94F59A9BB} = {2F305555-C296-497E-AC20-5FA1B237996A} - {A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {DA425894-6E13-404F-8DCB-78584EC0557A} = {2F305555-C296-497E-AC20-5FA1B237996A} - {060D75DA-2D1C-48E6-A4A1-6F0718B64661} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {748417CA-F17E-487F-9411-CAFB6D3F4877} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {217DF501-135C-4E38-BFC8-99D4821032EA} = {2F305555-C296-497E-AC20-5FA1B237996A} - {B1BCC8C6-46B5-4BFA-8F22-20F32D99EC6A} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} - {787B8AA6-CA93-4C84-96FE-DF31110AD1C4} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {08C8C05F-0362-41BC-818C-724572DF8B06} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} - {5D00D290-4016-4CFE-9E41-1E7C724509BA} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {4AED67B6-55FD-486F-B917-E543DEE2CB3C} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {42851751-CBC8-45A6-97F5-7A0753F7B4D1} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {8FFE09DA-FA4F-4EE1-B3A2-AD5497FBD1AD} = {2F305555-C296-497E-AC20-5FA1B237996A} - {655C9AF2-18D3-4DA6-80E4-85504A7722BA} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0} - {BA58206B-1493-4C75-BFEA-A85768A1E156} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0} - {1D78B84B-CA39-406C-98F4-71F7EC266CC0} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {03276A39-D4E9-417C-8FFD-200B0EE5E871} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {B81FB7B6-D30E-428F-908A-41422EFC1172} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {0F85E674-34AE-443D-954C-8321EB8B93B1} = {E885E71F-0B34-4A03-B13B-20F4E05E90BB} - {632BBE62-5421-49EA-835A-7FFA4F499BD6} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F} = {E4E03FE0-94FD-47C7-88C5-F17D0AA549D3} - {FD8EB419-FF9C-4D88-BB6F-BF6CED37747B} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {DA5A6FE9-0040-40CC-83CC-764AE5306590} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {0351ADA4-0C32-4652-9BA0-41F7B602372B} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD} = {E4E03FE0-94FD-47C7-88C5-F17D0AA549D3} - {6955446D-23F7-4023-9BB3-8657F904AF99} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {58736667-1027-4AD7-BFDF-7A3A6474103A} = {5A7818A8-109C-4E1C-850D-1A654E234B0E} - {D92131D6-7610-4D60-A7DB-1C169783F83B} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {1D5BE09D-78C0-4FD7-AF00-AE7C1AF7C525} = {D92131D6-7610-4D60-A7DB-1C169783F83B} - {031AC72E-FA28-4AB7-B690-6F7B9C28AA73} = {D92131D6-7610-4D60-A7DB-1C169783F83B} - {0B593A6C-4143-4337-860E-DB5710FB87DB} = {D92131D6-7610-4D60-A7DB-1C169783F83B} - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {5A7818A8-109C-4E1C-850D-1A654E234B0E} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {E4E03FE0-94FD-47C7-88C5-F17D0AA549D3} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {7319089E-46D6-4400-BC65-E39BDF1416EE} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {CABA8DFB-823B-4BF2-93AC-3F31984150D9} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {98537082-0FDB-40DE-ABD8-0DC5A4269BAB} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {B39DC643-4663-475E-B329-03F0C9918D48} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {8F62026A-294B-41C6-8839-87463613F216} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {C3A17DCA-217B-462C-BB0C-BE086AF80081} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {69E1EE8D-143A-4060-9129-4658ACF14AAF} = {2F305555-C296-497E-AC20-5FA1B237996A} - {ECC20689-002A-4354-95A6-B58DF089C6FF} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {4BABF3FE-3451-42FD-873F-3C332E18DCEF} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {0648DF05-5DDA-4BE1-B5F2-584926EBDB65} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {BA661F5B-1D5A-4FFC-9BF1-FC39DF280BDD} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {E496B7FC-1E99-4BAB-849B-0E8367040B02} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {7F4B3A60-BC27-45A7-8000-68B0B6EA7466} = {D9BD324E-1D80-44AA-8E7B-73EB00944434} - {8DF78B53-200E-451F-9328-01EB907193AE} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {23D2070D-E4AD-4ADD-85A7-083D9C76AD49} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {62173D9A-6724-4C00-A1C8-FB646480A9EC} = {D9BD324E-1D80-44AA-8E7B-73EB00944434} - {127F38E0-40AA-4594-B955-5616BF206882} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {5E7360A8-D048-4ED3-8F09-0BFD64C5529A} = {127F38E0-40AA-4594-B955-5616BF206882} - {D940E07F-532C-4FF3-883F-790DA014F19A} = {127F38E0-40AA-4594-B955-5616BF206882} - {BB23A474-5058-4F75-8FA3-5FE3DE53CDF4} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {3E424AD2-19E5-4AE6-B833-F53963EB5FC1} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {106CBECA-0701-4FC3-838C-9DF816A19AE2} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {2D604C07-51FC-46BB-9EB7-75AECC7F5E81} = {106CBECA-0701-4FC3-838C-9DF816A19AE2} - {2EDB3EB4-FA92-4BFF-B2D8-566584837231} = {106CBECA-0701-4FC3-838C-9DF816A19AE2} - {48804216-2A0E-4168-A6D8-9CD068D14227} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {FF1D7936-842A-4BBB-8BEA-E9FE796DE700} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {5043CECE-E6A7-4867-9CBE-02D27D83747A} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {11491FD8-F921-48BF-880C-7FEA185B80A1} = {2F305555-C296-497E-AC20-5FA1B237996A} - {F40C3397-1834-4530-B2D9-8F8B8456BCDF} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {4ED320BC-BA04-4D42-8D15-CBE62151F08B} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {322566EF-20DC-43A6-B9F8-616AF942579A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {E94FD11C-0591-456F-899F-EFC0CA548336} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {782A61BE-9D85-4081-B35C-1CCC9DCC1E88} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {809AA252-E17A-4FA2-B0A1-0450976B763F} = {2F305555-C296-497E-AC20-5FA1B237996A} - {133281D8-1BCE-4D07-B31E-796612A9609E} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {805306FF-A562-4415-8DEF-E493BDC45918} = {2F305555-C296-497E-AC20-5FA1B237996A} - {FCF3E52D-B80A-4FC3-98FD-6391354F0EE3} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {60CD2D4F-C3B9-4897-9821-FCA5098B41CE} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {1DC3BE92-CE89-43FB-8110-9C043A2FE7A2} = {60CD2D4F-C3B9-4897-9821-FCA5098B41CE} - {48A0A19E-A0BE-4256-ACF8-CC3B80291AF9} = {60CD2D4F-C3B9-4897-9821-FCA5098B41CE} - {9F94B303-5E21-4364-9362-64426F8DB932} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {EAE14C0E-7A6B-45DA-9080-A7D8C077BA6E} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {F7C8C0F1-5431-4347-89D0-8E5354F93CF2} = {2F305555-C296-497E-AC20-5FA1B237996A} - {F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {04B193D7-3E21-46B8-A958-89B63A8A69DE} = {2F305555-C296-497E-AC20-5FA1B237996A} - {5BDBD6C9-A31F-4CEB-871A-5E9E709197EF} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {FD464B4C-2F68-4D06-91E7-4208146C41F5} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {020A7474-3601-4160-A159-D7B70B77B15F} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} - {27718999-C175-450A-861C-89F911E16A88} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} - {1DBBB112-4BB1-444B-8EBB-E66555C76BA6} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} - {5A1DB2F0-0715-4B3B-98E6-79BC41540045} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {93B72A06-C8BD-484F-A6F7-C9F280B150BF} = {6C7F47CC-2151-44A3-A546-41C70025132C} - {18B3DB45-4FFE-4D01-97D6-5223FEEE1853} = {6C7F47CC-2151-44A3-A546-41C70025132C} - {0F14491C-6369-4C45-AAA8-135814E66E6B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {34A354C5-23C7-4343-916C-C52DAF4FC39D} = {0F14491C-6369-4C45-AAA8-135814E66E6B} - {3264DF53-C805-4B0C-867C-FCEAF7AEF762} = {0F14491C-6369-4C45-AAA8-135814E66E6B} - {31CAD28E-778A-441C-85BC-40AB3EAA2A10} = {0F14491C-6369-4C45-AAA8-135814E66E6B} - {A50C70A6-2DA0-4027-B90E-B1A40755A8A5} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {25C91A4E-BA4E-467A-85CD-8B62545BF674} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5} - {6AB6A2D6-F859-4A82-9184-0BD29C9F07D1} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5} - {B1234567-1234-1234-1234-123456789ABC} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5} - {212AD910-8488-4036-BE20-326931B75FB2} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {7AC943C9-52E8-44CF-9083-744D8049667B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {54A93AF7-60C7-4F6C-99D2-FBB1F75F853A} = {7AC943C9-52E8-44CF-9083-744D8049667B} - {92C39820-9F84-4529-BC7D-22AAE514D63B} = {7AC943C9-52E8-44CF-9083-744D8049667B} - {515554D1-D004-4F7F-A107-2211FC0F6B2C} = {7AC943C9-52E8-44CF-9083-744D8049667B} - {C97D9A5D-206C-454E-997E-009E227D7F02} = {0F14491C-6369-4C45-AAA8-135814E66E6B} - {31D1C81D-765F-4446-AA62-E743F6325049} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} - {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {E2D03E0F-7A75-4813-9F4B-D8763D43FD3A} = {1C48CD47-D610-463A-A53C-AF82DD6C47E7} - {B41B888C-7DB8-4747-B262-4062E05A230D} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} - {AB82E5DD-C32D-4F28-9746-2C780846188E} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {57175EC7-92A5-4C1E-8244-E3FBCA2A81DE} = {AB82E5DD-C32D-4F28-9746-2C780846188E} - {E69B044A-2F8A-45AA-AD0B-256C59421807} = {AB82E5DD-C32D-4F28-9746-2C780846188E} - {C604B37E-9D0E-4484-8778-E8B31B0E1B3A} = {AB82E5DD-C32D-4F28-9746-2C780846188E} - {E599C30B-9DC8-4E5A-BF27-93D4CCEDE788} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {00EE9BA6-4E8F-43CA-960D-D4882F0FBB97} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {17B4FA70-001E-4D33-BBBB-0D142DBC2E20} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {A1425B53-3D61-4679-8623-E64A0D3D0A48} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20} - {9D7A6DE0-7D27-424D-ABAE-41B2161F9A03} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20} - {17A99C7C-0BFF-45BB-A9FD-63A0DDC105BB} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20} - {AA9F0AF8-7924-4D59-BAA1-E36F1304E0DC} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20} - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A5} = {17B4FA70-001E-4D33-BBBB-0D142DBC2E20} - {ED9A1AC6-AEB0-4569-A6E9-E1696182B545} = {2F305555-C296-497E-AC20-5FA1B237996A} - {5A5DD09D-723A-44D3-8F2B-293584C3D731} = {2F305555-C296-497E-AC20-5FA1B237996A} - {B3E869C4-8210-4EBD-A621-FF4C4AFCBFA9} = {2F305555-C296-497E-AC20-5FA1B237996A} - {54F7C616-FD41-4E62-BFF9-015686914F4D} = {2F305555-C296-497E-AC20-5FA1B237996A} - {143F13E3-D2E3-4D83-B035-356612D99956} = {2F305555-C296-497E-AC20-5FA1B237996A} - {56CC2F10-6E41-453D-BE16-C593A5E58482} = {2F305555-C296-497E-AC20-5FA1B237996A} - {CA5518ED-0458-4B09-8F53-4122B9888655} = {2F305555-C296-497E-AC20-5FA1B237996A} - {D6DCC3AE-18C0-488A-B978-BAA9E3CFF09D} = {2F305555-C296-497E-AC20-5FA1B237996A} - {2BBC9E33-21EC-401C-84DA-BB6590A9B2AA} = {2F305555-C296-497E-AC20-5FA1B237996A} - {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {2833C9C6-AB32-4048-A5C7-A70898337B57} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} - {50B82783-242F-42D2-BC03-B3430BF01354} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} - {B5EB9FE9-37EF-47C3-B8B8-81AD3C2972C2} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} - {A663E672-B26D-4EC0-BEAB-FE2E424AC46F} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} - {8A08D663-4995-40E3-B42C-3F910625F284} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {923DF87C-CA99-4D1C-B1D2-959174E95BFA} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {D5E42C63-57C5-4EF6-AECE-1E2FCA725B77} = {2C318EC3-BA86-4372-B1BC-DB0F33C208B2} - {D962A009-834F-4EEC-AABB-430DF8F98E39} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {9873BA05-4C41-4819-9283-CF45D795431B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {FC373B24-3293-453C-AAF5-CF2909DCEE6A} = {9873BA05-4C41-4819-9283-CF45D795431B} - {9CE59ED5-7087-4353-88EB-788038A73CEC} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {FD86C06A-FB54-4D5E-9831-1CDADF60D45F} = {929C1324-22E8-4412-A9A8-80E85F3985A5} - {697C6AF9-0A48-49A9-866C-67DA12384015} = {929C1324-22E8-4412-A9A8-80E85F3985A5} - {929C1324-22E8-4412-A9A8-80E85F3985A5} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {9EBAA524-0EDA-470B-95D4-39383285CBB2} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {500DED3E-CFB5-4ED5-ACC6-02B3D6DC336D} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {D095BE44-1F2E-463E-A494-121892A75EA2} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {90F9FA90-2C20-4004-96E6-F3B78151F5A5} = {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} - {3B227528-4BA6-4CAF-B44A-A10C78A64849} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {F5E1146E-B7B3-4E11-85FD-270A500BD78C} = {3B227528-4BA6-4CAF-B44A-A10C78A64849} - {3157FA75-86CF-4EE2-8F62-C43F776493C6} = {3B227528-4BA6-4CAF-B44A-A10C78A64849} - {4C0D0746-BE5B-49EE-BD5D-A7811628AE8B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} - {B9420661-B0E4-4241-ABD4-4A27A1F64250} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} - {CCB5E44F-84D9-4203-83C6-1C9EC9302BC7} = {2F305555-C296-497E-AC20-5FA1B237996A} - {D949EC7D-48A9-4279-95D5-078E7FD1F048} = {2F305555-C296-497E-AC20-5FA1B237996A} - {3BAF9C81-A194-4925-A035-5E24A5D1E542} = {2F305555-C296-497E-AC20-5FA1B237996A} - {6B04803D-B418-4833-A67E-B0FC966636A5} = {2F305555-C296-497E-AC20-5FA1B237996A} - {3940AD4D-F748-4BE4-9083-85769CD553EF} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {0014D652-901F-4456-8D65-06FC5F997FB0} = {4C0D0746-BE5B-49EE-BD5D-A7811628AE8B} - {799A50D8-DE89-4ED1-8FF8-AD5A9ED8C0CA} = {AB82E5DD-C32D-4F28-9746-2C780846188E} - {9D52FD25-EF90-4F9A-A015-91EFC5DAF54F} = {AB82E5DD-C32D-4F28-9746-2C780846188E} - {C32D254F-7597-4CBE-BF74-D922D81CDF29} = {9873BA05-4C41-4819-9283-CF45D795431B} - {02DD46D3-F761-47D9-8894-2D6DA0124650} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} - {8E23E173-7127-4A5F-9F93-3049F2B68047} = {929C1324-22E8-4412-A9A8-80E85F3985A5} - {DFF88D16-D36F-40A4-A955-CDCAA76EF7B8} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} - {C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95} - {8ACB33D9-C95B-47D4-8363-9731EE0930A0} = {CA716AE6-FE5C-40AC-BB8F-2C87912687AC} - {CA716AE6-FE5C-40AC-BB8F-2C87912687AC} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {F055103B-F80B-4D0C-BF48-057C55620033} = {5A7818A8-109C-4E1C-850D-1A654E234B0E} - {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {BE126CBB-AE12-406A-9837-A05ACFCA57A7} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {14CB58B7-D280-4A7A-95DE-4B2DF14EA000} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {B31FCC55-B5A4-4EA7-B414-2DCEAE6AF332} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C} = {68328142-5B31-4715-BCBB-7B6345EE0971} - {9C53CC25-0623-4569-95BC-B05410675EE3} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {45285DF2-9742-4ECA-9AC9-58951FC26489} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {3D63307B-9D27-44FD-B033-B26F39245B85} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {367D7543-7DBA-4381-99F1-BF6142A996C4} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {2CAC093E-5FCF-4102-9C2C-AC7DD5D9EB96} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {37D07516-4185-43A4-924F-3C7A5D95ECF6} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {8F021B46-362B-485C-BFBA-CCF83E820CBD} = {8F62026A-294B-41C6-8839-87463613F216} - {66614C26-314C-4B91-9071-76133422CFEF} = {BFFB607F-7C78-434B-86B9-DA4C8196A1B5} - {3846508C-77EB-4034-A702-F8BB263C4F79} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {6CE438DF-C245-4997-A360-0A0939E4BA34} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {E09AA983-C755-474F-83D6-A5CDF528C070} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {6D56B64D-FF1F-488F-AFED-9B9854A5D399} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {92EC89E4-9972-453A-8A1A-3A9E230C146A} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {51939B4F-1F62-4BFF-A6A2-C08646E5BE95} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {D1160404-D3D1-497A-883A-4059C07C2273} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {40F6D69D-E321-400F-A767-5628C7AE453D} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {305DD37E-C85D-4B08-AAFE-7381FA890463} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} - {CA4D810F-C8F4-4B61-9DA9-71807E0B9F24} = {F3D09629-59A2-4924-A4B9-D6BFAA2C1B49} - {14E62033-58D0-4A7D-8990-52F50A08BBBD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {6515F03F-E56D-4DB4-B23D-AC4FB80DB36F} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} - {071E18A4-A530-46B8-AB7D-B862EE55E24E} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {C846F7A7-792A-47D9-B0CB-417C900EE03D} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} - {C831231F-891C-4572-9694-45062534B42A} = {071E18A4-A530-46B8-AB7D-B862EE55E24E} - {7520A2FE-00A2-49B8-83ED-DB216E874C04} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} - {C66020D1-CB10-4CF7-8715-84C97FD5E5E2} = {7520A2FE-00A2-49B8-83ED-DB216E874C04} - {79775343-7A3D-445D-9104-3DD5B2893DF9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {0ADEB797-C8C7-4FFA-ACD5-2AF6CAD7ECD8} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {89D0E199-B17A-418C-B2F8-7375B6708357} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {0DB0F63A-D2F8-4DA3-A650-2D0B8724218E} = {CA716AE6-FE5C-40AC-BB8F-2C87912687AC} - {453CBB73-A3CB-4D0B-8D24-6940B86FE21D} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {C0CE3B5E-16D3-495D-B335-CA791B660162} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {3A9A7297-92C4-4F16-B6F9-8D4AB652C86C} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {605E914B-7232-4789-AF46-BF5D3DDFC14E} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {E81A7D20-9862-ABDB-0AAE-9BC5B517A9F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {0A84F764-3A88-44CD-AA96-41BDBD48627B} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} - {E4585179-2AC1-4D5F-A3FF-CFC5392F694C} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} - {CA7D8106-30B9-4AEC-9D05-B69B31B8C461} = {DD6E12FE-5509-4ABC-ACC2-3D6DC98A238C} - {DCC6BD67-17BB-47AA-B507-FB0FE43A7449} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {A558C25D-2007-498E-8B6F-43405AFAE9E2} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {08F9155D-B6DC-46E5-9C83-AF60B655898B} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {4382A954-179A-4078-92AF-715187DFFF50} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {EBED240C-8702-452D-B764-6DB9DA9179AF} = {1C48CD47-D610-463A-A53C-AF82DD6C47E7} - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {1C48CD47-D610-463A-A53C-AF82DD6C47E7} - {5702B3CC-8575-48D5-83D8-15BB42269CD3} = {8131151D-B0E9-4E18-84A5-E5F946C4480A} - {64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2} - {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {66E1534A-1587-42B2-912F-45C994D32904} - {9E0CBC06-F29A-4810-B93C-97D53863B95E} = {2F305555-C296-497E-AC20-5FA1B237996A} - {F6088A11-1C9E-4420-AA90-CF7E78DD7F1C} = {2F305555-C296-497E-AC20-5FA1B237996A} - {47B0678C-806B-4FE1-9F50-46BA88989532} = {2F305555-C296-497E-AC20-5FA1B237996A} - {9BC1C986-1E97-4D07-A7B1-CE226C239EFA} = {2F305555-C296-497E-AC20-5FA1B237996A} - {99CA1509-FB73-456E-AFAF-AB89C017BD72} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {61CBF221-9452-4934-B685-146285E080D7} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} - {38F187B2-6638-5A40-072F-DBE5E54070A0} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {DA0744BC-E822-680E-9CEB-D0FBA903A8EE} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {2C318EC3-BA86-4372-B1BC-DB0F33C208B2} - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {68328142-5B31-4715-BCBB-7B6345EE0971} - {2CF78CF7-8FEB-4BE1-9591-55FA25B48FC6} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {14AFD976-B4D2-49D0-9E6C-AA93CC061B8A} = {1AFB6476-670D-4E80-A464-657E01DFF482} - {9D3F3793-EFE3-4525-8782-238015DABA62} = {66E1534A-1587-42B2-912F-45C994D32904} - {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {24133F7F-C1D1-DE04-EFA8-F5D5467FE027} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} - {0E556541-6A45-42CB-AE49-EE5A9BE05E7C} = {6C7F47CC-2151-44A3-A546-41C70025132C} - {27D9CB3A-46D1-402C-9273-F88CB8AC42F7} = {9873BA05-4C41-4819-9283-CF45D795431B} - {B9617A31-0F0A-4397-851D-BF2FBEE32D7F} = {C140A3EF-6DBF-4084-9D4C-4EB5A99FEE68} - {1C48CD47-D610-463A-A53C-AF82DD6C47E7} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} - {D9BD324E-1D80-44AA-8E7B-73EB00944434} = {38BDB927-829B-4C65-9CD9-93FB05D66D65} - {8EF25507-2575-4ADE-BF7E-D23376903AB8} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {070AC093-C9F2-20AD-0BCD-F318FC2761EA} = {B1234567-1234-1234-1234-123456789ABC} - {2C318EC3-BA86-4372-B1BC-DB0F33C208B2} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {BFFB607F-7C78-434B-86B9-DA4C8196A1B5} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} - {66E1534A-1587-42B2-912F-45C994D32904} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} - {E885E71F-0B34-4A03-B13B-20F4E05E90BB} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} - {264B412F-DB8B-4CF8-A74B-96998B183045} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {3527BF37-DFC5-4309-A032-29278CA21328} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0} - {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} = {2F305555-C296-497E-AC20-5FA1B237996A} - {68328142-5B31-4715-BCBB-7B6345EE0971} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} - {4122388B-59E4-CDB0-0338-EA6881DF86F0} = {27D9CB3A-46D1-402C-9273-F88CB8AC42F7} - {988C9FAF-5AEC-EB15-578D-FED0DF52BF55} = {27D9CB3A-46D1-402C-9273-F88CB8AC42F7} - {6748A29D-DA6A-033A-825B-752295FF6AA0} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {6EABCF9A-6526-441F-932F-658B1DC3E403} = {264B412F-DB8B-4CF8-A74B-96998B183045} - {69D76A76-6EF6-4846-94CD-EAAF0CAC9F15} = {264B412F-DB8B-4CF8-A74B-96998B183045} - {9BAFFC28-E1EF-4C14-A101-EEBFC0A50D83} = {264B412F-DB8B-4CF8-A74B-96998B183045} - {806BF185-8B89-5BE1-9AA1-DA5BC9487DB9} = {264B412F-DB8B-4CF8-A74B-96998B183045} - {F93C2817-C846-4259-84D8-B39A6B57C8DE} = {3527BF37-DFC5-4309-A032-29278CA21328} - {8131151D-B0E9-4E18-84A5-E5F946C4480A} = {929C1324-22E8-4412-A9A8-80E85F3985A5} - {E816D7AC-4688-4ECB-97CC-3D8E798F3825} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E816D7AD-4688-4ECB-97CC-3D8E798F3826} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E816D7AE-4688-4ECB-97CC-3D8E798F3827} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {5B201255-53C8-490B-A34F-01F05D48A477} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} - {38177D56-6AD1-4ADF-88C9-2843A7932166} = {5B201255-53C8-490B-A34F-01F05D48A477} - {08E71C67-6A7E-4CA1-B04E-2FB336410BAC} = {5B201255-53C8-490B-A34F-01F05D48A477} - {E11826E1-76DF-42AC-985C-164CC2EE57A1} = {7AC943C9-52E8-44CF-9083-744D8049667B} - {66C069F8-C548-4CA6-8CDE-239104D68E88} = {E11826E1-76DF-42AC-985C-164CC2EE57A1} - {9605B84E-FAC4-477B-B9EC-0753177EE6A8} = {557C4636-D7E1-4838-A504-7D19B725EE95} - {94CDC147-6137-45E9-AEDE-17FF809607C0} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8} - {A24BF1AF-79AA-4896-BAE3-CCBBE0380A78} = {9605B84E-FAC4-477B-B9EC-0753177EE6A8} - {E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477} - {F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94} - {4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - {A66E9270-5D93-EC9C-F06E-CE7295BB9A6C} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} - EndGlobalSection -EndGlobal diff --git a/PowerToys.slnx b/PowerToys.slnx new file mode 100644 index 0000000000..c946514fb5 --- /dev/null +++ b/PowerToys.slnx @@ -0,0 +1,1041 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 85fad26e1f..dd3abefe1c 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,23 @@

Microsoft PowerToys

- +

+ Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks. +

Installation - . + · Documentation - . + · Blog - . + · Release notes



-Microsoft PowerToys is a collection of utilities that help you customize Windows and streamline everyday tasks. -

+ +## 🔨 Utilities + +PowerToys includes over 25 utilities to help you customize and optimize your Windows experience: | | | | |---|---|---| @@ -37,41 +41,35 @@ Microsoft PowerToys is a collection of utilities that help you customize Windows ## 📋 Installation -For detailed installation instructions, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install). - -Before you begin, make sure your device meets the system requirements: - -> [!NOTE] -> - Windows 11 or Windows 10 version 2004 (20H1 / build 19041) or newer -> - 64-bit processor: x64 or ARM64 -> - Latest stable version of [Microsoft Edge WebView2 Runtime](https://go.microsoft.com/fwlink/p/?LinkId=2124703) is installed via the bootstrapper during setup - -Choose one of the installation methods below: - -
-Download .exe from GitHub +For detailed installation instructions and system requirements, visit the [installation docs](https://learn.microsoft.com/windows/powertoys/install). +But to get started quickly, choose one of the installation methods below: +

+
+Download .exe from GitHub +
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer. -[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22 -[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22 -[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-x64.exe -[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysUserSetup-0.95.0-arm64.exe -[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-x64.exe -[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.0/PowerToysSetup-0.95.0-arm64.exe +[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22 +[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22 +[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-x64.exe +[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-arm64.exe +[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-x64.exe +[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-arm64.exe | Description | Filename | |----------------|----------| -| Per user - x64 | [PowerToysUserSetup-0.95.0-x64.exe][ptUserX64] | -| Per user - ARM64 | [PowerToysUserSetup-0.95.0-arm64.exe][ptUserArm64] | -| Machine wide - x64 | [PowerToysSetup-0.95.0-x64.exe][ptMachineX64] | -| Machine wide - ARM64 | [PowerToysSetup-0.95.0-arm64.exe][ptMachineArm64] | +| Per user - x64 | [PowerToysUserSetup-0.96.1-x64.exe][ptUserX64] | +| Per user - ARM64 | [PowerToysUserSetup-0.96.1-arm64.exe][ptUserArm64] | +| Machine wide - x64 | [PowerToysSetup-0.96.1-x64.exe][ptMachineX64] | +| Machine wide - ARM64 | [PowerToysSetup-0.96.1-arm64.exe][ptMachineArm64] |
-Microsoft Store +Microsoft Store +
You can easily install PowerToys from the Microsoft Store:

@@ -82,10 +80,9 @@ You can easily install PowerToys from the Microsoft Store:

-
-WinGet - +WinGet +
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell: *User scope installer [default]* @@ -100,162 +97,137 @@ winget install --scope machine Microsoft.PowerToys -s winget
-Other methods - +Other methods +
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
## ✨ What's new -**Version 0.95 (October 2025)** +**Version 0.96 (November 2025)** For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog). **✨ Highlights** - - **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day. - - Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed. - - Peek can now be activated using just the Spacebar! - - Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility. - - Settings now lets you delete shortcuts entirely and ignore conflicts. - - Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)! - - PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)! - - ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)! + - Advanced Paste now supports multiple online and on-device AI model providers: Azure OpenAI, OpenAI, Google Gemini, Mistral, Foundry Local and Ollama. + - Command Palette received extensive improvements including file search filters, better clipboard history metadata, context-menu styling, and dozens of bug fixes and enhancements. + - PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`. + +### Advanced Paste + - Advanced Paste now lets you connect to multiple AI providers instead of being limited to a single OpenAI provider. See [Advanced Paste documentation](https://learn.microsoft.com/windows/powertoys/advanced-paste) for usage. + +### Awake + - The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)! + - Fixed Awake context menu positioning. The fix removed the conversion of the mouse cursor from screen to client-window coordinates, instead using the raw screen coordinates returned by GetCursorPos; the context menu now appears at the correct screen position. Thanks [@lzandman](https://github.com/lzandman)! ### Command Palette - - Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby) - - Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Enabled AOT by default for improved performance while simplifying publish configs. - - Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby) - - Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Ensured long links wrap correctly in details view. - - Removed animation and enforced minimum width on filter dropdown for clarity. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie) - - Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Fixed AOT template and theming issues for filter separators. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Introduced grid layouts (small, medium, gallery) for richer page presentation. - - Materialized result lists to avoid rescoring overhead. - - Disabled problematic selection TextToSuggest behind environment flag. - - Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions). - - Added context menu "Show Details" command when details pane is hidden. - - Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme) - - Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek) - - Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Hotkeys now always respect the “Ignore shortcut in fullscreen†setting. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Blocked Ctrl+I from inserting stray tabs in search box. - - Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek) - - Truncated overly long command labels with ellipsis to prevent overflow. - - Added a setting to configure the page transition animation. - - Collection of small improvements and nits for Run Commands. - - Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Added Ctrl+O shortcut in Clipboard History to open links directly. - - Resolved conflict with external software that blocked Command Palette from hiding. - - Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek) - - Improved the appearance of the search box in the context menu. - + - The search field in context menus now matches the look of the Command Palette, with a smoke backdrop and improved padding. + - Fallback items such as math calculations or the Run command now appear in results more quickly. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Ensured the command bar updates correctly after navigating to another page and commands are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - The Command Palette settings page has been reorganized. Activation-key options are grouped under an expander and extension settings are framed for improved readability. + - When you modify a command, its alias, hotkey, and tags now update in the top-level list, keeping the displayed information in sync. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Press `Ctrl + ,` to open Command Palette settings from anywhere. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - You can use `Page Up` and `Page Down` to navigate the list while focus is in the search box. Thanks [@samrueby](https://github.com/samrueby)! + - Fixed an issue where the search box could disappear when navigating pages. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Ensured search text is selected when *Go home when activated* and *Highlight search on activate* are both enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed an issue where Command Palette window occasionally appeared on the taskbar under certain Windows settings. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Ensured that labels and icons of list items and menu items update when they change. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed visibility of list filters when navigating to a content page. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)! + - Added search to the extension list and a link to extensions on the Microsoft Store. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Added options to open the Command Palette window at its last position or re-center it. + - The Command Palette now remembers its window size after restarting. + - Added a global error handler that logs fatal errors and provides feedback when unexpected failures force Command Palette to close. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed forms and extension settings not showing on some machines due to a missing VC++ runtime. + - Restored ranking of fallback commands for built-in extensions (Sleep, Shutdown, Windows settings, Web search, etc.). Thanks [@jiripolasek](https://github.com/jiripolasek). + - Improved and unified labels and texts across the application! + - Maintainance: Resolved numerous build warnings in Command Palette projects; no user-visible impact. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Maintainance: Fixed a logging issue so exception messages are properly recorded instead of placeholder text, improving troubleshooting. Thanks [@jiripolasek](https://github.com/jiripolasek)! ### Command Palette Extensions - - Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Enabled advanced markdown tables and emphasis extensions. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Added setting to choose Clipboard History primary action (Paste vs Copy). Thanks [@jiripolasek](https://github.com/jiripolasek) - - Added actionable empty-state hints for File Search (search PC / open indexing settings). Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Improved Run command line parsing for paths with spaces; sped up related tests. - - Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Added Terminal profile sort order setting including MRU tracking. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)! - - Deferred WinGet details loading and added timing logs. - - Removed LINQ from All Apps extension for performance. - - Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)! - - Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)! - -### Environment Variables - - Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI. - -### File Locksmith - - Adopted WinUI TitleBar to simplify window chrome while preserving appearance. + - Bookmarks: Added hints about bookmark placeholders to the Add/Edit Bookmark form. — Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Bookmarks: Improved migration of bookmarks from older versions and fixed an issue where aliases or keyboard shortcuts could be lost after restart. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Clipboard history: Items shown in Command Palette’s clipboard history now include helpful metadata. For example, image items show dimensions, text files show names and sizes, web links include page titles, and text entries display word counts. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - File search: Added filter buttons to show *all items*, *files only*, or *folders only*. Selecting a filter adds `kind:folders` or `kind:not folders` to narrow results. + - System commands: Replaced the `:red_circle:` placeholder with an actual red-circle emoji so the correct icon appears in the UI. Thanks [@samrueby](https://github.com/samrueby)! + - WinGet: Search performance feels more responsive because typed input is now processed via a task queue rather than complex cancellation tokens! + - Window Walker: UWP apps no longer show a "not responding" label when suspended. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Window Walker: Now displays the actual icon of each window rather than using the process icon, improving recognition of PWAs and Python GUIs. Thanks [@Lee-WonJun](https://github.com/Lee-WonJun)! +- Windows Terminal profiles: Fixed a rare crash in the Windows Terminal extension when the `LOCALAPPDATA` environment variable was missing. The path is now retrieved via a reliable API. Thanks [@jiripolasek](https://github.com/jiripolasek)! ### Find My Mouse - - Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs. + - Activating Find My Mouse no longer makes the cursor change to the busy (hourglass) icon or steals focus from your active application. ### Hosts File Editor - - Migrated to native WinUI TitleBar for cleaner, maintainable window chrome. + - Added customizable backup settings allowing users to configure backup frequency, location, and auto-deletion policies. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + +### Image Resizer + - Fixed settings consistency during batch resize operations by capturing settings once before processing. Thanks [@daverayment](https://github.com/daverayment)! ### Light Switch - - Introduced as a brand-new PowerToy module. - - Automatically switches between light and dark themes. - - Supports time-based scheduling or location-based sunrise/sunset switching. - - Supports using a keyboard shortcut to force a change. - - Supports filtering changes for Apps and/or System Theme. +- Introduced new UI to allow users to manually enter their latitude and longitude in Sunrise to Sunset mode. +- Refactored service with cleaner state management for stability. +- Removed logs from every tick, only logging key events to largely reduce log size. ### Mouse Pointer Crosshairs - - Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)! - - Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)! + - Enabled switching between Mouse Pointer Crosshairs and Gliding Cursor modes. Thanks [@mikehall-ms](https://github.com/mikehall-ms)! ### Mouse Without Borders - - Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)! - - - Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting! + - Added horizontal scrolling support. Thanks [@MasonBergstrom](https://github.com/MasonBergstrom)! ### Peek - - Added the option to activate Peek with just the Spacebar. +- Fixed media files remaining locked after preview window closes. Thanks [@daverayment](https://github.com/daverayment)! +- Added a command-line interface for file previewing. See the [Peek documentation](https://learn.microsoft.com/windows/powertoys/peek) for usage. Thanks [@prochan2](https://github.com/prochan2)! ### PowerRename - - Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)! +- PowerRename no longer crashes due to a missing resources file. +- Added photo metadata extraction support using EXIF and XMP for pattern-based renaming with camera info, GPS coordinates, and date taken. See [PowerRename Documentation](https://learn.microsoft.com/en-us/windows/powertoys/powerrename). -### Quick Accent - - Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)! +### PowerToys Run + - Added retry logic with exponential backoff to handle DWM composition errors during theme changes. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Updated OneNote icons to reflect new Microsoft 365 design. Thanks [@trevorNgo](https://github.com/trevorNgo)! -### Registry Preview - - Migrated to native TitleBar and AppWindow APIs for cleaner window chrome. + ### Quick Accent + - Added diameter symbol (⌀) for Shift+O in Special Characters mode, thanks to [@anselumjuju](https://github.com/anselumjuju)! -### Screen Ruler - - Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary. +### Zoomit + - Smoothed out zoom-animation in ZoomIt by coalescing mouse-move and timer events, thanks to [@foxmsft](https://github.com/foxmsft)! + - Enabled GIF support for ZoomIt, thanks to [@MarioHewardt](https://github.com/MarioHewardt)! + - Fixed spelling mistakes, and refactored some literal strings to string constants, thanks to [@lzandman](https://github.com/lzandman)! + - Fixed inaccurate "actual size" screenshots in ZoomIt and resolves a GDI handle leak, improving capture fidelity and long-session stability. thanks to [@daverayment](https://github.com/daverayment)! ### Settings - - Added ability to ignore specific hotkey conflicts to reduce noise. - - Stopped creating backup directory during dry-run status checks (cleaner first-run). - - Standardized casing and localization for ZoomIt and modules header. - - Improved search results page accessibility and conditional module grouping. +- Fixed title bar overlapping issue at smaller window sizes. +- Refined shortcut control visual design with improved consistency and spacing. +- Added dashboard utilities sorting by name or status. +- Made update notification InfoBar in flyout clickable for direct navigation to update page. +- Expanded installation instructions by default in README. +- Improved accessibility for shortcut conflict button with static resource-based automation properties. +- Added ScrollViewer to Command Palette page in PowerToys Settings. Thanks [@jiripolasek](https://github.com/jiripolasek)! +- Fixed module list glitches and Sort Status checkmark issue. Thanks [@daverayment](https://github.com/daverayment)! -### ZoomIt - - Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)! - - Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)! - - Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)! - - ### Documentation - - New Microsoft Learn documentation for the Light Switch module. - - New dev docs for the Light Switch module. - -### Development (Area-Build & Area-Tests) -- Allowed debug launches to continue when modules fail to load, speeding developer iteration. -- Fixed spell checker dictionary entry (advapi) to eliminate false error. -- Added VS Code development guide and launch configs to streamline cross-editor workflows. -- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features. -- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)! -- Corrected solution structure by returning misplaced Common project, reducing build confusion. -- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds. -- Standardized build scripts and platform detection to improve reliability and reuse. -- Added missing Command Palette version bump to align module release cadence. -- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)! -- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance. -- Resolved CI forbidden pattern spelling complaint to keep pipelines green. -- Added AI contributor instruction set to clarify code area expectations. -- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests. -- Added automatic log collection on UI test failures to speed root cause analysis. -- Stabilized Mouse Utils tests by switching to AccessibilityId selectors. -- Added Screen Ruler UI test coverage to validate core measurement workflows. +### Development +- Fixed accessibility by associating controls with labels for screen readers. +- Added accessible name to Shortcut Conflicts button for screen readers. +- Excluded TitleBars from tab navigation across multiple utilities. Thanks [@jiripolasek](https://github.com/jiripolasek)! +- Migrated build infrastructure from Windows Server 2019 to Server 2022 with improved failure logging and predictable NuGet package paths. +- Configured build agents to use larger P: drive for release builds to address disk space constraints. +- Enhanced DSC v3 support by organizing resource manifests in a dedicated subfolder with PATH configuration. +- Reduced installer bundle size by 6-7MB through centralized Hybrid CRT configuration across all C++ projects. +- Updated .NET packages to version 9.0.10 for security fixes. Thanks [@snickler](https://github.com/snickler)! +- Fixed spell check dictionary entries for consistency. +- Restored accidentally deleted NuGet configuration file for Command Palette extensions. +- Fixed package identity build by updating AppxManifest entry points to use PowerShell Core. +- Optimized CI pipeline by replacing file copy operations with hard links and moves, reducing build time and disk usage by 10-15GB. +- Updated Copilot guidance and PR prompt workflow. +- Included high-volume bugs in issue template header. Thanks [@daverayment](https://github.com/daverayment)! +- Fixed incorrect HRESULT logging for inner exceptions. Thanks [@jiripolasek](https://github.com/jiripolasek)! +- Introduced shared sparse package identity for PowerToys Win32 components to enable access to Windows platform APIs. +- Consolidated installer builds to produce both machine and user installers simultaneously, reducing build time and complexity. +- Migrated exclusively to WiX v5 installer infrastructure, removing legacy WiX v3 support. +- Temporarily removed PowerToys installer path from PATH environment variable to prevent application crashes. +- Added complete OCR UI test coverage with automated tests for activation, settings, language selection, and text extraction. +- Fixed test input for drive path normalization in bookmark resolver unit tests. +- Fixed Peek UI tests by restoring Ctrl+Space activation shortcut for test scenarios. +- Hided apps in PowerToys.SpareApps package from Start Menu. Thanks [@jiripolasek](https://github.com/jiripolasek)! ## ðŸ›£ï¸ Roadmap We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]! @@ -281,4 +253,4 @@ The application logs basic diagnostic data (telemetry). For more privacy informa [roadmap]: https://github.com/microsoft/PowerToys/wiki/Roadmap [privacy-link]: http://go.microsoft.com/fwlink/?LinkId=521839 [loc-bug]: https://github.com/microsoft/PowerToys/issues/new?assignees=&labels=&template=translation_issue.md&title= -[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs \ No newline at end of file +[usingPowerToys-docs-link]: https://aka.ms/powertoys-docs diff --git a/deps/cziplib b/deps/cziplib deleted file mode 160000 index 81314fff0a..0000000000 --- a/deps/cziplib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 81314fff0a882b72a9ad321e7a3311660125b56e diff --git a/doc/devdocs/commands.md b/doc/devdocs/commands.md new file mode 100644 index 0000000000..811625284e --- /dev/null +++ b/doc/devdocs/commands.md @@ -0,0 +1,34 @@ +# Issue/PR commands + +The PowerToys repository uses some special keywords to help manage issues and pull requests. Here is a list of the most important commands you can use in issue and PR descriptions or comments. + +| Command | Description | +|---------|-------------| +| `/azp run` | Triggers the Azure Pipelines CI build for the current PR. Useful if you want to re-run the build without creating a new commit. | +| `/bugreport` / `/reportbug` | Adds a comment with a manual for the Bug Report Tool, which helps users collect logs and system information for debugging purposes. It requests to upload this file and adds the `Needs-Author-Feedback` label. | +| `/feedbackhub` | Adds a comment with a link to the Feedback Hub app on Windows, where users can submit feedback about PowerToys. Closes the issue and adds the `Resolution-Please File on Feedback Hub` label. | +| `/dup #...` / `/duplicate #...` / `/dup https://...` / `/duplicate https://...` | Marks the current issue as a duplicate of another issue. It closes the current issue and applies the `Resolution-Duplicate` label. Replace `#...` with the issue number or a link to the issue. | +| `/needinfo` | Adds the `Needs-Author-Feedback` label to the issue or PR, indicating that more information is needed from the author. | +| `/helped` | Closes the issue and adds the `Resolution-Helped User` label. Furthermore a comment is added with a link to the PowerToys user documentation. | +| `/loc` | Adds a comment informing the user that the issue was forwarded to the localization team and will soon be fixed. It adds the `Loc-Sent To Team` label. | + +## Defining new commands + +Most of these commands are using the [Microsoft GitHub Policy Service](https://github.com/apps/microsoft-github-policy-service) bot. Its commands are defined in the [PowerToys policy configuration file](/.github/policies/resourceManagement.yml). + +## Other automated tasks + +### Automatic labeling + +The bot can automatically apply the correct `product-...` label for any opened issue. + +> [!NOTE] +> This feature is currently only available for the Workspaces module as a test. + +### The `Needs-Author-Feedback` label + +If an issue has this label and had no activity for 5 days, the bot will post a comment reminding the author to provide the needed information. It also adds the `Status-No recent activity` label. If no further activity occurs for another 5 days, the bot will close the issue. + +### Filtering users that want to contribute + +If a user utters their intention to contribute (e.g., by using the phrase "I want to contribute" in an issue or PR), the bot will add a comment with a link to the ["Would you like to contribute to PowerToys?" thread](https://github.com/microsoft/PowerToys/issues/28769). diff --git a/doc/devdocs/core/installer.md b/doc/devdocs/core/installer.md index 5bcbb0f87c..90af7668c2 100644 --- a/doc/devdocs/core/installer.md +++ b/doc/devdocs/core/installer.md @@ -134,7 +134,7 @@ If you prefer, you can alternatively build prerequisite projects for the install #### Locally compiling the installer -1. Open `installer\PowerToysSetup.sln` +1. Open `installer\PowerToysSetup.slnx` 1. In Visual Studio, in the `Solutions Configuration` drop-down menu select `Release` 1. From the `Build` menu choose `Build Solution`. @@ -144,9 +144,9 @@ To build the installer from the command line, run `Developer Command Prompt for ``` git clean -xfd -e *exe -- .\installer\ -MSBuild -t:restore .\installer\PowerToysSetup.sln -p:RestorePackagesConfig=true /p:Platform="x64" /p:Configuration=Release -MSBuild -t:Restore -m .\installer\PowerToysSetup.sln /t:PowerToysInstallerVNext /p:Configuration=Release /p:Platform="x64" -MSBuild -t:Restore -m .\installer\PowerToysSetup.sln /t:PowerToysBootstrapperVNext /p:Configuration=Release /p:Platform="x64" +MSBuild -t:restore .\installer\PowerToysSetup.slnx -p:RestorePackagesConfig=true /p:Platform="x64" /p:Configuration=Release +MSBuild -t:Restore -m .\installer\PowerToysSetup.slnx /t:PowerToysInstallerVNext /p:Configuration=Release /p:Platform="x64" +MSBuild -t:Restore -m .\installer\PowerToysSetup.slnx /t:PowerToysBootstrapperVNext /p:Configuration=Release /p:Platform="x64" ``` ### Supported arguments for the .EXE Bootstrapper installer diff --git a/doc/devdocs/development/debugging.md b/doc/devdocs/development/debugging.md index c3e0fdac89..3756dd9396 100644 --- a/doc/devdocs/development/debugging.md +++ b/doc/devdocs/development/debugging.md @@ -19,7 +19,7 @@ You can build the entire solution from the command line, which is sometimes fast 2. Navigate to the repository root directory 3. Run the following command(don't forget to set the correct platform): ```pwsh - msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln /tl /p:NuGetInteractive="true" + msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.slnx /tl /p:NuGetInteractive="true" ``` 4. This process should complete in approximately 13-14 minutes for a full build diff --git a/doc/devdocs/development/dev-with-vscode.md b/doc/devdocs/development/dev-with-vscode.md index 4b1b7def24..bd2f894f79 100644 --- a/doc/devdocs/development/dev-with-vscode.md +++ b/doc/devdocs/development/dev-with-vscode.md @@ -42,10 +42,10 @@ Or reach out to "tools\build\BUILD-GUIDELINES.md" ### Sample plain msbuild command ```powershell # Restore: -msbuild powertoys.sln -t:restore -p:configuration=debug -p:platform=x64 -m +msbuild powertoys.slnx -t:restore -p:configuration=debug -p:platform=x64 -m -# Build powertoys sln -msbuild powertoys.sln -p:configuration=debug -p:platform=x64 -m +# Build powertoys slnx +msbuild powertoys.slnx -p:configuration=debug -p:platform=x64 -m # dotnet project msbuild src\settings-ui\Settings.UI\PowerToys.Settings.csproj -p:Platform=x64 -p:Configuration=Debug -m @@ -122,7 +122,7 @@ Similar for attach to managed code. | Task | Command / Action | Notes | |------|------------------|-------| -| Clean | `git clean -xdf` (careful) or `msbuild /t:Clean PowerToys.sln` | Deep clean removes packages & build outputs | +| Clean | `git clean -xdf` (careful) or `msbuild /t:Clean PowerToys.slnx` | Deep clean removes packages & build outputs | | Rebuild single project | `msbuild path\to\proj.vcxproj /t:Rebuild -p:Platform=x64 -p:Configuration=Debug` | Faster than whole solution | | Generate installer (rare in inner loop) | See `tools\build\build-installer.ps1` | Usually not needed for local debug | -| Resource conversion errors | Re-run restore + build | Triggers custom PowerShell targets | \ No newline at end of file +| Resource conversion errors | Re-run restore + build | Triggers custom PowerShell targets | diff --git a/doc/devdocs/style.md b/doc/devdocs/development/style.md similarity index 100% rename from doc/devdocs/style.md rename to doc/devdocs/development/style.md diff --git a/doc/devdocs/development/ui-tests.md b/doc/devdocs/development/ui-tests.md index 63bddb0591..941f9dacd4 100644 --- a/doc/devdocs/development/ui-tests.md +++ b/doc/devdocs/development/ui-tests.md @@ -12,7 +12,7 @@ - Exit PowerToys if it's running. -- Open `PowerToys.sln` in Visual Studio and build the solution. +- Open `PowerToys.slnx` in Visual Studio and build the solution. - Run tests in the Test Explorer (`Test > Test Explorer` or `Ctrl+E, T`). diff --git a/doc/devdocs/localization.md b/doc/devdocs/localization.md deleted file mode 100644 index f9e7e50a67..0000000000 --- a/doc/devdocs/localization.md +++ /dev/null @@ -1,165 +0,0 @@ -# Localization - -> **NOTE**: THIS DOCUMENT IS OUTDATED. -> Follow [issue 15243](https://github.com/microsoft/PowerToys/issues/15243) for updates. - -## Table of Contents -1. [Localization on the pipeline (CDPX)](#localization-on-the-pipeline-cdpx) - 1. [UWP Special case](#uwp-special-case) -2. [Enabling localization on a new project](#enabling-localization-on-a-new-project) - 1. [C++](#c) - 2. [C#](#c-1) - 3. [UWP](#uwp) -3. [Lcl Files](#lcl-files) -4. [Possible Issues in localization PRs (LEGO)](#possible-issues-in-localization-prs-lego) -5. [Enabling localized MSI for a new project](#enabling-localized-msi-for-a-new-project) - -## Localization on the pipeline (CDPX) -[The localization step](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L45-L52) is run on the pipeline before the solution is built. This step runs the [build-localization](https://github.com/microsoft/PowerToys/blob/main/.pipelines/build-localization.cmd) script, which generates resx files for all the projects with localization enabled using the `Localization.XLoc` package. - -The [`Localization.XLoc`](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/build-localization.cmd#L24-L25) tool is run on the repo root, and it checks for all occurrences of `LocProject.json`. Each localized project has a `LocProject.json` file in the project root, which contains the location of the English resx file, list of languages for localization, and the output path where the localized resx files are to be copied to. In addition to this, some other parameters can be set, such as whether the language ID should be added as a folder in the file path or in the file name. When the CDPX pipeline is run, the localization team is notified of changes in the English resx files. For each project with localization enabled, a `loc` folder (see [this](https://github.com/microsoft/PowerToys/tree/main/src/modules/launcher/Microsoft.Launcher/loc) for example) is created in the same directory as the `LocProject.json` file. The folder contains language specific folders which in turn have a nested folder path equivalent to `OutputPath` in the `LocProject.json`. Each of these folders contain one `lcl` file. The `lcl` files contain the English resources along with their translation for that language. These are described in more detail in the [Lcl files section](#lcl-files). Once the `.resx` files are generated, they will be used during the `Build PowerToys` step for localized versions of the modules. - -Since the localization script requires certain nuget packages, the [`restore-localization`](https://github.com/microsoft/PowerToys/blob/main/.pipelines/restore-localization.cmd) script is run before running `build-localization` to install all the required packages. This script must [run in the `restore` step](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L37-L39) of pipeline because [the host is network isolated](https://onebranch.visualstudio.com/Pipeline/_wiki/wikis/Pipeline.wiki/2066/Consuming-Packages-in-a-CDPx-Pipeline?anchor=overview) at the `build` step. The [Toolset package source](https://github.com/microsoft/PowerToys/blob/86d77103e9c69686c297490acb04775d43ef8b76/.pipelines/pipeline.user.windows.yml#L23) is used for this. - -The process and variables that can be tweaked on the pipeline are described in more detail on [onebranch (account required) under Localization](https://onebranch.visualstudio.com/Pipeline/_wiki/wikis/Pipeline.wiki/290/Localization). - -The localized resource dlls for C# projects are added to the MSI only for build on the pipeline. This is done by checking if the [`IsPipeline` variable is defined](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L804-L805), which gets defined before [building the installer on the pipeline](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/.pipelines/build-installer.cmd#L4). This is done because the localized resx files are only present on the pipeline, and not having this check would result in the installer project failing to build locally. - -## Enabling localization on a new project -To enable localization on a new project, the first step is to create a file `LocProject.json` in the project root. - -For example, for a project in the folder `src\path` where the resx file is present in `resources\Resources.resx`, the LocProject.json file will contain the following: -``` -{ - "Projects": [ - { - "LanguageSet": "Azure_Languages", - "LocItems": [ - { - "SourceFile": "src\\path\\resources\\Resources.resx", - "CopyOption": "LangIDOnName", - "OutputPath": "src\\path\\resources" - } - ] - } - ] -} -``` -The rest of the steps depend on the project type and are covered in the sections below. The steps to add the localized files to the MSI can be found in [Enabling localized MSI for a new project](#Enabling-localized-MSI-for-a-new-project). - -### C++ -C++ projects do not support `resx` files, and instead use `rc` files along with `resource.h` files. The CDPX pipeline however doesn't support localizing `rc` files and the other alternative they support is directly translating the resources from the binary which makes it harder to maintain resources. To avoid this, a custom script has been added which expects a resx file and converts the entries to an rc file with a string table and adds resource declarations to a resource.h file so that the resources can be compiled with the C++ project. - -If you already have a .rc file, copy the string table to a separate txt file and run the [convert-stringtable-to-resx.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-stringtable-to-resx.ps1) script on it. This script is not very robust to input, and requires the data in a specific format, where `IDS_ResName L"ResourceValue"` and any number of spaces can be present in between. The script converts this file to the format expected by [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert), which will convert it to resx. The resource names are changed from all uppercase to title case, and the `IDS_` prefix is removed. Escape characters might have to be manually replaced, for example .rc files would have escaped double quotes as `""`, so this should be replaced with just `"` before converting to the resx files. - -After generating the resx file, rename the existing rc and h files to ProjName.base.rc and resource.base.h. In the rc file remove the string table which is to be localized and in the .h file remove all `#define`s corresponding to localized resources. In the vcxproj of the C++ project, add the following build event: -``` - - - -``` - -This event runs a script which generates a resource.h and ProjName.rc in the `Generated Files` folder using the strings in all the resx files along with the existing information in resource.base.h and ProjName.base.rc. The script is [convert-resx-to-rc.ps1](https://github.com/microsoft/PowerToys/blob/main/tools/build/convert-resx-to-rc.ps1). The script uses [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) to convert the resx file to a string table expected in the .rc file format. When the resources are added to the rc file the `IDS_` prefix is added and resource names are in upper case (as it was originally). Any occurrences of `"` in the string resource is escaped as `""` to prevent build errors. The string tables are added to the rc file in the following format: -``` -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -STRINGTABLE -BEGIN - strings -END - -#endif -``` -Since there is no API to identify the `AFX_TARG_*`, `LANG_*` or `SUBLANG_*` values from each langId from the pipeline, these are hardcoded in the script (for each language) as done in [lines 50-77 of `convert-resx-to-rc.ps1`](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/tools/build/convert-resx-to-rc.ps1#L50-L77). **If any other languages are added in the future, this script will have to be updated.** In order to determine what are the language codes, you can open the rc file in Resource View, right click the string table and press `Insert Copy` and choose the corresponding language. This autogenerates the required code and can be used to figure out the language codes. The files also add the resource declarations to a resource.h file, starting from 101 by default(this can be changed by an optional argument). Since the output files will be generated in `Generated Files`, any includes in these two files will require an additional `..\` and wherever resource.h is used, it will have to be included as `Generated Files\resource.h`. While adding `resource.base.h` and `ProjName.base.rc` to the vcxproj, these should be modified to not participate in the build to avoid build errors: -``` - -``` - -Some rc/resource.h files might be used in multiple projects (for example, KBM). To ensure the projects build for these cases, the build event can be added to the entire directory so that the rc files are generated before any project is built. See [Directory.Build.targets](https://github.com/microsoft/PowerToys/blob/main/src/modules/keyboardmanager/Directory.Build.targets) for an example. - -Check [this PR](https://github.com/microsoft/PowerToys/pull/6104) for an example for making these changes for a C++ project. - -### C# -Since C# projects natively support `resx` files, the only step required here is to include all the resx files in the build. For .NET Core projects this is done automatically and the .csproj does not need to be modified. For other projects, the following line needs to be added: -``` - -``` - -**Note:** Building with localized resources may cause a build warning `Referenced assembly 'mscorlib.dll' targets a different processor` which is a VS bug. More details can be found in [PowerToys issue #7269](https://github.com/microsoft/PowerToys/issues/7269). - -**Note:** If a project needs to be migrated from XAML resources to resx, the easiest way to convert the resources would be to change to format to `=` separates resources by either manually (by Ctrl+H on a text editor), or by a script, and then running [`resgen`](https://learn.microsoft.com/dotnet/framework/tools/resgen-exe-resource-file-generator#Convert) on `Developer Command Prompt for VS` to convert it to resx format. -``` -Calculator -Allows to do mathematical calculations.(Try 5*3-2 in Wox) -Not a number (NaN) -``` -to -``` -wox_plugin_calculator_plugin_name=Calculator -wox_plugin_calculator_plugin_description=Allows to do mathematical calculations.(Try 5*3-2 in Wox) -wox_plugin_calculator_not_a_number=Not a number (NaN) -``` -After adding the resx file to the project along with the resource generator, references to the strings will have to be replaced with `Properties.Resources.resName` rather than the custom APIs. Check [this PR](https://github.com/microsoft/PowerToys/pull/6165) for an example of the changes required. - -### UWP -UWP projects expect `resw` files rather than `resx` (the format is almost the same). Unlike other C# projects, the files are expected in the format `fullLangId\Resources.resw`. To include these files in the build, replace the following line in the csproj: -``` - -``` -to -``` - -``` - -## Lcl Files -Lcl files contain all the resources that are present in the English resx file, along with a translation if it has been added. - -For example, an entry for a resource in the lcl file looks like this: -``` - - - - - - - - - -``` -The `` element would not be present in the initial commits of the lcl files, as only the English version of the string would be present. - -**Note:** The CDPX Localization system has a fail-safe check on the lcl files, where if the English string value which is present inside `` does not match the value present in the English Resources.resx file then the translated value will not be copied to the localized resx file. This is present so that obsolete translations would not be loaded when the English resource has changed, and the English string will be used rather than the obsolete translation. - -## Possible Issues in localization PRs (LEGO) -Since the LEGO PRs update some of the strings in LCL files at a time, there can be multiple PRs which modify the same files, leading to merge conflicts. In most cases this would show up on GitHub as a merge conflict, but sometimes a bad git merge may occur, and the file could end up with incorrect formatting, such as two `` elements for a single resource. These can be fixed by ensuring the elements follow the format described in [this section](#lcl-files). To catch such errors, the build farm should be run for every LEGO PR and if any error occurs in the localization step, we should check the corresponding resx/lcl files for conflicts. - -## Enabling localized MSI for a new project -For C++ and UWP projects no additional files are generated with localization that need to be added to the MSI. For C++ projects all the resources are added to the dll/exe, while for UWP projects they are added to the `resources.pri` file (which is present even for an unlocalized project). To verify if the localized resources are added to the `resources.pri` file the following steps can be done: -- Open `Developer Command Prompt for VS` -- After navigating to the folder containing the pri file, run the following command: - - makepri.exe dump /if .\resources.pri -- Check the contents of the `resources.pri.xml` file that is generated from the command. The last section of the file will contain the resources with the strings in all the languages: -``` - - - Running as administrator - - - Running as administrator - - -``` - -For C# projects, satellite dlls are generated when the project is built. For a project named `ProjName`, files are created in the format `langId\ProjName.resources.dll` where `langId` is in the same format as the lcl files. The satellite dlls need to be included with the MSI, but they must be added only if the solution is built from the build farm, as the localized resx files will not be present on local machines (and that could cause local builds of the installer to fail). -This can be done by adding the directory name of the project to [Product.wxs near line 806](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L806) and a resource component for the project can be created in [Product.wxs near lines 845-847](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/installer/PowerToysSetup/Product.wxs#L845-L847) in this format: -``` - - - -``` - -We should also ensure the new dlls are signed by the pipeline. Currently all dlls of the form [`*.resources.dll` are signed](https://github.com/microsoft/PowerToys/blob/f92bd6ffd38014c228544bb8d68d0937ce4c2b6d/.pipelines/pipeline.user.windows.yml#L68). - -**Note:** The resource dlls should be added to the MSI project only after the initial commit with the lcl files has been done by the Localization team. Otherwise, the pipeline will fail as there wouldn't be any resx files to generate the dlls. diff --git a/doc/devdocs/modules/alwaysontop.md b/doc/devdocs/modules/alwaysontop.md index 2162245d83..d8948697f1 100644 --- a/doc/devdocs/modules/alwaysontop.md +++ b/doc/devdocs/modules/alwaysontop.md @@ -86,7 +86,7 @@ The module provides a user interface for configuring settings in the PowerToys S ### Building and Testing 1. Clone the repository: `git clone https://github.com/microsoft/PowerToys.git` -2. Open PowerToys.sln in Visual Studio +2. Open PowerToys.slnx in Visual Studio 3. Select the Release configuration and build the solution 4. Run PowerToys.exe from the output directory to test the module diff --git a/doc/devdocs/modules/fancyzones.md b/doc/devdocs/modules/fancyzones.md index 07ffe491f5..a4571eacd7 100644 --- a/doc/devdocs/modules/fancyzones.md +++ b/doc/devdocs/modules/fancyzones.md @@ -161,7 +161,7 @@ FancyZones is divided into several projects: ``` git clone https://github.com/microsoft/PowerToys.git ``` -2. Open `PowerToys.sln` in Visual Studio +2. Open `PowerToys.slnx` in Visual Studio 3. Select the Release configuration and build the solution 4. If you encounter build errors, try deleting the x64 output folder and rebuild @@ -244,7 +244,7 @@ UI tests are implemented using [Windows Application Driver](https://github.com/m - Exit PowerToys if it's running - Run WinAppDriver.exe from the installation directory. Skip this step if installed in the default directory (`C:\Program Files (x86)\Windows Application Driver`); in this case, it'll be launched automatically during tests. - - Open `PowerToys.sln` in Visual Studio and build the solution. + - Open `PowerToys.slnx` in Visual Studio and build the solution. - Run tests in the Test Explorer (`Test > Test Explorer` or `Ctrl+E, T`). >Note: notifications or other application windows, that are shown above the window under test, can disrupt the testing process. diff --git a/doc/devdocs/modules/keyboardmanager/debug.md b/doc/devdocs/modules/keyboardmanager/debug.md index 0efb3b0b3a..60b975da0f 100644 --- a/doc/devdocs/modules/keyboardmanager/debug.md +++ b/doc/devdocs/modules/keyboardmanager/debug.md @@ -11,7 +11,7 @@ Keyboard Manager consists of two main components: ## Development Environment Setup 1. Clone the PowerToys repository -2. Open `PowerToys.sln` in Visual Studio +2. Open `PowerToys.slnx` in Visual Studio 3. Ensure all NuGet packages are restored 4. Build the entire solution in Debug configuration @@ -91,4 +91,4 @@ If you encounter issues with multiple instances, check the mutex logic in `Keybo To debug both the Editor and Engine: 1. Launch the Engine first in debug mode -2. Attach the debugger to the Editor process when it starts \ No newline at end of file +2. Attach the debugger to the Editor process when it starts diff --git a/doc/devdocs/modules/lightswitch.md b/doc/devdocs/modules/lightswitch.md index 1e251dfff1..eb5e07aea6 100644 --- a/doc/devdocs/modules/lightswitch.md +++ b/doc/devdocs/modules/lightswitch.md @@ -33,9 +33,12 @@ The **Light Switch** module lets users automatically transition between light an > **Note:** Using the shortcut overrides the current schedule until the next transition event. -* **LightSwitchService** - Reads settings and applies theming. Runs a check every minute to ensure the state is correct. - +* **LightSwitchService.cpp** + is the heart beat of the module. Controls ticking every minute and depending on user actions (manual override, settings changing, etc) triggers the state manager to perform the corresponding operation. + +* **LightSwitchStateManager.cpp** + handles updating the state based on the signals sent by LightSwitchService. + * **SettingsXAML/LightSwitch** Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts. @@ -89,7 +92,7 @@ The module’s settings are exposed in the PowerToys Settings UI. Options includ 3. Build the solution: ```sh - msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.sln + msbuild -restore -p:RestorePackagesConfig=true -p:Platform=ARM64 -m PowerToys.slnx ``` > Note: This may take some time. diff --git a/doc/devdocs/modules/screenruler.md b/doc/devdocs/modules/screenruler.md index 08bdda764a..d43d1639ea 100644 --- a/doc/devdocs/modules/screenruler.md +++ b/doc/devdocs/modules/screenruler.md @@ -53,7 +53,7 @@ The Screen Ruler module consists of several components: ### Building -1. Open PowerToys.sln in Visual Studio +1. Open PowerToys.slnx in Visual Studio 2. In the Solutions Configuration drop-down menu, select Release or Debug 3. From the Build menu, choose Build Solution 4. The executable app for Screen Ruler is named PowerToys.MeasureToolUI.exe diff --git a/doc/devdocs/modules/shortcut_guide.md b/doc/devdocs/modules/shortcut_guide.md index c8cefbc3f3..f150a4456c 100644 --- a/doc/devdocs/modules/shortcut_guide.md +++ b/doc/devdocs/modules/shortcut_guide.md @@ -19,7 +19,7 @@ Shortcut Guide is a PowerToy that displays an overlay of available keyboard shor ## Build and Debug Instructions ### Build -1. Open PowerToys.sln in Visual Studio +1. Open PowerToys.slnx in Visual Studio 2. Select Release or Debug in the Solutions Configuration drop-down menu 3. From the Build menu, choose Build Solution 4. The executable is named PowerToys.ShortcutGuide.exe diff --git a/doc/devdocs/readme.md b/doc/devdocs/readme.md index 75e03c0629..38df894d1a 100644 --- a/doc/devdocs/readme.md +++ b/doc/devdocs/readme.md @@ -38,6 +38,11 @@ Welcome to the PowerToys developer documentation. This documentation provides in - [Update Process](processes/update-process.md) - How PowerToys updates work - [GPO Implementation](processes/gpo.md) - Group Policy Objects implementation details +## Other Resources + +- [aka.ms links](akaLinks.md) - List of short links +- [Issue/PR commands](commands.md) - Special commands for managing issues and pull requests + ## Fork, Clone, Branch and Create your PR Once you've discussed your proposed feature/fix/etc. with a team member, and an approach or a spec has been written and approved, it's time to start development: @@ -80,7 +85,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an ### Install Visual Studio dependencies -1. Open the `PowerToys.sln` file. +1. Open the `PowerToys.slnx` file. 1. If you see a dialog that says `install extra components` in the solution explorer pane, click `install` ### Get Submodules to compile @@ -93,7 +98,7 @@ We have submodules that need to be initialized before you can compile most parts ### Compiling Source Code -- Open `PowerToys.sln` in Visual Studio. +- Open `PowerToys.slnx` in Visual Studio. - In the `Solutions Configuration` drop-down menu select `Release` or `Debug`. - From the `Build` menu choose `Build Solution`, or press Control+Shift+b on your keyboard. - The build process may take several minutes depending on your computer's performance. Once it completes, the PowerToys binaries will be in your repo under `x64\Release\`. @@ -107,10 +112,10 @@ Our installer is two parts, an EXE and an MSI. The EXE (Bootstrapper) contains The installer can only be compiled in `Release` mode; steps 1 and 2 must be performed before the MSI can be compiled. -1. Compile `PowerToys.sln`. Instructions are listed above. +1. Compile `PowerToys.slnx`. Instructions are listed above. 1. Compile `BugReportTool.sln` tool. Path from root: `tools\BugReportTool\BugReportTool.sln` (details listed below) 1. Compile `StylesReportTool.sln` tool. Path from root: `tools\StylesReportTool\StylesReportTool.sln` (details listed below) -1. Compile `PowerToysSetup.sln` Path from root: `installer\PowerToysSetup.sln` (details listed below) +1. Compile `PowerToysSetup.slnx` Path from root: `installer\PowerToysSetup.slnx` (details listed below) See [Installer](core/installer.md) for more details on building and debugging the installer. diff --git a/doc/images/icons/CursorWrap.png b/doc/images/icons/CursorWrap.png new file mode 100644 index 0000000000..20db84fc9a Binary files /dev/null and b/doc/images/icons/CursorWrap.png differ diff --git a/doc/images/icons/Find My Mouse.png b/doc/images/icons/Find My Mouse.png index 71dd994569..82fbe59800 100644 Binary files a/doc/images/icons/Find My Mouse.png and b/doc/images/icons/Find My Mouse.png differ diff --git a/doc/images/icons/Mouse Highlighter.png b/doc/images/icons/Mouse Highlighter.png index b06843d941..0feb5cc15a 100644 Binary files a/doc/images/icons/Mouse Highlighter.png and b/doc/images/icons/Mouse Highlighter.png differ diff --git a/doc/images/icons/MouseJump.png b/doc/images/icons/MouseJump.png new file mode 100644 index 0000000000..2fbe450ac2 Binary files /dev/null and b/doc/images/icons/MouseJump.png differ diff --git a/doc/images/icons/MouseWithoutBorders.png b/doc/images/icons/MouseWithoutBorders.png index a29adf7d11..ee66893cbd 100644 Binary files a/doc/images/icons/MouseWithoutBorders.png and b/doc/images/icons/MouseWithoutBorders.png differ diff --git a/doc/thirdPartyRunPlugins.md b/doc/thirdPartyRunPlugins.md index a15cb542a8..9cfdc505ff 100644 --- a/doc/thirdPartyRunPlugins.md +++ b/doc/thirdPartyRunPlugins.md @@ -51,6 +51,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi | [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. | | [Open With Cursor](https://github.com/VictorNoxx/PowerToys-Run-Cursor/) | [VictorNoxx](https://github.com/VictorNoxx) | Open Visual Studio, VS Code recents with Cursor AI | | [CheatSheets](https://github.com/ruslanlap/PowerToysRun-CheatSheets) | [ruslanlap](https://github.com/ruslanlap) | 📚 Find cheat sheets and command examples instantly from tldr pages, cheat.sh, and devhints.io. Features include favorites system, categories, offline mode, and smart caching. | +| [QuickAI](https://github.com/ruslanlap/PowerToysRun-QuickAi) | [ruslanlap](https://github.com/ruslanlap) | AI-powered assistance with instant, smart responses from multiple providers (Groq, Together, Fireworks, OpenRouter, Cohere) | ## Extending software plugins diff --git a/installer/PowerToysSetup.sln b/installer/PowerToysSetup.sln deleted file mode 100644 index 77d38c94ab..0000000000 --- a/installer/PowerToysSetup.sln +++ /dev/null @@ -1,96 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.32414.318 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "spdlog", "..\src\logging\logging.vcxproj", "{7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "logger", "..\src\common\logger\logger.vcxproj", "{D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Version", "..\src\common\version\version.vcxproj", "{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EtwTrace", "..\src\common\Telemetry\EtwTrace\EtwTrace.vcxproj", "{8F021B46-362B-485C-BFBA-CCF83E820CBD}" -EndProject -Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "PowerToysInstallerVNext", "PowerToysSetupVNext\PowerToysInstallerVNext.wixproj", "{B6E94700-DF38-41F6-A3FD-18B69674AB1E}" -EndProject -Project("{930C7802-8A8C-48F9-8165-68863BCCD9DD}") = "PowerToysBootstrapperVNext", "PowerToysSetupVNext\PowerToysBootstrapperVNext.wixproj", "{DA4E9744-80BE-424C-B0F5-AFD8757DB575}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToysSetupCustomActionsVNext", "PowerToysSetupCustomActionsVNext\PowerToysSetupCustomActionsVNext.vcxproj", "{B3A354B0-1E54-4B55-A962-FB5AF9330C19}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SilentFilesInUseBAFunction", "PowerToysSetupVNext\SilentFilesInUseBA\SilentFilesInUseBAFunction.vcxproj", "{F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|ARM64 = Debug|ARM64 - Debug|x64 = Debug|x64 - Release|ARM64 = Release|ARM64 - Release|x64 = Release|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.ActiveCfg = Debug|x64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Debug|x64.Build.0 = Debug|x64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|ARM64.ActiveCfg = Release|ARM64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|ARM64.Build.0 = Release|ARM64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.ActiveCfg = Release|x64 - {7E1E3F13-2BD6-3F75-A6A7-873A2B55C60F}.Release|x64.Build.0 = Release|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.ActiveCfg = Debug|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Debug|x64.Build.0 = Debug|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|ARM64.ActiveCfg = Release|ARM64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|ARM64.Build.0 = Release|ARM64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.ActiveCfg = Release|x64 - {D9B8FC84-322A-4F9F-BBB9-20915C47DDFD}.Release|x64.Build.0 = Release|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|ARM64.Build.0 = Debug|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.ActiveCfg = Debug|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Debug|x64.Build.0 = Debug|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|ARM64.ActiveCfg = Release|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|ARM64.Build.0 = Release|ARM64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.ActiveCfg = Release|x64 - {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}.Release|x64.Build.0 = Release|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|ARM64.Build.0 = Debug|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|x64.ActiveCfg = Debug|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Debug|x64.Build.0 = Debug|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|ARM64.ActiveCfg = Release|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|ARM64.Build.0 = Release|ARM64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|x64.ActiveCfg = Release|x64 - {8F021B46-362B-485C-BFBA-CCF83E820CBD}.Release|x64.Build.0 = Release|x64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|ARM64.Build.0 = Debug|ARM64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|x64.ActiveCfg = Debug|x64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Debug|x64.Build.0 = Debug|x64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|ARM64.ActiveCfg = Release|ARM64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|ARM64.Build.0 = Release|ARM64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|x64.ActiveCfg = Release|x64 - {B6E94700-DF38-41F6-A3FD-18B69674AB1E}.Release|x64.Build.0 = Release|x64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|ARM64.Build.0 = Debug|ARM64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|x64.ActiveCfg = Debug|x64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Debug|x64.Build.0 = Debug|x64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|ARM64.ActiveCfg = Release|ARM64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|ARM64.Build.0 = Release|ARM64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|x64.ActiveCfg = Release|x64 - {DA4E9744-80BE-424C-B0F5-AFD8757DB575}.Release|x64.Build.0 = Release|x64 - {B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Debug|x64.ActiveCfg = Debug|x64 - {B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Debug|x64.Build.0 = Debug|x64 - {B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|ARM64.ActiveCfg = Release|ARM64 - {B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|ARM64.Build.0 = Release|ARM64 - {B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|x64.ActiveCfg = Release|x64 - {B3A354B0-1E54-4B55-A962-FB5AF9330C19}.Release|x64.Build.0 = Release|x64 - {F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Debug|x64.ActiveCfg = Debug|x64 - {F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Debug|x64.Build.0 = Debug|x64 - {F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|ARM64.ActiveCfg = Release|ARM64 - {F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|ARM64.Build.0 = Release|ARM64 - {F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|x64.ActiveCfg = Release|x64 - {F8B9F842-F5C3-4A2D-8C85-7F8B9E2B4F1D}.Release|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {B7A3DA30-D443-40FF-AC51-988AD41E3962} - EndGlobalSection -EndGlobal \ No newline at end of file diff --git a/installer/PowerToysSetup.slnx b/installer/PowerToysSetup.slnx new file mode 100644 index 0000000000..658310f9f6 --- /dev/null +++ b/installer/PowerToysSetup.slnx @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/generateDscResourcesWxs.ps1 b/installer/PowerToysSetup/generateDscResourcesWxs.ps1 deleted file mode 100644 index f76fd71953..0000000000 --- a/installer/PowerToysSetup/generateDscResourcesWxs.ps1 +++ /dev/null @@ -1,87 +0,0 @@ -[CmdletBinding()] -Param( - [Parameter(Mandatory = $True)] - [string]$dscWxsFile, - [Parameter(Mandatory = $True)] - [string]$Platform, - [Parameter(Mandatory = $True)] - [string]$Configuration -) - -$ErrorActionPreference = 'Stop' -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration" - -if (-not (Test-Path $buildOutputDir)) { - Write-Error "Build output directory not found: '$buildOutputDir'" - exit 1 -} - -$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File - -if (-not $dscFiles) { - Write-Warning "No DSC manifest files found in '$buildOutputDir'" - $wxsContent = @" - - - - - - - -"@ - Set-Content -Path $dscWxsFile -Value $wxsContent - exit 0 -} - -Write-Host "Found $($dscFiles.Count) DSC manifest file(s)" - -$wxsContent = @" - - - - -"@ - -$componentRefs = @() -foreach ($file in $dscFiles) { - $componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_') - $fileId = $componentId + "_File" - $guid = [System.Guid]::NewGuid().ToString().ToUpper() - $componentRefs += $componentId - - $wxsContent += @" - - - - - - - -"@ -} - -$wxsContent += @" - - - - - -"@ - -foreach ($componentId in $componentRefs) { - $wxsContent += @" - - -"@ -} - -$wxsContent += @" - - - - -"@ - -Set-Content -Path $dscWxsFile -Value $wxsContent -Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'" diff --git a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp index 0cfc3b1765..968fcc2530 100644 --- a/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActionsVNext/CustomAction.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "../../src/common/logger/logger.h" #include "../../src/common/utils/gpo.h" @@ -856,14 +857,69 @@ UINT __stdcall UnsetAdvancedPasteAPIKeyCA(MSIHANDLE hInstall) try { - winrt::Windows::Security::Credentials::PasswordVault vault; - winrt::Windows::Security::Credentials::PasswordCredential cred; - hr = WcaInitialize(hInstall, "UnsetAdvancedPasteAPIKey"); ExitOnFailure(hr, "Failed to initialize"); - cred = vault.Retrieve(L"https://platform.openai.com/api-keys", L"PowerToys_AdvancedPaste_OpenAIKey"); - vault.Remove(cred); + winrt::Windows::Security::Credentials::PasswordVault vault; + + auto hasPrefix = [](std::wstring_view value, wchar_t const* prefix) { + std::wstring_view prefixView{ prefix }; + return value.compare(0, prefixView.size(), prefixView) == 0; + }; + + const wchar_t* resourcePrefixes[] = { + L"https://platform.openai.com/api-keys", + L"https://azure.microsoft.com/products/ai-services/openai-service", + L"https://azure.microsoft.com/products/ai-services/ai-inference", + L"https://console.mistral.ai/account/api-keys", + L"https://ai.google.dev/", + }; + + const wchar_t* usernamePrefixes[] = { + L"PowerToys_AdvancedPaste_", + }; + + auto credentials = vault.RetrieveAll(); + for (auto const& credential : credentials) + { + bool shouldRemove = false; + + std::wstring resource{ credential.Resource() }; + for (auto const prefix : resourcePrefixes) + { + if (hasPrefix(resource, prefix)) + { + shouldRemove = true; + break; + } + } + + if (!shouldRemove) + { + std::wstring username{ credential.UserName() }; + for (auto const prefix : usernamePrefixes) + { + if (hasPrefix(username, prefix)) + { + shouldRemove = true; + break; + } + } + } + + if (!shouldRemove) + { + continue; + } + + try + { + vault.Remove(credential); + } + catch (...) + { + } + } } catch (...) { diff --git a/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj b/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj index 7cd49be6ea..ae50cdcedb 100644 --- a/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj +++ b/installer/PowerToysSetupCustomActionsVNext/PowerToysSetupCustomActionsVNext.vcxproj @@ -34,13 +34,8 @@ - $(Platform)\$(Configuration)\MachineSetup\ - $(Platform)\$(Configuration)\UserSetup\ - $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\MachineSetup\obj\ - $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\UserSetup\obj\ - - false - true + $(Platform)\$(Configuration)\SetupShared\ + $(SolutionDir)$(ProjectName)\$(Platform)\$(Configuration)\SetupShared\obj\ true @@ -59,6 +54,7 @@ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\CmdPal.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\CmdPal.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\ColorPicker.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\ColorPicker.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Core.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Core.wxs.bk"""" + call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\DscResources.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\DscResources.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\EnvironmentVariables.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\EnvironmentVariables.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\FileExplorerPreview.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\FileExplorerPreview.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\FileLocksmith.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\FileLocksmith.wxs.bk"""" @@ -80,8 +76,7 @@ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinAppSDK.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\WinUI3Applications.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs"" ""$(ProjectDir)..\PowerToysSetupVNext\Workspaces.wxs.bk"""" - if not "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform) - if "$(NormalizedPerUserValue)" == "true" call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform) -installscopeperuser $(NormalizedPerUserValue) + call powershell.exe -NonInteractive -executionpolicy Unrestricted -File ..\PowerToysSetupVNext\generateAllFileComponents.ps1 -platform $(Platform) Backing up original files and populating .NET and WPF Runtime dependencies for WiX3 based installer @@ -181,4 +176,4 @@ - \ No newline at end of file + diff --git a/installer/PowerToysSetupVNext/CmdPal.wxs b/installer/PowerToysSetupVNext/CmdPal.wxs index 8304551c12..f05a1f2f35 100644 --- a/installer/PowerToysSetupVNext/CmdPal.wxs +++ b/installer/PowerToysSetupVNext/CmdPal.wxs @@ -4,6 +4,13 @@ + + + + + + + @@ -18,14 +25,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetupVNext/Core.wxs b/installer/PowerToysSetupVNext/Core.wxs index f7da6162f9..a9cf083512 100644 --- a/installer/PowerToysSetupVNext/Core.wxs +++ b/installer/PowerToysSetupVNext/Core.wxs @@ -15,8 +15,8 @@ - - + + @@ -24,8 +24,8 @@ - - + + @@ -63,16 +63,6 @@ - - - - - - - - - - @@ -120,7 +110,6 @@ - @@ -128,16 +117,15 @@ - - - - - - + + + + + diff --git a/installer/PowerToysSetupVNext/DscResources.wxs b/installer/PowerToysSetupVNext/DscResources.wxs new file mode 100644 index 0000000000..2c08253229 --- /dev/null +++ b/installer/PowerToysSetupVNext/DscResources.wxs @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj index afce3d396d..18d6232140 100644 --- a/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj +++ b/installer/PowerToysSetupVNext/PowerToysInstallerVNext.wixproj @@ -14,7 +14,6 @@ SET PTRoot=$(SolutionDir)\.. call "..\..\..\publish.cmd" x64 ) call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2" -call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)" @@ -25,7 +24,6 @@ SET PTRoot=$(SolutionDir)\.. call "..\..\..\publish.cmd" arm64 ) call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateMonacoWxs.ps1 -monacoWxsFile "$(MSBuildThisFileDirectory)\MonacoSRC.wxs" -Platform "$(Platform)" -nugetHeatPath "$(NUGET_PACKAGES)\wixtoolset.heat\5.0.2" -call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuildThisFileDirectory)\generateDscResourcesWxs.ps1 -dscWxsFile "$(MSBuildThisFileDirectory)\DscResources.wxs" -Platform "$(Platform)" -Configuration "$(Configuration)" @@ -37,6 +35,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil call move /Y ..\..\..\CmdPal.wxs.bk ..\..\..\CmdPal.wxs call move /Y ..\..\..\ColorPicker.wxs.bk ..\..\..\ColorPicker.wxs call move /Y ..\..\..\Core.wxs.bk ..\..\..\Core.wxs + call move /Y ..\..\..\DscResources.wxs.bk ..\..\..\DscResources.wxs call move /Y ..\..\..\EnvironmentVariables.wxs.bk ..\..\..\EnvironmentVariables.wxs call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs @@ -60,6 +59,12 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil call move /Y ..\..\..\Workspaces.wxs.bk ..\..\..\Workspaces.wxs + + + + false + false + $(DefineConstants);PerUser=true diff --git a/installer/PowerToysSetupVNext/Product.wxs b/installer/PowerToysSetupVNext/Product.wxs index 556fddc7f4..3e812beb2e 100644 --- a/installer/PowerToysSetupVNext/Product.wxs +++ b/installer/PowerToysSetupVNext/Product.wxs @@ -120,8 +120,8 @@ - - + + diff --git a/installer/PowerToysSetupVNext/Settings.wxs b/installer/PowerToysSetupVNext/Settings.wxs index f9e5312ea7..cf7cf7f727 100644 --- a/installer/PowerToysSetupVNext/Settings.wxs +++ b/installer/PowerToysSetupVNext/Settings.wxs @@ -14,11 +14,16 @@ + + + - + + + @@ -45,6 +50,11 @@ + + + + + @@ -67,6 +77,7 @@ + diff --git a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 index b6f2f88dd0..6724d95170 100644 --- a/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 +++ b/installer/PowerToysSetupVNext/generateAllFileComponents.ps1 @@ -1,9 +1,7 @@ [CmdletBinding()] Param( [Parameter(Mandatory = $True, Position = 1)] - [string]$platform, - [Parameter(Mandatory = $False, Position = 2)] - [string]$installscopeperuser = "false" + [string]$platform ) Function Generate-FileList() { @@ -77,9 +75,7 @@ Function Generate-FileComponents() { [Parameter(Mandatory = $True, Position = 1)] [string]$fileListName, [Parameter(Mandatory = $True, Position = 2)] - [string]$wxsFilePath, - [Parameter(Mandatory = $True, Position = 3)] - [string]$regroot + [string]$wxsFilePath ) $wxsFile = Get-Content $wxsFilePath; @@ -100,7 +96,7 @@ Function Generate-FileComponents() { $componentDefs += @" - + `r`n "@ @@ -134,194 +130,194 @@ if ($platform -ceq "arm64") { $platform = "ARM64" } -if ($installscopeperuser -eq "true") { - $registryroot = "HKCU" -} else { - $registryroot = "HKLM" -} - #BaseApplications Generate-FileList -fileDepsJson "" -fileListName BaseApplicationsFiles -wxsFilePath $PSScriptRoot\BaseApplications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release" -Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs -regroot $registryroot +Generate-FileComponents -fileListName "BaseApplicationsFiles" -wxsFilePath $PSScriptRoot\BaseApplications.wxs #WinUI3Applications Generate-FileList -fileDepsJson "" -fileListName WinUI3ApplicationsFiles -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps" -Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs -regroot $registryroot +Generate-FileComponents -fileListName "WinUI3ApplicationsFiles" -wxsFilePath $PSScriptRoot\WinUI3Applications.wxs #AdvancedPaste Generate-FileList -fileDepsJson "" -fileListName AdvancedPasteAssetsFiles -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\AdvancedPaste" -Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs -regroot $registryroot +Generate-FileComponents -fileListName "AdvancedPasteAssetsFiles" -wxsFilePath $PSScriptRoot\AdvancedPaste.wxs #AwakeFiles Generate-FileList -fileDepsJson "" -fileListName AwakeImagesFiles -wxsFilePath $PSScriptRoot\Awake.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Awake" -Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs -regroot $registryroot +Generate-FileComponents -fileListName "AwakeImagesFiles" -wxsFilePath $PSScriptRoot\Awake.wxs #ColorPicker Generate-FileList -fileDepsJson "" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker" -Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot +Generate-FileComponents -fileListName "ColorPickerAssetsFiles" -wxsFilePath $PSScriptRoot\ColorPicker.wxs #Environment Variables Generate-FileList -fileDepsJson "" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables" -Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot +Generate-FileComponents -fileListName "EnvironmentVariablesAssetsFiles" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs #FileExplorerAdd-ons Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco" Generate-FileList -fileDepsJson "" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages" -Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot -Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -regroot $registryroot +Generate-FileComponents -fileListName "MonacoPreviewHandlerMonacoAssetsFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs +Generate-FileComponents -fileListName "MonacoPreviewHandlerCustomLanguagesFiles" -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs #FileLocksmith Generate-FileList -fileDepsJson "" -fileListName FileLocksmithAssetsFiles -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\FileLocksmith" -Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs -regroot $registryroot +Generate-FileComponents -fileListName "FileLocksmithAssetsFiles" -wxsFilePath $PSScriptRoot\FileLocksmith.wxs #Hosts Generate-FileList -fileDepsJson "" -fileListName HostsAssetsFiles -wxsFilePath $PSScriptRoot\Hosts.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Hosts" -Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs -regroot $registryroot +Generate-FileComponents -fileListName "HostsAssetsFiles" -wxsFilePath $PSScriptRoot\Hosts.wxs #ImageResizer Generate-FileList -fileDepsJson "" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\ImageResizer" -Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot +Generate-FileComponents -fileListName "ImageResizerAssetsFiles" -wxsFilePath $PSScriptRoot\ImageResizer.wxs # Light Switch Service Generate-FileList -fileDepsJson "" -fileListName LightSwitchFiles -wxsFilePath $PSScriptRoot\LightSwitch.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\LightSwitchService" -Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs -regroot $registryroot +Generate-FileComponents -fileListName "LightSwitchFiles" -wxsFilePath $PSScriptRoot\LightSwitch.wxs #New+ Generate-FileList -fileDepsJson "" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus" -Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot +Generate-FileComponents -fileListName "NewPlusAssetsFiles" -wxsFilePath $PSScriptRoot\NewPlus.wxs #Peek Generate-FileList -fileDepsJson "" -fileListName PeekAssetsFiles -wxsFilePath $PSScriptRoot\Peek.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Peek\" -Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs -regroot $registryroot +Generate-FileComponents -fileListName "PeekAssetsFiles" -wxsFilePath $PSScriptRoot\Peek.wxs #PowerRename Generate-FileList -fileDepsJson "" -fileListName PowerRenameAssetsFiles -wxsFilePath $PSScriptRoot\PowerRename.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\PowerRename\" -Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs -regroot $registryroot +Generate-FileComponents -fileListName "PowerRenameAssetsFiles" -wxsFilePath $PSScriptRoot\PowerRename.wxs #RegistryPreview Generate-FileList -fileDepsJson "" -fileListName RegistryPreviewAssetsFiles -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\RegistryPreview\" -Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs -regroot $registryroot +Generate-FileComponents -fileListName "RegistryPreviewAssetsFiles" -wxsFilePath $PSScriptRoot\RegistryPreview.wxs #Run Generate-FileList -fileDepsJson "" -fileListName launcherImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\PowerLauncher" -Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "launcherImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ## Plugins ###Calculator Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Microsoft.PowerToys.Run.Plugin.Calculator.deps.json" -fileListName calcComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName calcImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Calculator\Images" -Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "calcComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "calcImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###Folder Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Microsoft.Plugin.Folder.deps.json" -fileListName FolderComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName FolderImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Folder\Images" -Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "FolderComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "FolderImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###Program Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Microsoft.Plugin.Program.deps.json" -fileListName ProgramComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName ProgramImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Program\Images" -Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "ProgramComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "ProgramImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###Shell Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Microsoft.Plugin.Shell.deps.json" -fileListName ShellComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName ShellImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Shell\Images" -Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "ShellComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "ShellImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###Indexer Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Microsoft.Plugin.Indexer.deps.json" -fileListName IndexerComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName IndexerImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Indexer\Images" -Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "IndexerComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "IndexerImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###UnitConverter Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Community.PowerToys.Run.Plugin.UnitConverter.deps.json" -fileListName UnitConvCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName UnitConvImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\UnitConverter\Images" -Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "UnitConvCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "UnitConvImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###WebSearch Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Community.PowerToys.Run.Plugin.WebSearch.deps.json" -fileListName WebSrchCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName WebSrchImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WebSearch\Images" -Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "WebSrchCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "WebSrchImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###History Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Microsoft.PowerToys.Run.Plugin.History.deps.json" -fileListName HistoryPluginComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName HistoryPluginImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\History\Images" -Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "HistoryPluginComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "HistoryPluginImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###Uri Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Microsoft.Plugin.Uri.deps.json" -fileListName UriComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName UriImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Uri\Images" -Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "UriComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "UriImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###VSCodeWorkspaces Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Community.PowerToys.Run.Plugin.VSCodeWorkspaces.deps.json" -fileListName VSCWrkCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName VSCWrkImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\VSCodeWorkspaces\Images" -Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "VSCWrkCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "VSCWrkImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###WindowWalker Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Microsoft.Plugin.WindowWalker.deps.json" -fileListName WindowWlkrCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName WindowWlkrImagesCompFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowWalker\Images" -Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "WindowWlkrCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "WindowWlkrImagesCompFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###OneNote Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Microsoft.PowerToys.Run.Plugin.OneNote.deps.json" -fileListName OneNoteComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName OneNoteImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\OneNote\Images" -Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "OneNoteComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "OneNoteImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###Registry Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Microsoft.PowerToys.Run.Plugin.Registry.deps.json" -fileListName RegistryComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName RegistryImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Registry\Images" -Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "RegistryComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "RegistryImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###Service Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Microsoft.PowerToys.Run.Plugin.Service.deps.json" -fileListName ServiceComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName ServiceImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\Service\Images" -Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "ServiceComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "ServiceImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###System Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Microsoft.PowerToys.Run.Plugin.System.deps.json" -fileListName SystemComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName SystemImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\System\Images" -Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "SystemComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "SystemImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###TimeDate Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Microsoft.PowerToys.Run.Plugin.TimeDate.deps.json" -fileListName TimeDateComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName TimeDateImagesComponentFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\TimeDate\Images" -Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "TimeDateComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "TimeDateImagesComponentFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###WindowsSettings Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Microsoft.PowerToys.Run.Plugin.WindowsSettings.deps.json" -fileListName WinSetCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName WinSetImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsSettings\Images" -Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "WinSetCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "WinSetImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###WindowsTerminal Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.deps.json" -fileListName WinTermCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName WinTermImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\WindowsTerminal\Images" -Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "WinTermCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "WinTermImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###PowerToys Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Microsoft.PowerToys.Run.Plugin.PowerToys.deps.json" -fileListName PowerToysCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName PowerToysImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\PowerToys\Images" -Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "PowerToysCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "PowerToysImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs ###ValueGenerator Generate-FileList -fileDepsJson "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Community.PowerToys.Run.Plugin.ValueGenerator.deps.json" -fileListName ValueGeneratorCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -isLauncherPlugin 1 Generate-FileList -fileDepsJson "" -fileListName ValueGeneratorImagesCmpFiles -wxsFilePath $PSScriptRoot\Run.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\RunPlugins\ValueGenerator\Images" -Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot -Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs -regroot $registryroot +Generate-FileComponents -fileListName "ValueGeneratorCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs +Generate-FileComponents -fileListName "ValueGeneratorImagesCmpFiles" -wxsFilePath $PSScriptRoot\Run.wxs ## Plugins #ShortcutGuide Generate-FileList -fileDepsJson "" -fileListName ShortcutGuideSvgFiles -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\ShortcutGuide\" -Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs -regroot $registryroot +Generate-FileComponents -fileListName "ShortcutGuideSvgFiles" -wxsFilePath $PSScriptRoot\ShortcutGuide.wxs #Settings Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\" Generate-FileList -fileDepsJson "" -fileListName SettingsV2AssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\" Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsModulesFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Modules\OOBE\" Generate-FileList -fileDepsJson "" -fileListName SettingsV2OOBEAssetsFluentIconsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\" -Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot -Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot -Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot -Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs -regroot $registryroot +Generate-FileList -fileDepsJson "" -fileListName SettingsV2IconsModelsFiles -wxsFilePath $PSScriptRoot\Settings.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Settings\Icons\Models\" +Generate-FileComponents -fileListName "SettingsV2AssetsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs +Generate-FileComponents -fileListName "SettingsV2AssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs +Generate-FileComponents -fileListName "SettingsV2OOBEAssetsModulesFiles" -wxsFilePath $PSScriptRoot\Settings.wxs +Generate-FileComponents -fileListName "SettingsV2OOBEAssetsFluentIconsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs +Generate-FileComponents -fileListName "SettingsV2IconsModelsFiles" -wxsFilePath $PSScriptRoot\Settings.wxs #Workspaces Generate-FileList -fileDepsJson "" -fileListName WorkspacesImagesComponentFiles -wxsFilePath $PSScriptRoot\Workspaces.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\Assets\Workspaces\" -Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs -regroot $registryroot +Generate-FileComponents -fileListName "WorkspacesImagesComponentFiles" -wxsFilePath $PSScriptRoot\Workspaces.wxs + +#DSC Resources - JSON manifest files in DSCModules subfolder +Generate-FileList -fileDepsJson "" -fileListName DscJsonFiles -wxsFilePath $PSScriptRoot\DscResources.wxs -depsPath "$PSScriptRoot..\..\..\$platform\Release\DSCModules\" +Generate-FileComponents -fileListName "DscJsonFiles" -wxsFilePath $PSScriptRoot\DscResources.wxs diff --git a/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 b/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 deleted file mode 100644 index 14172db0bc..0000000000 --- a/installer/PowerToysSetupVNext/generateDscResourcesWxs.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -[CmdletBinding()] -Param( - [Parameter(Mandatory = $True)] - [string]$dscWxsFile, - [Parameter(Mandatory = $True)] - [string]$Platform, - [Parameter(Mandatory = $True)] - [string]$Configuration -) - -$ErrorActionPreference = 'Stop' - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path - -# Find build output directory -$buildOutputDir = Join-Path $scriptDir "..\..\$Platform\$Configuration" - -if (-not (Test-Path $buildOutputDir)) { - Write-Error "Build output directory not found: '$buildOutputDir'" - exit 1 -} - -# Find all DSC manifest JSON files -$dscFiles = Get-ChildItem -Path $buildOutputDir -Filter "microsoft.powertoys.*.settings.dsc.resource.json" -File - -if (-not $dscFiles) { - Write-Warning "No DSC manifest files found in '$buildOutputDir'" - # Create empty component group - $wxsContent = @" - - - - - - - - - -"@ - Set-Content -Path $dscWxsFile -Value $wxsContent - exit 0 -} - -Write-Host "Found $($dscFiles.Count) DSC manifest file(s)" - -# Generate WiX fragment -$wxsContent = @" - - - - - - -"@ - -$componentRefs = @() - -foreach ($file in $dscFiles) { - $componentId = "DscResource_" + ($file.BaseName -replace '[^A-Za-z0-9_]', '_') - $fileId = $componentId + "_File" - $guid = [System.Guid]::NewGuid().ToString().ToUpper() - - $componentRefs += $componentId - - $wxsContent += @" - - - - - - - -"@ -} - -$wxsContent += @" - - - - - - -"@ - -foreach ($componentId in $componentRefs) { - $wxsContent += @" - - -"@ -} - -$wxsContent += @" - - - - -"@ - -# Write the WiX file -Set-Content -Path $dscWxsFile -Value $wxsContent - -Write-Host "Generated DSC resources WiX fragment: '$dscWxsFile'" \ No newline at end of file diff --git a/src/PackageIdentity/AppxManifest.xml b/src/PackageIdentity/AppxManifest.xml index af637229ca..822daae8bc 100644 --- a/src/PackageIdentity/AppxManifest.xml +++ b/src/PackageIdentity/AppxManifest.xml @@ -36,31 +36,34 @@ - + + Square44x44Logo="Images\Square44x44Logo.png" + AppListEntry="none"> - + + Square44x44Logo="Images\Square44x44Logo.png" + AppListEntry="none"> - + + Square44x44Logo="Images\Square44x44Logo.png" + AppListEntry="none"> diff --git a/src/PackageIdentity/PackageIdentity.vcxproj b/src/PackageIdentity/PackageIdentity.vcxproj index b8bd2dc1f2..f8d34f5650 100644 --- a/src/PackageIdentity/PackageIdentity.vcxproj +++ b/src/PackageIdentity/PackageIdentity.vcxproj @@ -17,7 +17,7 @@ - diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index 361255f66f..2b256cd926 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -112,6 +112,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getConfiguredMousePointerCrosshairsEnabledValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredCursorWrapEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredCursorWrapEnabledValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredPowerRenameEnabledValue() { return static_cast(powertoys_gpo::getConfiguredPowerRenameEnabledValue()); @@ -192,6 +196,34 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue()); } + GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOpenAIValue() + { + return static_cast(powertoys_gpo::getAllowedAdvancedPasteOpenAIValue()); + } + GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteAzureOpenAIValue() + { + return static_cast(powertoys_gpo::getAllowedAdvancedPasteAzureOpenAIValue()); + } + GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteAzureAIInferenceValue() + { + return static_cast(powertoys_gpo::getAllowedAdvancedPasteAzureAIInferenceValue()); + } + GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteMistralValue() + { + return static_cast(powertoys_gpo::getAllowedAdvancedPasteMistralValue()); + } + GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteGoogleValue() + { + return static_cast(powertoys_gpo::getAllowedAdvancedPasteGoogleValue()); + } + GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOllamaValue() + { + return static_cast(powertoys_gpo::getAllowedAdvancedPasteOllamaValue()); + } + GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteFoundryLocalValue() + { + return static_cast(powertoys_gpo::getAllowedAdvancedPasteFoundryLocalValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusEnabledValue() { return static_cast(powertoys_gpo::getConfiguredNewPlusEnabledValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index c0fff9f542..e57cccccd9 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -35,6 +35,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue(); static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue(); static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue(); + static GpoRuleConfigured GetConfiguredCursorWrapEnabledValue(); static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); static GpoRuleConfigured GetConfiguredQuickAccentEnabledValue(); @@ -54,6 +55,13 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue(); static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue(); static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteOpenAIValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteAzureOpenAIValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue(); static GpoRuleConfigured GetConfiguredNewPlusEnabledValue(); static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue(); static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue(); diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 630beab9c9..06d035aa35 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -38,6 +38,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredMouseHighlighterEnabledValue(); static GpoRuleConfigured GetConfiguredMouseJumpEnabledValue(); static GpoRuleConfigured GetConfiguredMousePointerCrosshairsEnabledValue(); + static GpoRuleConfigured GetConfiguredCursorWrapEnabledValue(); static GpoRuleConfigured GetConfiguredMouseWithoutBordersEnabledValue(); static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); @@ -58,6 +59,13 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue(); static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue(); static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteOpenAIValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteAzureOpenAIValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteAzureAIInferenceValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteMistralValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteGoogleValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteOllamaValue(); + static GpoRuleConfigured GetAllowedAdvancedPasteFoundryLocalValue(); static GpoRuleConfigured GetConfiguredNewPlusEnabledValue(); static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue(); static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue(); diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryCachedModel.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryCachedModel.cs new file mode 100644 index 0000000000..489a779179 --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryCachedModel.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace LanguageModelProvider.FoundryLocal; + +internal sealed record FoundryCachedModel(string Name, string? Id); diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryCatalogModel.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryCatalogModel.cs new file mode 100644 index 0000000000..413bb47316 --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryCatalogModel.cs @@ -0,0 +1,61 @@ +// 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.Text.Json.Serialization; + +namespace LanguageModelProvider.FoundryLocal; + +internal sealed record FoundryCatalogModel +{ + [JsonPropertyName("name")] + public string Name { get; init; } = string.Empty; + + [JsonPropertyName("displayName")] + public string DisplayName { get; init; } = string.Empty; + + [JsonPropertyName("providerType")] + public string ProviderType { get; init; } = string.Empty; + + [JsonPropertyName("uri")] + public string Uri { get; init; } = string.Empty; + + [JsonPropertyName("version")] + public string Version { get; init; } = string.Empty; + + [JsonPropertyName("modelType")] + public string ModelType { get; init; } = string.Empty; + + [JsonPropertyName("promptTemplate")] + public PromptTemplate PromptTemplate { get; init; } = default!; + + [JsonPropertyName("publisher")] + public string Publisher { get; init; } = string.Empty; + + [JsonPropertyName("task")] + public string Task { get; init; } = string.Empty; + + [JsonPropertyName("runtime")] + public Runtime Runtime { get; init; } = default!; + + [JsonPropertyName("fileSizeMb")] + public long FileSizeMb { get; init; } + + [JsonPropertyName("modelSettings")] + public ModelSettings ModelSettings { get; init; } = default!; + + [JsonPropertyName("alias")] + public string Alias { get; init; } = string.Empty; + + [JsonPropertyName("supportsToolCalling")] + public bool SupportsToolCalling { get; init; } + + [JsonPropertyName("license")] + public string License { get; init; } = string.Empty; + + [JsonPropertyName("licenseDescription")] + public string LicenseDescription { get; init; } = string.Empty; + + [JsonPropertyName("parentModelUri")] + public string ParentModelUri { get; init; } = string.Empty; +} diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs new file mode 100644 index 0000000000..a279f7389a --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using ManagedCommon; +using Microsoft.AI.Foundry.Local; + +namespace LanguageModelProvider.FoundryLocal; + +internal sealed class FoundryClient +{ + public static async Task CreateAsync() + { + // First attempt with current environment + var client = await TryCreateClientAsync().ConfigureAwait(false); + if (client != null) + { + return client; + } + + // If failed, refresh PATH from registry and retry once + // This handles cases where PowerToys was launched by MSI installer. + Logger.LogInfo("[FoundryClient] First attempt failed, refreshing PATH and retrying"); + RefreshEnvironmentPath(); + + return await TryCreateClientAsync().ConfigureAwait(false); + } + + private static async Task TryCreateClientAsync() + { + try + { + Logger.LogInfo("[FoundryClient] Creating Foundry Local client"); + + var manager = new FoundryLocalManager(); + + // Check if service is already running + if (manager.IsServiceRunning) + { + Logger.LogInfo("[FoundryClient] Foundry service is already running"); + return new FoundryClient(manager); + } + + // Start the service using SDK's method + Logger.LogInfo("[FoundryClient] Starting Foundry service using manager.StartServiceAsync()"); + await manager.StartServiceAsync().ConfigureAwait(false); + + Logger.LogInfo("[FoundryClient] Foundry service started successfully"); + return new FoundryClient(manager); + } + catch (Exception ex) + { + Logger.LogError($"[FoundryClient] Error creating client: {ex.Message}"); + if (ex.InnerException != null) + { + Logger.LogError($"[FoundryClient] Inner exception: {ex.InnerException.Message}"); + } + + return null; + } + } + + private readonly FoundryLocalManager _foundryManager; + private readonly List _catalogModels = []; + + private FoundryClient(FoundryLocalManager foundryManager) + { + _foundryManager = foundryManager; + } + + public Task GetServiceUrl() + { + try + { + return Task.FromResult(_foundryManager.Endpoint?.ToString()); + } + catch + { + return Task.FromResult(null); + } + } + + public Uri? GetServiceUri() + { + try + { + return _foundryManager.ServiceUri; + } + catch + { + return null; + } + } + + public async Task> ListCatalogModels() + { + if (_catalogModels.Count > 0) + { + return _catalogModels; + } + + try + { + Logger.LogInfo("[FoundryClient] Listing catalog models"); + var models = await _foundryManager.ListCatalogModelsAsync().ConfigureAwait(false); + + if (models != null) + { + foreach (var model in models) + { + _catalogModels.Add(new FoundryCatalogModel + { + Name = model.ModelId ?? string.Empty, + DisplayName = model.DisplayName ?? string.Empty, + ProviderType = model.ProviderType ?? string.Empty, + Uri = model.Uri ?? string.Empty, + Version = model.Version ?? string.Empty, + ModelType = model.ModelType ?? string.Empty, + Publisher = model.Publisher ?? string.Empty, + Task = model.Task ?? string.Empty, + FileSizeMb = model.FileSizeMb, + Alias = model.Alias ?? string.Empty, + License = model.License ?? string.Empty, + LicenseDescription = model.LicenseDescription ?? string.Empty, + ParentModelUri = model.ParentModelUri ?? string.Empty, + SupportsToolCalling = model.SupportsToolCalling, + }); + } + + Logger.LogInfo($"[FoundryClient] Found {_catalogModels.Count} catalog models"); + } + } + catch (Exception ex) + { + Logger.LogError($"[FoundryClient] Error listing catalog models: {ex.Message}"); + + // Surfacing errors here prevents listing other providers; swallow and return cached list instead. + } + + return _catalogModels; + } + + public async Task> ListCachedModels() + { + try + { + Logger.LogInfo("[FoundryClient] Listing cached models"); + var cachedModels = await _foundryManager.ListCachedModelsAsync().ConfigureAwait(false); + var catalogModels = await ListCatalogModels().ConfigureAwait(false); + + List models = []; + + foreach (var model in cachedModels) + { + var catalogModel = catalogModels.FirstOrDefault(m => m.Name == model.ModelId); + var alias = catalogModel?.Alias ?? model.Alias; + models.Add(new FoundryCachedModel(model.ModelId ?? string.Empty, alias)); + } + + Logger.LogInfo($"[FoundryClient] Found {models.Count} cached models"); + return models; + } + catch (Exception ex) + { + Logger.LogError($"[FoundryClient] Error listing cached models: {ex.Message}"); + return []; + } + } + + public async Task IsModelLoaded(string modelId) + { + try + { + var loadedModels = await _foundryManager.ListLoadedModelsAsync().ConfigureAwait(false); + var isLoaded = loadedModels.Any(m => m.ModelId == modelId); + Logger.LogInfo($"[FoundryClient] IsModelLoaded({modelId}): {isLoaded}"); + Logger.LogInfo($"[FoundryClient] Loaded models: {string.Join(", ", loadedModels.Select(m => m.ModelId))}"); + return isLoaded; + } + catch (Exception ex) + { + Logger.LogError($"[FoundryClient] IsModelLoaded exception: {ex.Message}"); + return false; + } + } + + public async Task EnsureModelLoaded(string modelId) + { + Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}"); + + // Check if already loaded + if (await IsModelLoaded(modelId).ConfigureAwait(false)) + { + Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}"); + return true; + } + + // Load the model + Logger.LogInfo($"[FoundryClient] Loading model: {modelId}"); + await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false); + + // Verify it's loaded + var loaded = await IsModelLoaded(modelId).ConfigureAwait(false); + Logger.LogInfo($"[FoundryClient] Model load result: {loaded}"); + return loaded; + } + + public async Task EnsureRunning() + { + if (!_foundryManager.IsServiceRunning) + { + await _foundryManager.StartServiceAsync(); + } + } + + /// + /// Refreshes the PATH environment variable from the system registry. + /// This is necessary when tools are installed while PowerToys is running, + /// as the installer updates the system PATH but running processes don't see the change. + /// + private static void RefreshEnvironmentPath() + { + try + { + Logger.LogInfo("[FoundryClient] Refreshing PATH environment variable from system"); + + var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) ?? string.Empty; + var machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty; + var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty; + + var pathsToAdd = new List(); + + if (!string.IsNullOrWhiteSpace(currentPath)) + { + pathsToAdd.AddRange(currentPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)); + } + + if (!string.IsNullOrWhiteSpace(userPath)) + { + var userPaths = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in userPaths) + { + if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase)) + { + pathsToAdd.Add(path); + } + } + } + + if (!string.IsNullOrWhiteSpace(machinePath)) + { + var machinePaths = machinePath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries); + foreach (var path in machinePaths) + { + if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase)) + { + pathsToAdd.Add(path); + } + } + } + + var newPath = string.Join(Path.PathSeparator.ToString(), pathsToAdd); + + if (currentPath != newPath) + { + Logger.LogInfo("[FoundryClient] Updating process PATH with latest system values"); + Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process); + } + else + { + Logger.LogInfo("[FoundryClient] PATH is already up to date"); + } + } + catch (Exception ex) + { + Logger.LogError($"[FoundryClient] Failed to refresh PATH: {ex.Message}"); + } + } +} diff --git a/src/common/LanguageModelProvider/FoundryLocal/FoundryJsonContext.cs b/src/common/LanguageModelProvider/FoundryLocal/FoundryJsonContext.cs new file mode 100644 index 0000000000..5dcb4076ed --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocal/FoundryJsonContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace LanguageModelProvider.FoundryLocal; + +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = false)] +[JsonSerializable(typeof(FoundryCatalogModel))] +[JsonSerializable(typeof(List))] +internal sealed partial class FoundryJsonContext : JsonSerializerContext +{ +} diff --git a/src/common/LanguageModelProvider/FoundryLocal/ModelSettings.cs b/src/common/LanguageModelProvider/FoundryLocal/ModelSettings.cs new file mode 100644 index 0000000000..fda91217eb --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocal/ModelSettings.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LanguageModelProvider.FoundryLocal; + +internal sealed record ModelSettings +{ + // The sample shows an empty array; keep it open-ended. + [JsonPropertyName("parameters")] + public List Parameters { get; init; } = []; +} diff --git a/src/common/LanguageModelProvider/FoundryLocal/PromptTemplate.cs b/src/common/LanguageModelProvider/FoundryLocal/PromptTemplate.cs new file mode 100644 index 0000000000..a2cbb9fe45 --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocal/PromptTemplate.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +namespace LanguageModelProvider.FoundryLocal; + +internal sealed record PromptTemplate +{ + [JsonPropertyName("assistant")] + public string Assistant { get; init; } = string.Empty; + + [JsonPropertyName("prompt")] + public string Prompt { get; init; } = string.Empty; +} diff --git a/src/common/LanguageModelProvider/FoundryLocal/Runtime.cs b/src/common/LanguageModelProvider/FoundryLocal/Runtime.cs new file mode 100644 index 0000000000..e2019c8f87 --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocal/Runtime.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +namespace LanguageModelProvider.FoundryLocal; + +internal sealed record Runtime +{ + [JsonPropertyName("deviceType")] + public string DeviceType { get; init; } = string.Empty; + + [JsonPropertyName("executionProvider")] + public string ExecutionProvider { get; init; } = string.Empty; +} diff --git a/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs b/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs new file mode 100644 index 0000000000..5158e4334e --- /dev/null +++ b/src/common/LanguageModelProvider/FoundryLocalModelProvider.cs @@ -0,0 +1,156 @@ +// 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.ClientModel; +using LanguageModelProvider.FoundryLocal; +using ManagedCommon; +using Microsoft.Extensions.AI; +using OpenAI; + +namespace LanguageModelProvider; + +public sealed class FoundryLocalModelProvider : ILanguageModelProvider +{ + private FoundryClient? _foundryClient; + private IEnumerable? _catalogModels; + private string? _serviceUrl; + + public static FoundryLocalModelProvider Instance { get; } = new(); + + public string Name => "FoundryLocal"; + + public string ProviderDescription => "The model will run locally via Foundry Local"; + + public IChatClient? GetIChatClient(string modelId) + { + Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}"); + InitializeAsync().GetAwaiter().GetResult(); + + if (string.IsNullOrWhiteSpace(modelId)) + { + Logger.LogError("[FoundryLocal] Model ID is empty after extraction"); + return null; + } + + // Check if model is in catalog + var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false; + if (!isInCatalog) + { + var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings."; + Logger.LogError($"[FoundryLocal] {errorMessage}"); + throw new InvalidOperationException(errorMessage); + } + + // Ensure the model is loaded before returning chat client + var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult(); + if (!isLoaded) + { + Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}"); + throw new InvalidOperationException($"Failed to load the model '{modelId}'."); + } + + // Use ServiceUri instead of Endpoint since Endpoint already includes /v1 + var baseUri = _foundryClient.GetServiceUri(); + if (baseUri == null) + { + const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running."; + Logger.LogError($"[FoundryLocal] {message}"); + throw new InvalidOperationException(message); + } + + var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1"); + Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}"); + + return new OpenAIClient( + new ApiKeyCredential("none"), + new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) }) + .GetChatClient(modelId) + .AsIChatClient(); + } + + public string GetIChatClientString(string url) + { + try + { + InitializeAsync().GetAwaiter().GetResult(); + } + catch + { + return string.Empty; + } + + var modelId = url.Split('/').LastOrDefault(); + + if (string.IsNullOrWhiteSpace(_serviceUrl) || string.IsNullOrWhiteSpace(modelId)) + { + return string.Empty; + } + + return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()"; + } + + public async Task> GetModelsAsync(CancellationToken cancelationToken = default) + { + await InitializeAsync(cancelationToken); + + if (_foundryClient == null) + { + return Array.Empty(); + } + + var cachedModels = await _foundryClient.ListCachedModels(); + List downloadedModels = []; + + foreach (var model in cachedModels) + { + Logger.LogInfo($"[FoundryLocal] Adding unmatched cached model: {model.Name}"); + downloadedModels.Add(new ModelDetails + { + Id = $"fl-{model.Name}", + Name = model.Name, + Url = $"fl://{model.Name}", + Description = $"{model.Name} running locally with Foundry Local", + HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL], + ProviderModelDetails = model, + }); + } + + return downloadedModels; + } + + private async Task InitializeAsync(CancellationToken cancelationToken = default) + { + if (_foundryClient != null && _catalogModels != null && _catalogModels.Any()) + { + await _foundryClient.EnsureRunning().ConfigureAwait(false); + return; + } + + Logger.LogInfo("[FoundryLocal] Initializing provider"); + _foundryClient ??= await FoundryClient.CreateAsync(); + + if (_foundryClient == null) + { + const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running."; + Logger.LogError($"[FoundryLocal] {message}"); + throw new InvalidOperationException(message); + } + + _serviceUrl ??= await _foundryClient.GetServiceUrl(); + Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}"); + + var catalogModels = await _foundryClient.ListCatalogModels(); + Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models"); + _catalogModels = catalogModels; + } + + public async Task IsAvailable() + { + Logger.LogInfo("[FoundryLocal] Checking availability"); + await InitializeAsync(); + var available = _foundryClient != null; + Logger.LogInfo($"[FoundryLocal] Available: {available}"); + return available; + } +} diff --git a/src/common/LanguageModelProvider/HardwareAccelerator.cs b/src/common/LanguageModelProvider/HardwareAccelerator.cs new file mode 100644 index 0000000000..d2c94b8155 --- /dev/null +++ b/src/common/LanguageModelProvider/HardwareAccelerator.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace LanguageModelProvider; + +public enum HardwareAccelerator +{ + CPU, + DML, + QNN, + WCRAPI, + OLLAMA, + OPENAI, + FOUNDRYLOCAL, + LEMONADE, + NPU, + GPU, + VitisAI, + OpenVINO, + NvTensorRT, +} diff --git a/src/common/LanguageModelProvider/ILanguageModelProvider.cs b/src/common/LanguageModelProvider/ILanguageModelProvider.cs new file mode 100644 index 0000000000..9d203adaf6 --- /dev/null +++ b/src/common/LanguageModelProvider/ILanguageModelProvider.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.AI; + +namespace LanguageModelProvider; + +public interface ILanguageModelProvider +{ + string Name { get; } + + string ProviderDescription { get; } + + Task> GetModelsAsync(CancellationToken cancelationToken = default); + + IChatClient? GetIChatClient(string modelId); + + string GetIChatClientString(string url); +} diff --git a/src/common/LanguageModelProvider/LanguageModelProvider.csproj b/src/common/LanguageModelProvider/LanguageModelProvider.csproj new file mode 100644 index 0000000000..4dba9247a3 --- /dev/null +++ b/src/common/LanguageModelProvider/LanguageModelProvider.csproj @@ -0,0 +1,20 @@ + + + + + + enable + enable + + + + + + + + + + + + + diff --git a/src/common/LanguageModelProvider/ModelDetails.cs b/src/common/LanguageModelProvider/ModelDetails.cs new file mode 100644 index 0000000000..e383aa7d27 --- /dev/null +++ b/src/common/LanguageModelProvider/ModelDetails.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace LanguageModelProvider; + +public class ModelDetails +{ + public string Id { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public long Size { get; set; } + + public bool IsUserAdded { get; set; } + + public string Icon { get; set; } = string.Empty; + + public List HardwareAccelerators { get; set; } = []; + + public string License { get; set; } = string.Empty; + + public object? ProviderModelDetails { get; set; } +} diff --git a/src/common/ManagedCommon/Logger.cs b/src/common/ManagedCommon/Logger.cs index 11115b1846..7f72cdd78b 100644 --- a/src/common/ManagedCommon/Logger.cs +++ b/src/common/ManagedCommon/Logger.cs @@ -26,6 +26,21 @@ namespace ManagedCommon private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "Unknown"; + /// + /// Gets the path to the log directory for the current version of the app. + /// + public static string CurrentVersionLogDirectoryPath { get; private set; } + + /// + /// Gets the path to the current log file. + /// + public static string CurrentLogFile { get; private set; } + + /// + /// Gets the path to the log directory for the app. + /// + public static string AppLogDirectoryPath { get; private set; } + /// /// Initializes the logger and sets the path for logging. /// @@ -42,7 +57,12 @@ namespace ManagedCommon Directory.CreateDirectory(versionedPath); } - var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"); + AppLogDirectoryPath = basePath; + CurrentVersionLogDirectoryPath = versionedPath; + + var logFile = "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"; + var logFilePath = Path.Combine(versionedPath, logFile); + CurrentLogFile = logFilePath; Trace.Listeners.Add(new TextWriterTraceListener(logFilePath)); @@ -130,7 +150,7 @@ namespace ManagedCommon { exMessage += "Inner exception: " + Environment.NewLine + - ex.InnerException.GetType() + " (" + ex.HResult + "): " + ex.InnerException.Message + Environment.NewLine; + ex.InnerException.GetType() + " (" + ex.InnerException.HResult + "): " + ex.InnerException.Message + Environment.NewLine; } exMessage += diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs index aa741e2f3a..d7ae386191 100644 --- a/src/common/ManagedCommon/ModuleType.cs +++ b/src/common/ManagedCommon/ModuleType.cs @@ -12,6 +12,7 @@ namespace ManagedCommon ColorPicker, CmdPal, CropAndLock, + CursorWrap, EnvironmentVariables, FancyZones, FileLocksmith, diff --git a/src/common/UITestAutomation/SettingsConfigHelper.cs b/src/common/UITestAutomation/SettingsConfigHelper.cs new file mode 100644 index 0000000000..833ec4f19d --- /dev/null +++ b/src/common/UITestAutomation/SettingsConfigHelper.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text.Json; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Helper class for configuring PowerToys settings for UI tests. + /// + public class SettingsConfigHelper + { + private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true }; + private static readonly SettingsUtils SettingsUtils = new SettingsUtils(); + + /// + /// Configures global PowerToys settings to enable only specified modules and disable all others. + /// + /// Array of module names to enable (e.g., "Peek", "FancyZones"). All other modules will be disabled. + /// Thrown when modulesToEnable is null. + /// Thrown when settings file operations fail. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")] + public static void ConfigureGlobalModuleSettings(params string[] modulesToEnable) + { + ArgumentNullException.ThrowIfNull(modulesToEnable); + + try + { + GeneralSettings settings; + try + { + settings = SettingsUtils.GetSettingsOrDefault(); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to load settings, creating defaults: {ex.Message}"); + settings = new GeneralSettings(); + } + + string settingsJson = settings.ToJsonString(); + using (JsonDocument doc = JsonDocument.Parse(settingsJson)) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + var root = doc.RootElement.Clone(); + + if (root.TryGetProperty("enabled", out var enabledElement)) + { + var enabledModules = new Dictionary(); + + foreach (var property in enabledElement.EnumerateObject()) + { + string moduleName = property.Name; + + bool shouldEnable = Array.Exists(modulesToEnable, m => string.Equals(m, moduleName, StringComparison.Ordinal)); + enabledModules[moduleName] = shouldEnable; + } + + var settingsDict = JsonSerializer.Deserialize>(settingsJson); + if (settingsDict != null) + { + settingsDict["enabled"] = enabledModules; + settingsJson = JsonSerializer.Serialize(settingsDict, IndentedJsonOptions); + } + } + } + + SettingsUtils.SaveSettings(settingsJson); + + string enabledList = modulesToEnable.Length > 0 ? string.Join(", ", modulesToEnable) : "none"; + Debug.WriteLine($"Successfully updated global settings"); + Debug.WriteLine($"Enabled modules: {enabledList}"); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR in ConfigureGlobalModuleSettings: {ex.Message}"); + throw new InvalidOperationException($"Failed to configure global module settings: {ex.Message}", ex); + } + } + + /// + /// Updates a module's settings file. If the file doesn't exist, creates it with default content. + /// If the file exists, reads it and applies the provided update function to modify the settings. + /// + /// The name of the module (e.g., "Peek", "FancyZones"). + /// The default JSON content to use if the settings file doesn't exist. + /// + /// A callback function that modifies the settings dictionary. The function receives the deserialized settings + /// and should modify it in-place. The function should accept a Dictionary<string, object> and not return a value. + /// Example: (settings) => { ((Dictionary<string, object>)settings["properties"])["SomeSetting"] = newValue; } + /// + /// Thrown when moduleName or updateSettingsAction is null. + /// Thrown when settings file operations fail. + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "This is test code and will not be trimmed")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "This is test code and will not be AOT compiled")] + public static void UpdateModuleSettings( + string moduleName, + string defaultSettingsContent, + Action> updateSettingsAction) + { + ArgumentNullException.ThrowIfNull(moduleName); + ArgumentNullException.ThrowIfNull(updateSettingsAction); + + try + { + // Build the path to the module settings file + string powerToysSettingsDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Microsoft", + "PowerToys"); + + string moduleDirectory = Path.Combine(powerToysSettingsDirectory, moduleName); + string settingsPath = Path.Combine(moduleDirectory, "settings.json"); + + // Ensure directory exists + Directory.CreateDirectory(moduleDirectory); + + // Read existing settings or use default + string existingJson = string.Empty; + if (File.Exists(settingsPath)) + { + existingJson = File.ReadAllText(settingsPath); + } + + Dictionary? settings; + + // If file doesn't exist or is empty, create from defaults + if (string.IsNullOrWhiteSpace(existingJson)) + { + if (string.IsNullOrWhiteSpace(defaultSettingsContent)) + { + throw new ArgumentException("Default settings content must be provided when file doesn't exist.", nameof(defaultSettingsContent)); + } + + settings = JsonSerializer.Deserialize>(defaultSettingsContent) + ?? throw new InvalidOperationException($"Failed to deserialize default settings for {moduleName}"); + + Debug.WriteLine($"Created default settings for {moduleName} at {settingsPath}"); + } + else + { + // Parse existing settings + settings = JsonSerializer.Deserialize>(existingJson) + ?? throw new InvalidOperationException($"Failed to deserialize existing settings for {moduleName}"); + + Debug.WriteLine($"Loaded existing settings for {moduleName} from {settingsPath}"); + } + + // Apply the update action to modify settings + updateSettingsAction(settings); + + // Serialize and save the updated settings using SettingsUtils + string updatedJson = JsonSerializer.Serialize(settings, IndentedJsonOptions); + SettingsUtils.SaveSettings(updatedJson, moduleName); + + Debug.WriteLine($"Successfully updated settings for {moduleName}"); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR in UpdateModuleSettings for {moduleName}: {ex.Message}"); + throw new InvalidOperationException($"Failed to update settings for {moduleName}: {ex.Message}", ex); + } + } + } +} diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj index add7acfeb9..549b8a430b 100644 --- a/src/common/UITestAutomation/UITestAutomation.csproj +++ b/src/common/UITestAutomation/UITestAutomation.csproj @@ -8,7 +8,7 @@ enable true true - net9.0-windows10.0.22621.0 + net9.0-windows10.0.26100.0 true false @@ -21,4 +21,8 @@ + + + + diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index b2e05fadfe..881633e05e 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -59,6 +59,7 @@ struct LogSettings inline const static std::string mouseHighlighterLoggerName = "mouse-highlighter"; inline const static std::string mouseJumpLoggerName = "mouse-jump"; inline const static std::string mousePointerCrosshairsLoggerName = "mouse-pointer-crosshairs"; + inline const static std::string cursorWrapLoggerName = "cursor-wrap"; inline const static std::string imageResizerLoggerName = "imageresizer"; inline const static std::string powerRenameLoggerName = "powerrename"; inline const static std::string alwaysOnTopLoggerName = "always-on-top"; diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 471cefe480..ab71d09d0b 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -3,6 +3,7 @@ #include #include #include +#include namespace powertoys_gpo { @@ -51,6 +52,7 @@ namespace powertoys_gpo const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_HIGHLIGHTER = L"ConfigureEnabledUtilityMouseHighlighter"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_JUMP = L"ConfigureEnabledUtilityMouseJump"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS = L"ConfigureEnabledUtilityMousePointerCrosshairs"; + const std::wstring POLICY_CONFIGURE_ENABLED_CURSOR_WRAP = L"ConfigureEnabledUtilityCursorWrap"; const std::wstring POLICY_CONFIGURE_ENABLED_POWER_RENAME = L"ConfigureEnabledUtilityPowerRename"; const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER = L"ConfigureEnabledUtilityPowerLauncher"; const std::wstring POLICY_CONFIGURE_ENABLED_QUICK_ACCENT = L"ConfigureEnabledUtilityQuickAccent"; @@ -82,6 +84,13 @@ namespace powertoys_gpo const std::wstring POLICY_CONFIGURE_RUN_AT_STARTUP = L"ConfigureRunAtStartup"; const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS = L"PowerLauncherAllPluginsEnabledState"; const std::wstring POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS = L"AllowPowerToysAdvancedPasteOnlineAIModels"; + const std::wstring POLICY_ALLOW_ADVANCED_PASTE_OPENAI = L"AllowAdvancedPasteOpenAI"; + const std::wstring POLICY_ALLOW_ADVANCED_PASTE_AZURE_OPENAI = L"AllowAdvancedPasteAzureOpenAI"; + const std::wstring POLICY_ALLOW_ADVANCED_PASTE_AZURE_AI_INFERENCE = L"AllowAdvancedPasteAzureAIInference"; + const std::wstring POLICY_ALLOW_ADVANCED_PASTE_MISTRAL = L"AllowAdvancedPasteMistral"; + const std::wstring POLICY_ALLOW_ADVANCED_PASTE_GOOGLE = L"AllowAdvancedPasteGoogle"; + const std::wstring POLICY_ALLOW_ADVANCED_PASTE_OLLAMA = L"AllowAdvancedPasteOllama"; + const std::wstring POLICY_ALLOW_ADVANCED_PASTE_FOUNDRY_LOCAL = L"AllowAdvancedPasteFoundryLocal"; const std::wstring POLICY_MWB_CLIPBOARD_SHARING_ENABLED = L"MwbClipboardSharingEnabled"; const std::wstring POLICY_MWB_FILE_TRANSFER_ENABLED = L"MwbFileTransferEnabled"; const std::wstring POLICY_MWB_USE_ORIGINAL_USER_INTERFACE = L"MwbUseOriginalUserInterface"; @@ -401,6 +410,11 @@ namespace powertoys_gpo return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_MOUSE_POINTER_CROSSHAIRS); } + inline gpo_rule_configured_t getConfiguredCursorWrapEnabledValue() + { + return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_CURSOR_WRAP); + } + inline gpo_rule_configured_t getConfiguredPowerRenameEnabledValue() { return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_POWER_RENAME); @@ -575,6 +589,41 @@ namespace powertoys_gpo return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS); } + inline gpo_rule_configured_t getAllowedAdvancedPasteOpenAIValue() + { + return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_OPENAI); + } + + inline gpo_rule_configured_t getAllowedAdvancedPasteAzureOpenAIValue() + { + return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_AZURE_OPENAI); + } + + inline gpo_rule_configured_t getAllowedAdvancedPasteAzureAIInferenceValue() + { + return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_AZURE_AI_INFERENCE); + } + + inline gpo_rule_configured_t getAllowedAdvancedPasteMistralValue() + { + return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_MISTRAL); + } + + inline gpo_rule_configured_t getAllowedAdvancedPasteGoogleValue() + { + return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_GOOGLE); + } + + inline gpo_rule_configured_t getAllowedAdvancedPasteOllamaValue() + { + return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_OLLAMA); + } + + inline gpo_rule_configured_t getAllowedAdvancedPasteFoundryLocalValue() + { + return getConfiguredValue(POLICY_ALLOW_ADVANCED_PASTE_FOUNDRY_LOCAL); + } + inline gpo_rule_configured_t getConfiguredMwbClipboardSharingEnabledValue() { return getConfiguredValue(POLICY_MWB_CLIPBOARD_SHARING_ENABLED); diff --git a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj index 3186a01d43..b36e602d25 100644 --- a/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj +++ b/src/dsc/PowerToys.Settings.DSC.Schema.Generator/PowerToys.Settings.DSC.Schema.Generator.csproj @@ -33,9 +33,4 @@ - - - - - diff --git a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs index deae2eb832..45fd36d10e 100644 --- a/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs +++ b/src/dsc/v3/PowerToys.DSC.UnitTests/SettingsResourceTests/SettingsResourceAdvancedPasteModuleTest.cs @@ -23,7 +23,8 @@ public sealed class SettingsResourceAdvancedPasteModuleTest : SettingsResourceMo { s.Properties.ShowCustomPreview = !s.Properties.ShowCustomPreview; s.Properties.CloseAfterLosingFocus = !s.Properties.CloseAfterLosingFocus; - s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled; + + // s.Properties.IsAdvancedAIEnabled = !s.Properties.IsAdvancedAIEnabled; s.Properties.AdvancedPasteUIShortcut = new HotkeySettings { Key = "mock", diff --git a/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs index dcb6abf4a1..5eb91acec3 100644 --- a/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs +++ b/src/dsc/v3/PowerToys.DSC/Models/DscManifest.cs @@ -13,7 +13,7 @@ namespace PowerToys.DSC.Models; public sealed class DscManifest { private const string Schema = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.vscode.json"; - private const string Executable = @"PowerToys.DSC.exe"; + private const string Executable = @"..\PowerToys.DSC.exe"; private readonly string _type; private readonly string _version; diff --git a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj index 230cd4556b..9dc11a0a8a 100644 --- a/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj +++ b/src/dsc/v3/PowerToys.DSC/PowerToys.DSC.csproj @@ -40,9 +40,11 @@ - - - - + + + + + + \ No newline at end of file diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index 07d4f44bde..4b77a6783f 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -1,11 +1,11 @@ - + - + @@ -26,6 +26,7 @@ + @@ -614,6 +615,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index 2703358bb0..1bfa55866d 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -1,7 +1,7 @@ - + PowerToys PowerToys @@ -33,6 +33,7 @@ PowerToys version 0.88.0 or later PowerToys version 0.89.0 or later PowerToys version 0.90.0 or later + PowerToys version 0.96.0 or later From PowerToys version 0.64.0 until PowerToys version 0.87.1 This policy configures the enabled state for all PowerToys utilities. @@ -291,6 +292,54 @@ If you don't configure this policy, the user will be able to control the setting QOI file preview: Configure enabled state QOI file thumbnail: Configure enabled state Allow using online AI models + Advanced Paste: Allow OpenAI endpoint + This policy controls whether users can use the OpenAI endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use OpenAI as their AI provider. + +If you disable this policy, users will not be able to select or use OpenAI endpoint in Advanced Paste settings. + Advanced Paste: Allow Azure OpenAI endpoint + This policy controls whether users can use the Azure OpenAI endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use Azure OpenAI as their AI provider. + +If you disable this policy, users will not be able to select or use Azure OpenAI endpoint in Advanced Paste settings. + Advanced Paste: Allow Azure AI Inference endpoint + This policy controls whether users can use the Azure AI Inference endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use Azure AI Inference as their AI provider. + +If you disable this policy, users will not be able to select or use Azure AI Inference endpoint in Advanced Paste settings. + Advanced Paste: Allow Mistral endpoint + This policy controls whether users can use the Mistral AI endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use Mistral as their AI provider. + +If you disable this policy, users will not be able to select or use Mistral endpoint in Advanced Paste settings. + Advanced Paste: Allow Google endpoint + This policy controls whether users can use the Google (Gemini) endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use Google as their AI provider. + +If you disable this policy, users will not be able to select or use Google endpoint in Advanced Paste settings. + Advanced Paste: Allow Anthropic endpoint + This policy controls whether users can use the Anthropic (Claude) endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use Anthropic as their AI provider. + +If you disable this policy, users will not be able to select or use Anthropic endpoint in Advanced Paste settings. + Advanced Paste: Allow Ollama endpoint + This policy controls whether users can use the Ollama local model endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use Ollama as their AI provider. + +If you disable this policy, users will not be able to select or use Ollama endpoint in Advanced Paste settings. + Advanced Paste: Allow Foundry Local endpoint + This policy controls whether users can use the Foundry Local model endpoint in Advanced Paste. + +If you enable or don't configure this policy, users can configure and use Foundry Local as their AI provider. + +If you disable this policy, users will not be able to select or use Foundry Local endpoint in Advanced Paste settings. Clipboard sharing enabled File transfer enabled Original user interface is available diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ConvertersTests/HexColorToColorConverterTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ConvertersTests/HexColorToColorConverterTests.cs new file mode 100644 index 0000000000..b8915f278e --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ConvertersTests/HexColorToColorConverterTests.cs @@ -0,0 +1,56 @@ +// 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 AdvancedPaste.Converters; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Windows.UI; + +namespace AdvancedPaste.UnitTests.ConvertersTests; + +[TestClass] +public sealed class HexColorToColorConverterTests +{ + [TestMethod] + public void TestConvert_ValidSixDigitHex_ReturnsColor() + { + Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#FFBFAB"); + Assert.IsNotNull(result); + + var color = (Windows.UI.Color)result; + Assert.AreEqual(255, color.R); + Assert.AreEqual(191, color.G); + Assert.AreEqual(171, color.B); + Assert.AreEqual(255, color.A); + } + + [TestMethod] + public void TestConvert_ValidThreeDigitHex_ReturnsColor() + { + Color? result = HexColorConverterHelper.ConvertHexColorToRgb("#abc"); + Assert.IsNotNull(result); + + var color = (Windows.UI.Color)result; + + // #abc should expand to #aabbcc + Assert.AreEqual(170, color.R); // 0xaa + Assert.AreEqual(187, color.G); // 0xbb + Assert.AreEqual(204, color.B); // 0xcc + Assert.AreEqual(255, color.A); + } + + [TestMethod] + public void TestConvert_NullOrEmpty_ReturnsNull() + { + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(null)); + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(string.Empty)); + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb(" ")); + } + + [TestMethod] + public void TestConvert_InvalidHex_ReturnsNull() + { + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#GGGGGG")); + Assert.IsNull(HexColorConverterHelper.ConvertHexColorToRgb("#12345")); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/HelpersTests/ClipboardItemHelperTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/HelpersTests/ClipboardItemHelperTests.cs new file mode 100644 index 0000000000..2b2a2c7595 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/HelpersTests/ClipboardItemHelperTests.cs @@ -0,0 +1,36 @@ +// 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 AdvancedPaste.Helpers; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace AdvancedPaste.UnitTests.HelpersTests; + +[TestClass] +public sealed class ClipboardItemHelperTests +{ + [TestMethod] + [DataRow("#FFBFAB", true)] + [DataRow("#000000", true)] + [DataRow("#FFFFFF", true)] + [DataRow("#fff", true)] + [DataRow("#abc", true)] + [DataRow("#123456", true)] + [DataRow("#AbCdEf", true)] + [DataRow("FFBFAB", false)] // Missing # + [DataRow("#GGGGGG", false)] // Invalid hex characters + [DataRow("#12345", false)] // Wrong length + [DataRow("#1234567", false)] // Too long + [DataRow("", false)] + [DataRow(null, false)] + [DataRow(" #FFF ", true)] // Whitespace should be trimmed + [DataRow("Not a color", false)] + [DataRow("#", false)] + [DataRow("##FFFFFF", false)] + public void TestIsRgbHexColor(string input, bool expected) + { + bool result = ClipboardItemHelper.IsRgbHexColor(input); + Assert.AreEqual(expected, result, $"IsRgbHexColor(\"{input}\") should return {expected}"); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs new file mode 100644 index 0000000000..4446e24dde --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/Mocks/IntegrationTestUserSettings.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using AdvancedPaste.Models; +using AdvancedPaste.Settings; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace AdvancedPaste.UnitTests.Mocks; + +/// +/// Minimal implementation used by integration tests that +/// need to construct the runtime Advanced Paste services. +/// +internal sealed class IntegrationTestUserSettings : IUserSettings +{ + private readonly PasteAIConfiguration _configuration; + private readonly IReadOnlyList _customActions; + private readonly IReadOnlyList _additionalActions; + + public IntegrationTestUserSettings() + { + var provider = new PasteAIProviderDefinition + { + Id = "integration-openai", + EnableAdvancedAI = true, + ServiceTypeKind = AIServiceType.OpenAI, + ModelName = "gpt-4o", + ModerationEnabled = true, + }; + + _configuration = new PasteAIConfiguration + { + ActiveProviderId = provider.Id, + Providers = new ObservableCollection { provider }, + }; + + _customActions = Array.Empty(); + _additionalActions = Array.Empty(); + } + + public bool IsAIEnabled => true; + + public bool ShowCustomPreview => false; + + public bool CloseAfterLosingFocus => false; + + public bool EnableClipboardPreview => true; + + public IReadOnlyList CustomActions => _customActions; + + public IReadOnlyList AdditionalActions => _additionalActions; + + public PasteAIConfiguration PasteAIConfiguration => _configuration; + + public event EventHandler Changed; + + public Task SetActiveAIProviderAsync(string providerId) + { + _configuration.ActiveProviderId = providerId ?? string.Empty; + Changed?.Invoke(this, EventArgs.Empty); + return Task.CompletedTask; + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs index 3782b057f1..17b8139bad 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/AIServiceBatchIntegrationTests.cs @@ -13,6 +13,8 @@ using System.Threading.Tasks; using AdvancedPaste.Helpers; using AdvancedPaste.Models; +using AdvancedPaste.Services; +using AdvancedPaste.Services.CustomActions; using AdvancedPaste.Services.OpenAI; using AdvancedPaste.UnitTests.Mocks; using ManagedCommon; @@ -79,7 +81,9 @@ public sealed class AIServiceBatchIntegrationTests Assert.IsTrue(results.Count <= inputs.Count); CollectionAssert.AreEqual(results.Select(result => result.ToInput()).ToList(), inputs.Take(results.Count).ToList()); + #pragma warning disable IL2026, IL3050 // The tests rely on runtime JSON serialization for ad-hoc data files. async Task WriteResultsAsync() => await File.WriteAllTextAsync(resultsFile, JsonSerializer.Serialize(results, SerializerOptions)); + #pragma warning restore IL2026, IL3050 Logger.LogInfo($"Starting {nameof(TestGenerateBatchResults)}; Count={inputs.Count}, InCache={results.Count}"); @@ -101,8 +105,12 @@ public sealed class AIServiceBatchIntegrationTests await WriteResultsAsync(); } - private static async Task> GetDataListAsync(string filePath) => - File.Exists(filePath) ? JsonSerializer.Deserialize>(await File.ReadAllTextAsync(filePath)) : []; + private static async Task> GetDataListAsync(string filePath) + { + #pragma warning disable IL2026, IL3050 // Tests only run locally and can depend on runtime JSON serialization. + return File.Exists(filePath) ? JsonSerializer.Deserialize>(await File.ReadAllTextAsync(filePath)) : []; + #pragma warning restore IL2026, IL3050 + } private static async Task GetTextOutputAsync(BatchTestInput input, PasteFormats format) { @@ -130,23 +138,35 @@ public sealed class AIServiceBatchIntegrationTests private static async Task GetOutputDataPackageAsync(BatchTestInput batchTestInput, PasteFormats format) { - VaultCredentialsProvider credentialsProvider = new(); - PromptModerationService promptModerationService = new(credentialsProvider); + var services = CreateServices(); NoOpProgress progress = new(); - CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService); switch (format) { case PasteFormats.CustomTextTransformation: - return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress)); + var transformResult = await services.CustomActionTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress); + return DataPackageHelpers.CreateFromText(transformResult.Content ?? string.Empty); case PasteFormats.KernelQuery: var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView(); - KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService); - return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress); + return await services.KernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress); default: throw new InvalidOperationException($"Unexpected format {format}"); } } + + private static IntegrationTestServices CreateServices() + { + IntegrationTestUserSettings userSettings = new(); + EnhancedVaultCredentialsProvider credentialsProvider = new(userSettings); + PromptModerationService promptModerationService = new(credentialsProvider); + PasteAIProviderFactory providerFactory = new(); + ICustomActionTransformService customActionTransformService = new CustomActionTransformService(promptModerationService, providerFactory, credentialsProvider, userSettings); + IKernelService kernelService = new AdvancedAIKernelService(credentialsProvider, new NoOpKernelQueryCacheService(), promptModerationService, userSettings, customActionTransformService); + + return new IntegrationTestServices(customActionTransformService, kernelService); + } + + private readonly record struct IntegrationTestServices(ICustomActionTransformService CustomActionTransformService, IKernelService KernelService); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs index 998534cf5e..7c16089cd5 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste.UnitTests/ServicesTests/KernelServiceIntegrationTests.cs @@ -11,6 +11,8 @@ using System.Threading.Tasks; using AdvancedPaste.Helpers; using AdvancedPaste.Models; +using AdvancedPaste.Services; +using AdvancedPaste.Services.CustomActions; using AdvancedPaste.Services.OpenAI; using AdvancedPaste.Telemetry; using AdvancedPaste.UnitTests.Mocks; @@ -27,16 +29,19 @@ namespace AdvancedPaste.UnitTests.ServicesTests; public sealed class KernelServiceIntegrationTests : IDisposable { private const string StandardImageFile = "image_with_text_example.png"; - private KernelService _kernelService; + private IKernelService _kernelService; private AdvancedPasteEventListener _eventListener; [TestInitialize] public void TestInitialize() { - VaultCredentialsProvider credentialsProvider = new(); + IntegrationTestUserSettings userSettings = new(); + EnhancedVaultCredentialsProvider credentialsProvider = new(userSettings); PromptModerationService promptModerationService = new(credentialsProvider); + PasteAIProviderFactory providerFactory = new(); + CustomActionTransformService customActionTransformService = new(promptModerationService, providerFactory, credentialsProvider, userSettings); - _kernelService = new KernelService(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, new CustomTextTransformService(credentialsProvider, promptModerationService)); + _kernelService = new AdvancedAIKernelService(credentialsProvider, new NoOpKernelQueryCacheService(), promptModerationService, userSettings, customActionTransformService); _eventListener = new(); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj index fba18de07c..1c80479c2d 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPaste.csproj @@ -33,11 +33,14 @@ + + + @@ -49,7 +52,6 @@ - @@ -57,10 +59,15 @@ + + + + + @@ -102,6 +109,7 @@ + @@ -114,9 +122,38 @@ true + + + MSBuild:Compile + + MSBuild:Compile + + + MSBuild:Compile + + + + + + + Assets\Settings\Icons\Models\%(Filename)%(Extension) + PreserveNewest + + + + PreserveNewest + + + PreserveNewest + + + + PreserveNewest + + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml index da79c36a11..df6ed811ac 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml @@ -9,6 +9,7 @@ + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs index 3ac3baa9d0..3fa940952e 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/App.xaml.cs @@ -10,10 +10,10 @@ using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; - using AdvancedPaste.Helpers; using AdvancedPaste.Models; using AdvancedPaste.Services; +using AdvancedPaste.Services.CustomActions; using AdvancedPaste.Settings; using AdvancedPaste.ViewModels; using ManagedCommon; @@ -77,11 +77,12 @@ namespace AdvancedPaste { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); }).Build(); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml index f03a579821..a250fdffdc 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/AnimatedContentControl/AnimatedContentControl.xaml @@ -11,7 +11,7 @@ - + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml new file mode 100644 index 0000000000..996f0c5b4f --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml.cs b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml.cs new file mode 100644 index 0000000000..765ba0e076 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/ClipboardHistoryItemPreviewControl.xaml.cs @@ -0,0 +1,129 @@ +// 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 AdvancedPaste.Helpers; +using AdvancedPaste.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; + +namespace AdvancedPaste.Controls +{ + public sealed partial class ClipboardHistoryItemPreviewControl : UserControl + { + public static readonly DependencyProperty ClipboardItemProperty = DependencyProperty.Register( + nameof(ClipboardItem), + typeof(ClipboardItem), + typeof(ClipboardHistoryItemPreviewControl), + new PropertyMetadata(defaultValue: null, OnClipboardItemChanged)); + + public ClipboardItem ClipboardItem + { + get => (ClipboardItem)GetValue(ClipboardItemProperty); + set => SetValue(ClipboardItemProperty, value); + } + + // Computed properties for display + public string Header => ClipboardItem != null ? GetHeaderFromFormat(ClipboardItem.Format) : string.Empty; + + public string IconGlyph => ClipboardItem != null ? GetGlyphFromFormat(ClipboardItem.Format) : string.Empty; + + public string ContentText => ClipboardItem?.Content ?? string.Empty; + + public ImageSource ContentImage => ClipboardItem?.Image; + + public DateTimeOffset? Timestamp => ClipboardItem?.Timestamp ?? ClipboardItem?.Item?.Timestamp; + + public bool HasImage => ContentImage is not null; + + public bool HasText => !string.IsNullOrEmpty(ContentText) && !HasImage && !HasColor; + + public bool HasGlyph => !HasImage && !HasText && !HasColor && !string.IsNullOrEmpty(IconGlyph); + + public bool HasColor => ClipboardItemHelper.IsRgbHexColor(ContentText); + + public ClipboardHistoryItemPreviewControl() + { + InitializeComponent(); + } + + private static void OnClipboardItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ClipboardHistoryItemPreviewControl control) + { + // Notify bindings that all computed properties may have changed + control.Bindings.Update(); + } + } + + private static string GetHeaderFromFormat(ClipboardFormat format) + { + // Check flags in priority order (most specific first) + if (format.HasFlag(ClipboardFormat.Image)) + { + return GetStringOrFallback("ClipboardPreviewCategoryImage", "Image"); + } + + if (format.HasFlag(ClipboardFormat.Video)) + { + return GetStringOrFallback("ClipboardPreviewCategoryVideo", "Video"); + } + + if (format.HasFlag(ClipboardFormat.Audio)) + { + return GetStringOrFallback("ClipboardPreviewCategoryAudio", "Audio"); + } + + if (format.HasFlag(ClipboardFormat.File)) + { + return GetStringOrFallback("ClipboardPreviewCategoryFile", "File"); + } + + if (format.HasFlag(ClipboardFormat.Text) || format.HasFlag(ClipboardFormat.Html)) + { + return GetStringOrFallback("ClipboardPreviewCategoryText", "Text"); + } + + return GetStringOrFallback("ClipboardPreviewCategoryUnknown", "Clipboard"); + } + + private static string GetGlyphFromFormat(ClipboardFormat format) + { + // Check flags in priority order (most specific first) + if (format.HasFlag(ClipboardFormat.Image)) + { + return "\uEB9F"; // Image icon + } + + if (format.HasFlag(ClipboardFormat.Video)) + { + return "\uE714"; // Video icon + } + + if (format.HasFlag(ClipboardFormat.Audio)) + { + return "\uE189"; // Audio icon + } + + if (format.HasFlag(ClipboardFormat.File)) + { + return "\uE8A5"; // File icon + } + + if (format.HasFlag(ClipboardFormat.Text) || format.HasFlag(ClipboardFormat.Html)) + { + return "\uE8D2"; // Text icon + } + + return "\uE77B"; // Generic clipboard icon + } + + private static string GetStringOrFallback(string resourceKey, string fallback) + { + var value = ResourceLoaderInstance.ResourceLoader.GetString(resourceKey); + return string.IsNullOrEmpty(value) ? fallback : value; + } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml index dd09c717b0..6303564d9b 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml +++ b/src/modules/AdvancedPaste/AdvancedPaste/AdvancedPasteXAML/Controls/PromptBox.xaml @@ -7,34 +7,21 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:AdvancedPaste.Controls" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:settings="using:Microsoft.PowerToys.Settings.UI.Library" xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters" xmlns:ui="using:CommunityToolkit.WinUI" + x:Name="PromptBoxControl" mc:Ignorable="d"> - - - #65C8F2 - - - - - - - - #005FB8 - - - - - - - - #48B1E9 - - - + + + + + + + 44 + diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png b/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png index 73621edfc0..78a9a18606 100644 Binary files a/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png and b/src/modules/AdvancedPaste/AdvancedPaste/Assets/AdvancedPaste/Gradient.png differ diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceFormatEvent.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceFormatEvent.cs index 1ab58bf269..b74192213b 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceFormatEvent.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceFormatEvent.cs @@ -18,6 +18,7 @@ namespace AdvancedPaste.Helpers PromptTokens = semanticKernelFormatEvent.PromptTokens; CompletionTokens = semanticKernelFormatEvent.CompletionTokens; ModelName = semanticKernelFormatEvent.ModelName; + ProviderType = semanticKernelFormatEvent.ProviderType; ActionChain = semanticKernelFormatEvent.ActionChain; } @@ -38,6 +39,8 @@ namespace AdvancedPaste.Helpers public string ModelName { get; set; } + public string ProviderType { get; set; } + public string ActionChain { get; set; } public string ToJsonString() => JsonSerializer.Serialize(this, SourceGenerationContext.Default.AIServiceFormatEvent); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceUsageHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceUsageHelper.cs new file mode 100644 index 0000000000..ba7d33f4fb --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/AIServiceUsageHelper.cs @@ -0,0 +1,54 @@ +// 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 AdvancedPaste.Models; +using Microsoft.SemanticKernel; + +namespace AdvancedPaste.Helpers; + +/// +/// Helper class for extracting AI service usage information from chat messages. +/// +public static class AIServiceUsageHelper +{ + /// + /// Extracts AI service usage information from OpenAI chat message metadata. + /// + /// The chat message containing usage metadata. + /// AI service usage information or AIServiceUsage.None if extraction fails. + public static AIServiceUsage GetOpenAIServiceUsage(ChatMessageContent chatMessage) + { + // Try to get usage information from metadata + if (chatMessage.Metadata?.TryGetValue("Usage", out var usageObj) == true) + { + // Handle different possible usage types through reflection to be version-agnostic + var usageType = usageObj.GetType(); + + try + { + // Try common property names for prompt tokens + var promptTokensProp = usageType.GetProperty("PromptTokens") ?? + usageType.GetProperty("InputTokens") ?? + usageType.GetProperty("InputTokenCount"); + + var completionTokensProp = usageType.GetProperty("CompletionTokens") ?? + usageType.GetProperty("OutputTokens") ?? + usageType.GetProperty("OutputTokenCount"); + + if (promptTokensProp != null && completionTokensProp != null) + { + var promptTokens = (int)(promptTokensProp.GetValue(usageObj) ?? 0); + var completionTokens = (int)(completionTokensProp.GetValue(usageObj) ?? 0); + return new AIServiceUsage(promptTokens, completionTokens); + } + } + catch + { + // If reflection fails, fall back to no usage + } + } + + return AIServiceUsage.None; + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs new file mode 100644 index 0000000000..e4e18338c9 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/ClipboardItemHelper.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using AdvancedPaste.Models; +using Microsoft.UI.Xaml.Media.Imaging; +using Windows.ApplicationModel.DataTransfer; + +namespace AdvancedPaste.Helpers +{ + internal static partial class ClipboardItemHelper + { + // Compiled regex for better performance when checking multiple clipboard items + private static readonly Regex HexColorRegex = HexColorCompiledRegex(); + + /// + /// Creates a ClipboardItem from current clipboard data. + /// + public static async Task CreateFromCurrentClipboardAsync( + DataPackageView clipboardData, + ClipboardFormat availableFormats, + DateTimeOffset? timestamp = null, + BitmapImage existingImage = null) + { + if (clipboardData == null || availableFormats == ClipboardFormat.None) + { + return null; + } + + var clipboardItem = new ClipboardItem + { + Format = availableFormats, + Timestamp = timestamp, + }; + + // Text or HTML content + if (availableFormats.HasFlag(ClipboardFormat.Text) || availableFormats.HasFlag(ClipboardFormat.Html)) + { + clipboardItem.Content = await clipboardData.GetTextOrEmptyAsync(); + } + + // Image content + else if (availableFormats.HasFlag(ClipboardFormat.Image)) + { + // Reuse existing image if provided + if (existingImage != null) + { + clipboardItem.Image = existingImage; + } + else + { + clipboardItem.Image = await TryCreateBitmapImageAsync(clipboardData); + } + } + + return clipboardItem; + } + + /// + /// Checks if text is a valid RGB hex color (e.g., #FFBFAB or #fff). + /// + public static bool IsRgbHexColor(string text) + { + if (text == null) + { + return false; + } + + string trimmedText = text.Trim(); + if (trimmedText.Length > 7) + { + return false; + } + + if (string.IsNullOrWhiteSpace(trimmedText)) + { + return false; + } + + // Match #RGB or #RRGGBB format (case-insensitive) + return HexColorRegex.IsMatch(trimmedText); + } + + /// + /// Creates a BitmapImage from clipboard data. + /// + private static async Task TryCreateBitmapImageAsync(DataPackageView clipboardData) + { + try + { + var imageReference = await clipboardData.GetBitmapAsync(); + if (imageReference != null) + { + using (var imageStream = await imageReference.OpenReadAsync()) + { + var bitmapImage = new BitmapImage(); + await bitmapImage.SetSourceAsync(imageStream); + return bitmapImage; + } + } + } + catch + { + // Silently fail - caller can check for null + } + + return null; + } + + [GeneratedRegex(@"^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$")] + private static partial Regex HexColorCompiledRegex(); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs index 529773f9a6..2cd7554a50 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/DataPackageHelpers.cs @@ -6,11 +6,13 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; +using System.Threading; using System.Threading.Tasks; - using AdvancedPaste.Models; using ManagedCommon; +using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.Win32; using Windows.ApplicationModel.DataTransfer; using Windows.Data.Html; @@ -180,6 +182,46 @@ internal static class DataPackageHelpers } } + internal static async Task GetClipboardTextOrThrowAsync(this DataPackageView dataPackageView, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(dataPackageView); + + try + { + if (dataPackageView.Contains(StandardDataFormats.Text)) + { + return await dataPackageView.GetTextAsync(); + } + + if (dataPackageView.Contains(StandardDataFormats.Html)) + { + var html = await dataPackageView.GetHtmlFormatAsync(); + return HtmlUtilities.ConvertToText(html); + } + + if (dataPackageView.Contains(StandardDataFormats.Bitmap)) + { + var bitmap = await dataPackageView.GetImageContentAsync(); + if (bitmap != null) + { + return await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken); + } + } + } + catch (Exception ex) when (ex is COMException or InvalidOperationException) + { + throw CreateClipboardTextMissingException(ex); + } + + throw CreateClipboardTextMissingException(); + } + + private static PasteActionException CreateClipboardTextMissingException(Exception innerException = null) + { + var message = ResourceLoaderInstance.ResourceLoader.GetString("ClipboardEmptyWarning"); + return new PasteActionException(message, innerException ?? new InvalidOperationException("Clipboard does not contain text content.")); + } + internal static async Task GetHtmlContentAsync(this DataPackageView dataPackageView) => dataPackageView.Contains(StandardDataFormats.Html) ? await dataPackageView.GetHtmlFormatAsync() : string.Empty; @@ -195,6 +237,22 @@ internal static class DataPackageHelpers return null; } + internal static async Task GetPreviewBitmapAsync(this DataPackageView dataPackageView) + { + var stream = await dataPackageView.GetImageStreamAsync(); + if (stream == null) + { + return null; + } + + using (stream) + { + var bitmapImage = new BitmapImage(); + bitmapImage.SetSource(stream); + return bitmapImage; + } + } + private static async Task GetImageStreamAsync(this DataPackageView dataPackageView) { if (dataPackageView.Contains(StandardDataFormats.StorageItems)) diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs index 105fe2c0d8..d692263dc1 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/IUserSettings.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using AdvancedPaste.Models; using Microsoft.PowerToys.Settings.UI.Library; @@ -12,16 +13,22 @@ namespace AdvancedPaste.Settings { public interface IUserSettings { - public bool IsAdvancedAIEnabled { get; } + public bool IsAIEnabled { get; } public bool ShowCustomPreview { get; } public bool CloseAfterLosingFocus { get; } + public bool EnableClipboardPreview { get; } + public IReadOnlyList CustomActions { get; } public IReadOnlyList AdditionalActions { get; } + public PasteAIConfiguration PasteAIConfiguration { get; } + public event EventHandler Changed; + + Task SetActiveAIProviderAsync(string providerId); } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs index 8a25b70f07..59f31f0e99 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/UserSettings.cs @@ -13,6 +13,7 @@ using AdvancedPaste.Models; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Utilities; +using Windows.Security.Credentials; namespace AdvancedPaste.Settings { @@ -33,23 +34,29 @@ namespace AdvancedPaste.Settings public event EventHandler Changed; - public bool IsAdvancedAIEnabled { get; private set; } + public bool IsAIEnabled { get; private set; } public bool ShowCustomPreview { get; private set; } public bool CloseAfterLosingFocus { get; private set; } + public bool EnableClipboardPreview { get; private set; } + public IReadOnlyList AdditionalActions => _additionalActions; public IReadOnlyList CustomActions => _customActions; + public PasteAIConfiguration PasteAIConfiguration { get; private set; } + public UserSettings(IFileSystem fileSystem) { _settingsUtils = new SettingsUtils(fileSystem); - IsAdvancedAIEnabled = false; + IsAIEnabled = false; ShowCustomPreview = true; CloseAfterLosingFocus = false; + EnableClipboardPreview = true; + PasteAIConfiguration = new PasteAIConfiguration(); _additionalActions = []; _customActions = []; _taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); @@ -94,13 +101,17 @@ namespace AdvancedPaste.Settings var settings = _settingsUtils.GetSettingsOrDefault(AdvancedPasteModuleName); if (settings != null) { + bool migratedLegacyEnablement = TryMigrateLegacyAIEnablement(settings); + void UpdateSettings() { var properties = settings.Properties; - IsAdvancedAIEnabled = properties.IsAdvancedAIEnabled; + IsAIEnabled = properties.IsAIEnabled; ShowCustomPreview = properties.ShowCustomPreview; CloseAfterLosingFocus = properties.CloseAfterLosingFocus; + EnableClipboardPreview = properties.EnableClipboardPreview; + PasteAIConfiguration = properties.PasteAIConfiguration ?? new PasteAIConfiguration(); var sourceAdditionalActions = properties.AdditionalActions; (PasteFormats Format, IAdvancedPasteAction[] Actions)[] additionalActionFormats = @@ -126,6 +137,11 @@ namespace AdvancedPaste.Settings Task.Factory .StartNew(UpdateSettings, CancellationToken.None, TaskCreationOptions.None, _taskScheduler) .Wait(); + + if (migratedLegacyEnablement) + { + settings.Save(_settingsUtils); + } } retry = false; @@ -144,6 +160,220 @@ namespace AdvancedPaste.Settings } } + private static bool TryMigrateLegacyAIEnablement(AdvancedPasteSettings settings) + { + if (settings?.Properties is null) + { + return false; + } + + var properties = settings.Properties; + bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag); + bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag; + PasswordCredential legacyCredential = TryGetLegacyOpenAICredential(); + + if (legacyCredential is null) + { + return legacyAdvancedAIConsumed; + } + + var configuration = properties.PasteAIConfiguration; + + if (configuration is null) + { + configuration = new PasteAIConfiguration(); + properties.PasteAIConfiguration = configuration; + } + + bool configurationUpdated = false; + + var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration); + PasteAIProviderDefinition openAIProvider = ensureResult.Provider; + configurationUpdated |= ensureResult.Updated; + + if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled) + { + openAIProvider.EnableAdvancedAI = legacyAdvancedAIEnabled; + configurationUpdated = true; + } + + if (openAIProvider is not null) + { + StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password); + RemoveLegacyOpenAICredential(); + } + + const bool shouldEnableAI = true; + bool enabledUpdated = false; + if (properties.IsAIEnabled != shouldEnableAI) + { + properties.IsAIEnabled = shouldEnableAI; + enabledUpdated = true; + } + + return configurationUpdated || enabledUpdated || legacyAdvancedAIConsumed; + } + + private static PasswordCredential TryGetLegacyOpenAICredential() + { + try + { + PasswordVault vault = new(); + var credential = vault.Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); + credential?.RetrievePassword(); + return credential; + } + catch (Exception) + { + return null; + } + } + + private static void RemoveLegacyOpenAICredential() + { + try + { + PasswordVault vault = new(); + TryRemoveCredential(vault, "https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey"); + } + catch (Exception) + { + } + } + + private static void StoreMigratedOpenAICredential(string providerId, string serviceType, string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + return; + } + + try + { + var serviceKind = serviceType.ToAIServiceType(); + if (serviceKind != AIServiceType.OpenAI) + { + return; + } + + string resource = "https://platform.openai.com/api-keys"; + string username = $"PowerToys_AdvancedPaste_PasteAI_openai_{NormalizeProviderIdentifier(providerId)}"; + + PasswordVault vault = new(); + TryRemoveCredential(vault, resource, username); + + PasswordCredential credential = new(resource, username, password); + vault.Add(credential); + } + catch (Exception ex) + { + Logger.LogError("Failed to migrate legacy OpenAI credential", ex); + } + } + + private static void TryRemoveCredential(PasswordVault vault, string credentialResource, string credentialUserName) + { + try + { + PasswordCredential existingCred = vault.Retrieve(credentialResource, credentialUserName); + vault.Remove(existingCred); + } + catch (Exception) + { + // Credential doesn't exist, which is fine + } + } + + private static string NormalizeProviderIdentifier(string providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + { + return "default"; + } + + var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray()); + return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant(); + } + + public async Task SetActiveAIProviderAsync(string providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + { + return; + } + + await Task.Run(() => + { + lock (_loadingSettingsLock) + { + var settings = _settingsUtils.GetSettingsOrDefault(AdvancedPasteModuleName); + var configuration = settings?.Properties?.PasteAIConfiguration; + var providers = configuration?.Providers; + + if (configuration == null || providers == null || providers.Count == 0) + { + return; + } + + var target = providers.FirstOrDefault(provider => string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase)); + if (target == null) + { + return; + } + + if (string.Equals(configuration.ActiveProvider?.Id, providerId, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + configuration.ActiveProviderId = providerId; + + foreach (var provider in providers) + { + provider.IsActive = string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase); + } + + try + { + settings.Save(_settingsUtils); + } + catch (Exception ex) + { + Logger.LogError("Failed to set active AI provider", ex); + return; + } + + try + { + Task.Factory + .StartNew( + () => + { + PasteAIConfiguration.ActiveProviderId = providerId; + + if (PasteAIConfiguration.Providers is not null) + { + foreach (var provider in PasteAIConfiguration.Providers) + { + provider.IsActive = string.Equals(provider.Id, providerId, StringComparison.OrdinalIgnoreCase); + } + } + + Changed?.Invoke(this, EventArgs.Empty); + }, + CancellationToken.None, + TaskCreationOptions.None, + _taskScheduler) + .Wait(); + } + catch (Exception ex) + { + Logger.LogError("Failed to dispatch active AI provider change", ex); + } + } + }); + } + public void Dispose() { Dispose(true); diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs index 1013108bc9..16814e7001 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Models/ClipboardItem.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using AdvancedPaste.Helpers; using Microsoft.UI.Xaml.Media.Imaging; using Windows.ApplicationModel.DataTransfer; @@ -12,10 +13,15 @@ public class ClipboardItem { public string Content { get; set; } - public ClipboardHistoryItem Item { get; set; } - public BitmapImage Image { get; set; } + public ClipboardFormat Format { get; set; } + + public DateTimeOffset? Timestamp { get; set; } + + // Only used for clipboard history items that have a ClipboardHistoryItem + public ClipboardHistoryItem Item { get; set; } + public string Description => !string.IsNullOrEmpty(Content) ? Content : Image is not null ? ResourceLoaderInstance.ResourceLoader.GetString("ClipboardHistoryImage") : string.Empty; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/AdvancedAIKernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/AdvancedAIKernelService.cs new file mode 100644 index 0000000000..c886bcef43 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/AdvancedAIKernelService.cs @@ -0,0 +1,220 @@ +// 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.Linq; +using AdvancedPaste.Helpers; +using AdvancedPaste.Models; +using AdvancedPaste.Services.CustomActions; +using AdvancedPaste.Settings; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace AdvancedPaste.Services; + +public sealed class AdvancedAIKernelService : KernelServiceBase +{ + private sealed record RuntimeConfiguration( + AIServiceType ServiceType, + string ModelName, + string Endpoint, + string DeploymentName, + string ModelPath, + string SystemPrompt, + bool ModerationEnabled) : IKernelRuntimeConfiguration; + + private readonly IAICredentialsProvider credentialsProvider; + + public AdvancedAIKernelService( + IAICredentialsProvider credentialsProvider, + IKernelQueryCacheService queryCacheService, + IPromptModerationService promptModerationService, + IUserSettings userSettings, + ICustomActionTransformService customActionTransformService) + : base(queryCacheService, promptModerationService, userSettings, customActionTransformService) + { + ArgumentNullException.ThrowIfNull(credentialsProvider); + + this.credentialsProvider = credentialsProvider; + } + + protected override string AdvancedAIModelName => GetRuntimeConfiguration().ModelName; + + protected override PromptExecutionSettings PromptExecutionSettings => CreatePromptExecutionSettings(); + + protected override void AddChatCompletionService(IKernelBuilder kernelBuilder) + { + ArgumentNullException.ThrowIfNull(kernelBuilder); + + var runtimeConfig = GetRuntimeConfiguration(); + var serviceType = runtimeConfig.ServiceType; + var modelName = runtimeConfig.ModelName; + var requiresApiKey = RequiresApiKey(serviceType); + var apiKey = string.Empty; + if (requiresApiKey) + { + this.credentialsProvider.Refresh(); + apiKey = (this.credentialsProvider.GetKey() ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(apiKey)) + { + throw new InvalidOperationException($"An API key is required for {serviceType} but none was found in the credential vault."); + } + } + + var endpoint = string.IsNullOrWhiteSpace(runtimeConfig.Endpoint) ? null : runtimeConfig.Endpoint.Trim(); + var deployment = string.IsNullOrWhiteSpace(runtimeConfig.DeploymentName) ? modelName : runtimeConfig.DeploymentName; + + switch (serviceType) + { + case AIServiceType.OpenAI: + kernelBuilder.AddOpenAIChatCompletion(modelName, apiKey, serviceId: modelName); + break; + case AIServiceType.AzureOpenAI: + kernelBuilder.AddAzureOpenAIChatCompletion(deployment, RequireEndpoint(endpoint, serviceType), apiKey, serviceId: modelName); + break; + default: + throw new NotSupportedException($"Service type '{runtimeConfig.ServiceType}' is not supported"); + } + } + + protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage) + { + return AIServiceUsageHelper.GetOpenAIServiceUsage(chatMessage); + } + + protected override bool ShouldModerateAdvancedAI() + { + if (!TryGetRuntimeConfiguration(out var runtimeConfig)) + { + return false; + } + + return runtimeConfig.ModerationEnabled && (runtimeConfig.ServiceType == AIServiceType.OpenAI || runtimeConfig.ServiceType == AIServiceType.AzureOpenAI); + } + + private static string GetModelName(PasteAIProviderDefinition config) + { + if (!string.IsNullOrWhiteSpace(config?.ModelName)) + { + return config.ModelName; + } + + return "gpt-4o"; + } + + protected override IKernelRuntimeConfiguration GetRuntimeConfiguration() + { + if (TryGetRuntimeConfiguration(out var runtimeConfig)) + { + return runtimeConfig; + } + + throw new InvalidOperationException("No Advanced AI provider is configured."); + } + + private bool TryGetRuntimeConfiguration(out IKernelRuntimeConfiguration runtimeConfig) + { + runtimeConfig = null; + + if (!TryResolveAdvancedProvider(out var provider)) + { + return false; + } + + var serviceType = NormalizeServiceType(provider.ServiceTypeKind); + if (!IsServiceTypeSupported(serviceType)) + { + return false; + } + + runtimeConfig = new RuntimeConfiguration( + serviceType, + GetModelName(provider), + provider.EndpointUrl, + provider.DeploymentName, + provider.ModelPath, + provider.SystemPrompt, + provider.ModerationEnabled); + return true; + } + + private bool TryResolveAdvancedProvider(out PasteAIProviderDefinition provider) + { + provider = null; + + var configuration = this.UserSettings?.PasteAIConfiguration; + if (configuration is null) + { + return false; + } + + var activeProvider = configuration.ActiveProvider; + if (IsAdvancedProvider(activeProvider)) + { + provider = activeProvider; + return true; + } + + if (activeProvider is not null) + { + return false; + } + + var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedProvider); + if (fallback is not null) + { + provider = fallback; + return true; + } + + return false; + } + + private static bool IsAdvancedProvider(PasteAIProviderDefinition provider) + { + if (provider is null || !provider.EnableAdvancedAI) + { + return false; + } + + var serviceType = NormalizeServiceType(provider.ServiceTypeKind); + return IsServiceTypeSupported(serviceType); + } + + private static bool IsServiceTypeSupported(AIServiceType serviceType) + { + return serviceType is AIServiceType.OpenAI or AIServiceType.AzureOpenAI; + } + + private static AIServiceType NormalizeServiceType(AIServiceType serviceType) + { + return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType; + } + + private static bool RequiresApiKey(AIServiceType serviceType) + { + return true; + } + + private static string RequireEndpoint(string endpoint, AIServiceType serviceType) + { + if (!string.IsNullOrWhiteSpace(endpoint)) + { + return endpoint; + } + + throw new InvalidOperationException($"Endpoint is required for {serviceType} configuration but was not provided."); + } + + private PromptExecutionSettings CreatePromptExecutionSettings() + { + var serviceType = GetRuntimeConfiguration().ServiceType; + return new OpenAIPromptExecutionSettings + { + FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(), + }; + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformResult.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformResult.cs new file mode 100644 index 0000000000..562ea3976c --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformResult.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using AdvancedPaste.Models; + +namespace AdvancedPaste.Services.CustomActions +{ + public sealed class CustomActionTransformResult + { + public CustomActionTransformResult(string content, AIServiceUsage usage) + { + Content = content; + Usage = usage; + } + + public string Content { get; } + + public AIServiceUsage Usage { get; } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs new file mode 100644 index 0000000000..57d55492a4 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/CustomActionTransformService.cs @@ -0,0 +1,205 @@ +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using AdvancedPaste.Helpers; +using AdvancedPaste.Models; +using AdvancedPaste.Settings; +using AdvancedPaste.Telemetry; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Telemetry; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace AdvancedPaste.Services.CustomActions +{ + public sealed class CustomActionTransformService : ICustomActionTransformService + { + private const string DefaultSystemPrompt = """ + You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. + Do not output anything else besides the reformatted clipboard content. + """; + + private readonly IPromptModerationService promptModerationService; + private readonly IPasteAIProviderFactory providerFactory; + private readonly IAICredentialsProvider credentialsProvider; + private readonly IUserSettings userSettings; + + public CustomActionTransformService(IPromptModerationService promptModerationService, IPasteAIProviderFactory providerFactory, IAICredentialsProvider credentialsProvider, IUserSettings userSettings) + { + this.promptModerationService = promptModerationService; + this.providerFactory = providerFactory; + this.credentialsProvider = credentialsProvider; + this.userSettings = userSettings; + } + + public async Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress) + { + var pasteConfig = userSettings?.PasteAIConfiguration; + var providerConfig = BuildProviderConfig(pasteConfig); + + return await TransformAsync(prompt, inputText, providerConfig, cancellationToken, progress); + } + + private async Task TransformAsync(string prompt, string inputText, PasteAIConfig providerConfig, CancellationToken cancellationToken, IProgress progress) + { + ArgumentNullException.ThrowIfNull(providerConfig); + + if (string.IsNullOrWhiteSpace(prompt)) + { + return new CustomActionTransformResult(string.Empty, AIServiceUsage.None); + } + + if (string.IsNullOrWhiteSpace(inputText)) + { + Logger.LogWarning("Clipboard has no usable text data"); + return new CustomActionTransformResult(string.Empty, AIServiceUsage.None); + } + + var systemPrompt = providerConfig.SystemPrompt ?? DefaultSystemPrompt; + + var fullPrompt = (systemPrompt ?? string.Empty) + "\n\n" + (inputText ?? string.Empty); + + if (ShouldModerate(providerConfig)) + { + await promptModerationService.ValidateAsync(fullPrompt, cancellationToken); + } + + try + { + var provider = providerFactory.CreateProvider(providerConfig); + + var request = new PasteAIRequest + { + Prompt = prompt, + InputText = inputText, + SystemPrompt = systemPrompt, + }; + + var operationStart = DateTime.UtcNow; + + var providerContent = await provider.ProcessPasteAsync( + request, + cancellationToken, + progress); + + var durationMs = (int)Math.Round((DateTime.UtcNow - operationStart).TotalMilliseconds); + + var usage = request.Usage; + var content = providerContent ?? string.Empty; + + // Log endpoint usage (custom action pipeline is not the advanced SK flow) + var endpointEvent = new AdvancedPasteEndpointUsageEvent(providerConfig.ProviderType, providerConfig.Model ?? string.Empty, isAdvanced: false, durationMs: durationMs); + PowerToysTelemetry.Log.WriteEvent(endpointEvent); + + Logger.LogDebug($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} complete; ModelName={providerConfig.Model ?? string.Empty}, PromptTokens={usage.PromptTokens}, CompletionTokens={usage.CompletionTokens}, DurationMs={durationMs}"); + + return new CustomActionTransformResult(content, usage); + } + catch (Exception ex) + { + Logger.LogError($"{nameof(CustomActionTransformService)}.{nameof(TransformAsync)} failed", ex); + var statusCode = ExtractStatusCode(ex); + var modelName = providerConfig.Model ?? string.Empty; + AdvancedPasteCustomActionErrorEvent errorEvent = new(providerConfig.ProviderType, modelName, statusCode, ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message); + PowerToysTelemetry.Log.WriteEvent(errorEvent); + + if (ex is PasteActionException or OperationCanceledException) + { + throw; + } + + var failureMessage = providerConfig.ProviderType switch + { + AIServiceType.OpenAI or AIServiceType.AzureOpenAI => ErrorHelpers.TranslateErrorText(statusCode), + _ => ResourceLoaderInstance.ResourceLoader.GetString("PasteError"), + }; + + throw new PasteActionException(failureMessage, ex); + } + } + + private static int ExtractStatusCode(Exception exception) + { + if (exception is HttpOperationException httpOperationException) + { + return (int?)httpOperationException.StatusCode ?? -1; + } + + if (exception is HttpRequestException httpRequestException && httpRequestException.StatusCode is HttpStatusCode statusCode) + { + return (int)statusCode; + } + + return -1; + } + + private static AIServiceType NormalizeServiceType(AIServiceType serviceType) + { + return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType; + } + + private PasteAIConfig BuildProviderConfig(PasteAIConfiguration config) + { + config ??= new PasteAIConfiguration(); + var provider = config.ActiveProvider ?? config.Providers?.FirstOrDefault() ?? new PasteAIProviderDefinition(); + var serviceType = NormalizeServiceType(provider.ServiceTypeKind); + var systemPrompt = string.IsNullOrWhiteSpace(provider.SystemPrompt) ? DefaultSystemPrompt : provider.SystemPrompt; + var apiKey = AcquireApiKey(serviceType); + var modelName = provider.ModelName; + + var providerConfig = new PasteAIConfig + { + ProviderType = serviceType, + ApiKey = apiKey, + Model = modelName, + Endpoint = provider.EndpointUrl, + DeploymentName = provider.DeploymentName, + LocalModelPath = provider.ModelPath, + ModelPath = provider.ModelPath, + SystemPrompt = systemPrompt, + ModerationEnabled = provider.ModerationEnabled, + }; + + return providerConfig; + } + + private string AcquireApiKey(AIServiceType serviceType) + { + if (!RequiresApiKey(serviceType)) + { + return string.Empty; + } + + credentialsProvider.Refresh(); + return credentialsProvider.GetKey() ?? string.Empty; + } + + private static bool RequiresApiKey(AIServiceType serviceType) + { + return serviceType switch + { + AIServiceType.Onnx => false, + AIServiceType.Ollama => false, + _ => true, + }; + } + + private static bool ShouldModerate(PasteAIConfig providerConfig) + { + if (providerConfig is null || !providerConfig.ModerationEnabled) + { + return false; + } + + return providerConfig.ProviderType == AIServiceType.OpenAI || providerConfig.ProviderType == AIServiceType.AzureOpenAI; + } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs new file mode 100644 index 0000000000..8b57baae74 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/FoundryLocalPasteProvider.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AdvancedPaste.Helpers; +using AdvancedPaste.Models; +using LanguageModelProvider; +using Microsoft.Extensions.AI; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace AdvancedPaste.Services.CustomActions; + +public sealed class FoundryLocalPasteProvider : IPasteAIProvider +{ + private static readonly IReadOnlyCollection SupportedTypes = new[] + { + AIServiceType.FoundryLocal, + }; + + public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new FoundryLocalPasteProvider(config)); + + private static readonly FoundryLocalModelProvider _modelProvider = FoundryLocalModelProvider.Instance; + + private readonly PasteAIConfig _config; + + public FoundryLocalPasteProvider(PasteAIConfig config) + { + ArgumentNullException.ThrowIfNull(config); + _config = config; + } + + public async Task IsAvailableAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return await FoundryLocalModelProvider.Instance.IsAvailable().ConfigureAwait(false); + } + + public async Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress) + { + ArgumentNullException.ThrowIfNull(request); + + try + { + var systemPrompt = request.SystemPrompt; + if (string.IsNullOrWhiteSpace(systemPrompt)) + { + throw new PasteActionException( + "System prompt is required for Foundry Local", + new ArgumentException("System prompt must be provided", nameof(request))); + } + + var prompt = request.Prompt; + var inputText = request.InputText; + if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText)) + { + throw new PasteActionException( + "Prompt and input text are required", + new ArgumentException("Prompt and input text must be provided", nameof(request))); + } + + var modelReference = _config?.Model; + if (string.IsNullOrWhiteSpace(modelReference)) + { + throw new PasteActionException( + "No Foundry Local model selected", + new InvalidOperationException("Model identifier is required"), + aiServiceMessage: "Please select a model in the AI provider settings."); + } + + cancellationToken.ThrowIfCancellationRequested(); + + IChatClient chatClient; + try + { + chatClient = _modelProvider.GetIChatClient(modelReference); + } + catch (InvalidOperationException ex) + { + // GetIChatClient throws InvalidOperationException for user-facing errors + var errorMessage = string.Format(System.Globalization.CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("FoundryLocal_UnableToLoadModel"), modelReference); + throw new PasteActionException( + errorMessage, + ex, + aiServiceMessage: ex.Message); + } + + var userMessageContent = $""" + User instructions: + {prompt} + + Text: + {inputText} + + Output: + """; + + var chatMessages = new List + { + new(ChatRole.System, systemPrompt), + new(ChatRole.User, userMessageContent), + }; + + var chatOptions = CreateChatOptions(_config?.SystemPrompt, modelReference); + + progress?.Report(0.1); + + var response = await chatClient.GetResponseAsync(chatMessages, chatOptions, cancellationToken).ConfigureAwait(false); + + progress?.Report(0.8); + + var responseText = GetResponseText(response); + request.Usage = ToUsage(response.Usage); + + progress?.Report(1.0); + + return responseText ?? string.Empty; + } + catch (OperationCanceledException) + { + // Let cancellation exceptions pass through unchanged + throw; + } + catch (PasteActionException) + { + // Let our custom exceptions pass through unchanged + throw; + } + catch (Exception ex) + { + // Wrap any other exceptions with context + var modelInfo = !string.IsNullOrWhiteSpace(_config?.Model) ? $" (Model: {_config.Model})" : string.Empty; + throw new PasteActionException( + $"Failed to generate response using Foundry Local{modelInfo}", + ex, + aiServiceMessage: $"Error details: {ex.Message}"); + } + } + + private static ChatOptions CreateChatOptions(string systemPrompt, string modelReference) + { + var options = new ChatOptions + { + ModelId = modelReference, + MaxOutputTokens = 2048, + }; + + if (!string.IsNullOrWhiteSpace(systemPrompt)) + { + options.Instructions = systemPrompt; + } + + return options; + } + + private static string GetResponseText(ChatResponse response) + { + if (!string.IsNullOrWhiteSpace(response.Text)) + { + return response.Text; + } + + if (response.Messages is { Count: > 0 }) + { + var lastMessage = response.Messages.LastOrDefault(m => !string.IsNullOrWhiteSpace(m.Text)); + if (!string.IsNullOrWhiteSpace(lastMessage?.Text)) + { + return lastMessage.Text; + } + } + + return string.Empty; + } + + private static AIServiceUsage ToUsage(UsageDetails usageDetails) + { + if (usageDetails is null) + { + return AIServiceUsage.None; + } + + int promptTokens = (int)(usageDetails.InputTokenCount ?? 0); + int completionTokens = (int)(usageDetails.OutputTokenCount ?? 0); + + if (promptTokens == 0 && completionTokens == 0) + { + return AIServiceUsage.None; + } + + return new AIServiceUsage(promptTokens, completionTokens); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs new file mode 100644 index 0000000000..1c3ecb980c --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/ICustomActionTransformService.cs @@ -0,0 +1,17 @@ +// 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.Threading; +using System.Threading.Tasks; + +using AdvancedPaste.Settings; + +namespace AdvancedPaste.Services.CustomActions +{ + public interface ICustomActionTransformService + { + Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProvider.cs new file mode 100644 index 0000000000..764d99f942 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProvider.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace AdvancedPaste.Services.CustomActions +{ + public interface IPasteAIProvider + { + Task IsAvailableAsync(CancellationToken cancellationToken); + + Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProviderFactory.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProviderFactory.cs new file mode 100644 index 0000000000..aacc61bec9 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/IPasteAIProviderFactory.cs @@ -0,0 +1,11 @@ +// 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 AdvancedPaste.Services.CustomActions +{ + public interface IPasteAIProviderFactory + { + IPasteAIProvider CreateProvider(PasteAIConfig config); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs new file mode 100644 index 0000000000..f4d45ccd74 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/LocalModelPasteProvider.cs @@ -0,0 +1,43 @@ +// 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.Threading; +using System.Threading.Tasks; +using AdvancedPaste.Models; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace AdvancedPaste.Services.CustomActions +{ + public sealed class LocalModelPasteProvider : IPasteAIProvider + { + private static readonly IReadOnlyCollection SupportedTypes = new[] + { + AIServiceType.Onnx, + AIServiceType.ML, + }; + + public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new LocalModelPasteProvider(config)); + + private readonly PasteAIConfig _config; + + public LocalModelPasteProvider(PasteAIConfig config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + } + + public Task IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true); + + public Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress) + { + ArgumentNullException.ThrowIfNull(request); + + // TODO: Implement local model inference logic using _config.LocalModelPath/_config.ModelPath + var content = request.InputText ?? string.Empty; + request.Usage = AIServiceUsage.None; + return Task.FromResult(content); + } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIConfig.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIConfig.cs new file mode 100644 index 0000000000..1d8a60f041 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIConfig.cs @@ -0,0 +1,32 @@ +// 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 AdvancedPaste.Models; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace AdvancedPaste.Services.CustomActions +{ + public class PasteAIConfig + { + public AIServiceType ProviderType { get; set; } + + public string Model { get; set; } + + public string ApiKey { get; set; } + + public string Endpoint { get; set; } + + public string DeploymentName { get; set; } + + public string LocalModelPath { get; set; } + + public string ModelPath { get; set; } + + public string SystemPrompt { get; set; } + + public bool ModerationEnabled { get; set; } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs new file mode 100644 index 0000000000..7339b4e4e3 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderFactory.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Microsoft.PowerToys.Settings.UI.Library; + +namespace AdvancedPaste.Services.CustomActions +{ + public sealed class PasteAIProviderFactory : IPasteAIProviderFactory + { + private static readonly IReadOnlyList ProviderRegistrations = new[] + { + SemanticKernelPasteProvider.Registration, + LocalModelPasteProvider.Registration, + FoundryLocalPasteProvider.Registration, + }; + + private static readonly IReadOnlyDictionary> ProviderFactories = CreateProviderFactories(); + + public IPasteAIProvider CreateProvider(PasteAIConfig config) + { + ArgumentNullException.ThrowIfNull(config); + + var serviceType = config.ProviderType; + if (serviceType == AIServiceType.Unknown) + { + serviceType = AIServiceType.OpenAI; + config.ProviderType = serviceType; + } + + if (!ProviderFactories.TryGetValue(serviceType, out var factory)) + { + throw new NotSupportedException($"Provider {config.ProviderType} not supported"); + } + + return factory(config); + } + + private static IReadOnlyDictionary> CreateProviderFactories() + { + var map = new Dictionary>(); + + foreach (var registration in ProviderRegistrations) + { + Register(map, registration.SupportedTypes, registration.Factory); + } + + return map; + } + + private static void Register(Dictionary> map, IReadOnlyCollection types, Func factory) + { + foreach (var type in types) + { + map[type] = factory; + } + } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderRegistration.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderRegistration.cs new file mode 100644 index 0000000000..6bd78450e8 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIProviderRegistration.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace AdvancedPaste.Services.CustomActions +{ + public sealed class PasteAIProviderRegistration + { + public PasteAIProviderRegistration(IReadOnlyCollection supportedTypes, Func factory) + { + SupportedTypes = supportedTypes ?? throw new ArgumentNullException(nameof(supportedTypes)); + Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public IReadOnlyCollection SupportedTypes { get; } + + public Func Factory { get; } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIRequest.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIRequest.cs new file mode 100644 index 0000000000..0e15c93e05 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/PasteAIRequest.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using AdvancedPaste.Models; + +namespace AdvancedPaste.Services.CustomActions +{ + public sealed class PasteAIRequest + { + public string Prompt { get; init; } + + public string InputText { get; init; } + + public string SystemPrompt { get; init; } + + public AIServiceUsage Usage { get; set; } = AIServiceUsage.None; + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs new file mode 100644 index 0000000000..eb2f56e01f --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/CustomActions/SemanticKernelPasteProvider.cs @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using AdvancedPaste.Helpers; +using AdvancedPaste.Models; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureAIInference; +using Microsoft.SemanticKernel.Connectors.Google; +using Microsoft.SemanticKernel.Connectors.MistralAI; +using Microsoft.SemanticKernel.Connectors.Ollama; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace AdvancedPaste.Services.CustomActions +{ + public sealed class SemanticKernelPasteProvider : IPasteAIProvider + { + private static readonly IReadOnlyCollection SupportedTypes = new[] + { + AIServiceType.OpenAI, + AIServiceType.AzureOpenAI, + AIServiceType.Mistral, + AIServiceType.Google, + AIServiceType.AzureAIInference, + AIServiceType.Ollama, + }; + + public static PasteAIProviderRegistration Registration { get; } = new(SupportedTypes, config => new SemanticKernelPasteProvider(config)); + + private readonly PasteAIConfig _config; + private readonly AIServiceType _serviceType; + + public SemanticKernelPasteProvider(PasteAIConfig config) + { + ArgumentNullException.ThrowIfNull(config); + _config = config; + _serviceType = config.ProviderType; + if (_serviceType == AIServiceType.Unknown) + { + _serviceType = AIServiceType.OpenAI; + _config.ProviderType = _serviceType; + } + } + + public IReadOnlyCollection SupportedServiceTypes => SupportedTypes; + + public Task IsAvailableAsync(CancellationToken cancellationToken) => Task.FromResult(true); + + public async Task ProcessPasteAsync(PasteAIRequest request, CancellationToken cancellationToken, IProgress progress) + { + ArgumentNullException.ThrowIfNull(request); + + var systemPrompt = request.SystemPrompt; + if (string.IsNullOrWhiteSpace(systemPrompt)) + { + throw new ArgumentException("System prompt must be provided", nameof(request)); + } + + var prompt = request.Prompt; + var inputText = request.InputText; + if (string.IsNullOrWhiteSpace(prompt) || string.IsNullOrWhiteSpace(inputText)) + { + throw new ArgumentException("Prompt and input text must be provided", nameof(request)); + } + + var userMessageContent = $""" + User instructions: + {prompt} + + Clipboard Content: + {inputText} + + Output: + """; + + var executionSettings = CreateExecutionSettings(); + var kernel = CreateKernel(); + var modelId = _config.Model; + + IChatCompletionService chatService; + if (!string.IsNullOrWhiteSpace(modelId)) + { + try + { + chatService = kernel.GetRequiredService(modelId); + } + catch (Exception) + { + chatService = kernel.GetRequiredService(); + } + } + else + { + chatService = kernel.GetRequiredService(); + } + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage(systemPrompt); + chatHistory.AddUserMessage(userMessageContent); + + var response = await chatService.GetChatMessageContentAsync(chatHistory, executionSettings, kernel, cancellationToken); + chatHistory.Add(response); + + request.Usage = AIServiceUsageHelper.GetOpenAIServiceUsage(response); + return response.Content; + } + + private Kernel CreateKernel() + { + var kernelBuilder = Kernel.CreateBuilder(); + var endpoint = string.IsNullOrWhiteSpace(_config.Endpoint) ? null : _config.Endpoint.Trim(); + var apiKey = _config.ApiKey?.Trim() ?? string.Empty; + + if (RequiresApiKey(_serviceType) && string.IsNullOrWhiteSpace(apiKey)) + { + throw new InvalidOperationException($"API key is required for {_serviceType} but was not provided."); + } + + switch (_serviceType) + { + case AIServiceType.OpenAI: + kernelBuilder.AddOpenAIChatCompletion(_config.Model, apiKey, serviceId: _config.Model); + break; + case AIServiceType.AzureOpenAI: + var deploymentName = string.IsNullOrWhiteSpace(_config.DeploymentName) ? _config.Model : _config.DeploymentName; + kernelBuilder.AddAzureOpenAIChatCompletion(deploymentName, RequireEndpoint(endpoint, _serviceType), apiKey, serviceId: _config.Model); + break; + case AIServiceType.Mistral: + kernelBuilder.AddMistralChatCompletion(_config.Model, apiKey: apiKey); + break; + case AIServiceType.Google: + kernelBuilder.AddGoogleAIGeminiChatCompletion(_config.Model, apiKey: apiKey); + break; + case AIServiceType.AzureAIInference: + kernelBuilder.AddAzureAIInferenceChatCompletion(_config.Model, apiKey: apiKey, endpoint: new Uri(endpoint)); + break; + case AIServiceType.Ollama: + kernelBuilder.AddOllamaChatCompletion(_config.Model, endpoint: new Uri(endpoint)); + break; + + default: + throw new NotSupportedException($"Provider '{_config.ProviderType}' is not supported by {nameof(SemanticKernelPasteProvider)}"); + } + + return kernelBuilder.Build(); + } + + private PromptExecutionSettings CreateExecutionSettings() + { + return _serviceType switch + { + AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings + { + FunctionChoiceBehavior = null, + }, + _ => new PromptExecutionSettings(), + }; + } + + private static bool RequiresApiKey(AIServiceType serviceType) + { + return serviceType switch + { + AIServiceType.Ollama => false, + _ => true, + }; + } + + private static string RequireEndpoint(string endpoint, AIServiceType serviceType) + { + if (!string.IsNullOrWhiteSpace(endpoint)) + { + return endpoint; + } + + throw new InvalidOperationException($"Endpoint is required for {serviceType} but was not provided."); + } + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs new file mode 100644 index 0000000000..648881fba0 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/EnhancedVaultCredentialsProvider.cs @@ -0,0 +1,182 @@ +// 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.Linq; +using System.Threading; +using AdvancedPaste.Settings; +using Microsoft.PowerToys.Settings.UI.Library; +using Windows.Security.Credentials; + +namespace AdvancedPaste.Services; + +/// +/// Enhanced credentials provider that supports different AI service types +/// Keys are stored in Windows Credential Vault with service-specific identifiers +/// +public sealed class EnhancedVaultCredentialsProvider : IAICredentialsProvider +{ + private sealed class CredentialSlot + { + public AIServiceType ServiceType { get; set; } = AIServiceType.Unknown; + + public string ProviderId { get; set; } = string.Empty; + + public (string Resource, string Username)? Entry { get; set; } + + public string Key { get; set; } = string.Empty; + } + + private readonly IUserSettings _userSettings; + private readonly CredentialSlot _slot; + private readonly Lock _syncRoot = new(); + + public EnhancedVaultCredentialsProvider(IUserSettings userSettings) + { + _userSettings = userSettings ?? throw new ArgumentNullException(nameof(userSettings)); + + _slot = new CredentialSlot(); + + Refresh(); + } + + public string GetKey() + { + using (_syncRoot.EnterScope()) + { + UpdateSlot(forceRefresh: false); + return _slot.Key; + } + } + + public bool IsConfigured() + { + return !string.IsNullOrEmpty(GetKey()); + } + + public bool Refresh() + { + using (_syncRoot.EnterScope()) + { + return UpdateSlot(forceRefresh: true); + } + } + + private bool UpdateSlot(bool forceRefresh) + { + var (serviceType, providerId) = ResolveCredentialTarget(); + var desiredServiceType = NormalizeServiceType(serviceType); + providerId ??= string.Empty; + + var hasChanged = false; + + if (_slot.ServiceType != desiredServiceType || !string.Equals(_slot.ProviderId, providerId, StringComparison.Ordinal)) + { + _slot.ServiceType = desiredServiceType; + _slot.ProviderId = providerId; + _slot.Entry = BuildCredentialEntry(desiredServiceType, providerId); + forceRefresh = true; + hasChanged = true; + } + + if (!forceRefresh) + { + return hasChanged; + } + + var newKey = LoadKey(_slot.Entry); + if (!string.Equals(_slot.Key, newKey, StringComparison.Ordinal)) + { + _slot.Key = newKey; + hasChanged = true; + } + + return hasChanged; + } + + private (AIServiceType ServiceType, string ProviderId) ResolveCredentialTarget() + { + var provider = _userSettings.PasteAIConfiguration?.ActiveProvider; + if (provider is null) + { + return (AIServiceType.OpenAI, string.Empty); + } + + return (provider.ServiceTypeKind, provider.Id ?? string.Empty); + } + + private static AIServiceType NormalizeServiceType(AIServiceType serviceType) + { + return serviceType == AIServiceType.Unknown ? AIServiceType.OpenAI : serviceType; + } + + private static string LoadKey((string Resource, string Username)? entry) + { + if (entry is null) + { + return string.Empty; + } + + try + { + var credential = new PasswordVault().Retrieve(entry.Value.Resource, entry.Value.Username); + return credential?.Password ?? string.Empty; + } + catch (Exception) + { + return string.Empty; + } + } + + private static (string Resource, string Username)? BuildCredentialEntry(AIServiceType serviceType, string providerId) + { + string resource; + string serviceKey; + + switch (serviceType) + { + case AIServiceType.OpenAI: + resource = "https://platform.openai.com/api-keys"; + serviceKey = "openai"; + break; + case AIServiceType.AzureOpenAI: + resource = "https://azure.microsoft.com/products/ai-services/openai-service"; + serviceKey = "azureopenai"; + break; + case AIServiceType.AzureAIInference: + resource = "https://azure.microsoft.com/products/ai-services/ai-inference"; + serviceKey = "azureaiinference"; + break; + case AIServiceType.Mistral: + resource = "https://console.mistral.ai/account/api-keys"; + serviceKey = "mistral"; + break; + case AIServiceType.Google: + resource = "https://ai.google.dev/"; + serviceKey = "google"; + break; + case AIServiceType.FoundryLocal: + case AIServiceType.ML: + case AIServiceType.Onnx: + case AIServiceType.Ollama: + return null; + default: + return null; + } + + string username = $"PowerToys_AdvancedPaste_PasteAI_{serviceKey}_{NormalizeProviderIdentifier(providerId)}"; + return (resource, username); + } + + private static string NormalizeProviderIdentifier(string providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + { + return "default"; + } + + var filtered = new string(providerId.Where(char.IsLetterOrDigit).ToArray()); + return string.IsNullOrWhiteSpace(filtered) ? "default" : filtered.ToLowerInvariant(); + } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs index 54759b7dc8..7aa6f63b19 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IAICredentialsProvider.cs @@ -4,11 +4,26 @@ namespace AdvancedPaste.Services; +/// +/// Provides access to AI credentials stored for Advanced Paste scenarios. +/// public interface IAICredentialsProvider { - bool IsConfigured { get; } + /// + /// Gets a value indicating whether any credential is configured. + /// + /// when a non-empty credential exists for the active AI provider. + bool IsConfigured(); - string Key { get; } + /// + /// Retrieves the credential for the active AI provider. + /// + /// Credential string or when missing. + string GetKey(); + /// + /// Refreshes the cached credential for the active AI provider. + /// + /// when the credential changed. bool Refresh(); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs deleted file mode 100644 index 75f1df259e..0000000000 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/ICustomTextTransformService.cs +++ /dev/null @@ -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. - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace AdvancedPaste.Services; - -public interface ICustomTextTransformService -{ - Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress); -} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelRuntimeConfiguration.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelRuntimeConfiguration.cs new file mode 100644 index 0000000000..d634c13e30 --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/IKernelRuntimeConfiguration.cs @@ -0,0 +1,27 @@ +// 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.PowerToys.Settings.UI.Library; + +namespace AdvancedPaste.Services; + +/// +/// Represents runtime information required to configure an AI kernel service. +/// +public interface IKernelRuntimeConfiguration +{ + AIServiceType ServiceType { get; } + + string ModelName { get; } + + string Endpoint { get; } + + string DeploymentName { get; } + + string ModelPath { get; } + + string SystemPrompt { get; } + + bool ModerationEnabled { get; } +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs index e921b21e54..47e208eb49 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/KernelServiceBase.cs @@ -5,15 +5,16 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; - using AdvancedPaste.Helpers; using AdvancedPaste.Models; using AdvancedPaste.Models.KernelQueryCache; +using AdvancedPaste.Services.CustomActions; +using AdvancedPaste.Settings; using AdvancedPaste.Telemetry; using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Telemetry; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; @@ -21,15 +22,21 @@ using Windows.ApplicationModel.DataTransfer; namespace AdvancedPaste.Services; -public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheService, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) : IKernelService +public abstract class KernelServiceBase( + IKernelQueryCacheService queryCacheService, + IPromptModerationService promptModerationService, + IUserSettings userSettings, + ICustomActionTransformService customActionTransformService) : IKernelService { private const string PromptParameterName = "prompt"; + private const string DefaultSystemPrompt = "You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. Call function when necessary to help user finish the transformation task. You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best. The user will put in a request to format their clipboard data and you will fulfill it. Do not output anything else besides the reformatted clipboard content."; private readonly IKernelQueryCacheService _queryCacheService = queryCacheService; private readonly IPromptModerationService _promptModerationService = promptModerationService; - private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService; + private readonly IUserSettings _userSettings = userSettings; + private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService; - protected abstract string ModelName { get; } + protected abstract string AdvancedAIModelName { get; } protected abstract PromptExecutionSettings PromptExecutionSettings { get; } @@ -37,6 +44,8 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage); + protected abstract IKernelRuntimeConfiguration GetRuntimeConfiguration(); + public async Task TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress progress) { Logger.LogTrace(); @@ -132,21 +141,21 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt, CancellationToken cancellationToken) { + var runtimeConfig = GetRuntimeConfiguration(); + ChatHistory chatHistory = []; - chatHistory.AddSystemMessage(""" - You are an agent who is tasked with helping users paste their clipboard data. You have functions available to help you with this task. - You never need to ask permission, always try to do as the user asks. The user will only input one message and will not be available for further questions, so try your best. - The user will put in a request to format their clipboard data and you will fulfill it. - You will not directly see the output clipboard content, and do not need to provide it in the chat. You just need to do the transform operations as needed. - If you are unable to fulfill the request, end with an error message in the language of the user's request. - """); + var systemPrompt = string.IsNullOrWhiteSpace(runtimeConfig.SystemPrompt) ? DefaultSystemPrompt : runtimeConfig.SystemPrompt; + chatHistory.AddSystemMessage(systemPrompt); chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}"); chatHistory.AddUserMessage(prompt); - await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken); + if (ShouldModerateAdvancedAI()) + { + await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken); + } - var chatResult = await kernel.GetRequiredService() + var chatResult = await kernel.GetRequiredService(AdvancedAIModelName) .GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken); chatHistory.Add(chatResult); @@ -175,10 +184,26 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi return ([], AIServiceUsage.None); } + protected IUserSettings UserSettings => _userSettings; + private void LogResult(bool cacheUsed, bool isSavedQuery, IEnumerable actionChain, AIServiceUsage usage) { - AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new(cacheUsed, isSavedQuery, usage.PromptTokens, usage.CompletionTokens, ModelName, AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain)); + var runtimeConfig = GetRuntimeConfiguration(); + + AdvancedPasteSemanticKernelFormatEvent telemetryEvent = new( + cacheUsed, + isSavedQuery, + usage.PromptTokens, + usage.CompletionTokens, + AdvancedAIModelName, + runtimeConfig.ServiceType.ToString(), + AdvancedPasteSemanticKernelFormatEvent.FormatActionChain(actionChain)); PowerToysTelemetry.Log.WriteEvent(telemetryEvent); + + // Log endpoint usage + var endpointEvent = new AdvancedPasteEndpointUsageEvent(runtimeConfig.ServiceType, AdvancedAIModelName, isAdvanced: true); + PowerToysTelemetry.Log.WriteEvent(endpointEvent); + var logEvent = new AIServiceFormatEvent(telemetryEvent); Logger.LogDebug($"{nameof(TransformClipboardAsync)} complete; {logEvent.ToJsonString()}"); } @@ -191,20 +216,96 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi return kernelBuilder.Build(); } - private IEnumerable GetKernelFunctions() => - from format in Enum.GetValues() - let metadata = PasteFormat.MetadataDict[format] - let coreDescription = metadata.KernelFunctionDescription - where !string.IsNullOrEmpty(coreDescription) - let requiresPrompt = metadata.RequiresPrompt - orderby requiresPrompt descending - select KernelFunctionFactory.CreateFromMethod( - method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt) - : async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format), - functionName: format.ToString(), - description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.", - parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null, - returnParameter: new() { Description = "Array of available clipboard formats after operation" }); + private IEnumerable GetKernelFunctions() + { + // Get standard format functions + var standardFunctions = + from format in Enum.GetValues() + let metadata = PasteFormat.MetadataDict[format] + let coreDescription = metadata.KernelFunctionDescription + where !string.IsNullOrEmpty(coreDescription) + let requiresPrompt = metadata.RequiresPrompt + orderby requiresPrompt descending + select KernelFunctionFactory.CreateFromMethod( + method: requiresPrompt ? async (Kernel kernel, string prompt) => await ExecutePromptTransformAsync(kernel, format, prompt) + : async (Kernel kernel) => await ExecuteStandardTransformAsync(kernel, format), + functionName: format.ToString(), + description: requiresPrompt ? coreDescription : $"{coreDescription} Puts the result back on the clipboard.", + parameters: requiresPrompt ? [new(PromptParameterName) { Description = "Input instructions to AI", ParameterType = typeof(string) }] : null, + returnParameter: new() { Description = "Array of available clipboard formats after operation" }); + + HashSet usedFunctionNames = new(Enum.GetNames(), StringComparer.OrdinalIgnoreCase); + + // Get custom action functions + var customActionFunctions = _userSettings.CustomActions + .Where(customAction => !string.IsNullOrWhiteSpace(customAction.Name) && !string.IsNullOrWhiteSpace(customAction.Prompt)) + .Select(customAction => + { + var sanitizedBaseName = SanitizeFunctionName(customAction.Name); + var functionName = GetUniqueFunctionName(sanitizedBaseName, usedFunctionNames, customAction.Id); + var description = string.IsNullOrWhiteSpace(customAction.Description) + ? $"Runs the \"{customAction.Name}\" custom action." + : customAction.Description; + return KernelFunctionFactory.CreateFromMethod( + method: async (Kernel kernel) => await ExecuteCustomActionAsync(kernel, customAction.Prompt), + functionName: functionName, + description: description, + parameters: null, + returnParameter: new() { Description = "Array of available clipboard formats after operation" }); + }); + + return standardFunctions.Concat(customActionFunctions); + } + + private static string GetUniqueFunctionName(string baseName, HashSet usedFunctionNames, int customActionId) + { + ArgumentNullException.ThrowIfNull(usedFunctionNames); + + var candidate = string.IsNullOrEmpty(baseName) ? "_CustomAction" : baseName; + + if (usedFunctionNames.Add(candidate)) + { + return candidate; + } + + int suffix = 1; + while (true) + { + var nextCandidate = $"{candidate}_{customActionId}_{suffix}"; + if (usedFunctionNames.Add(nextCandidate)) + { + return nextCandidate; + } + + suffix++; + } + } + + private static string SanitizeFunctionName(string name) + { + // Remove invalid characters and ensure the function name is valid for kernel + var sanitized = new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray()); + + // Ensure it starts with a letter or underscore + if (sanitized.Length > 0 && !char.IsLetter(sanitized[0]) && sanitized[0] != '_') + { + sanitized = "_" + sanitized; + } + + // Ensure it's not empty + return string.IsNullOrEmpty(sanitized) ? "_CustomAction" : sanitized; + } + + private Task ExecuteCustomActionAsync(Kernel kernel, string fixedPrompt) => + ExecuteTransformAsync( + kernel, + new ActionChainItem(PasteFormats.CustomTextTransformation, Arguments: new() { { PromptParameterName, fixedPrompt } }), + async dataPackageView => + { + var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken()); + var result = await _customActionTransformService.TransformTextAsync(fixedPrompt, input, kernel.GetCancellationToken(), kernel.GetProgress()); + return DataPackageHelpers.CreateFromText(result?.Content ?? string.Empty); + }); private Task ExecutePromptTransformAsync(Kernel kernel, PasteFormats format, string prompt) => ExecuteTransformAsync( @@ -212,7 +313,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi new ActionChainItem(format, Arguments: new() { { PromptParameterName, prompt } }), async dataPackageView => { - var input = await dataPackageView.GetTextAsync(); + var input = await dataPackageView.GetClipboardTextOrThrowAsync(kernel.GetCancellationToken()); string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress()); return DataPackageHelpers.CreateFromText(output); }); @@ -220,7 +321,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi private async Task GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress progress) => format switch { - PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress), + PasteFormats.CustomTextTransformation => (await _customActionTransformService.TransformTextAsync(prompt, input, cancellationToken, progress))?.Content ?? string.Empty, _ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)), }; @@ -281,4 +382,9 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi var usageString = usage.HasUsage ? $" [{usage}]" : string.Empty; return $"-> {role}: {redactedContent}{usageString}"; } + + protected virtual bool ShouldModerateAdvancedAI() + { + return false; + } } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs deleted file mode 100644 index b6aa156b9d..0000000000 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/CustomTextTransformService.cs +++ /dev/null @@ -1,113 +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.Text.Json; -using System.Threading; -using System.Threading.Tasks; - -using AdvancedPaste.Helpers; -using AdvancedPaste.Models; -using AdvancedPaste.Telemetry; -using Azure; -using Azure.AI.OpenAI; -using ManagedCommon; -using Microsoft.PowerToys.Telemetry; - -namespace AdvancedPaste.Services.OpenAI; - -public sealed class CustomTextTransformService(IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService) : ICustomTextTransformService -{ - private const string ModelName = "gpt-3.5-turbo-instruct"; - - private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider; - private readonly IPromptModerationService _promptModerationService = promptModerationService; - - private async Task GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken) - { - var fullPrompt = systemInstructions + "\n\n" + userMessage; - - await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken); - - OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key); - - var response = await azureAIClient.GetCompletionsAsync( - new() - { - DeploymentName = ModelName, - Prompts = - { - fullPrompt, - }, - Temperature = 0.01F, - MaxTokens = 2000, - }, - cancellationToken); - - if (response.Value.Choices[0].FinishReason == "length") - { - Logger.LogDebug("Cut off due to length constraints"); - } - - return response; - } - - public async Task TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress progress) - { - if (string.IsNullOrWhiteSpace(prompt)) - { - return string.Empty; - } - - if (string.IsNullOrWhiteSpace(inputText)) - { - Logger.LogWarning("Clipboard has no usable text data"); - return string.Empty; - } - - string systemInstructions = -$@"You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it. -Do not output anything else besides the reformatted clipboard content."; - - string userMessage = -$@"User instructions: -{prompt} - -Clipboard Content: -{inputText} - -Output: -"; - - try - { - var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken); - - var usage = response.Usage; - AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName); - PowerToysTelemetry.Log.WriteEvent(telemetryEvent); - var logEvent = new AIServiceFormatEvent(telemetryEvent); - - Logger.LogDebug($"{nameof(TransformTextAsync)} complete; {logEvent.ToJsonString()}"); - - return response.Choices[0].Text; - } - catch (Exception ex) - { - Logger.LogError($"{nameof(TransformTextAsync)} failed", ex); - - AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message); - PowerToysTelemetry.Log.WriteEvent(errorEvent); - - if (ex is PasteActionException or OperationCanceledException) - { - throw; - } - else - { - throw new PasteActionException(ErrorHelpers.TranslateErrorText((ex as RequestFailedException)?.Status ?? -1), ex); - } - } - } -} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs deleted file mode 100644 index b19a6d51cb..0000000000 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/KernelService.cs +++ /dev/null @@ -1,34 +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.Collections.Generic; - -using AdvancedPaste.Models; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace AdvancedPaste.Services.OpenAI; - -public sealed class KernelService(IKernelQueryCacheService queryCacheService, IAICredentialsProvider aiCredentialsProvider, IPromptModerationService promptModerationService, ICustomTextTransformService customTextTransformService) : - KernelServiceBase(queryCacheService, promptModerationService, customTextTransformService) -{ - private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider; - - protected override string ModelName => "gpt-4o"; - - protected override PromptExecutionSettings PromptExecutionSettings => - new OpenAIPromptExecutionSettings() - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions, - Temperature = 0.01, - }; - - protected override void AddChatCompletionService(IKernelBuilder kernelBuilder) => kernelBuilder.AddOpenAIChatCompletion(ModelName, _aiCredentialsProvider.Key); - - protected override AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage) => - chatMessage.Metadata?.GetValueOrDefault("Usage") is CompletionsUsage completionsUsage - ? new(PromptTokens: completionsUsage.PromptTokens, CompletionTokens: completionsUsage.CompletionTokens) - : AIServiceUsage.None; -} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs index 0ca15e4161..2668300526 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/PromptModerationService.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using AdvancedPaste.Helpers; using AdvancedPaste.Models; +using AdvancedPaste.Services; using ManagedCommon; using OpenAI.Moderations; @@ -23,7 +24,16 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials { try { - ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key); + _aiCredentialsProvider.Refresh(); + var apiKey = _aiCredentialsProvider.GetKey()?.Trim() ?? string.Empty; + + if (string.IsNullOrEmpty(apiKey)) + { + Logger.LogWarning("Skipping OpenAI moderation because no credential is configured."); + return; + } + + ModerationClient moderationClient = new(ModelName, apiKey); var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken); var moderationResult = moderationClientResult.Value; diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/VaultCredentialsProvider.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/VaultCredentialsProvider.cs deleted file mode 100644 index 169c1c2422..0000000000 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/OpenAI/VaultCredentialsProvider.cs +++ /dev/null @@ -1,37 +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 Windows.Security.Credentials; - -namespace AdvancedPaste.Services.OpenAI; - -public sealed class VaultCredentialsProvider : IAICredentialsProvider -{ - public VaultCredentialsProvider() => Refresh(); - - public string Key { get; private set; } - - public bool IsConfigured => !string.IsNullOrEmpty(Key); - - public bool Refresh() - { - var oldKey = Key; - Key = LoadKey(); - return oldKey != Key; - } - - private static string LoadKey() - { - try - { - return new PasswordVault().Retrieve("https://platform.openai.com/api-keys", "PowerToys_AdvancedPaste_OpenAIKey")?.Password ?? string.Empty; - } - catch (Exception) - { - return string.Empty; - } - } -} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs index 5d6740977b..aef9e39bb9 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Services/PasteFormatExecutor.cs @@ -8,15 +8,16 @@ using System.Threading.Tasks; using AdvancedPaste.Helpers; using AdvancedPaste.Models; +using AdvancedPaste.Services.CustomActions; using Microsoft.PowerToys.Telemetry; using Windows.ApplicationModel.DataTransfer; namespace AdvancedPaste.Services; -public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTextTransformService customTextTransformService) : IPasteFormatExecutor +public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomActionTransformService customActionTransformService) : IPasteFormatExecutor { private readonly IKernelService _kernelService = kernelService; - private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService; + private readonly ICustomActionTransformService _customActionTransformService = customActionTransformService; public async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress progress) { @@ -36,7 +37,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex pasteFormat.Format switch { PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress), - PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)), + PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText((await _customActionTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetClipboardTextOrThrowAsync(cancellationToken), cancellationToken, progress))?.Content ?? string.Empty), _ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress), }); } diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw index 604cbf403b..f365778321 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw +++ b/src/modules/AdvancedPaste/AdvancedPaste/Strings/en-us/Resources.resw @@ -144,16 +144,67 @@ The paste operation was moderated due to sensitive content. Please try another query. - + Clipboard history Clipboard history + + AI provider selector + + + Select an AI provider + + + Active provider: {0} + + + Configured models + + + No models configured + + + Configure models in Settings + Image data Label used to represent an image in the clipboard history + + Text + + + Image + + + Audio + + + Video + + + File + + + Clipboard + + + Copied just now + + + Copied {0} sec ago + + + Copied {0} min ago + + + Copied {0} hr ago + + + Copied {0} day ago + More options @@ -196,7 +247,7 @@ Transcode to .mp3 Option to transcode audio files to MP3 format - + Transcode to .mp4 (H.264/AAC) Option to transcode video files to MP4 format with H.264 video codec and AAC audio codec @@ -272,11 +323,11 @@ Next result - - OpenAI Privacy + + Privacy Policy - - OpenAI Terms + + Terms To custom with AI is disabled by your organization @@ -287,4 +338,38 @@ PowerToys_Paste_ + + Just now + + + 1 minute ago + + + {0} minutes ago + + + Today, {0} + + + Yesterday, {0} + + + {0}, {1} + (e.g., “Wednesday, 17:05â€) + + + {0}, {1} + (e.g., "10/20/2025, 17:05" in the user's locale) + + + You are using a custom endpoint. Verify all answers. + + + Local + Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally + + + Unable to load Foundry Local model: {0} + {0} is the model identifier. Do not translate {0}. + \ No newline at end of file diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteCustomActionErrorEvent.cs b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteCustomActionErrorEvent.cs new file mode 100644 index 0000000000..06f45a98ae --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteCustomActionErrorEvent.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace AdvancedPaste.Telemetry; + +[EventData] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +public sealed class AdvancedPasteCustomActionErrorEvent : EventBase, IEvent +{ + public AdvancedPasteCustomActionErrorEvent(AIServiceType providerType, string modelName, int statusCode, string error) + { + ProviderType = providerType.ToString(); + ModelName = modelName; + StatusCode = statusCode; + Error = error; + } + + public string ProviderType { get; set; } + + public string ModelName { get; set; } + + public int StatusCode { get; set; } + + public string Error { get; set; } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteEndpointUsageEvent.cs b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteEndpointUsageEvent.cs new file mode 100644 index 0000000000..671f6a7b9c --- /dev/null +++ b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteEndpointUsageEvent.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace AdvancedPaste.Telemetry; + +[EventData] +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] +public class AdvancedPasteEndpointUsageEvent : EventBase, IEvent +{ + /// + /// Gets or sets the AI provider type (e.g., OpenAI, AzureOpenAI, Google). + /// + public string ProviderType { get; set; } + + /// + /// Gets or sets the configured model name. + /// + public string ModelName { get; set; } + + /// + /// Gets or sets a value indicating whether the advanced AI pipeline was used. + /// + public bool IsAdvanced { get; set; } + + /// + /// Gets or sets the total duration in milliseconds, or -1 if unavailable. + /// + public int DurationMs { get; set; } + + public AdvancedPasteEndpointUsageEvent(AIServiceType providerType, string modelName, bool isAdvanced, int durationMs = -1) + { + ProviderType = providerType.ToString(); + ModelName = modelName; + IsAdvanced = isAdvanced; + DurationMs = durationMs; + } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; +} diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteSemanticKernelFormatEvent.cs b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteSemanticKernelFormatEvent.cs index 70542da6c8..53b4008782 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteSemanticKernelFormatEvent.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/Telemetry/AdvancedPasteSemanticKernelFormatEvent.cs @@ -14,7 +14,7 @@ namespace AdvancedPaste.Telemetry; [EventData] [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] -public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string actionChain) : EventBase, IEvent +public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSavedQuery, int promptTokens, int completionTokens, string modelName, string providerType, string actionChain) : EventBase, IEvent { public static string FormatActionChain(IEnumerable actionChain) => FormatActionChain(actionChain.Select(item => item.Format)); @@ -30,6 +30,8 @@ public class AdvancedPasteSemanticKernelFormatEvent(bool cacheUsed, bool isSaved public string ModelName { get; set; } = modelName; + public string ProviderType { get; set; } = providerType; + ///
/// Gets or sets a comma-separated list of paste formats used - in the same order they were executed. /// Conceptually an array but formatted this way to work around https://github.com/dotnet/runtime/issues/10428 diff --git a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs index 688c3047e2..8edd9b76ad 100644 --- a/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs +++ b/src/modules/AdvancedPaste/AdvancedPaste/ViewModels/OptionsViewModel.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Globalization; using System.IO.Abstractions; using System.Linq; using System.Runtime.InteropServices; @@ -22,6 +23,8 @@ using CommunityToolkit.Mvvm.Input; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.Win32; using Windows.ApplicationModel.DataTransfer; using Windows.System; @@ -37,12 +40,20 @@ namespace AdvancedPaste.ViewModels private readonly DispatcherTimer _clipboardTimer; private readonly IUserSettings _userSettings; private readonly IPasteFormatExecutor _pasteFormatExecutor; - private readonly IAICredentialsProvider _aiCredentialsProvider; + private readonly IAICredentialsProvider _credentialsProvider; private CancellationTokenSource _pasteActionCancellationTokenSource; + private string _currentClipboardHistoryId; + private DateTimeOffset? _currentClipboardTimestamp; + private ClipboardFormat _lastClipboardFormats = ClipboardFormat.None; + private bool _clipboardHistoryUnavailableLogged; + public DataPackageView ClipboardData { get; set; } + [ObservableProperty] + private ClipboardItem _currentClipboardItem; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))] [NotifyPropertyChangedFor(nameof(ClipboardHasData))] @@ -52,12 +63,21 @@ namespace AdvancedPaste.ViewModels private ClipboardFormat _availableClipboardFormats; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowClipboardHistoryButton))] private bool _clipboardHistoryEnabled; [ObservableProperty] [NotifyPropertyChangedFor(nameof(CustomAIUnavailableErrorText))] [NotifyPropertyChangedFor(nameof(IsCustomAIServiceEnabled))] [NotifyPropertyChangedFor(nameof(IsCustomAIAvailable))] + [NotifyPropertyChangedFor(nameof(AllowedAIProviders))] + [NotifyPropertyChangedFor(nameof(ActiveAIProvider))] + [NotifyPropertyChangedFor(nameof(ActiveAIProviderTooltip))] + [NotifyPropertyChangedFor(nameof(TermsLinkUri))] + [NotifyPropertyChangedFor(nameof(PrivacyLinkUri))] + [NotifyPropertyChangedFor(nameof(HasTermsLink))] + [NotifyPropertyChangedFor(nameof(HasPrivacyLink))] + [NotifyPropertyChangedFor(nameof(HasLegalLinks))] private bool _isAllowedByGPO; [ObservableProperty] @@ -79,19 +99,146 @@ namespace AdvancedPaste.ViewModels public ObservableCollection CustomActionPasteFormats { get; } = []; - public bool IsCustomAIServiceEnabled => IsAllowedByGPO && _aiCredentialsProvider.IsConfigured; + public bool IsCustomAIServiceEnabled + { + get + { + if (!IsAllowedByGPO || !_userSettings.IsAIEnabled) + { + return false; + } + + // Check if there are any allowed providers + if (!AllowedAIProviders.Any()) + { + return false; + } + + // We should handle the IsAIEnabled logic in settings, don't check again here. + // If setting says yes, and here should pass check, and if error happens, it happens. + return true; + } + } public bool IsCustomAIAvailable => IsCustomAIServiceEnabled && ClipboardHasDataForCustomAI; - public bool IsAdvancedAIEnabled => IsCustomAIServiceEnabled && _userSettings.IsAdvancedAIEnabled; + public bool IsAdvancedAIEnabled + { + get + { + if (!IsAllowedByGPO || !_userSettings.IsAIEnabled) + { + return false; + } + + if (!TryResolveAdvancedAIProvider(out _)) + { + return false; + } + + return _credentialsProvider.IsConfigured(); + } + } + + public ObservableCollection AIProviders => _userSettings?.PasteAIConfiguration?.Providers ?? new ObservableCollection(); + + public IEnumerable AllowedAIProviders + { + get + { + var providers = AIProviders; + if (providers is null || providers.Count == 0) + { + return Enumerable.Empty(); + } + + return providers.Where(IsProviderAllowedByGPO); + } + } + + public PasteAIProviderDefinition ActiveAIProvider + { + get + { + var provider = _userSettings?.PasteAIConfiguration?.ActiveProvider; + if (provider is null || !IsProviderAllowedByGPO(provider)) + { + return null; + } + + return provider; + } + } + + public string ActiveAIProviderTooltip + { + get + { + var resourceLoader = ResourceLoaderInstance.ResourceLoader; + var provider = ActiveAIProvider; + + if (provider is null) + { + return resourceLoader.GetString("AIProviderButtonTooltipEmpty"); + } + + var format = resourceLoader.GetString("AIProviderButtonTooltipFormat"); + var displayName = provider.DisplayName; + + if (!string.IsNullOrEmpty(format)) + { + return string.Format(CultureInfo.CurrentCulture, format, displayName); + } + + return displayName; + } + } + + private AIServiceTypeMetadata GetActiveProviderMetadata() + { + var provider = ActiveAIProvider ?? AllowedAIProviders.FirstOrDefault(); + var serviceType = provider?.ServiceTypeKind ?? AIServiceType.OpenAI; + return AIServiceTypeRegistry.GetMetadata(serviceType); + } + + public Uri TermsLinkUri + { + get + { + var metadata = GetActiveProviderMetadata(); + return metadata.HasTermsLink ? metadata.TermsUri : null; + } + } + + public Uri PrivacyLinkUri + { + get + { + var metadata = GetActiveProviderMetadata(); + return metadata.HasPrivacyLink ? metadata.PrivacyUri : null; + } + } + + public bool HasTermsLink => GetActiveProviderMetadata().HasTermsLink; + + public bool HasPrivacyLink => GetActiveProviderMetadata().HasPrivacyLink; + + public bool HasLegalLinks => HasTermsLink || HasPrivacyLink; public bool ClipboardHasData => AvailableClipboardFormats != ClipboardFormat.None; public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats); + public bool ShowClipboardPreview => _userSettings.EnableClipboardPreview; + + public bool ShowClipboardHistoryButton => ClipboardHistoryEnabled; + public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress); - private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation; + private PasteFormats CustomAIFormat => + _userSettings.IsAIEnabled && TryResolveAdvancedAIProvider(out _) + ? PasteFormats.KernelQuery + : PasteFormats.CustomTextTransformation; private bool Visible { @@ -110,9 +257,9 @@ namespace AdvancedPaste.ViewModels public event EventHandler PreviewRequested; - public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider aiCredentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor) + public OptionsViewModel(IFileSystem fileSystem, IAICredentialsProvider credentialsProvider, IUserSettings userSettings, IPasteFormatExecutor pasteFormatExecutor) { - _aiCredentialsProvider = aiCredentialsProvider; + _credentialsProvider = credentialsProvider; _userSettings = userSettings; _pasteFormatExecutor = pasteFormatExecutor; @@ -130,6 +277,7 @@ namespace AdvancedPaste.ViewModels _clipboardTimer.Start(); RefreshPasteFormats(); + UpdateAIProviderActiveFlags(); _userSettings.Changed += UserSettings_Changed; PropertyChanged += (_, e) => { @@ -158,15 +306,21 @@ namespace AdvancedPaste.ViewModels if (Visible) { await ReadClipboardAsync(); - UpdateAllowedByGPO(); } } private void UserSettings_Changed(object sender, EventArgs e) { + UpdateAIProviderActiveFlags(); + OnPropertyChanged(nameof(IsCustomAIServiceEnabled)); OnPropertyChanged(nameof(ClipboardHasDataForCustomAI)); OnPropertyChanged(nameof(IsCustomAIAvailable)); OnPropertyChanged(nameof(IsAdvancedAIEnabled)); + OnPropertyChanged(nameof(AIProviders)); + OnPropertyChanged(nameof(AllowedAIProviders)); + OnPropertyChanged(nameof(ShowClipboardPreview)); + + NotifyActiveProviderChanged(); EnqueueRefreshPasteFormats(); } @@ -192,6 +346,33 @@ namespace AdvancedPaste.ViewModels private PasteFormat CreateCustomAIPasteFormat(string name, string prompt, bool isSavedQuery) => PasteFormat.CreateCustomAIFormat(CustomAIFormat, name, prompt, isSavedQuery, AvailableClipboardFormats, IsCustomAIServiceEnabled); + private void UpdateAIProviderActiveFlags() + { + var providers = _userSettings?.PasteAIConfiguration?.Providers; + if (providers is not null) + { + var activeId = ActiveAIProvider?.Id; + + foreach (var provider in providers) + { + provider.IsActive = !string.IsNullOrEmpty(activeId) && string.Equals(provider.Id, activeId, StringComparison.OrdinalIgnoreCase); + } + } + + NotifyActiveProviderChanged(); + } + + private void NotifyActiveProviderChanged() + { + OnPropertyChanged(nameof(ActiveAIProvider)); + OnPropertyChanged(nameof(ActiveAIProviderTooltip)); + OnPropertyChanged(nameof(TermsLinkUri)); + OnPropertyChanged(nameof(PrivacyLinkUri)); + OnPropertyChanged(nameof(HasTermsLink)); + OnPropertyChanged(nameof(HasPrivacyLink)); + OnPropertyChanged(nameof(HasLegalLinks)); + } + private void RefreshPasteFormats() { var ctrlString = ResourceLoaderInstance.ResourceLoader.GetString("CtrlKey"); @@ -253,8 +434,96 @@ namespace AdvancedPaste.ViewModels return; } - ClipboardData = Clipboard.GetContent(); - AvailableClipboardFormats = await ClipboardData.GetAvailableFormatsAsync(); + try + { + ClipboardData = Clipboard.GetContent(); + AvailableClipboardFormats = ClipboardData != null ? await ClipboardData.GetAvailableFormatsAsync() : ClipboardFormat.None; + } + catch (Exception ex) when (ex is COMException or InvalidOperationException) + { + // Logger.LogDebug("Failed to read clipboard content", ex); + ClipboardData = null; + AvailableClipboardFormats = ClipboardFormat.None; + } + + await UpdateClipboardPreviewAsync(); + } + + private async Task UpdateClipboardPreviewAsync() + { + if (ClipboardData is null || !ClipboardHasData) + { + ResetClipboardPreview(); + _currentClipboardHistoryId = null; + _currentClipboardTimestamp = null; + _lastClipboardFormats = ClipboardFormat.None; + return; + } + + var formatsChanged = AvailableClipboardFormats != _lastClipboardFormats; + _lastClipboardFormats = AvailableClipboardFormats; + + var clipboardChanged = await UpdateClipboardTimestampAsync(formatsChanged); + + // Create ClipboardItem directly from current clipboard data using helper + CurrentClipboardItem = await ClipboardItemHelper.CreateFromCurrentClipboardAsync( + ClipboardData, + AvailableClipboardFormats, + _currentClipboardTimestamp, + clipboardChanged ? null : CurrentClipboardItem?.Image); + } + + private async Task UpdateClipboardTimestampAsync(bool formatsChanged) + { + bool clipboardChanged = formatsChanged; + + if (Clipboard.IsHistoryEnabled()) + { + try + { + var historyItems = await Clipboard.GetHistoryItemsAsync(); + if (historyItems.Status == ClipboardHistoryItemsResultStatus.Success && historyItems.Items.Count > 0) + { + var latest = historyItems.Items[0]; + if (_currentClipboardHistoryId != latest.Id) + { + clipboardChanged = true; + _currentClipboardHistoryId = latest.Id; + } + + _currentClipboardTimestamp = latest.Timestamp; + _clipboardHistoryUnavailableLogged = false; + return clipboardChanged; + } + } + catch (Exception ex) + { + if (!_clipboardHistoryUnavailableLogged) + { + Logger.LogDebug("Failed to access clipboard history timestamp", ex.Message); + _clipboardHistoryUnavailableLogged = true; + } + } + } + + if (!_currentClipboardTimestamp.HasValue || clipboardChanged) + { + _currentClipboardTimestamp = DateTimeOffset.Now; + clipboardChanged = true; + } + + return clipboardChanged; + } + + private void ResetClipboardPreview() + { + // Clear to avoid leaks due to Garbage Collection not clearing the bitmap from memory + if (CurrentClipboardItem?.Image is not null) + { + CurrentClipboardItem.Image.ClearValue(BitmapImage.UriSourceProperty); + } + + CurrentClipboardItem = null; } public async Task OnShowAsync() @@ -270,7 +539,7 @@ namespace AdvancedPaste.ViewModels _dispatcherQueue.TryEnqueue(() => { - GetMainWindow()?.FinishLoading(_aiCredentialsProvider.IsConfigured); + GetMainWindow()?.FinishLoading(IsCustomAIServiceEnabled); OnPropertyChanged(nameof(InputTxtBoxPlaceholderText)); OnPropertyChanged(nameof(CustomAIUnavailableErrorText)); OnPropertyChanged(nameof(IsCustomAIServiceEnabled)); @@ -319,7 +588,7 @@ namespace AdvancedPaste.ViewModels return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled"); } - if (!_aiCredentialsProvider.IsConfigured) + if (!IsCustomAIServiceEnabled) { return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured"); } @@ -515,11 +784,113 @@ namespace AdvancedPaste.ViewModels IsAllowedByGPO = PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled; } + private bool IsProviderAllowedByGPO(PasteAIProviderDefinition provider) + { + if (provider is null) + { + return false; + } + + var serviceType = provider.ServiceType.ToAIServiceType(); + var metadata = AIServiceTypeRegistry.GetMetadata(serviceType); + + // Check global online AI GPO for online services + if (metadata.IsOnlineService && !IsAllowedByGPO) + { + return false; + } + + // Check individual endpoint GPO + return serviceType switch + { + AIServiceType.OpenAI => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOpenAIValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled, + AIServiceType.AzureOpenAI => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteAzureOpenAIValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled, + AIServiceType.AzureAIInference => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteAzureAIInferenceValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled, + AIServiceType.Mistral => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteMistralValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled, + AIServiceType.Google => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteGoogleValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled, + AIServiceType.Ollama => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOllamaValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled, + AIServiceType.FoundryLocal => PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteFoundryLocalValue() != PowerToys.GPOWrapper.GpoRuleConfigured.Disabled, + _ => true, // Allow unknown types by default + }; + } + + private bool TryResolveAdvancedAIProvider(out PasteAIProviderDefinition provider) + { + provider = null; + + var configuration = _userSettings?.PasteAIConfiguration; + if (configuration is null) + { + return false; + } + + var activeProvider = configuration.ActiveProvider; + if (IsAdvancedAIProvider(activeProvider)) + { + provider = activeProvider; + return true; + } + + if (activeProvider is not null) + { + return false; + } + + var fallback = configuration.Providers?.FirstOrDefault(IsAdvancedAIProvider); + if (fallback is not null) + { + provider = fallback; + return true; + } + + return false; + } + + private static bool IsAdvancedAIProvider(PasteAIProviderDefinition provider) + { + return provider is not null && provider.EnableAdvancedAI && SupportsAdvancedAI(provider.ServiceTypeKind); + } + + private static bool SupportsAdvancedAI(AIServiceType serviceType) + { + return serviceType is AIServiceType.OpenAI + or AIServiceType.AzureOpenAI; + } + private bool UpdateOpenAIKey() { UpdateAllowedByGPO(); - return IsAllowedByGPO && _aiCredentialsProvider.Refresh(); + return _credentialsProvider.Refresh(); + } + + [RelayCommand] + private async Task SetActiveProviderAsync(PasteAIProviderDefinition provider) + { + if (provider is null || string.IsNullOrEmpty(provider.Id)) + { + return; + } + + if (string.Equals(ActiveAIProvider?.Id, provider.Id, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + try + { + await _userSettings.SetActiveAIProviderAsync(provider.Id); + } + catch (Exception ex) + { + Logger.LogError("Failed to activate AI provider", ex); + return; + } + + UpdateAIProviderActiveFlags(); + OnPropertyChanged(nameof(AIProviders)); + NotifyActiveProviderChanged(); + EnqueueRefreshPasteFormats(); } public async Task CancelPasteActionAsync() diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj index 083aa868d3..2cf2920673 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/AdvancedPasteModuleInterface.vcxproj @@ -2,7 +2,7 @@ - + 15.0 diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index 6af0d636ac..c7d22d474f 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -16,7 +16,8 @@ #include #include -#include +#include +#include #include BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) @@ -54,12 +55,14 @@ namespace const wchar_t JSON_KEY_ADVANCED_PASTE_UI_HOTKEY[] = L"advanced-paste-ui-hotkey"; const wchar_t JSON_KEY_PASTE_AS_MARKDOWN_HOTKEY[] = L"paste-as-markdown-hotkey"; const wchar_t JSON_KEY_PASTE_AS_JSON_HOTKEY[] = L"paste-as-json-hotkey"; - const wchar_t JSON_KEY_IS_ADVANCED_AI_ENABLED[] = L"IsAdvancedAIEnabled"; + const wchar_t JSON_KEY_IS_AI_ENABLED[] = L"IsAIEnabled"; + const wchar_t JSON_KEY_IS_OPEN_AI_ENABLED[] = L"IsOpenAIEnabled"; const wchar_t JSON_KEY_SHOW_CUSTOM_PREVIEW[] = L"ShowCustomPreview"; + const wchar_t JSON_KEY_PASTE_AI_CONFIGURATION[] = L"paste-ai-configuration"; + const wchar_t JSON_KEY_PROVIDERS[] = L"providers"; + const wchar_t JSON_KEY_SERVICE_TYPE[] = L"service-type"; + const wchar_t JSON_KEY_ENABLE_ADVANCED_AI[] = L"enable-advanced-ai"; const wchar_t JSON_KEY_VALUE[] = L"value"; - - const wchar_t OPENAI_VAULT_RESOURCE[] = L"https://platform.openai.com/api-keys"; - const wchar_t OPENAI_VAULT_USERNAME[] = L"PowerToys_AdvancedPaste_OpenAIKey"; } class AdvancedPaste : public PowertoyModuleIface @@ -94,6 +97,7 @@ private: using CustomAction = ActionData; std::vector m_custom_actions; + bool m_is_ai_enabled = false; bool m_is_advanced_ai_enabled = false; bool m_preview_custom_format_output = true; @@ -145,32 +149,11 @@ private: return jsonObject; } - static bool open_ai_key_exists() - { - try - { - winrt::Windows::Security::Credentials::PasswordVault().Retrieve(OPENAI_VAULT_RESOURCE, OPENAI_VAULT_USERNAME); - return true; - } - catch (const winrt::hresult_error& ex) - { - // Looks like the only way to access the PasswordVault is through an API that throws an exception in case the resource doesn't exist. - // If the debugger breaks here, just continue. - // If you want to disable breaking here in a more permanent way, just add a condition in Visual Studio's Exception Settings to not break on win::hresult_error, but that might make you not hit other exceptions you might want to catch. - if (ex.code() == HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) - { - return false; // Credential doesn't exist. - } - Logger::error("Unexpected error while retrieving OpenAI key from vault: {}", winrt::to_string(ex.message())); - return false; - } - } - - bool is_open_ai_enabled() + bool is_ai_enabled() { return gpo_policy_enabled_configuration() != powertoys_gpo::gpo_rule_configured_disabled && powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue() != powertoys_gpo::gpo_rule_configured_disabled && - open_ai_key_exists(); + m_is_ai_enabled; } static std::wstring kebab_to_pascal_case(const std::wstring& kebab_str) @@ -201,6 +184,13 @@ private: return result; } + static std::wstring to_lower_case(const std::wstring& value) + { + std::wstring result = value; + std::transform(result.begin(), result.end(), result.begin(), [](wchar_t ch) { return std::towlower(ch); }); + return result; + } + bool migrate_data_and_remove_data_file(Hotkey& old_paste_as_plain_hotkey) { const wchar_t OLD_JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut"; @@ -267,6 +257,61 @@ private: } } + bool has_advanced_ai_provider(const winrt::Windows::Data::Json::JsonObject& propertiesObject) + { + if (!propertiesObject.HasKey(JSON_KEY_PASTE_AI_CONFIGURATION)) + { + return false; + } + + const auto configValue = propertiesObject.GetNamedValue(JSON_KEY_PASTE_AI_CONFIGURATION); + if (configValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object) + { + return false; + } + + const auto configObject = configValue.GetObjectW(); + if (!configObject.HasKey(JSON_KEY_PROVIDERS)) + { + return false; + } + + const auto providersValue = configObject.GetNamedValue(JSON_KEY_PROVIDERS); + if (providersValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Array) + { + return false; + } + + const auto providers = providersValue.GetArray(); + for (const auto providerValue : providers) + { + if (providerValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object) + { + continue; + } + + const auto providerObject = providerValue.GetObjectW(); + if (!providerObject.GetNamedBoolean(JSON_KEY_ENABLE_ADVANCED_AI, false)) + { + continue; + } + + if (!providerObject.HasKey(JSON_KEY_SERVICE_TYPE)) + { + continue; + } + + const std::wstring serviceType = providerObject.GetNamedString(JSON_KEY_SERVICE_TYPE, L"").c_str(); + const auto normalizedServiceType = to_lower_case(serviceType); + if (normalizedServiceType == L"openai" || normalizedServiceType == L"azureopenai") + { + return true; + } + } + + return false; + } + void read_settings(PowerToysSettings::PowerToyValues& settings) { const auto settingsObject = settings.get_raw_json(); @@ -341,7 +386,7 @@ private: if (propertiesObject.HasKey(JSON_KEY_CUSTOM_ACTIONS)) { const auto customActions = propertiesObject.GetNamedObject(JSON_KEY_CUSTOM_ACTIONS).GetNamedArray(JSON_KEY_VALUE); - if (customActions.Size() > 0 && is_open_ai_enabled()) + if (customActions.Size() > 0 && is_ai_enabled()) { for (const auto& customAction : customActions) { @@ -365,9 +410,19 @@ private: { const auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES); - if (propertiesObject.HasKey(JSON_KEY_IS_ADVANCED_AI_ENABLED)) + m_is_advanced_ai_enabled = has_advanced_ai_provider(propertiesObject); + + if (propertiesObject.HasKey(JSON_KEY_IS_AI_ENABLED)) { - m_is_advanced_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_ADVANCED_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE); + m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false); + } + else if (propertiesObject.HasKey(JSON_KEY_IS_OPEN_AI_ENABLED)) + { + m_is_ai_enabled = propertiesObject.GetNamedObject(JSON_KEY_IS_OPEN_AI_ENABLED).GetNamedBoolean(JSON_KEY_VALUE, false); + } + else + { + m_is_ai_enabled = false; } if (propertiesObject.HasKey(JSON_KEY_SHOW_CUSTOM_PREVIEW)) diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json index 31ad05c701..bc0803796e 100644 --- a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json @@ -1 +1 @@ -{"properties":{"IsAdvancedAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}}},"name":"AdvancedPaste","version":"1"} \ No newline at end of file +{"properties":{"IsAIEnabled":{"value":false},"ShowCustomPreview":{"value":true},"CloseAfterLosingFocus":{"value":false},"advanced-paste-ui-hotkey":{"win":true,"ctrl":false,"alt":false,"shift":true,"code":86,"key":""},"paste-as-plain-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":79,"key":""},"paste-as-markdown-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":77,"key":""},"paste-as-json-hotkey":{"win":true,"ctrl":true,"alt":true,"shift":false,"code":74,"key":""},"custom-actions":{"value":[]},"additional-actions":{"image-to-text":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-file":{"isShown":true,"paste-as-txt-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-png-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"paste-as-html-file":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}},"transcode":{"isShown":true,"transcode-to-mp3":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true},"transcode-to-mp4":{"shortcut":{"win":false,"ctrl":false,"alt":false,"shift":false,"code":0,"key":""},"isShown":true}}},"paste-ai-configuration":{"active-provider-id":"","providers":[],"use-shared-credentials":true}},"name":"AdvancedPaste","version":"1"} \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml index c48b7fbb25..ae77a78caa 100644 --- a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml @@ -20,7 +20,7 @@ - + - + _userSettings; private static Mock _elevationHelper; + private static Mock _backupManager; // Case1: Fuzzing method for ValidIPv4 public static void FuzzValidIPv4(ReadOnlySpan input) @@ -73,9 +70,10 @@ namespace Hosts.FuzzTests _userSettings = new Mock(); _elevationHelper = new Mock(); _elevationHelper.Setup(m => m.IsElevated).Returns(true); + _backupManager = new Mock(); var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); string input = System.Text.Encoding.UTF8.GetString(data); diff --git a/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj b/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj index 667cfcc0ad..51dee7a40b 100644 --- a/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj +++ b/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj @@ -30,8 +30,11 @@ + + + diff --git a/src/modules/Hosts/Hosts.Tests/BackupManagerTest.cs b/src/modules/Hosts/Hosts.Tests/BackupManagerTest.cs new file mode 100644 index 0000000000..6aeb834029 --- /dev/null +++ b/src/modules/Hosts/Hosts.Tests/BackupManagerTest.cs @@ -0,0 +1,156 @@ +// 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.IO.Abstractions.TestingHelpers; +using HostsUILib.Helpers; +using HostsUILib.Settings; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Hosts.Tests +{ + [TestClass] + public class BackupManagerTest + { + private const string HostsPath = @"C:\Windows\System32\Drivers\etc\hosts"; + private const string BackupPath = @"C:\Backup\hosts"; + private const string BackupSearchPattern = $"*_PowerToysBackup_*"; + + [TestMethod] + public void Hosts_Backup_Not_Executed() + { + var fileSystem = new MockFileSystem(); + SetupFiles(fileSystem, true); + var userSettings = new Mock(); + userSettings.Setup(m => m.BackupHosts).Returns(false); + userSettings.Setup(m => m.BackupPath).Returns(BackupPath); + var backupManager = new BackupManager(fileSystem, userSettings.Object); + backupManager.Create(HostsPath); + + Assert.AreEqual(0, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length); + } + + [TestMethod] + public void Hosts_Backup_Executed_Once() + { + var fileSystem = new MockFileSystem(); + SetupFiles(fileSystem, true); + var userSettings = new Mock(); + userSettings.Setup(m => m.BackupHosts).Returns(true); + userSettings.Setup(m => m.BackupPath).Returns(BackupPath); + var backupManager = new BackupManager(fileSystem, userSettings.Object); + backupManager.Create(HostsPath); + backupManager.Create(HostsPath); + + Assert.AreEqual(1, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length); + var hostsContent = fileSystem.File.ReadAllText(HostsPath); + var backupContent = fileSystem.File.ReadAllText(fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern)[0]); + Assert.AreEqual(hostsContent, backupContent); + } + + [DataTestMethod] + [DataRow(-10, -10)] + [DataRow(-10, 0)] + [DataRow(-10, 10)] + [DataRow(0, -10)] + [DataRow(0, 0)] + [DataRow(0, 10)] + [DataRow(10, -10)] + [DataRow(10, 0)] + [DataRow(10, 10)] + public void Hosts_Backups_Delete_Never(int count, int days) + { + var fileSystem = new MockFileSystem(); + SetupFiles(fileSystem, false); + var userSettings = new Mock(); + userSettings.Setup(m => m.BackupPath).Returns(BackupPath); + userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Never); + var backupManager = new BackupManager(fileSystem, userSettings.Object); + backupManager.Delete(); + + Assert.AreEqual(30, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length); + Assert.AreEqual(31, fileSystem.Directory.GetFiles(BackupPath).Length); + } + + [DataTestMethod] + [DataRow(-10, 30)] + [DataRow(0, 30)] + [DataRow(10, 10)] + public void Hosts_Backups_Delete_ByCount(int count, int expectedBackups) + { + var fileSystem = new MockFileSystem(); + SetupFiles(fileSystem, false); + var userSettings = new Mock(); + userSettings.Setup(m => m.BackupPath).Returns(BackupPath); + userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Count); + userSettings.Setup(m => m.DeleteBackupsCount).Returns(count); + var backupManager = new BackupManager(fileSystem, userSettings.Object); + backupManager.Delete(); + + Assert.AreEqual(expectedBackups, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length); + Assert.AreEqual(expectedBackups + 1, fileSystem.Directory.GetFiles(BackupPath).Length); + } + + [DataTestMethod] + [DataRow(-10, -10, 30)] + [DataRow(-10, 0, 30)] + [DataRow(-10, 10, 5)] + [DataRow(0, -10, 30)] + [DataRow(0, 0, 30)] + [DataRow(0, 10, 5)] + [DataRow(10, -10, 30)] + [DataRow(10, 0, 30)] + [DataRow(5, 1, 5)] + [DataRow(1, 15, 10)] + [DataRow(2, 2, 2)] + public void Hosts_Backups_Delete_ByAge(int count, int days, int expectedBackups) + { + var fileSystem = new MockFileSystem(); + SetupFiles(fileSystem, false); + var userSettings = new Mock(); + userSettings.Setup(m => m.BackupPath).Returns(BackupPath); + userSettings.Setup(m => m.DeleteBackupsMode).Returns(HostsDeleteBackupMode.Age); + userSettings.Setup(m => m.DeleteBackupsCount).Returns(count); + userSettings.Setup(m => m.DeleteBackupsDays).Returns(days); + var backupManager = new BackupManager(fileSystem, userSettings.Object); + backupManager.Delete(); + + Assert.AreEqual(expectedBackups, fileSystem.Directory.GetFiles(BackupPath, BackupSearchPattern).Length); + Assert.AreEqual(expectedBackups + 1, fileSystem.Directory.GetFiles(BackupPath).Length); + } + + private void SetupFiles(MockFileSystem fileSystem, bool hostsOnly) + { + fileSystem.AddDirectory(BackupPath); + fileSystem.AddFile(HostsPath, new MockFileData("HOSTS FILE CONTENT")); + + if (hostsOnly) + { + return; + } + + var today = new DateTimeOffset(DateTime.Today); + + var notBackupData = new MockFileData("NOT A BACKUP") + { + CreationTime = today.AddDays(-100), + }; + + fileSystem.AddFile(fileSystem.Path.Combine(BackupPath, "hosts_not_a_backup"), notBackupData); + + // The first backup is from 5 days ago. There are 30 backups, one for each day. + var offset = 5; + for (var i = 0; i < 30; i++) + { + var backupData = new MockFileData("THIS IS A BACKUP") + { + CreationTime = today.AddDays(-i - offset), + }; + + fileSystem.AddFile(fileSystem.Path.Combine(BackupPath, $"hosts_PowerToysBackup_{i}"), backupData); + } + } + } +} diff --git a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs index 81052fd101..4c6ee77f8c 100644 --- a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs +++ b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs @@ -20,8 +20,10 @@ namespace Hosts.Tests [TestClass] public class HostsServiceTest { + private const string BackupPath = @"C:\Backup\hosts"; private static Mock _userSettings; private static Mock _elevationHelper; + private static Mock _backupManager; [ClassInitialize] public static void ClassInitialize(TestContext context) @@ -29,27 +31,7 @@ namespace Hosts.Tests _userSettings = new Mock(); _elevationHelper = new Mock(); _elevationHelper.Setup(m => m.IsElevated).Returns(true); - } - - [TestMethod] - public void Hosts_Exists() - { - var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); - fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty)); - var result = service.Exists(); - - Assert.IsTrue(result); - } - - [TestMethod] - public void Hosts_Not_Exists() - { - var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); - var result = service.Exists(); - - Assert.IsFalse(result); + _backupManager = new Mock(); } [TestMethod] @@ -67,7 +49,7 @@ namespace Hosts.Tests "; var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); var data = await service.ReadAsync(); @@ -92,7 +74,7 @@ namespace Hosts.Tests "; var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); var data = await service.ReadAsync(); @@ -118,7 +100,7 @@ namespace Hosts.Tests "; var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); var data = await service.ReadAsync(); @@ -137,7 +119,7 @@ namespace Hosts.Tests public async Task Empty_Hosts() { var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(string.Empty)); await service.WriteAsync(string.Empty, Enumerable.Empty()); @@ -168,7 +150,7 @@ namespace Hosts.Tests var fileSystem = new CustomMockFileSystem(); var userSettings = new Mock(); userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Top); - var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); var data = await service.ReadAsync(); @@ -200,7 +182,7 @@ namespace Hosts.Tests var fileSystem = new CustomMockFileSystem(); var userSettings = new Mock(); userSettings.Setup(m => m.AdditionalLinesPosition).Returns(HostsAdditionalLinesPosition.Bottom); - var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, userSettings.Object, _elevationHelper.Object, _backupManager.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); var data = await service.ReadAsync(); @@ -224,7 +206,7 @@ namespace Hosts.Tests "; var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); var data = await service.ReadAsync(); @@ -241,7 +223,7 @@ namespace Hosts.Tests var elevationHelper = new Mock(); elevationHelper.Setup(m => m.IsElevated).Returns(false); - var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, elevationHelper.Object, _backupManager.Object); await Assert.ThrowsExceptionAsync(async () => await service.WriteAsync("# Empty hosts file", Enumerable.Empty())); } @@ -249,7 +231,7 @@ namespace Hosts.Tests public async Task Save_ReadOnlyHostsException() { var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); var hostsFile = new MockFileData(string.Empty) { @@ -265,7 +247,7 @@ namespace Hosts.Tests public void Remove_ReadOnly_Attribute() { var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); var hostsFile = new MockFileData(string.Empty) { @@ -284,7 +266,7 @@ namespace Hosts.Tests public async Task Save_Hidden_Hosts() { var fileSystem = new CustomMockFileSystem(); - var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, _backupManager.Object); var hostsFile = new MockFileData(string.Empty) { @@ -316,7 +298,7 @@ namespace Hosts.Tests var fs = new CustomMockFileSystem(); var settings = new Mock(); settings.Setup(s => s.NoLeadingSpaces).Returns(true); - var svc = new HostsService(fs, settings.Object, _elevationHelper.Object); + var svc = new HostsService(fs, settings.Object, _elevationHelper.Object, _backupManager.Object); fs.AddFile(svc.HostsFilePath, new MockFileData(content)); var data = await svc.ReadAsync(); @@ -327,5 +309,57 @@ namespace Hosts.Tests var result = fs.GetFile(svc.HostsFilePath); Assert.AreEqual(expected, result.TextContents); } + + [TestMethod] + public async Task Hosts_Backup_Not_Executed() + { + var content = +@"10.1.1.1 host host.local # comment +10.1.1.2 host2 host2.local # another comment +"; + + var fileSystem = new CustomMockFileSystem(); + fileSystem.AddDirectory(BackupPath); + _userSettings.Setup(m => m.BackupHosts).Returns(false); + _userSettings.Setup(m => m.BackupPath).Returns(BackupPath); + var backupManager = new BackupManager(fileSystem, _userSettings.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, backupManager); + + fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); + + var data = await service.ReadAsync(); + var entries = data.Entries.ToList(); + entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false)); + await service.WriteAsync(data.AdditionalLines, data.Entries); + + Assert.AreEqual(0, fileSystem.Directory.GetFiles(BackupPath).Length); + } + + [TestMethod] + public async Task Hosts_Backup_Executed_Once() + { + var content = +@"10.1.1.1 host host.local # comment +10.1.1.2 host2 host2.local # another comment +"; + + var fileSystem = new CustomMockFileSystem(); + _userSettings.Setup(m => m.BackupHosts).Returns(true); + _userSettings.Setup(m => m.BackupPath).Returns(BackupPath); + var backupManager = new BackupManager(fileSystem, _userSettings.Object); + var service = new HostsService(fileSystem, _userSettings.Object, _elevationHelper.Object, backupManager); + + fileSystem.AddFile(service.HostsFilePath, new MockFileData(content)); + + var data = await service.ReadAsync(); + var entries = data.Entries.ToList(); + entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false)); + await service.WriteAsync(data.AdditionalLines, data.Entries); + await service.WriteAsync(data.AdditionalLines, data.Entries); + + Assert.AreEqual(1, fileSystem.Directory.GetFiles(BackupPath).Length); + var backupContent = fileSystem.File.ReadAllText(fileSystem.Directory.GetFiles(BackupPath)[0]); + Assert.AreEqual(content, backupContent); + } } } diff --git a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs index fbe5d3662d..8dbec70de8 100644 --- a/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs +++ b/src/modules/Hosts/Hosts/HostsXAML/App.xaml.cs @@ -56,6 +56,7 @@ namespace Hosts { // Core Services services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -74,7 +75,7 @@ namespace Hosts }). Build(); - var cleanupBackupThread = new Thread(() => + var deleteBackupThread = new Thread(() => { // Delete old backups only if running elevated if (!Host.GetService().IsElevated) @@ -84,7 +85,7 @@ namespace Hosts try { - Host.GetService().CleanupBackup(); + Host.GetService().Delete(); } catch (Exception ex) { @@ -92,8 +93,8 @@ namespace Hosts } }); - cleanupBackupThread.IsBackground = true; - cleanupBackupThread.Start(); + deleteBackupThread.IsBackground = true; + deleteBackupThread.Start(); UnhandledException += App_UnhandledException; diff --git a/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml b/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml index 92d1594556..001cbeb3ed 100644 --- a/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml +++ b/src/modules/Hosts/Hosts/HostsXAML/MainWindow.xaml @@ -20,7 +20,7 @@ - + - + 16.0 @@ -46,7 +46,7 @@ - $(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories) + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) diff --git a/src/modules/Hosts/HostsUILib/Helpers/BackupManager.cs b/src/modules/Hosts/HostsUILib/Helpers/BackupManager.cs new file mode 100644 index 0000000000..5417408409 --- /dev/null +++ b/src/modules/Hosts/HostsUILib/Helpers/BackupManager.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO.Abstractions; +using System.Linq; +using HostsUILib.Settings; + +namespace HostsUILib.Helpers +{ + public class BackupManager : IBackupManager + { + private const string BackupSuffix = "_PowerToysBackup_"; + private readonly IFileSystem _fileSystem; + private readonly IUserSettings _userSettings; + private bool _backupDone; + + public BackupManager(IFileSystem fileSystem, IUserSettings userSettings) + { + _fileSystem = fileSystem; + _userSettings = userSettings; + } + + public void Create(string hostsFilePath) + { + if (_backupDone || !_userSettings.BackupHosts || !_fileSystem.File.Exists(hostsFilePath)) + { + return; + } + + try + { + if (!_fileSystem.Directory.Exists(_userSettings.BackupPath)) + { + _fileSystem.Directory.CreateDirectory(_userSettings.BackupPath); + } + + var backupPath = _fileSystem.Path.Combine(_userSettings.BackupPath, $"hosts{BackupSuffix}{DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)}"); + + _fileSystem.File.Copy(hostsFilePath, backupPath); + _backupDone = true; + } + catch (Exception ex) + { + LoggerInstance.Logger.LogError("Backup failed", ex); + } + } + + public void Delete() + { + switch (_userSettings.DeleteBackupsMode) + { + case HostsDeleteBackupMode.Count: + DeleteByCount(_userSettings.DeleteBackupsCount); + break; + case HostsDeleteBackupMode.Age: + DeleteByAge(_userSettings.DeleteBackupsDays, _userSettings.DeleteBackupsCount); + break; + } + } + + public void DeleteByCount(int count) + { + if (count < 1) + { + return; + } + + var backups = GetAll().OrderByDescending(f => f.CreationTime).Skip(count).ToArray(); + DeleteAll(backups); + } + + public void DeleteByAge(int days, int count) + { + if (days < 1) + { + return; + } + + var backupsEnumerable = GetAll(); + + if (count > 0) + { + backupsEnumerable = backupsEnumerable.OrderByDescending(f => f.CreationTime).Skip(count); + } + + var backups = backupsEnumerable.Where(f => f.CreationTime < DateTime.Now.AddDays(-days)).ToArray(); + DeleteAll(backups); + } + + private IEnumerable GetAll() + { + if (!_fileSystem.Directory.Exists(_userSettings.BackupPath)) + { + return []; + } + + return _fileSystem.Directory.GetFiles(_userSettings.BackupPath, $"*{BackupSuffix}*").Select(_fileSystem.FileInfo.New); + } + + private void DeleteAll(IFileInfo[] files) + { + foreach (var f in files) + { + _fileSystem.File.Delete(f.FullName); + } + } + } +} diff --git a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs index 83aa3544b1..9b16e04f20 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.IO; using System.IO.Abstractions; using System.Linq; @@ -23,16 +22,15 @@ namespace HostsUILib.Helpers { public partial class HostsService : IHostsService, IDisposable { - private const string _backupSuffix = $"_PowerToysBackup_"; - private const int _defaultBufferSize = 4096; // From System.IO.File source code + private const int DefaultBufferSize = 4096; // From System.IO.File source code private readonly SemaphoreSlim _asyncLock = new SemaphoreSlim(1, 1); private readonly IFileSystem _fileSystem; private readonly IUserSettings _userSettings; private readonly IElevationHelper _elevationHelper; private readonly IFileSystemWatcher _fileSystemWatcher; + private readonly IBackupManager _backupManager; private readonly string _hostsFilePath; - private bool _backupDone; private bool _disposed; public string HostsFilePath => _hostsFilePath; @@ -44,11 +42,13 @@ namespace HostsUILib.Helpers public HostsService( IFileSystem fileSystem, IUserSettings userSettings, - IElevationHelper elevationHelper) + IElevationHelper elevationHelper, + IBackupManager backupManager) { _fileSystem = fileSystem; _userSettings = userSettings; _elevationHelper = elevationHelper; + _backupManager = backupManager; _hostsFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc\hosts"); @@ -60,18 +60,13 @@ namespace HostsUILib.Helpers _fileSystemWatcher.EnableRaisingEvents = true; } - public bool Exists() - { - return _fileSystem.File.Exists(HostsFilePath); - } - public async Task ReadAsync() { var entries = new List(); var unparsedBuilder = new StringBuilder(); var splittedEntries = false; - if (!Exists()) + if (!_fileSystem.File.Exists(HostsFilePath)) { return new HostsData(entries, unparsedBuilder.ToString(), false); } @@ -192,15 +187,10 @@ namespace HostsUILib.Helpers { await _asyncLock.WaitAsync(); _fileSystemWatcher.EnableRaisingEvents = false; - - if (!_backupDone && Exists()) - { - _fileSystem.File.Copy(HostsFilePath, HostsFilePath + _backupSuffix + DateTime.Now.ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture)); - _backupDone = true; - } + _backupManager.Create(HostsFilePath); // FileMode.OpenOrCreate is necessary to prevent UnauthorizedAccessException when the hosts file is hidden - using var stream = _fileSystem.FileStream.New(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, _defaultBufferSize, FileOptions.Asynchronous); + using var stream = _fileSystem.FileStream.New(HostsFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous); using var writer = new StreamWriter(stream, Encoding); foreach (var line in lines) { @@ -231,15 +221,6 @@ namespace HostsUILib.Helpers } } - public void CleanupBackup() - { - Directory.GetFiles(Path.GetDirectoryName(HostsFilePath), $"*{_backupSuffix}*") - .Select(f => new FileInfo(f)) - .Where(f => f.CreationTime < DateTime.Now.AddDays(-15)) - .ToList() - .ForEach(f => f.Delete()); - } - public void OpenHostsFile() { var notepadFallback = false; diff --git a/src/modules/Hosts/HostsUILib/Helpers/IBackupManager.cs b/src/modules/Hosts/HostsUILib/Helpers/IBackupManager.cs new file mode 100644 index 0000000000..9da9802a26 --- /dev/null +++ b/src/modules/Hosts/HostsUILib/Helpers/IBackupManager.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace HostsUILib.Helpers +{ + public interface IBackupManager + { + void Create(string hostsFilePath); + + void Delete(); + } +} diff --git a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs index fe75946a12..c6f2678156 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/IHostsService.cs @@ -22,8 +22,6 @@ namespace HostsUILib.Helpers Task PingAsync(string address); - void CleanupBackup(); - void OpenHostsFile(); void RemoveReadOnlyAttribute(); diff --git a/src/modules/Hosts/HostsUILib/Settings/HostsDeleteBackupMode.cs b/src/modules/Hosts/HostsUILib/Settings/HostsDeleteBackupMode.cs new file mode 100644 index 0000000000..d1e1d79ded --- /dev/null +++ b/src/modules/Hosts/HostsUILib/Settings/HostsDeleteBackupMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace HostsUILib.Settings +{ + public enum HostsDeleteBackupMode + { + Never = 0, + Count = 1, + Age = 2, + } +} diff --git a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs index 46c7a7dab5..4f175398ad 100644 --- a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs +++ b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs @@ -16,6 +16,16 @@ namespace HostsUILib.Settings public HostsEncoding Encoding { get; } + public bool BackupHosts { get; } + + public string BackupPath { get; } + + public HostsDeleteBackupMode DeleteBackupsMode { get; } + + public int DeleteBackupsDays { get; } + + public int DeleteBackupsCount { get; } + event EventHandler LoopbackDuplicatesChanged; public delegate void OpenSettingsFunction(); diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp index 7ebe4a67eb..845e24fa93 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include "ThemeScheduler.h" #include "ThemeHelper.h" @@ -11,17 +11,17 @@ #include #include #include +#include "LightSwitchStateManager.h" +#include SERVICE_STATUS g_ServiceStatus = {}; SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; HANDLE g_ServiceStopEvent = nullptr; -static int g_lastUpdatedDay = -1; -static ScheduleMode prevMode = ScheduleMode::Off; -static std::wstring prevLat, prevLon; VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); DWORD WINAPI ServiceWorkerThread(LPVOID lpParam); +void ApplyTheme(bool shouldBeLight); // Entry point for the executable int _tmain(int argc, TCHAR* argv[]) @@ -122,33 +122,67 @@ VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl) } } -static void update_sun_times(auto& settings) +void ApplyTheme(bool shouldBeLight) { - double latitude = std::stod(settings.latitude); - double longitude = std::stod(settings.longitude); + const auto& s = LightSwitchSettings::settings(); + + if (s.changeSystem) + { + bool isSystemCurrentlyLight = GetCurrentSystemTheme(); + if (shouldBeLight != isSystemCurrentlyLight) + { + SetSystemTheme(shouldBeLight); + Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark"); + } + } + + if (s.changeApps) + { + bool isAppsCurrentlyLight = GetCurrentAppsTheme(); + if (shouldBeLight != isAppsCurrentlyLight) + { + SetAppsTheme(shouldBeLight); + Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark"); + } + } +} + +static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateManager) +{ + const auto& s = LightSwitchSettings::settings(); + if (s.scheduleMode == ScheduleMode::Off) + return; SYSTEMTIME st; GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; - SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay); + // Compute effective boundaries (with offsets if needed) + int effectiveLight = s.lightTime; + int effectiveDark = s.darkTime; - int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute; - int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute; - try + if (s.scheduleMode == ScheduleMode::SunsetToSunrise) { - auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); - values.add_property(L"lightTime", newLightTime); - values.add_property(L"darkTime", newDarkTime); - values.save_to_settings_file(); + effectiveLight = (s.lightTime + s.sunrise_offset) % 1440; + effectiveDark = (s.darkTime + s.sunset_offset) % 1440; + } - Logger::info(L"[LightSwitchService] Updated sun times and saved to config."); - } - catch (const std::exception& e) + // Use shared helper (handles wraparound logic) + bool shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark); + + // Compare current system/apps theme + bool currentSystemLight = GetCurrentSystemTheme(); + bool currentAppsLight = GetCurrentAppsTheme(); + + bool systemMismatch = s.changeSystem && (currentSystemLight != shouldBeLight); + bool appsMismatch = s.changeApps && (currentAppsLight != shouldBeLight); + + // Trigger manual override only if mismatch and not already active + if ((systemMismatch || appsMismatch) && !stateManager.GetState().isManualOverride) { - std::wstring wmsg(e.what(), e.what() + strlen(e.what())); - Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg); + Logger::info(L"[LightSwitchService] External theme change detected (Windows Settings). Entering manual override mode."); + stateManager.OnManualOverride(); } - } DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) @@ -161,244 +195,101 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) Logger::info(L"[LightSwitchService] Worker thread starting..."); Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid); - // Initialize settings system + // ──────────────────────────────────────────────────────────────── + // Initialization + // ──────────────────────────────────────────────────────────────── + static LightSwitchStateManager stateManager; + LightSwitchSettings::instance().InitFileWatcher(); - // Open the manual override event created by the module interface HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); + HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent(); - auto applyTheme = [](int nowMinutes, int lightMinutes, int darkMinutes, const auto& settings) { - bool isLightActive = false; - - if (lightMinutes < darkMinutes) - { - // Normal case: sunrise < sunset - isLightActive = (nowMinutes >= lightMinutes && nowMinutes < darkMinutes); - } - else - { - // Wraparound case: e.g. light at 21:00, dark at 06:00 - isLightActive = (nowMinutes >= lightMinutes || nowMinutes < darkMinutes); - } - - bool isSystemCurrentlyLight = GetCurrentSystemTheme(); - bool isAppsCurrentlyLight = GetCurrentAppsTheme(); - - if (isLightActive) - { - if (settings.changeSystem && !isSystemCurrentlyLight) - { - SetSystemTheme(true); - Logger::info(L"[LightSwitchService] Changing system theme to light mode."); - } - if (settings.changeApps && !isAppsCurrentlyLight) - { - SetAppsTheme(true); - Logger::info(L"[LightSwitchService] Changing apps theme to light mode."); - } - } - else - { - if (settings.changeSystem && isSystemCurrentlyLight) - { - SetSystemTheme(false); - Logger::info(L"[LightSwitchService] Changing system theme to dark mode."); - } - if (settings.changeApps && isAppsCurrentlyLight) - { - SetAppsTheme(false); - Logger::info(L"[LightSwitchService] Changing apps theme to dark mode."); - } - } - }; - - // --- Initial settings load --- LightSwitchSettings::instance().LoadSettings(); - auto& settings = LightSwitchSettings::instance().settings(); + const auto& settings = LightSwitchSettings::instance().settings(); - // --- Initial theme application (if schedule enabled) --- - if (settings.scheduleMode != ScheduleMode::Off) - { - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - applyTheme(nowMinutes, settings.lightTime + settings.sunrise_offset, settings.darkTime + settings.sunset_offset, settings); - } - else - { - Logger::info(L"[LightSwitchService] Schedule mode is OFF - ticker suspended, waiting for manual action or mode change."); - } + SYSTEMTIME st; + GetLocalTime(&st); + int nowMinutes = st.wHour * 60 + st.wMinute; - // --- Main loop --- + Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute); + stateManager.SyncInitialThemeState(); + stateManager.OnTick(nowMinutes); + + // ──────────────────────────────────────────────────────────────── + // Worker Loop + // ──────────────────────────────────────────────────────────────── for (;;) { - HANDLE waits[2] = { g_ServiceStopEvent, hParent }; - DWORD count = hParent ? 2 : 1; - - LightSwitchSettings::instance().LoadSettings(); - const auto& settings = LightSwitchSettings::instance().settings(); - - // Check for changes in schedule mode or coordinates - bool modeChangedToSunset = (prevMode != settings.scheduleMode && - settings.scheduleMode == ScheduleMode::SunsetToSunrise); - bool coordsChanged = (prevLat != settings.latitude || prevLon != settings.longitude); - - if ((modeChangedToSunset || coordsChanged) && settings.scheduleMode == ScheduleMode::SunsetToSunrise) - { - Logger::info(L"[LightSwitchService] Mode or coordinates changed, recalculating sun times."); - update_sun_times(settings); - SYSTEMTIME st; - GetLocalTime(&st); - g_lastUpdatedDay = st.wDay; - prevMode = settings.scheduleMode; - prevLat = settings.latitude; - prevLon = settings.longitude; - } - - // If schedule is off, idle but keep watching settings and manual override - if (settings.scheduleMode == ScheduleMode::Off) - { - Logger::info(L"[LightSwitchService] Schedule mode OFF - suspending scheduler but keeping service alive."); - - if (!hManualOverride) - { - hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); - } - - HANDLE waits[4]; - DWORD count = 0; - waits[count++] = g_ServiceStopEvent; - if (hParent) - waits[count++] = hParent; - if (hManualOverride) - waits[count++] = hManualOverride; - waits[count++] = LightSwitchSettings::instance().GetSettingsChangedEvent(); - - for (;;) - { - DWORD wait = WaitForMultipleObjects(count, waits, FALSE, INFINITE); - - // --- Handle exit signals --- - if (wait == WAIT_OBJECT_0) // stop event - { - Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop."); - break; - } - if (hParent && wait == WAIT_OBJECT_0 + 1) - { - Logger::info(L"[LightSwitchService] Parent exited - stopping service."); - break; - } - - // --- Manual override triggered --- - if (wait == WAIT_OBJECT_0 + (hParent ? 2 : 1)) - { - Logger::info(L"[LightSwitchService] Manual override received while schedule OFF."); - ResetEvent(hManualOverride); - continue; - } - - // --- Settings file changed --- - if (wait == WAIT_OBJECT_0 + (hParent ? 3 : 2)) - { - Logger::trace(L"[LightSwitchService] Settings change event triggered, reloading settings..."); - - ResetEvent(LightSwitchSettings::instance().GetSettingsChangedEvent()); - - LightSwitchSettings::instance().LoadSettings(); - const auto& newSettings = LightSwitchSettings::instance().settings(); - - if (newSettings.scheduleMode != ScheduleMode::Off) - { - Logger::info(L"[LightSwitchService] Schedule re-enabled, resuming normal loop."); - break; - } - } - } - } - - - // --- When schedule is active, run once per minute --- - SYSTEMTIME st; - GetLocalTime(&st); - int nowMinutes = st.wHour * 60 + st.wMinute; - - // Refresh suntimes at day boundary - if ((g_lastUpdatedDay != st.wDay) && (settings.scheduleMode == ScheduleMode::SunsetToSunrise)) - { - update_sun_times(settings); - g_lastUpdatedDay = st.wDay; - Logger::info(L"[LightSwitchService] Recalculated sun times at new day boundary."); - } - - // Have to do this again in case settings got updated in the refresh suntimes chunk - LightSwitchSettings::instance().LoadSettings(); - const auto& currentSettings = LightSwitchSettings::instance().settings(); - - wchar_t msg[160]; - swprintf_s(msg, - L"[LightSwitchService] now=%02d:%02d | light=%02d:%02d | dark=%02d:%02d | mode=%d", - st.wHour, - st.wMinute, - currentSettings.lightTime / 60, - currentSettings.lightTime % 60, - currentSettings.darkTime / 60, - currentSettings.darkTime % 60, - static_cast(currentSettings.scheduleMode)); - Logger::info(msg); - - // --- Manual override check --- - bool manualOverrideActive = false; + HANDLE waits[4]; + DWORD count = 0; + waits[count++] = g_ServiceStopEvent; + if (hParent) + waits[count++] = hParent; if (hManualOverride) - { - manualOverrideActive = (WaitForSingleObject(hManualOverride, 0) == WAIT_OBJECT_0); - } + waits[count++] = hManualOverride; + waits[count++] = hSettingsChanged; - if (manualOverrideActive) - { - if (nowMinutes == (currentSettings.lightTime + currentSettings.sunrise_offset) % 1440 || - nowMinutes == (currentSettings.darkTime + currentSettings.sunset_offset) % 1440) - { - ResetEvent(hManualOverride); - Logger::info(L"[LightSwitchService] Manual override cleared at boundary"); - } - else - { - Logger::info(L"[LightSwitchService] Skipping schedule due to manual override"); - goto sleep_until_next_minute; - } - } - - applyTheme(nowMinutes, currentSettings.lightTime + currentSettings.sunrise_offset, currentSettings.darkTime + currentSettings.sunset_offset, currentSettings); - - sleep_until_next_minute: + // Wait for one of these to trigger or for a new minute tick + SYSTEMTIME st; GetLocalTime(&st); int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds; if (msToNextMinute < 50) msToNextMinute = 50; DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute); + + if (wait == WAIT_TIMEOUT) + { + // regular minute tick + GetLocalTime(&st); + nowMinutes = st.wHour * 60 + st.wMinute; + DetectAndHandleExternalThemeChange(stateManager); + stateManager.OnTick(nowMinutes); + continue; + } + if (wait == WAIT_OBJECT_0) { - Logger::info(L"[LightSwitchService] Stop event triggered - exiting worker loop."); + Logger::info(L"[LightSwitchService] Stop event triggered — exiting."); break; } + if (hParent && wait == WAIT_OBJECT_0 + 1) { - Logger::info(L"[LightSwitchService] Parent process exited - stopping service."); + Logger::info(L"[LightSwitchService] Parent process exited — stopping service."); break; } + + if (hManualOverride && wait == WAIT_OBJECT_0 + (hParent ? 2 : 1)) + { + Logger::info(L"[LightSwitchService] Manual override event detected."); + stateManager.OnManualOverride(); + ResetEvent(hManualOverride); + continue; + } + + if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2)) + { + ResetEvent(hSettingsChanged); + LightSwitchSettings::instance().LoadSettings(); + stateManager.OnSettingsChanged(); + continue; + } } + // ──────────────────────────────────────────────────────────────── + // Cleanup + // ──────────────────────────────────────────────────────────────── if (hManualOverride) CloseHandle(hManualOverride); if (hParent) CloseHandle(hParent); + Logger::info(L"[LightSwitchService] Worker thread exiting cleanly."); return 0; } - int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) { if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj index b082250f61..a3a505f897 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj @@ -75,6 +75,7 @@ + @@ -85,6 +86,8 @@ + + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters index f5aa05afc3..795df99aba 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchService.vcxproj.filters @@ -33,6 +33,9 @@ Source Files + + Source Files + @@ -53,6 +56,12 @@ Header Files + + Header Files + + + Header Files + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp index a7f44cca6d..5221a197fe 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.cpp @@ -2,10 +2,8 @@ #include #include #include "SettingsObserver.h" - #include #include -#include #include using namespace std; @@ -38,12 +36,79 @@ void LightSwitchSettings::InitFileWatcher() m_settingsFileWatcher = std::make_unique( GetSettingsFileName(), [this]() { - Logger::info(L"[LightSwitchSettings] Settings file changed, signaling event."); - SetEvent(m_settingsChangedEvent); + using namespace std::chrono; + + { + std::lock_guard lock(m_debounceMutex); + m_lastChangeTime = steady_clock::now(); + if (m_debouncePending) + return; + m_debouncePending = true; + } + + m_debounceThread = std::jthread([this](std::stop_token stop) { + using namespace std::chrono; + while (!stop.stop_requested()) + { + std::this_thread::sleep_for(seconds(3)); + + auto elapsed = steady_clock::now() - m_lastChangeTime; + if (elapsed >= seconds(1)) + break; + } + + { + std::lock_guard lock(m_debounceMutex); + m_debouncePending = false; + } + + Logger::info(L"[LightSwitchSettings] Settings file stabilized, reloading."); + + try + { + LoadSettings(); + SetEvent(m_settingsChangedEvent); + } + catch (const std::exception& e) + { + std::wstring wmsg; + wmsg.assign(e.what(), e.what() + strlen(e.what())); + Logger::error(L"[LightSwitchSettings] Exception during debounced reload: {}", wmsg); + } + }); }); } } +LightSwitchSettings::~LightSwitchSettings() +{ + Logger::info(L"[LightSwitchSettings] Cleaning up settings resources..."); + + // Stop and join the debounce thread (std::jthread auto-joins, but we can signal stop too) + if (m_debounceThread.joinable()) + { + m_debounceThread.request_stop(); + } + + // Release the file watcher so it closes file handles and background threads + if (m_settingsFileWatcher) + { + m_settingsFileWatcher.reset(); + Logger::info(L"[LightSwitchSettings] File watcher stopped."); + } + + // Close the Windows event handle + if (m_settingsChangedEvent) + { + CloseHandle(m_settingsChangedEvent); + m_settingsChangedEvent = nullptr; + Logger::info(L"[LightSwitchSettings] Settings changed event closed."); + } + + Logger::info(L"[LightSwitchSettings] Cleanup complete."); +} + + void LightSwitchSettings::AddObserver(SettingsObserver& observer) { m_observers.insert(&observer); @@ -65,8 +130,14 @@ void LightSwitchSettings::NotifyObservers(SettingId id) const } } +HANDLE LightSwitchSettings::GetSettingsChangedEvent() const +{ + return m_settingsChangedEvent; +} + void LightSwitchSettings::LoadSettings() { + std::lock_guard guard(m_settingsMutex); try { PowerToysSettings::PowerToyValues values = @@ -175,4 +246,4 @@ void LightSwitchSettings::LoadSettings() { // Keeps defaults if load fails } -} \ No newline at end of file +} diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h index 32d011313f..d4029d072d 100644 --- a/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchSettings.h @@ -5,7 +5,10 @@ #include #include #include - +#include +#include +#include +#include #include #include #include @@ -79,11 +82,11 @@ public: void LoadSettings(); - HANDLE GetSettingsChangedEvent() const { return m_settingsChangedEvent; } + HANDLE GetSettingsChangedEvent() const; private: LightSwitchSettings(); - ~LightSwitchSettings() = default; + ~LightSwitchSettings(); LightSwitchConfig m_settings; std::unique_ptr m_settingsFileWatcher; @@ -92,4 +95,11 @@ private: void NotifyObservers(SettingId id) const; HANDLE m_settingsChangedEvent = nullptr; + mutable std::mutex m_settingsMutex; + + // Debounce state + std::atomic_bool m_debouncePending{ false }; + std::mutex m_debounceMutex; + std::chrono::steady_clock::time_point m_lastChangeTime{}; + std::jthread m_debounceThread; }; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp new file mode 100644 index 0000000000..4fba4ae9a6 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.cpp @@ -0,0 +1,232 @@ +#include "pch.h" +#include "LightSwitchStateManager.h" +#include +#include +#include "ThemeScheduler.h" +#include + +void ApplyTheme(bool shouldBeLight); + +// Constructor +LightSwitchStateManager::LightSwitchStateManager() +{ + Logger::info(L"[LightSwitchStateManager] Initialized"); +} + +// Called when settings.json changes +void LightSwitchStateManager::OnSettingsChanged() +{ + std::lock_guard lock(_stateMutex); + + // If manual override was active, clear it so new settings take effect + if (_state.isManualOverride) + { + _state.isManualOverride = false; + } + + EvaluateAndApplyIfNeeded(); +} + +// Called once per minute +void LightSwitchStateManager::OnTick(int currentMinutes) +{ + std::lock_guard lock(_stateMutex); + EvaluateAndApplyIfNeeded(); +} + +// Called when manual override is triggered +void LightSwitchStateManager::OnManualOverride() +{ + std::lock_guard lock(_stateMutex); + Logger::info(L"[LightSwitchStateManager] Manual override triggered"); + _state.isManualOverride = !_state.isManualOverride; + + // When entering manual override, sync internal theme state to match the current system + if (_state.isManualOverride) + { + _state.isSystemLightActive = GetCurrentSystemTheme(); + + _state.isAppsLightActive = GetCurrentAppsTheme(); + + Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).", + (_state.isSystemLightActive ? L"light" : L"dark"), + (_state.isAppsLightActive ? L"light" : L"dark")); + } + + EvaluateAndApplyIfNeeded(); +} + +// Helpers +bool LightSwitchStateManager::CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon) +{ + try + { + double latVal = std::stod(lat); + double lonVal = std::stod(lon); + return !(latVal == 0 && lonVal == 0) && (latVal >= -90.0 && latVal <= 90.0) && (lonVal >= -180.0 && lonVal <= 180.0); + } + catch (...) + { + return false; + } +} + +void LightSwitchStateManager::SyncInitialThemeState() +{ + std::lock_guard lock(_stateMutex); + _state.isSystemLightActive = GetCurrentSystemTheme(); + _state.isAppsLightActive = GetCurrentAppsTheme(); + Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})", + _state.isSystemLightActive ? L"light" : L"dark"); + Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})", + _state.isAppsLightActive ? L"light" : L"dark"); +} + +static std::pair update_sun_times(auto& settings) +{ + double latitude = std::stod(settings.latitude); + double longitude = std::stod(settings.longitude); + + SYSTEMTIME st; + GetLocalTime(&st); + + SunTimes newTimes = CalculateSunriseSunset(latitude, longitude, st.wYear, st.wMonth, st.wDay); + + int newLightTime = newTimes.sunriseHour * 60 + newTimes.sunriseMinute; + int newDarkTime = newTimes.sunsetHour * 60 + newTimes.sunsetMinute; + + try + { + auto values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"LightSwitch"); + values.add_property(L"lightTime", newLightTime); + values.add_property(L"darkTime", newDarkTime); + values.save_to_settings_file(); + + Logger::info(L"[LightSwitchService] Updated sun times and saved to config."); + } + catch (const std::exception& e) + { + std::string msg = e.what(); + std::wstring wmsg(msg.begin(), msg.end()); + Logger::error(L"[LightSwitchService] Exception during sun time update: {}", wmsg); + } + + return { newLightTime, newDarkTime }; +} + +// Internal: decide what should happen now +void LightSwitchStateManager::EvaluateAndApplyIfNeeded() +{ + LightSwitchSettings::instance().LoadSettings(); + const auto& _currentSettings = LightSwitchSettings::settings(); + auto now = GetNowMinutes(); + + // Early exit: OFF mode just pauses activity + if (_currentSettings.scheduleMode == ScheduleMode::Off) + { + _state.lastTickMinutes = now; + return; + } + + bool coordsValid = CoordinatesAreValid(_currentSettings.latitude, _currentSettings.longitude); + + // Handle Sun Mode recalculation + if (_currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise && coordsValid) + { + SYSTEMTIME st; + GetLocalTime(&st); + bool newDay = (_state.lastEvaluatedDay != st.wDay); + bool modeChangedToSun = (_state.lastAppliedMode != ScheduleMode::SunsetToSunrise && + _currentSettings.scheduleMode == ScheduleMode::SunsetToSunrise); + + if (newDay || modeChangedToSun) + { + auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings); + _state.lastEvaluatedDay = st.wDay; + _state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset; + _state.effectiveDarkMinutes = newDarkTime + _currentSettings.sunset_offset; + } + else + { + _state.effectiveLightMinutes = _currentSettings.lightTime + _currentSettings.sunrise_offset; + _state.effectiveDarkMinutes = _currentSettings.darkTime + _currentSettings.sunset_offset; + } + } + else if (_currentSettings.scheduleMode == ScheduleMode::FixedHours) + { + _state.effectiveLightMinutes = _currentSettings.lightTime; + _state.effectiveDarkMinutes = _currentSettings.darkTime; + } + + // Handle manual override logic + if (_state.isManualOverride) + { + bool crossedBoundary = false; + if (_state.lastTickMinutes != -1) + { + int prev = _state.lastTickMinutes; + + // Handle midnight wraparound safely + if (now < prev) + { + crossedBoundary = + (prev <= _state.effectiveLightMinutes || now >= _state.effectiveLightMinutes) || + (prev <= _state.effectiveDarkMinutes || now >= _state.effectiveDarkMinutes); + } + else + { + crossedBoundary = + (prev < _state.effectiveLightMinutes && now >= _state.effectiveLightMinutes) || + (prev < _state.effectiveDarkMinutes && now >= _state.effectiveDarkMinutes); + } + } + + if (crossedBoundary) + { + _state.isManualOverride = false; + } + else + { + _state.lastTickMinutes = now; + return; + } + } + + _state.lastAppliedMode = _currentSettings.scheduleMode; + + bool shouldBeLight = ShouldBeLight(now, _state.effectiveLightMinutes, _state.effectiveDarkMinutes); + + bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight); + bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight); + + /* Logger::debug( + L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})", + now / 60, + now % 60, + _state.effectiveLightMinutes / 60, + _state.effectiveLightMinutes % 60, + _state.effectiveLightMinutes, + _state.effectiveDarkMinutes / 60, + _state.effectiveDarkMinutes % 60, + _state.effectiveDarkMinutes); */ + + /* Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}", + shouldBeLight ? "true" : "false", + appsNeedsToChange ? "true" : "false", + systemNeedsToChange ? "true" : "false"); */ + + // Only apply theme if there's a change or no override active + if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange)) + { + Logger::info(L"[LightSwitchStateManager] Applying {} theme", shouldBeLight ? L"light" : L"dark"); + ApplyTheme(shouldBeLight); + + _state.isSystemLightActive = GetCurrentSystemTheme(); + _state.isAppsLightActive = GetCurrentAppsTheme(); + } + + _state.lastTickMinutes = now; +} + + + diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h new file mode 100644 index 0000000000..5c9bcc6e25 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchStateManager.h @@ -0,0 +1,47 @@ +#pragma once +#include "LightSwitchSettings.h" +#include + +// Represents runtime-only information (not saved in settings.json) +struct LightSwitchState +{ + ScheduleMode lastAppliedMode = ScheduleMode::Off; + bool isManualOverride = false; + bool isSystemLightActive = false; + bool isAppsLightActive = false; + int lastEvaluatedDay = -1; + int lastTickMinutes = -1; + + // Derived, runtime-resolved times + int effectiveLightMinutes = 0; // the boundary we actually act on + int effectiveDarkMinutes = 0; // includes offsets if needed +}; + +// The controller that reacts to settings changes, time ticks, and manual overrides. +class LightSwitchStateManager +{ +public: + LightSwitchStateManager(); + + // Called when settings.json changes or stabilizes. + void OnSettingsChanged(); + + // Called every minute (from service worker tick). + void OnTick(int currentMinutes); + + // Called when manual override is toggled (via shortcut or system change). + void OnManualOverride(); + + // Initial sync at startup to align internal state with system theme + void SyncInitialThemeState(); + + // Accessor for current state (optional, for debugging or telemetry) + const LightSwitchState& GetState() const { return _state; } + +private: + LightSwitchState _state; + std::mutex _stateMutex; + + void EvaluateAndApplyIfNeeded(); + bool CoordinatesAreValid(const std::wstring& lat, const std::wstring& lon); +}; diff --git a/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h b/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h new file mode 100644 index 0000000000..0f4462bb65 --- /dev/null +++ b/src/modules/LightSwitch/LightSwitchService/LightSwitchUtils.h @@ -0,0 +1,24 @@ +#pragma once +#include + +constexpr bool ShouldBeLight(int nowMinutes, int lightTime, int darkTime) +{ + // Normalize values into [0, 1439] + int normalizedLightTime = (lightTime % 1440 + 1440) % 1440; + int normalizedDarkTime = (darkTime % 1440 + 1440) % 1440; + int normalizedNowMinutes = (nowMinutes % 1440 + 1440) % 1440; + + // Case 1: Normal range, e.g. light mode comes before dark mode in the same day + if (normalizedLightTime < normalizedDarkTime) + return normalizedNowMinutes >= normalizedLightTime && normalizedNowMinutes < normalizedDarkTime; + + // Case 2: Wrap-around range, e.g. light mode starts in the evening and dark mode starts in the morning + return normalizedNowMinutes >= normalizedLightTime || normalizedNowMinutes < normalizedDarkTime; +} + +inline int GetNowMinutes() +{ + SYSTEMTIME st; + GetLocalTime(&st); + return st.wHour * 60 + st.wMinute; +} diff --git a/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h index 88d0194eef..b0ddde72ec 100644 --- a/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h +++ b/src/modules/LightSwitch/LightSwitchService/SettingsObserver.h @@ -2,6 +2,7 @@ #include #include "SettingsConstants.h" +#include "LightSwitchSettings.h" class LightSwitchSettings; @@ -22,7 +23,7 @@ public: // Override this in your class to respond to updates virtual void SettingsUpdate(SettingId type) {} - bool WantsToBeNotified(SettingId type) const noexcept + virtual bool WantsToBeNotified(SettingId type) const noexcept { return m_observedSettings.contains(type); } diff --git a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs index 7b586301f6..37041b4b2d 100644 --- a/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs +++ b/src/modules/LightSwitch/Tests/LightSwitch.UITests/TestHelper.cs @@ -152,16 +152,16 @@ namespace LightSwitch.UITests var neededTabs = 6; - if (modeCombobox.Text != "Manual") + if (modeCombobox.Text != "Fixed hours") { modeCombobox.Click(); var manualListItem = testBase.Session.Find(By.AccessibilityId("ManualCBItem_LightSwitch"), 5000); - Assert.IsNotNull(manualListItem, "Manual combobox item not found."); + Assert.IsNotNull(manualListItem, "Fixed Hours combobox item not found."); manualListItem.Click(); neededTabs = 1; } - Assert.AreEqual("Manual", modeCombobox.Text, "Mode combobox should be set to Manual."); + Assert.AreEqual("Fixed hours", modeCombobox.Text, "Mode combobox should be set to Fixed hours."); var timeline = testBase.Session.Find(By.AccessibilityId("Timeline_LightSwitch"), 5000); Assert.IsNotNull(timeline, "Timeline not found."); @@ -198,7 +198,7 @@ namespace LightSwitch.UITests } /// - /// Perform a update geolocation test operation + /// Perform a update manual location test operation /// public static void PerformUserSelectedLocationTest(UITestBase testBase) { @@ -216,19 +216,22 @@ namespace LightSwitch.UITests Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise."); + // Click the select location button var setLocationButton = testBase.Session.Find(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000); Assert.IsNotNull(setLocationButton, "Set location button not found."); - setLocationButton.Click(); + setLocationButton.Click(msPostAction: 1000); - var autoSuggestTextbox = testBase.Session.Find(By.AccessibilityId("CitySearchBox_LightSwitch"), 5000); - Assert.IsNotNull(autoSuggestTextbox, "City search box not found."); - autoSuggestTextbox.Click(); - autoSuggestTextbox.SendKeys("Seattle"); - autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Down); - autoSuggestTextbox.SendKeys(OpenQA.Selenium.Keys.Enter); + var latitudeBox = testBase.Session.Find(By.AccessibilityId("LatitudeBox_LightSwitch"), 5000); + Assert.IsNotNull(latitudeBox, "Latitude text box not found."); + latitudeBox.Click(); - var latLong = testBase.Session.Find(By.AccessibilityId("LocationResultText_LightSwitch"), 5000); - Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text)); + testBase.Session.SendKeys(Key.Up); + + var longitudeBox = testBase.Session.Find(By.AccessibilityId("LongitudeBox_LightSwitch"), 5000); + Assert.IsNotNull(longitudeBox, "Longitude text box not found."); + longitudeBox.Click(); + + testBase.Session.SendKeys(Key.Down); var sunrise = testBase.Session.Find(By.AccessibilityId("SunriseText_LightSwitch"), 5000); Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text)); @@ -256,13 +259,14 @@ namespace LightSwitch.UITests Assert.AreEqual("Sunset to sunrise", modeCombobox.Text, "Mode combobox should be set to Sunset to sunrise."); - // Click the select city button + // Click the select location button var setLocationButton = testBase.Session.Find(By.AccessibilityId("SetLocationButton_LightSwitch"), 5000); Assert.IsNotNull(setLocationButton, "Set location button not found."); - setLocationButton.Click(msPostAction: 8000); + setLocationButton.Click(msPostAction: 1000); - var latLong = testBase.Session.Find(By.AccessibilityId("LocationResultText_LightSwitch"), 5000); - Assert.IsFalse(string.IsNullOrWhiteSpace(latLong.Text)); + var syncLocationButton = testBase.Session.Find(By.AccessibilityId("SyncLocationButton_LightSwitch"), 5000); + Assert.IsNotNull(syncLocationButton, "Sync location button not found."); + syncLocationButton.Click(msPostAction: 8000); var sunrise = testBase.Session.Find(By.AccessibilityId("SunriseText_LightSwitch"), 5000); Assert.IsFalse(string.IsNullOrWhiteSpace(sunrise.Text)); @@ -363,6 +367,7 @@ namespace LightSwitch.UITests var systemBeforeValue = GetSystemTheme(); var appsBeforeValue = GetAppsTheme(); + Task.Delay(1000).Wait(); testBase.Session.SendKeys(activationKeys); Task.Delay(5000).Wait(); @@ -389,6 +394,7 @@ namespace LightSwitch.UITests var noneSystemBeforeValue = GetSystemTheme(); var noneAppsBeforeValue = GetAppsTheme(); + Task.Delay(1000).Wait(); testBase.Session.SendKeys(activationKeys); Task.Delay(5000).Wait(); diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrap.rc b/src/modules/MouseUtils/CursorWrap/CursorWrap.rc new file mode 100644 index 0000000000..37752edae0 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/CursorWrap.rc @@ -0,0 +1,46 @@ +#include +#include "resource.h" +#include "../../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +#include "winres.h" +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO + FILEVERSION FILE_VERSION + PRODUCTVERSION PRODUCT_VERSION + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_DLL + FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", "PowerToys CursorWrap" + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", "CursorWrap" + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", "PowerToys.CursorWrap.dll" + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +STRINGTABLE +BEGIN + IDS_CURSORWRAP_NAME L"CursorWrap" + IDS_CURSORWRAP_DISABLE_WRAP_DURING_DRAG L"Disable wrapping during drag" +END \ No newline at end of file diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj b/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj new file mode 100644 index 0000000000..59e2095ca7 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/CursorWrap.vcxproj @@ -0,0 +1,130 @@ + + + + + 15.0 + {48a1db8c-5df8-4fb3-9e14-2b67f3f2d8b5} + Win32Proj + CursorWrap + CursorWrap + + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + ..\..\..\..\$(Platform)\$(Configuration)\ + PowerToys.CursorWrap + + + true + + + false + + + + Level3 + Disabled + true + _DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreadedDebug + stdcpplatest + + + Windows + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + Level3 + MaxSpeed + true + true + true + NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + MultiThreaded + stdcpplatest + + + Windows + true + true + true + $(OutDir)$(TargetName)$(TargetExt) + + + + + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + + + + + + + + + + + Create + + + + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + diff --git a/src/modules/MouseUtils/CursorWrap/CursorWrapTests.h b/src/modules/MouseUtils/CursorWrap/CursorWrapTests.h new file mode 100644 index 0000000000..4274ad714f --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/CursorWrapTests.h @@ -0,0 +1,213 @@ +#pragma once + +#include +#include + +// Test case structure for comprehensive monitor layout testing +struct MonitorTestCase +{ + std::string name; + std::string description; + int grid[3][3]; // 3x3 grid representing monitor layout (0 = no monitor, 1-9 = monitor ID) + + // Test scenarios to validate + struct TestScenario + { + int sourceMonitor; // Which monitor to start cursor on (1-based) + int edgeDirection; // 0=top, 1=right, 2=bottom, 3=left + int expectedTargetMonitor; // Expected destination monitor (1-based, -1 = wrap within same monitor) + std::string description; + }; + + std::vector scenarios; +}; + +// Comprehensive test cases for all possible 3x3 monitor grid configurations +class CursorWrapTestSuite +{ +public: + static std::vector GetAllTestCases() + { + std::vector testCases; + + // Test Case 1: Single monitor (center) + testCases.push_back({ + "Single_Center", + "Single monitor in center position", + { + {0, 0, 0}, + {0, 1, 0}, + {0, 0, 0} + }, + { + {1, 0, -1, "Top edge wraps to bottom of same monitor"}, + {1, 1, -1, "Right edge wraps to left of same monitor"}, + {1, 2, -1, "Bottom edge wraps to top of same monitor"}, + {1, 3, -1, "Left edge wraps to right of same monitor"} + } + }); + + // Test Case 2: Two monitors horizontal (left + right) + testCases.push_back({ + "Dual_Horizontal_Left_Right", + "Two monitors: left + right", + { + {0, 0, 0}, + {1, 0, 2}, + {0, 0, 0} + }, + { + {1, 0, -1, "Monitor 1 top wraps to bottom of monitor 1"}, + {1, 1, 2, "Monitor 1 right edge moves to monitor 2 left"}, + {1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"}, + {1, 3, -1, "Monitor 1 left edge wraps to right of monitor 1"}, + {2, 0, -1, "Monitor 2 top wraps to bottom of monitor 2"}, + {2, 1, -1, "Monitor 2 right edge wraps to left of monitor 2"}, + {2, 2, -1, "Monitor 2 bottom wraps to top of monitor 2"}, + {2, 3, 1, "Monitor 2 left edge moves to monitor 1 right"} + } + }); + + // Test Case 3: Two monitors vertical (Monitor 2 above Monitor 1) - CORRECTED FOR USER'S SETUP + testCases.push_back({ + "Dual_Vertical_2_Above_1", + "Two monitors: Monitor 2 (top) above Monitor 1 (bottom/main)", + { + {0, 2, 0}, // Row 0: Monitor 2 (physically top monitor) + {0, 0, 0}, // Row 1: Empty + {0, 1, 0} // Row 2: Monitor 1 (physically bottom/main monitor) + }, + { + // Monitor 1 (bottom/main monitor) tests + {1, 0, 2, "Monitor 1 (bottom) top edge should move to Monitor 2 (top) bottom"}, + {1, 1, -1, "Monitor 1 right wraps to left of monitor 1"}, + {1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"}, + {1, 3, -1, "Monitor 1 left wraps to right of monitor 1"}, + + // Monitor 2 (top monitor) tests + {2, 0, -1, "Monitor 2 (top) top wraps to bottom of monitor 2"}, + {2, 1, -1, "Monitor 2 right wraps to left of monitor 2"}, + {2, 2, 1, "Monitor 2 (top) bottom edge should move to Monitor 1 (bottom) top"}, + {2, 3, -1, "Monitor 2 left wraps to right of monitor 2"} + } + }); + + // Test Case 4: Three monitors L-shape (center + left + top) + testCases.push_back({ + "Triple_L_Shape", + "Three monitors in L-shape: center + left + top", + { + {0, 3, 0}, + {2, 1, 0}, + {0, 0, 0} + }, + { + {1, 0, 3, "Monitor 1 top moves to monitor 3 bottom"}, + {1, 1, -1, "Monitor 1 right wraps to left of monitor 1"}, + {1, 2, -1, "Monitor 1 bottom wraps to top of monitor 1"}, + {1, 3, 2, "Monitor 1 left moves to monitor 2 right"}, + {2, 0, -1, "Monitor 2 top wraps to bottom of monitor 2"}, + {2, 1, 1, "Monitor 2 right moves to monitor 1 left"}, + {2, 2, -1, "Monitor 2 bottom wraps to top of monitor 2"}, + {2, 3, -1, "Monitor 2 left wraps to right of monitor 2"}, + {3, 0, -1, "Monitor 3 top wraps to bottom of monitor 3"}, + {3, 1, -1, "Monitor 3 right wraps to left of monitor 3"}, + {3, 2, 1, "Monitor 3 bottom moves to monitor 1 top"}, + {3, 3, -1, "Monitor 3 left wraps to right of monitor 3"} + } + }); + + // Test Case 5: Three monitors horizontal (left + center + right) + testCases.push_back({ + "Triple_Horizontal", + "Three monitors horizontal: left + center + right", + { + {0, 0, 0}, + {1, 2, 3}, + {0, 0, 0} + }, + { + {1, 0, -1, "Monitor 1 top wraps to bottom"}, + {1, 1, 2, "Monitor 1 right moves to monitor 2"}, + {1, 2, -1, "Monitor 1 bottom wraps to top"}, + {1, 3, -1, "Monitor 1 left wraps to right"}, + {2, 0, -1, "Monitor 2 top wraps to bottom"}, + {2, 1, 3, "Monitor 2 right moves to monitor 3"}, + {2, 2, -1, "Monitor 2 bottom wraps to top"}, + {2, 3, 1, "Monitor 2 left moves to monitor 1"}, + {3, 0, -1, "Monitor 3 top wraps to bottom"}, + {3, 1, -1, "Monitor 3 right wraps to left"}, + {3, 2, -1, "Monitor 3 bottom wraps to top"}, + {3, 3, 2, "Monitor 3 left moves to monitor 2"} + } + }); + + // Test Case 6: Three monitors vertical (top + center + bottom) + testCases.push_back({ + "Triple_Vertical", + "Three monitors vertical: top + center + bottom", + { + {0, 1, 0}, + {0, 2, 0}, + {0, 3, 0} + }, + { + {1, 0, -1, "Monitor 1 top wraps to bottom"}, + {1, 1, -1, "Monitor 1 right wraps to left"}, + {1, 2, 2, "Monitor 1 bottom moves to monitor 2"}, + {1, 3, -1, "Monitor 1 left wraps to right"}, + {2, 0, 1, "Monitor 2 top moves to monitor 1"}, + {2, 1, -1, "Monitor 2 right wraps to left"}, + {2, 2, 3, "Monitor 2 bottom moves to monitor 3"}, + {2, 3, -1, "Monitor 2 left wraps to right"}, + {3, 0, 2, "Monitor 3 top moves to monitor 2"}, + {3, 1, -1, "Monitor 3 right wraps to left"}, + {3, 2, -1, "Monitor 3 bottom wraps to top"}, + {3, 3, -1, "Monitor 3 left wraps to right"} + } + }); + + return testCases; + } + + // Helper function to print test case in a readable format + static std::string FormatTestCase(const MonitorTestCase& testCase) + { + std::string result = "Test Case: " + testCase.name + "\n"; + result += "Description: " + testCase.description + "\n"; + result += "Layout:\n"; + + for (int row = 0; row < 3; row++) + { + result += " "; + for (int col = 0; col < 3; col++) + { + if (testCase.grid[row][col] == 0) + { + result += ". "; + } + else + { + result += std::to_string(testCase.grid[row][col]) + " "; + } + } + result += "\n"; + } + + result += "Test Scenarios:\n"; + for (const auto& scenario : testCase.scenarios) + { + result += " - " + scenario.description + "\n"; + } + + return result; + } + + // Helper function to validate a specific test case against actual behavior + static bool ValidateTestCase(const MonitorTestCase& testCase) + { + // This would be called with actual CursorWrap instance to validate behavior + // For now, just return true - this would need actual implementation + return true; + } +}; \ No newline at end of file diff --git a/src/modules/MouseUtils/CursorWrap/dllmain.cpp b/src/modules/MouseUtils/CursorWrap/dllmain.cpp new file mode 100644 index 0000000000..74524ed9f9 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/dllmain.cpp @@ -0,0 +1,1046 @@ +#include "pch.h" +#include "../../../interface/powertoy_module_interface.h" +#include "../../../common/SettingsAPI/settings_objects.h" +#include "trace.h" +#include "../../../common/utils/process_path.h" +#include "../../../common/utils/resources.h" +#include "../../../common/logger/logger.h" +#include "../../../common/utils/logger_helper.h" +#include +#include +#include +#include +#include +#include +#include +#include "resource.h" +#include "CursorWrapTests.h" + +// Disable C26451 arithmetic overflow warning for this file since the operations are safe in this context +#pragma warning(disable: 26451) + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; +} + +// Non-Localizable strings +namespace +{ + const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; + const wchar_t JSON_KEY_VALUE[] = L"value"; + const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut"; + const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate"; + const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag"; +} + +// The PowerToy name that will be shown in the settings. +const static wchar_t* MODULE_NAME = L"CursorWrap"; +// Add a description that will we shown in the module settings page. +const static wchar_t* MODULE_DESC = L""; + +// Mouse hook data structure +struct MonitorInfo +{ + RECT rect; + bool isPrimary; + int monitorId; // Add monitor ID for easier debugging +}; + +// Add structure for logical monitor grid position +struct LogicalPosition +{ + int row; + int col; + bool isValid; +}; + +// Add monitor topology helper +struct MonitorTopology +{ + std::vector> grid; // 3x3 grid of monitors + std::map monitorToPosition; + std::map, HMONITOR> positionToMonitor; + + void Initialize(const std::vector& monitors); + LogicalPosition GetPosition(HMONITOR monitor) const; + HMONITOR GetMonitorAt(int row, int col) const; + HMONITOR FindAdjacentMonitor(HMONITOR current, int deltaRow, int deltaCol) const; +}; + +// Forward declaration +class CursorWrap; + +// Global instance pointer for the mouse hook +static CursorWrap* g_cursorWrapInstance = nullptr; + +// Implement the PowerToy Module Interface and all the required methods. +class CursorWrap : public PowertoyModuleIface +{ +private: + // The PowerToy state. + bool m_enabled = false; + bool m_autoActivate = false; + bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag + + // Mouse hook + HHOOK m_mouseHook = nullptr; + std::atomic m_hookActive{ false }; + + // Monitor information + std::vector m_monitors; + MonitorTopology m_topology; + + // Hotkey + Hotkey m_activationHotkey{}; + +public: + // Constructor + CursorWrap() + { + LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::cursorWrapLoggerName); + init_settings(); + UpdateMonitorInfo(); + g_cursorWrapInstance = this; // Set global instance pointer + }; + + // Destroy the powertoy and free memory + virtual void destroy() override + { + StopMouseHook(); + g_cursorWrapInstance = nullptr; // Clear global instance pointer + delete this; + } + + // Return the localized display name of the powertoy + virtual const wchar_t* get_name() override + { + return MODULE_NAME; + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return MODULE_NAME; + } + + // Return the configured status for the gpo policy for the module + virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override + { + return powertoys_gpo::getConfiguredCursorWrapEnabledValue(); + } + + // Return JSON with the configuration options. + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + PowerToysSettings::Settings settings(hinstance, get_name()); + + settings.set_description(IDS_CURSORWRAP_NAME); + settings.set_icon_key(L"pt-cursor-wrap"); + + // Create HotkeyObject from the Hotkey struct for the settings + auto hotkey_object = PowerToysSettings::HotkeyObject::from_settings( + m_activationHotkey.win, + m_activationHotkey.ctrl, + m_activationHotkey.alt, + m_activationHotkey.shift, + m_activationHotkey.key); + + settings.add_hotkey(JSON_KEY_ACTIVATION_SHORTCUT, IDS_CURSORWRAP_NAME, hotkey_object); + settings.add_bool_toggle(JSON_KEY_AUTO_ACTIVATE, IDS_CURSORWRAP_NAME, m_autoActivate); + settings.add_bool_toggle(JSON_KEY_DISABLE_WRAP_DURING_DRAG, IDS_CURSORWRAP_NAME, m_disableWrapDuringDrag); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + // Signal from the Settings editor to call a custom action. + // This can be used to spawn more complex editors. + virtual void call_custom_action(const wchar_t* /*action*/) override {} + + // Called by the runner to pass the updated settings values as a serialized JSON. + virtual void set_config(const wchar_t* config) override + { + try + { + // Parse the input JSON string. + PowerToysSettings::PowerToyValues values = + PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + + parse_settings(values); + } + catch (std::exception&) + { + Logger::error("Invalid json when trying to parse CursorWrap settings json."); + } + } + + // Enable the powertoy + virtual void enable() + { + m_enabled = true; + Trace::EnableCursorWrap(true); + + // Always start the mouse hook when the module is enabled + // This ensures cursor wrapping is active immediately after enabling + StartMouseHook(); + Logger::info("CursorWrap enabled - mouse hook started"); + } + + // Disable the powertoy + virtual void disable() + { + m_enabled = false; + Trace::EnableCursorWrap(false); + StopMouseHook(); + Logger::info("CursorWrap disabled - mouse hook stopped"); + } + + // Returns if the powertoys is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + // Returns whether the PowerToys should be enabled by default + virtual bool is_enabled_by_default() const override + { + return false; + } + + // Legacy hotkey support + virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override + { + if (buffer && buffer_size >= 1) + { + buffer[0] = m_activationHotkey; + } + return 1; + } + + virtual bool on_hotkey(size_t hotkeyId) override + { + if (!m_enabled || hotkeyId != 0) + { + return false; + } + + // Toggle cursor wrapping + if (m_hookActive) + { + StopMouseHook(); + } + else + { + StartMouseHook(); +#ifdef _DEBUG + // Run comprehensive tests when hook is started in debug builds + RunComprehensiveTests(); +#endif + } + + return true; + } + +private: + // Load the settings file. + void init_settings() + { + try + { + // Load and parse the settings file for this PowerToy. + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(CursorWrap::get_key()); + parse_settings(settings); + } + catch (std::exception&) + { + Logger::error("Invalid json when trying to load the CursorWrap settings json from file."); + } + } + + void parse_settings(PowerToysSettings::PowerToyValues& settings) + { + auto settingsObject = settings.get_raw_json(); + if (settingsObject.GetView().Size()) + { + try + { + // Parse activation HotKey + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT); + auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject); + + m_activationHotkey.win = hotkey.win_pressed(); + m_activationHotkey.ctrl = hotkey.ctrl_pressed(); + m_activationHotkey.shift = hotkey.shift_pressed(); + m_activationHotkey.alt = hotkey.alt_pressed(); + m_activationHotkey.key = static_cast(hotkey.get_code()); + } + catch (...) + { + Logger::warn("Failed to initialize CursorWrap activation shortcut"); + } + + try + { + // Parse auto activate + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_AUTO_ACTIVATE); + m_autoActivate = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE); + } + catch (...) + { + Logger::warn("Failed to initialize CursorWrap auto activate from settings. Will use default value"); + } + + try + { + // Parse disable wrap during drag + auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES); + if (propertiesObject.HasKey(JSON_KEY_DISABLE_WRAP_DURING_DRAG)) + { + auto disableDragObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_WRAP_DURING_DRAG); + m_disableWrapDuringDrag = disableDragObject.GetNamedBoolean(JSON_KEY_VALUE); + } + } + catch (...) + { + Logger::warn("Failed to initialize CursorWrap disable wrap during drag from settings. Will use default value (true)"); + } + } + else + { + Logger::info("CursorWrap settings are empty"); + } + + // Set default hotkey if not configured + if (m_activationHotkey.key == 0) + { + m_activationHotkey.win = true; + m_activationHotkey.alt = true; + m_activationHotkey.ctrl = false; + m_activationHotkey.shift = false; + m_activationHotkey.key = 'U'; // Win+Alt+U + } + } + + void UpdateMonitorInfo() + { + m_monitors.clear(); + + EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) -> BOOL { + auto* self = reinterpret_cast(lParam); + + MONITORINFO mi{}; + mi.cbSize = sizeof(MONITORINFO); + if (GetMonitorInfo(hMonitor, &mi)) + { + MonitorInfo info{}; + info.rect = mi.rcMonitor; + info.isPrimary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0; + info.monitorId = static_cast(self->m_monitors.size()); + self->m_monitors.push_back(info); + } + + return TRUE; + }, reinterpret_cast(this)); + + // Initialize monitor topology + m_topology.Initialize(m_monitors); + } + + void StartMouseHook() + { + if (m_mouseHook || m_hookActive) + { + Logger::info("CursorWrap mouse hook already active"); + return; + } + + UpdateMonitorInfo(); + + m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, GetModuleHandle(nullptr), 0); + if (m_mouseHook) + { + m_hookActive = true; + Logger::info("CursorWrap mouse hook started successfully"); +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Hook installed"); +#endif + } + else + { + DWORD error = GetLastError(); + Logger::error(L"Failed to install CursorWrap mouse hook, error: {}", error); + } + } + + void StopMouseHook() + { + if (m_mouseHook) + { + UnhookWindowsHookEx(m_mouseHook); + m_mouseHook = nullptr; + m_hookActive = false; + Logger::info("CursorWrap mouse hook stopped"); +#ifdef _DEBUG + Logger::info("CursorWrap DEBUG: Mouse hook stopped"); +#endif + } + } + + static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) + { + if (nCode >= 0 && wParam == WM_MOUSEMOVE) + { + auto* pMouseStruct = reinterpret_cast(lParam); + POINT currentPos = { pMouseStruct->pt.x, pMouseStruct->pt.y }; + + if (g_cursorWrapInstance && g_cursorWrapInstance->m_hookActive) + { + POINT newPos = g_cursorWrapInstance->HandleMouseMove(currentPos); + if (newPos.x != currentPos.x || newPos.y != currentPos.y) + { +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Wrapping cursor from ({}, {}) to ({}, {})", + currentPos.x, currentPos.y, newPos.x, newPos.y); +#endif + SetCursorPos(newPos.x, newPos.y); + return 1; // Suppress the original message + } + } + } + + return CallNextHookEx(nullptr, nCode, wParam, lParam); + } + + // *** COMPLETELY REWRITTEN CURSOR WRAPPING LOGIC *** + // Implements vertical scrolling to bottom/top of vertical stack as requested + POINT HandleMouseMove(const POINT& currentPos) + { + POINT newPos = currentPos; + + // Check if we should skip wrapping during drag if the setting is enabled + if (m_disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000)) + { +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Left mouse button is down and disable_wrap_during_drag is enabled - skipping wrap"); +#endif + return currentPos; // Return unchanged position (no wrapping) + } + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: ======= HANDLE MOUSE MOVE START ======="); + Logger::info(L"CursorWrap DEBUG: Input position ({}, {})", currentPos.x, currentPos.y); +#endif + + // Find which monitor the cursor is currently on + HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST); + MONITORINFO currentMonitorInfo{}; + currentMonitorInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(currentMonitor, ¤tMonitorInfo); + + LogicalPosition currentLogicalPos = m_topology.GetPosition(currentMonitor); + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Current monitor bounds: Left={}, Top={}, Right={}, Bottom={}", + currentMonitorInfo.rcMonitor.left, currentMonitorInfo.rcMonitor.top, + currentMonitorInfo.rcMonitor.right, currentMonitorInfo.rcMonitor.bottom); + Logger::info(L"CursorWrap DEBUG: Logical position: Row={}, Col={}, Valid={}", + currentLogicalPos.row, currentLogicalPos.col, currentLogicalPos.isValid); +#endif + + bool wrapped = false; + + // *** VERTICAL WRAPPING LOGIC - CONFIRMED WORKING *** + // Move to bottom of vertical stack when hitting top edge + if (currentPos.y <= currentMonitorInfo.rcMonitor.top) + { +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: TOP EDGE DETECTED ======="); +#endif + + // Find the bottom-most monitor in the vertical stack (same column) + HMONITOR bottomMonitor = nullptr; + + if (currentLogicalPos.isValid) { + // Search down from current position to find the bottom-most monitor in same column + for (int row = 2; row >= 0; row--) { // Start from bottom and work up + HMONITOR candidateMonitor = m_topology.GetMonitorAt(row, currentLogicalPos.col); + if (candidateMonitor) { + bottomMonitor = candidateMonitor; + break; // Found the bottom-most monitor + } + } + } + + if (bottomMonitor && bottomMonitor != currentMonitor) { + // *** MOVE TO BOTTOM OF VERTICAL STACK *** + MONITORINFO bottomInfo{}; + bottomInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(bottomMonitor, &bottomInfo); + + // Calculate relative X position to maintain cursor X alignment + double relativeX = static_cast(currentPos.x - currentMonitorInfo.rcMonitor.left) / + (currentMonitorInfo.rcMonitor.right - currentMonitorInfo.rcMonitor.left); + + int targetWidth = bottomInfo.rcMonitor.right - bottomInfo.rcMonitor.left; + newPos.x = bottomInfo.rcMonitor.left + static_cast(relativeX * targetWidth); + newPos.y = bottomInfo.rcMonitor.bottom - 1; // Bottom edge of bottom monitor + + // Clamp X to target monitor bounds + newPos.x = max(bottomInfo.rcMonitor.left, min(newPos.x, bottomInfo.rcMonitor.right - 1)); + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP SUCCESS - Moved to bottom of vertical stack"); + Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); +#endif + } else { + // *** NO OTHER MONITOR IN VERTICAL STACK - WRAP WITHIN CURRENT MONITOR *** + newPos.y = currentMonitorInfo.rcMonitor.bottom - 1; + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP - No other monitor in stack, wrapping within current monitor"); +#endif + } + } + else if (currentPos.y >= currentMonitorInfo.rcMonitor.bottom - 1) + { +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: BOTTOM EDGE DETECTED ======="); +#endif + + // Find the top-most monitor in the vertical stack (same column) + HMONITOR topMonitor = nullptr; + + if (currentLogicalPos.isValid) { + // Search up from current position to find the top-most monitor in same column + for (int row = 0; row <= 2; row++) { // Start from top and work down + HMONITOR candidateMonitor = m_topology.GetMonitorAt(row, currentLogicalPos.col); + if (candidateMonitor) { + topMonitor = candidateMonitor; + break; // Found the top-most monitor + } + } + } + + if (topMonitor && topMonitor != currentMonitor) { + // *** MOVE TO TOP OF VERTICAL STACK *** + MONITORINFO topInfo{}; + topInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(topMonitor, &topInfo); + + // Calculate relative X position to maintain cursor X alignment + double relativeX = static_cast(currentPos.x - currentMonitorInfo.rcMonitor.left) / + (currentMonitorInfo.rcMonitor.right - currentMonitorInfo.rcMonitor.left); + + int targetWidth = topInfo.rcMonitor.right - topInfo.rcMonitor.left; + newPos.x = topInfo.rcMonitor.left + static_cast(relativeX * targetWidth); + newPos.y = topInfo.rcMonitor.top; // Top edge of top monitor + + // Clamp X to target monitor bounds + newPos.x = max(topInfo.rcMonitor.left, min(newPos.x, topInfo.rcMonitor.right - 1)); + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP SUCCESS - Moved to top of vertical stack"); + Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); +#endif + } else { + // *** NO OTHER MONITOR IN VERTICAL STACK - WRAP WITHIN CURRENT MONITOR *** + newPos.y = currentMonitorInfo.rcMonitor.top; + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP - No other monitor in stack, wrapping within current monitor"); +#endif + } + } + + // *** FIXED HORIZONTAL WRAPPING LOGIC *** + // Move to opposite end of horizontal stack when hitting left/right edge + // Only handle horizontal wrapping if we haven't already wrapped vertically + if (!wrapped && currentPos.x <= currentMonitorInfo.rcMonitor.left) + { +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: LEFT EDGE DETECTED ======="); +#endif + + // Find the right-most monitor in the horizontal stack (same row) + HMONITOR rightMonitor = nullptr; + + if (currentLogicalPos.isValid) { + // Search right from current position to find the right-most monitor in same row + for (int col = 2; col >= 0; col--) { // Start from right and work left + HMONITOR candidateMonitor = m_topology.GetMonitorAt(currentLogicalPos.row, col); + if (candidateMonitor) { + rightMonitor = candidateMonitor; + break; // Found the right-most monitor + } + } + } + + if (rightMonitor && rightMonitor != currentMonitor) { + // *** MOVE TO RIGHT END OF HORIZONTAL STACK *** + MONITORINFO rightInfo{}; + rightInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(rightMonitor, &rightInfo); + + // Calculate relative Y position to maintain cursor Y alignment + double relativeY = static_cast(currentPos.y - currentMonitorInfo.rcMonitor.top) / + (currentMonitorInfo.rcMonitor.bottom - currentMonitorInfo.rcMonitor.top); + + int targetHeight = rightInfo.rcMonitor.bottom - rightInfo.rcMonitor.top; + newPos.y = rightInfo.rcMonitor.top + static_cast(relativeY * targetHeight); + newPos.x = rightInfo.rcMonitor.right - 1; // Right edge of right monitor + + // Clamp Y to target monitor bounds + newPos.y = max(rightInfo.rcMonitor.top, min(newPos.y, rightInfo.rcMonitor.bottom - 1)); + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP SUCCESS - Moved to right end of horizontal stack"); + Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); +#endif + } else { + // *** NO OTHER MONITOR IN HORIZONTAL STACK - WRAP WITHIN CURRENT MONITOR *** + newPos.x = currentMonitorInfo.rcMonitor.right - 1; + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP - No other monitor in stack, wrapping within current monitor"); +#endif + } + } + else if (!wrapped && currentPos.x >= currentMonitorInfo.rcMonitor.right - 1) + { +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: RIGHT EDGE DETECTED ======="); +#endif + + // Find the left-most monitor in the horizontal stack (same row) + HMONITOR leftMonitor = nullptr; + + if (currentLogicalPos.isValid) { + // Search left from current position to find the left-most monitor in same row + for (int col = 0; col <= 2; col++) { // Start from left and work right + HMONITOR candidateMonitor = m_topology.GetMonitorAt(currentLogicalPos.row, col); + if (candidateMonitor) { + leftMonitor = candidateMonitor; + break; // Found the left-most monitor + } + } + } + + if (leftMonitor && leftMonitor != currentMonitor) { + // *** MOVE TO LEFT END OF HORIZONTAL STACK *** + MONITORINFO leftInfo{}; + leftInfo.cbSize = sizeof(MONITORINFO); + GetMonitorInfo(leftMonitor, &leftInfo); + + // Calculate relative Y position to maintain cursor Y alignment + double relativeY = static_cast(currentPos.y - currentMonitorInfo.rcMonitor.top) / + (currentMonitorInfo.rcMonitor.bottom - currentMonitorInfo.rcMonitor.top); + + int targetHeight = leftInfo.rcMonitor.bottom - leftInfo.rcMonitor.top; + newPos.y = leftInfo.rcMonitor.top + static_cast(relativeY * targetHeight); + newPos.x = leftInfo.rcMonitor.left; // Left edge of left monitor + + // Clamp Y to target monitor bounds + newPos.y = max(leftInfo.rcMonitor.top, min(newPos.y, leftInfo.rcMonitor.bottom - 1)); + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP SUCCESS - Moved to left end of horizontal stack"); + Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y); +#endif + } else { + // *** NO OTHER MONITOR IN HORIZONTAL STACK - WRAP WITHIN CURRENT MONITOR *** + newPos.x = currentMonitorInfo.rcMonitor.left; + wrapped = true; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP - No other monitor in stack, wrapping within current monitor"); +#endif + } + } + +#ifdef _DEBUG + if (wrapped) + { + Logger::info(L"CursorWrap DEBUG: ======= WRAP RESULT ======="); + Logger::info(L"CursorWrap DEBUG: Original: ({}, {}) -> New: ({}, {})", + currentPos.x, currentPos.y, newPos.x, newPos.y); + } + else + { + Logger::info(L"CursorWrap DEBUG: No wrapping performed - cursor not at edge"); + } + Logger::info(L"CursorWrap DEBUG: ======= HANDLE MOUSE MOVE END ======="); +#endif + + return newPos; + } + + // Add test method for monitor topology validation + void RunMonitorTopologyTests() + { +#ifdef _DEBUG + Logger::info(L"CursorWrap: Running monitor topology tests..."); + + // Test all 9 possible monitor positions in 3x3 grid + const char* gridNames[3][3] = { + {"TL", "TC", "TR"}, // Top-Left, Top-Center, Top-Right + {"ML", "MC", "MR"}, // Middle-Left, Middle-Center, Middle-Right + {"BL", "BC", "BR"} // Bottom-Left, Bottom-Center, Bottom-Right + }; + + for (int row = 0; row < 3; row++) + { + for (int col = 0; col < 3; col++) + { + HMONITOR monitor = m_topology.GetMonitorAt(row, col); + if (monitor) + { + std::string gridName(gridNames[row][col]); + std::wstring wGridName(gridName.begin(), gridName.end()); + Logger::info(L"CursorWrap TEST: Monitor at [{}][{}] ({}) exists", + row, col, wGridName.c_str()); + + // Test adjacent monitor finding + HMONITOR up = m_topology.FindAdjacentMonitor(monitor, -1, 0); + HMONITOR down = m_topology.FindAdjacentMonitor(monitor, 1, 0); + HMONITOR left = m_topology.FindAdjacentMonitor(monitor, 0, -1); + HMONITOR right = m_topology.FindAdjacentMonitor(monitor, 0, 1); + + Logger::info(L"CursorWrap TEST: Adjacent monitors - Up: {}, Down: {}, Left: {}, Right: {}", + up ? L"YES" : L"NO", down ? L"YES" : L"NO", + left ? L"YES" : L"NO", right ? L"YES" : L"NO"); + } + } + } + + Logger::info(L"CursorWrap: Monitor topology tests completed."); +#endif + } + + // Add method to trigger test suite (can be called via hotkey in debug builds) + void RunComprehensiveTests() + { +#ifdef _DEBUG + RunMonitorTopologyTests(); + + // Test cursor wrapping scenarios + Logger::info(L"CursorWrap: Testing cursor wrapping scenarios..."); + + // Simulate cursor positions at each monitor edge and verify expected behavior + for (const auto& monitor : m_monitors) + { + HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); + LogicalPosition pos = m_topology.GetPosition(hMonitor); + + if (pos.isValid) + { + Logger::info(L"CursorWrap TEST: Testing monitor at position [{}][{}]", pos.row, pos.col); + + // Test top edge + POINT topEdge = {(monitor.rect.left + monitor.rect.right) / 2, monitor.rect.top}; + POINT newPos = HandleMouseMove(topEdge); + Logger::info(L"CursorWrap TEST: Top edge ({}, {}) -> ({}, {})", + topEdge.x, topEdge.y, newPos.x, newPos.y); + + // Test bottom edge + POINT bottomEdge = {(monitor.rect.left + monitor.rect.right) / 2, monitor.rect.bottom - 1}; + newPos = HandleMouseMove(bottomEdge); + Logger::info(L"CursorWrap TEST: Bottom edge ({}, {}) -> ({}, {})", + bottomEdge.x, bottomEdge.y, newPos.x, newPos.y); + + // Test left edge + POINT leftEdge = {monitor.rect.left, (monitor.rect.top + monitor.rect.bottom) / 2}; + newPos = HandleMouseMove(leftEdge); + Logger::info(L"CursorWrap TEST: Left edge ({}, {}) -> ({}, {})", + leftEdge.x, leftEdge.y, newPos.x, newPos.y); + + // Test right edge + POINT rightEdge = {monitor.rect.right - 1, (monitor.rect.top + monitor.rect.bottom) / 2}; + newPos = HandleMouseMove(rightEdge); + Logger::info(L"CursorWrap TEST: Right edge ({}, {}) -> ({}, {})", + rightEdge.x, rightEdge.y, newPos.x, newPos.y); + } + } + + Logger::info(L"CursorWrap: Comprehensive tests completed."); +#endif + } +}; + +// Implementation of MonitorTopology methods +void MonitorTopology::Initialize(const std::vector& monitors) +{ + // Clear existing data + grid.assign(3, std::vector(3, nullptr)); + monitorToPosition.clear(); + positionToMonitor.clear(); + + if (monitors.empty()) return; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: ======= TOPOLOGY INITIALIZATION START ======="); + Logger::info(L"CursorWrap DEBUG: Initializing topology for {} monitors", monitors.size()); + for (const auto& monitor : monitors) + { + Logger::info(L"CursorWrap DEBUG: Monitor {}: bounds=({},{},{},{}), isPrimary={}", + monitor.monitorId, monitor.rect.left, monitor.rect.top, + monitor.rect.right, monitor.rect.bottom, monitor.isPrimary); + } +#endif + + // Special handling for 2 monitors - use physical position, not discovery order + if (monitors.size() == 2) + { + // Determine if arrangement is horizontal or vertical by comparing centers + POINT center0 = {(monitors[0].rect.left + monitors[0].rect.right) / 2, + (monitors[0].rect.top + monitors[0].rect.bottom) / 2}; + POINT center1 = {(monitors[1].rect.left + monitors[1].rect.right) / 2, + (monitors[1].rect.top + monitors[1].rect.bottom) / 2}; + + int xDiff = abs(center0.x - center1.x); + int yDiff = abs(center0.y - center1.y); + + bool isHorizontal = xDiff > yDiff; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Monitor centers: M0=({}, {}), M1=({}, {})", + center0.x, center0.y, center1.x, center1.y); + Logger::info(L"CursorWrap DEBUG: Differences: X={}, Y={}, IsHorizontal={}", + xDiff, yDiff, isHorizontal); +#endif + + if (isHorizontal) + { + // Horizontal arrangement - place in middle row [1,0] and [1,2] + for (const auto& monitor : monitors) + { + HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); + POINT center = {(monitor.rect.left + monitor.rect.right) / 2, + (monitor.rect.top + monitor.rect.bottom) / 2}; + + int row = 1; // Middle row + int col = (center.x < (center0.x + center1.x) / 2) ? 0 : 2; // Left or right based on center + + grid[row][col] = hMonitor; + monitorToPosition[hMonitor] = {row, col, true}; + positionToMonitor[{row, col}] = hMonitor; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Monitor {} (horizontal) placed at grid[{}][{}]", + monitor.monitorId, row, col); +#endif + } + } + else + { + // *** VERTICAL ARRANGEMENT - CRITICAL LOGIC *** + // Sort monitors by Y coordinate to determine vertical order + std::vector> sortedMonitors; + for (int i = 0; i < 2; i++) { + sortedMonitors.push_back({i, monitors[i]}); + } + + // Sort by Y coordinate (top to bottom) + std::sort(sortedMonitors.begin(), sortedMonitors.end(), + [](const std::pair& a, const std::pair& b) { + int centerA = (a.second.rect.top + a.second.rect.bottom) / 2; + int centerB = (b.second.rect.top + b.second.rect.bottom) / 2; + return centerA < centerB; // Top first + }); + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: VERTICAL ARRANGEMENT DETECTED"); + Logger::info(L"CursorWrap DEBUG: Top monitor: ID={}, Y-center={}", + sortedMonitors[0].second.monitorId, + (sortedMonitors[0].second.rect.top + sortedMonitors[0].second.rect.bottom) / 2); + Logger::info(L"CursorWrap DEBUG: Bottom monitor: ID={}, Y-center={}", + sortedMonitors[1].second.monitorId, + (sortedMonitors[1].second.rect.top + sortedMonitors[1].second.rect.bottom) / 2); +#endif + + // Place monitors in grid based on sorted order + for (int i = 0; i < 2; i++) { + const auto& monitorPair = sortedMonitors[i]; + const auto& monitor = monitorPair.second; + HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); + + int col = 1; // Middle column for vertical arrangement + int row = (i == 0) ? 0 : 2; // Top monitor at row 0, bottom at row 2 + + grid[row][col] = hMonitor; + monitorToPosition[hMonitor] = {row, col, true}; + positionToMonitor[{row, col}] = hMonitor; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Monitor {} (vertical) placed at grid[{}][{}] - {} position", + monitor.monitorId, row, col, (i == 0) ? L"TOP" : L"BOTTOM"); +#endif + } + } + } + else + { + // For more than 2 monitors, use the general algorithm + RECT totalBounds = monitors[0].rect; + for (const auto& monitor : monitors) + { + totalBounds.left = min(totalBounds.left, monitor.rect.left); + totalBounds.top = min(totalBounds.top, monitor.rect.top); + totalBounds.right = max(totalBounds.right, monitor.rect.right); + totalBounds.bottom = max(totalBounds.bottom, monitor.rect.bottom); + } + + int totalWidth = totalBounds.right - totalBounds.left; + int totalHeight = totalBounds.bottom - totalBounds.top; + int gridWidth = max(1, totalWidth / 3); + int gridHeight = max(1, totalHeight / 3); + + // Place monitors in the 3x3 grid based on their center points + for (const auto& monitor : monitors) + { + HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); + + // Calculate center point of monitor + int centerX = (monitor.rect.left + monitor.rect.right) / 2; + int centerY = (monitor.rect.top + monitor.rect.bottom) / 2; + + // Map to grid position + int col = (centerX - totalBounds.left) / gridWidth; + int row = (centerY - totalBounds.top) / gridHeight; + + // Ensure we stay within bounds + col = max(0, min(2, col)); + row = max(0, min(2, row)); + + grid[row][col] = hMonitor; + monitorToPosition[hMonitor] = {row, col, true}; + positionToMonitor[{row, col}] = hMonitor; + +#ifdef _DEBUG + Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}], center=({}, {})", + monitor.monitorId, row, col, centerX, centerY); +#endif + } + } + +#ifdef _DEBUG + // *** CRITICAL: Print topology map using OutputDebugString for debug builds *** + Logger::info(L"CursorWrap DEBUG: ======= FINAL TOPOLOGY MAP ======="); + OutputDebugStringA("CursorWrap TOPOLOGY MAP:\n"); + for (int r = 0; r < 3; r++) + { + std::string rowStr = " "; + for (int c = 0; c < 3; c++) + { + if (grid[r][c]) + { + // Find monitor ID for this handle + int monitorId = -1; + for (const auto& monitor : monitors) + { + HMONITOR handle = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); + if (handle == grid[r][c]) + { + monitorId = monitor.monitorId + 1; // Convert to 1-based for display + break; + } + } + rowStr += std::to_string(monitorId) + " "; + } + else + { + rowStr += ". "; + } + } + rowStr += "\n"; + OutputDebugStringA(rowStr.c_str()); + + // Also log to PowerToys logger + std::wstring wRowStr(rowStr.begin(), rowStr.end()); + Logger::info(wRowStr.c_str()); + } + OutputDebugStringA("======= END TOPOLOGY MAP =======\n"); + + // Additional validation logging + Logger::info(L"CursorWrap DEBUG: ======= GRID POSITION VALIDATION ======="); + for (const auto& monitor : monitors) + { + HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST); + LogicalPosition pos = GetPosition(hMonitor); + if (pos.isValid) + { + Logger::info(L"CursorWrap DEBUG: Monitor {} -> grid[{}][{}]", monitor.monitorId, pos.row, pos.col); + OutputDebugStringA(("Monitor " + std::to_string(monitor.monitorId) + " -> grid[" + std::to_string(pos.row) + "][" + std::to_string(pos.col) + "]\n").c_str()); + + // Test adjacent finding + HMONITOR up = FindAdjacentMonitor(hMonitor, -1, 0); + HMONITOR down = FindAdjacentMonitor(hMonitor, 1, 0); + HMONITOR left = FindAdjacentMonitor(hMonitor, 0, -1); + HMONITOR right = FindAdjacentMonitor(hMonitor, 0, 1); + + Logger::info(L"CursorWrap DEBUG: Monitor {} adjacents - Up: {}, Down: {}, Left: {}, Right: {}", + monitor.monitorId, up ? L"YES" : L"NO", down ? L"YES" : L"NO", + left ? L"YES" : L"NO", right ? L"YES" : L"NO"); + } + } + Logger::info(L"CursorWrap DEBUG: ======= TOPOLOGY INITIALIZATION COMPLETE ======="); +#endif +} + +LogicalPosition MonitorTopology::GetPosition(HMONITOR monitor) const +{ + auto it = monitorToPosition.find(monitor); + if (it != monitorToPosition.end()) + { + return it->second; + } + return {-1, -1, false}; +} + +HMONITOR MonitorTopology::GetMonitorAt(int row, int col) const +{ + if (row >= 0 && row < 3 && col >= 0 && col < 3) + { + return grid[row][col]; + } + return nullptr; +} + +HMONITOR MonitorTopology::FindAdjacentMonitor(HMONITOR current, int deltaRow, int deltaCol) const +{ + LogicalPosition currentPos = GetPosition(current); + if (!currentPos.isValid) return nullptr; + + int newRow = currentPos.row + deltaRow; + int newCol = currentPos.col + deltaCol; + + return GetMonitorAt(newRow, newCol); +} + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new CursorWrap(); +} diff --git a/src/modules/MouseUtils/CursorWrap/packages.config b/src/modules/MouseUtils/CursorWrap/packages.config new file mode 100644 index 0000000000..2c5d71ae86 --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/CursorWrap/pch.cpp b/src/modules/MouseUtils/CursorWrap/pch.cpp new file mode 100644 index 0000000000..17305716aa --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" \ No newline at end of file diff --git a/src/modules/MouseUtils/CursorWrap/pch.h b/src/modules/MouseUtils/CursorWrap/pch.h new file mode 100644 index 0000000000..86f11c99ba --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/pch.h @@ -0,0 +1,13 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#include + +#include +#include +#include + +// Note: Common includes moved to individual source files due to include path issues +// #include +// #include +// #include \ No newline at end of file diff --git a/src/modules/MouseUtils/CursorWrap/resource.h b/src/modules/MouseUtils/CursorWrap/resource.h new file mode 100644 index 0000000000..9b49c0e3cc --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/resource.h @@ -0,0 +1,4 @@ +#pragma once + +#define IDS_CURSORWRAP_NAME 101 +#define IDS_CURSORWRAP_DISABLE_WRAP_DURING_DRAG 102 diff --git a/src/modules/MouseUtils/CursorWrap/trace.cpp b/src/modules/MouseUtils/CursorWrap/trace.cpp new file mode 100644 index 0000000000..ebfe32c23c --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/trace.cpp @@ -0,0 +1,31 @@ +#include "pch.h" +#include "trace.h" + +#include "../../../../common/Telemetry/TraceBase.h" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + "Microsoft.PowerToys", + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::RegisterProvider() +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() +{ + TraceLoggingUnregister(g_hProvider); +} + +void Trace::EnableCursorWrap(const bool enabled) noexcept +{ + TraceLoggingWriteWrapper( + g_hProvider, + "CursorWrap_EnableCursorWrap", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} \ No newline at end of file diff --git a/src/modules/MouseUtils/CursorWrap/trace.h b/src/modules/MouseUtils/CursorWrap/trace.h new file mode 100644 index 0000000000..b2f6a9a8eb --- /dev/null +++ b/src/modules/MouseUtils/CursorWrap/trace.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +class Trace : public telemetry::TraceBase +{ +public: + static void RegisterProvider(); + static void UnregisterProvider(); + static void EnableCursorWrap(const bool enabled) noexcept; +}; \ No newline at end of file diff --git a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp index c94c79e178..f953af0fdd 100644 --- a/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp +++ b/src/modules/MouseUtils/FindMyMouse/FindMyMouse.cpp @@ -189,7 +189,7 @@ bool SuperSonar::Initialize(HINSTANCE hinst) return false; } - DWORD exStyle = WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle(); + DWORD exStyle = WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | Shim()->GetExtendedStyle(); HWND created = CreateWindowExW(exStyle, className, windowTitle, WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, m_hwndOwner, nullptr, hinst, this); if (!created) { @@ -269,10 +269,6 @@ LRESULT SuperSonar::BaseWndProc(UINT message, WPARAM wParam, LPARAM lParam) n case WM_NCHITTEST: return HTTRANSPARENT; - - case WM_SETCURSOR: - SetCursor(LoadCursor(nullptr, IDC_ARROW)); - return TRUE; } if (message == WM_PRIV_SHORTCUT) @@ -539,7 +535,7 @@ void SuperSonar::StartSonar() Trace::MousePointerFocused(); // Cover the entire virtual screen. // HACK: Draw with 1 pixel off. Otherwise, Windows glitches the task bar transparency when a transparent window fill the whole screen. - SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, SWP_NOACTIVATE); + SetWindowPos(m_hwnd, HWND_TOPMOST, GetSystemMetrics(SM_XVIRTUALSCREEN) + 1, GetSystemMetrics(SM_YVIRTUALSCREEN) + 1, GetSystemMetrics(SM_CXVIRTUALSCREEN) - 2, GetSystemMetrics(SM_CYVIRTUALSCREEN) - 2, 0); m_sonarPos = ptNowhere; OnMouseTimer(); UpdateMouseSnooping(); diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp index fd144e807b..b460e29643 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp @@ -14,6 +14,9 @@ extern void InclusiveCrosshairsRequestUpdatePosition(); extern void InclusiveCrosshairsEnsureOn(); extern void InclusiveCrosshairsEnsureOff(); extern void InclusiveCrosshairsSetExternalControl(bool enabled); +extern void InclusiveCrosshairsSetOrientation(CrosshairsOrientation orientation); +extern bool InclusiveCrosshairsIsEnabled(); +extern void InclusiveCrosshairsSwitch(); // Non-Localizable strings namespace @@ -244,12 +247,19 @@ public: return false; } - if (hotkeyId == 0) + if (hotkeyId == 0) // Crosshairs activation { + // If gliding cursor is active, cancel it and activate crosshairs + if (m_glideState.load() != 0) + { + CancelGliding(true /*activateCrosshairs*/); + return true; + } + // Otherwise, normal crosshairs toggle InclusiveCrosshairsSwitch(); return true; } - if (hotkeyId == 1) + if (hotkeyId == 1) // Gliding cursor activation { HandleGlidingHotkey(); return true; @@ -268,25 +278,44 @@ private: SendInput(2, inputs, sizeof(INPUT)); } - // Cancel gliding without performing the final click (Escape handling) - void CancelGliding() + // Cancel gliding with option to activate crosshairs in user's preferred orientation + void CancelGliding(bool activateCrosshairs) { int state = m_glideState.load(); if (state == 0) { return; // nothing to cancel } + + // Stop all gliding operations StopXTimer(); StopYTimer(); m_glideState = 0; - InclusiveCrosshairsEnsureOff(); + UninstallKeyboardHook(); + + // Reset crosshairs control and restore user settings InclusiveCrosshairsSetExternalControl(false); + InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation); + + if (activateCrosshairs) + { + // User is switching to crosshairs mode - enable with their settings + InclusiveCrosshairsEnsureOn(); + } + else + { + // User canceled (Escape) - turn off crosshairs completely + InclusiveCrosshairsEnsureOff(); + } + + // Reset gliding state if (auto s = m_state) { s->xFraction = 0.0; s->yFraction = 0.0; } - Logger::debug("Gliding cursor cancelled via Escape key"); + + Logger::debug("Gliding cursor cancelled (activateCrosshairs={})", activateCrosshairs ? 1 : 0); } // Stateless helpers operating on shared State @@ -425,21 +454,22 @@ private: { return; } - // Simulate the AHK state machine + int state = m_glideState.load(); switch (state) { - case 0: + case 0: // Starting gliding { - // For detect for cancel key + // Install keyboard hook for Escape cancellation InstallKeyboardHook(); - // Ensure crosshairs on (do not toggle off if already on) - InclusiveCrosshairsEnsureOn(); - // Disable internal mouse hook so we control position updates explicitly + + // Force crosshairs visible in BOTH orientation for gliding, regardless of user setting + // Set external control before enabling to prevent internal movement hook from attaching InclusiveCrosshairsSetExternalControl(true); - // Override crosshairs to show both for Gliding Cursor InclusiveCrosshairsSetOrientation(CrosshairsOrientation::Both); + InclusiveCrosshairsEnsureOn(); // Always ensure they are visible + // Initialize gliding state s->currentXPos = 0; s->currentXSpeed = s->fastHSpeed; s->xFraction = 0.0; @@ -447,20 +477,17 @@ private: int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2; SetCursorPos(0, y); InclusiveCrosshairsRequestUpdatePosition(); + m_glideState = 1; StartXTimer(); break; } - case 1: - { - // Slow horizontal + case 1: // Slow horizontal s->currentXSpeed = s->slowHSpeed; m_glideState = 2; break; - } - case 2: + case 2: // Switch to vertical fast { - // Stop horizontal, start vertical (fast) StopXTimer(); s->currentYSpeed = s->fastVSpeed; s->currentYPos = 0; @@ -471,33 +498,37 @@ private: StartYTimer(); break; } - case 3: - { - // Slow vertical + case 3: // Slow vertical s->currentYSpeed = s->slowVSpeed; m_glideState = 4; break; - } - case 4: + case 4: // Finalize (click and end) default: { - UninstallKeyboardHook(); - // Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state + // Complete the gliding sequence StopYTimer(); m_glideState = 0; LeftClick(); - InclusiveCrosshairsEnsureOff(); + + // Restore normal crosshairs operation and turn them off InclusiveCrosshairsSetExternalControl(false); - // Restore original crosshairs orientation setting InclusiveCrosshairsSetOrientation(m_inclusiveCrosshairsSettings.crosshairsOrientation); - s->xFraction = 0.0; - s->yFraction = 0.0; + InclusiveCrosshairsEnsureOff(); + + UninstallKeyboardHook(); + + // Reset state + if (auto sp = m_state) + { + sp->xFraction = 0.0; + sp->yFraction = 0.0; + } break; } } } - // Low-level keyboard hook procedures + // Low-level keyboard hook for Escape cancellation static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { if (nCode == HC_ACTION) @@ -509,14 +540,11 @@ private: { if (inst->m_enabled && inst->m_glideState.load() != 0) { - inst->UninstallKeyboardHook(); - inst->CancelGliding(); + inst->CancelGliding(false); // Escape cancels without activating crosshairs } } } } - - // Do not swallow Escape; pass it through return CallNextHookEx(nullptr, nCode, wParam, lParam); } diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.Encryption.cs b/src/modules/MouseWithoutBorders/App/Class/Common.Encryption.cs deleted file mode 100644 index 1293a4ef39..0000000000 --- a/src/modules/MouseWithoutBorders/App/Class/Common.Encryption.cs +++ /dev/null @@ -1,248 +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. - -// -// Encrypt/decrypt implementation. -// -// -// 2008 created by Truong Do (ductdo). -// 2009-... modified by Truong Do (TruongDo). -// 2023- Included in PowerToys. -// -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Threading.Tasks; - -using MouseWithoutBorders.Core; - -namespace MouseWithoutBorders -{ - internal partial class Common - { -#pragma warning disable SYSLIB0021 - private static AesCryptoServiceProvider symAl; -#pragma warning restore SYSLIB0021 -#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter - internal static string myKey; -#pragma warning restore SA1307 - private static uint magicNumber; - private static Random ran = new(); // Used for non encryption related functionality. - internal const int SymAlBlockSize = 16; - - /// - /// This is used for the first encryption block, the following blocks will be combined with the cipher text of the previous block. - /// Thus identical blocks in the socket stream would be encrypted to different cipher text blocks. - /// The first block is a handshake one containing random data. - /// Related Unit Test: TestEncryptDecrypt - /// - internal static readonly string InitialIV = ulong.MaxValue.ToString(CultureInfo.InvariantCulture); - - internal static Random Ran - { - get => Common.ran ??= new Random(); - set => Common.ran = value; - } - - internal static uint MagicNumber - { - get => Common.magicNumber; - set => Common.magicNumber = value; - } - - internal static string MyKey - { - get => Common.myKey; - - set - { - if (Common.myKey != value) - { - Common.myKey = value; - _ = Task.Factory.StartNew( - () => Common.GenLegalKey(), - System.Threading.CancellationToken.None, - TaskCreationOptions.None, - TaskScheduler.Default); // Cache the key to improve UX. - } - } - } - - internal static string KeyDisplayedText(string key) - { - string displayedValue = string.Empty; - int i = 0; - - do - { - int length = Math.Min(4, key.Length - i); - displayedValue += string.Concat(key.AsSpan(i, length), " "); - i += 4; - } - while (i < key.Length - 1); - - return displayedValue.Trim(); - } - - internal static bool GeneratedKey { get; set; } - - internal static bool KeyCorrupted { get; set; } - - internal static void InitEncryption() - { - try - { - if (symAl == null) - { -#pragma warning disable SYSLIB0021 // No proper replacement for now - symAl = new AesCryptoServiceProvider(); -#pragma warning restore SYSLIB0021 - symAl.KeySize = 256; - symAl.BlockSize = SymAlBlockSize * 8; - symAl.Padding = PaddingMode.Zeros; - symAl.Mode = CipherMode.CBC; - symAl.GenerateIV(); - } - } - catch (Exception e) - { - Logger.Log(e); - } - } - - private static readonly ConcurrentDictionary LegalKeyDictionary = new(StringComparer.OrdinalIgnoreCase); - - internal static byte[] GenLegalKey() - { - byte[] rv; - string myKey = Common.MyKey; - - if (!LegalKeyDictionary.TryGetValue(myKey, out byte[] value)) - { - Rfc2898DeriveBytes key = new( - myKey, - Common.GetBytesU(InitialIV), - 50000, - HashAlgorithmName.SHA512); - rv = key.GetBytes(32); - _ = LegalKeyDictionary.AddOrUpdate(myKey, rv, (k, v) => rv); - } - else - { - rv = value; - } - - return rv; - } - - private static byte[] GenLegalIV() - { - string st = InitialIV; - int ivLength = symAl.IV.Length; - if (st.Length > ivLength) - { - st = st[..ivLength]; - } - else if (st.Length < ivLength) - { - st = st.PadRight(ivLength, ' '); - } - - return GetBytes(st); - } - - internal static Stream GetEncryptedStream(Stream encryptedStream) - { - ICryptoTransform encryptor; - encryptor = symAl.CreateEncryptor(GenLegalKey(), GenLegalIV()); - return new CryptoStream(encryptedStream, encryptor, CryptoStreamMode.Write); - } - - internal static Stream GetDecryptedStream(Stream encryptedStream) - { - ICryptoTransform decryptor; - decryptor = symAl.CreateDecryptor(GenLegalKey(), GenLegalIV()); - return new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read); - } - - internal static uint Get24BitHash(string st) - { - if (string.IsNullOrEmpty(st)) - { - return 0; - } - - byte[] bytes = new byte[PACKAGE_SIZE]; - for (int i = 0; i < PACKAGE_SIZE; i++) - { - if (i < st.Length) - { - bytes[i] = (byte)st[i]; - } - } - - var hash = SHA512.Create(); - byte[] hashValue = hash.ComputeHash(bytes); - - for (int i = 0; i < 50000; i++) - { - hashValue = hash.ComputeHash(hashValue); - } - - Logger.LogDebug(string.Format(CultureInfo.CurrentCulture, "magic: {0},{1},{2}", hashValue[0], hashValue[1], hashValue[^1])); - hash.Clear(); - return (uint)((hashValue[0] << 23) + (hashValue[1] << 16) + (hashValue[^1] << 8) + hashValue[2]); - } - - internal static string GetDebugInfo(string st) - { - return string.IsNullOrEmpty(st) ? st : ((byte)(Common.GetBytesU(st).Sum(value => value) % 256)).ToString(CultureInfo.InvariantCulture); - } - - internal static string CreateDefaultKey() - { - return CreateRandomKey(); - } - - private const int PW_LENGTH = 16; - - public static string CreateRandomKey() - { - // Not including characters like "'`O0& since they are confusing to users. - string[] chars = new[] { "abcdefghjkmnpqrstuvxyz", "ABCDEFGHJKMNPQRSTUVXYZ", "123456789", "~!@#$%^*()_-+=:;<,>.?/\\|[]" }; - char[][] charactersUsedForKey = chars.Select(charset => Enumerable.Range(0, charset.Length - 1).Select(i => charset[i]).ToArray()).ToArray(); - byte[] randomData = new byte[1]; - string key = string.Empty; - - do - { - foreach (string set in chars) - { - randomData = RandomNumberGenerator.GetBytes(1); - key += set[randomData[0] % set.Length]; - - if (key.Length >= PW_LENGTH) - { - break; - } - } - } - while (key.Length < PW_LENGTH); - - return key; - } - - internal static bool IsKeyValid(string key, out string error) - { - error = string.IsNullOrEmpty(key) || key.Length < 16 - ? "Key must have at least 16 characters in length (spaces are discarded). Key must be auto generated in one of the machines." - : null; - - return error == null; - } - } -} diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.Package.cs b/src/modules/MouseWithoutBorders/App/Class/Common.Package.cs deleted file mode 100644 index baaf1c0544..0000000000 --- a/src/modules/MouseWithoutBorders/App/Class/Common.Package.cs +++ /dev/null @@ -1,262 +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. - -// -// Package format/conversion. -// -// -// 2008 created by Truong Do (ductdo). -// 2009-... modified by Truong Do (TruongDo). -// 2023- Included in PowerToys. -// -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -// In X64, we are WOW -[module: SuppressMessage("Microsoft.Portability", "CA1900:ValueTypeFieldsShouldBePortable", Scope = "type", Target = "MouseWithoutBorders.DATA", Justification = "Dotnet port with style preservation")] - -namespace MouseWithoutBorders -{ - internal enum PackageType// : int - { - // Search for PACKAGE_TYPE_RELATED before changing these! - Invalid = 0xFF, - - Error = 0xFE, - - Hi = 2, - Hello = 3, - ByeBye = 4, - - Heartbeat = 20, - Awake = 21, - HideMouse = 50, - Heartbeat_ex = 51, - Heartbeat_ex_l2 = 52, - Heartbeat_ex_l3 = 53, - - Clipboard = 69, - ClipboardDragDrop = 70, - ClipboardDragDropEnd = 71, - ExplorerDragDrop = 72, - ClipboardCapture = 73, - CaptureScreenCommand = 74, - ClipboardDragDropOperation = 75, - ClipboardDataEnd = 76, - MachineSwitched = 77, - ClipboardAsk = 78, - ClipboardPush = 79, - - NextMachine = 121, - Keyboard = 122, - Mouse = 123, - ClipboardText = 124, - ClipboardImage = 125, - - Handshake = 126, - HandshakeAck = 127, - - Matrix = 128, - MatrixSwapFlag = 2, - MatrixTwoRowFlag = 4, - } - - internal struct PackageMonitor - { - internal ulong Keyboard; - internal ulong Mouse; - internal ulong Heartbeat; - internal ulong ByeBye; - internal ulong Hello; - internal ulong Matrix; - internal ulong ClipboardText; - internal ulong ClipboardImage; - internal ulong Clipboard; - internal ulong ClipboardDragDrop; - internal ulong ClipboardDragDropEnd; - internal ulong ClipboardAsk; - internal ulong ExplorerDragDrop; - internal ulong Nil; - - internal PackageMonitor(ulong value) - { - ClipboardDragDrop = ClipboardDragDropEnd = ExplorerDragDrop = - Keyboard = Mouse = Heartbeat = ByeBye = Hello = Clipboard = - Matrix = ClipboardImage = ClipboardText = Nil = ClipboardAsk = value; - } - } - - internal enum ID : uint - { - NONE = 0, - ALL = 255, - } - - internal enum ClipboardPostAction : uint - { - Other = 0, - Desktop = 1, - Mspaint = 2, - } - - [StructLayout(LayoutKind.Sequential)] - internal struct KEYBDDATA - { - [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Same name as in winAPI")] - internal int wVk; - [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Same name as in winAPI")] - internal int dwFlags; - } - - [StructLayout(LayoutKind.Sequential)] - internal struct MOUSEDATA - { - internal int X; - internal int Y; - internal int WheelDelta; - [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Same name as in winAPI")] - internal int dwFlags; - } - - // The beauty of "union" in C# - [StructLayout(LayoutKind.Explicit)] - internal class DATA - { - [FieldOffset(0)] - internal PackageType Type; // 4 (first byte = package type, 1 = checksum, 2+3 = magic no.) - - [FieldOffset(sizeof(PackageType))] - internal int Id; // 4 - - [FieldOffset(sizeof(PackageType) + sizeof(uint))] - internal ID Src; // 4 - - [FieldOffset(sizeof(PackageType) + (2 * sizeof(uint)))] - internal ID Des; // 4 - - [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] - internal long DateTime; - - [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)) + sizeof(long))] - internal KEYBDDATA Kd; - - [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] - internal MOUSEDATA Md; - - [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] - internal ID Machine1; - - [FieldOffset(sizeof(PackageType) + (4 * sizeof(uint)))] - internal ID Machine2; - - [FieldOffset(sizeof(PackageType) + (5 * sizeof(uint)))] - internal ID Machine3; - - [FieldOffset(sizeof(PackageType) + (6 * sizeof(uint)))] - internal ID Machine4; - - [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] - internal ClipboardPostAction PostAction; - - [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)))] - private long machineNameP1; - - [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)) + sizeof(long))] - private long machineNameP2; - - [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)) + (2 * sizeof(long)))] - private long machineNameP3; - - [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)) + (3 * sizeof(long)))] - private long machineNameP4; - - internal string MachineName - { - get - { - string name = Common.GetString(BitConverter.GetBytes(machineNameP1)) - + Common.GetString(BitConverter.GetBytes(machineNameP2)) - + Common.GetString(BitConverter.GetBytes(machineNameP3)) - + Common.GetString(BitConverter.GetBytes(machineNameP4)); - return name.Trim(); - } - - set - { - byte[] machineName = Common.GetBytes(value.PadRight(32, ' ')); - machineNameP1 = BitConverter.ToInt64(machineName, 0); - machineNameP2 = BitConverter.ToInt64(machineName, 8); - machineNameP3 = BitConverter.ToInt64(machineName, 16); - machineNameP4 = BitConverter.ToInt64(machineName, 24); - } - } - - public DATA() - { - } - - public DATA(byte[] initialData) - { - Bytes = initialData; - } - - internal byte[] Bytes - { - get - { - byte[] buf = new byte[IsBigPackage ? Common.PACKAGE_SIZE_EX : Common.PACKAGE_SIZE]; - Array.Copy(StructToBytes(this), buf, IsBigPackage ? Common.PACKAGE_SIZE_EX : Common.PACKAGE_SIZE); - - return buf; - } - - set - { - Debug.Assert(value.Length <= Common.PACKAGE_SIZE_EX, "Length > package size"); - byte[] buf = new byte[Common.PACKAGE_SIZE_EX]; - Array.Copy(value, buf, value.Length); - BytesToStruct(buf, this); - } - } - - internal bool IsBigPackage - { - get => Type == 0 - ? throw new InvalidOperationException("Package type not set.") - : Type switch - { - PackageType.Hello or PackageType.Awake or PackageType.Heartbeat or PackageType.Heartbeat_ex or PackageType.Handshake or PackageType.HandshakeAck or PackageType.ClipboardPush or PackageType.Clipboard or PackageType.ClipboardAsk or PackageType.ClipboardImage or PackageType.ClipboardText or PackageType.ClipboardDataEnd => true, - _ => (Type & PackageType.Matrix) == PackageType.Matrix, - }; - } - - private byte[] StructToBytes(object structObject) - { - byte[] bytes = new byte[Common.PACKAGE_SIZE_EX]; - GCHandle bHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned); - Marshal.StructureToPtr(structObject, Marshal.UnsafeAddrOfPinnedArrayElement(bytes, 0), false); - bHandle.Free(); - return bytes; - } - - private void BytesToStruct(byte[] value, object structObject) - { - GCHandle bHandle = GCHandle.Alloc(value, GCHandleType.Pinned); - Marshal.PtrToStructure(Marshal.UnsafeAddrOfPinnedArrayElement(value, 0), structObject); - bHandle.Free(); - } - } - - internal partial class Common - { - internal const byte PACKAGE_SIZE = 32; - internal const byte PACKAGE_SIZE_EX = 64; - internal const byte WP_PACKAGE_SIZE = 6; - internal static PackageMonitor PackageSent; - internal static PackageMonitor PackageReceived; - internal static int PackageID; - } -} diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.ShutdownWithPowerToys.cs b/src/modules/MouseWithoutBorders/App/Class/Common.ShutdownWithPowerToys.cs deleted file mode 100644 index 7c0dd4eb9b..0000000000 --- a/src/modules/MouseWithoutBorders/App/Class/Common.ShutdownWithPowerToys.cs +++ /dev/null @@ -1,33 +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 ManagedCommon; -using Microsoft.PowerToys.Telemetry; -using MouseWithoutBorders.Class; - -using Logger = MouseWithoutBorders.Core.Logger; - -namespace MouseWithoutBorders -{ - internal class ShutdownWithPowerToys - { - public static void WaitForPowerToysRunner(ETWTrace etwTrace) - { - try - { - RunnerHelper.WaitForPowerToysRunnerExitFallback(() => - { - etwTrace?.Dispose(); - Common.MainForm.Quit(true, false); - }); - } - catch (Exception e) - { - Logger.Log(e); - } - } - } -} diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.VK.cs b/src/modules/MouseWithoutBorders/App/Class/Common.VK.cs deleted file mode 100644 index 3f54a0281d..0000000000 --- a/src/modules/MouseWithoutBorders/App/Class/Common.VK.cs +++ /dev/null @@ -1,131 +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. - -// -// Virtual key constants. -// -// -// 2008 created by Truong Do (ductdo). -// 2009-... modified by Truong Do (TruongDo). -// 2023- Included in PowerToys. -// -using System; - -namespace MouseWithoutBorders -{ - internal enum VK : ushort - { - CAPITAL = 0x14, - NUMLOCK = 0x90, - SHIFT = 0x10, - CONTROL = 0x11, - MENU = 0x12, - ESCAPE = 0x1B, - BACK = 0x08, - TAB = 0x09, - RETURN = 0x0D, - PRIOR = 0x21, - NEXT = 0x22, - END = 0x23, - HOME = 0x24, - LEFT = 0x25, - UP = 0x26, - RIGHT = 0x27, - DOWN = 0x28, - SELECT = 0x29, - PRINT = 0x2A, - EXECUTE = 0x2B, - SNAPSHOT = 0x2C, - INSERT = 0x2D, - DELETE = 0x2E, - HELP = 0x2F, - NUMPAD0 = 0x60, - NUMPAD1 = 0x61, - NUMPAD2 = 0x62, - NUMPAD3 = 0x63, - NUMPAD4 = 0x64, - NUMPAD5 = 0x65, - NUMPAD6 = 0x66, - NUMPAD7 = 0x67, - NUMPAD8 = 0x68, - NUMPAD9 = 0x69, - MULTIPLY = 0x6A, - ADD = 0x6B, - SEPARATOR = 0x6C, - SUBTRACT = 0x6D, - DECIMAL = 0x6E, - DIVIDE = 0x6F, - F1 = 0x70, - F2 = 0x71, - F3 = 0x72, - F4 = 0x73, - F5 = 0x74, - F6 = 0x75, - F7 = 0x76, - F8 = 0x77, - F9 = 0x78, - F10 = 0x79, - F11 = 0x7A, - F12 = 0x7B, - OEM_1 = 0xBA, - OEM_PLUS = 0xBB, - OEM_COMMA = 0xBC, - OEM_MINUS = 0xBD, - OEM_PERIOD = 0xBE, - OEM_2 = 0xBF, - OEM_3 = 0xC0, - MEDIA_NEXT_TRACK = 0xB0, - MEDIA_PREV_TRACK = 0xB1, - MEDIA_STOP = 0xB2, - MEDIA_PLAY_PAUSE = 0xB3, - LWIN = 0x5B, - RWIN = 0x5C, - LSHIFT = 0xA0, - RSHIFT = 0xA1, - LCONTROL = 0xA2, - RCONTROL = 0xA3, - LMENU = 0xA4, - RMENU = 0xA5, - } - - internal partial class Common - { - internal const ushort KEYEVENTF_KEYDOWN = 0x0001; - internal const ushort KEYEVENTF_KEYUP = 0x0002; - - internal const int WH_MOUSE = 7; - internal const int WH_KEYBOARD = 2; - internal const int WH_MOUSE_LL = 14; - internal const int WH_KEYBOARD_LL = 13; - - internal const int WM_MOUSEMOVE = 0x200; - internal const int WM_LBUTTONDOWN = 0x201; - internal const int WM_RBUTTONDOWN = 0x204; - internal const int WM_MBUTTONDOWN = 0x207; - internal const int WM_XBUTTONDOWN = 0x20B; - internal const int WM_LBUTTONUP = 0x202; - internal const int WM_RBUTTONUP = 0x205; - internal const int WM_MBUTTONUP = 0x208; - internal const int WM_XBUTTONUP = 0x20C; - internal const int WM_LBUTTONDBLCLK = 0x203; - internal const int WM_RBUTTONDBLCLK = 0x206; - internal const int WM_MBUTTONDBLCLK = 0x209; - internal const int WM_MOUSEWHEEL = 0x020A; - internal const int WM_MOUSEHWHEEL = 0x020E; - - internal const int WM_KEYDOWN = 0x100; - internal const int WM_KEYUP = 0x101; - internal const int WM_SYSKEYDOWN = 0x104; - internal const int WM_SYSKEYUP = 0x105; - - [Flags] - internal enum LLKHF - { - EXTENDED = 0x01, - INJECTED = 0x10, - ALTDOWN = 0x20, - UP = 0x80, - } - } -} diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.WinAPI.cs b/src/modules/MouseWithoutBorders/App/Class/Common.WinAPI.cs deleted file mode 100644 index ee2d99398c..0000000000 --- a/src/modules/MouseWithoutBorders/App/Class/Common.WinAPI.cs +++ /dev/null @@ -1,363 +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.Diagnostics; -using System.Drawing; -using System.Globalization; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using System.Windows.Forms; - -// -// Screen/Desktop helper functions. -// -// -// 2008 created by Truong Do (ductdo). -// 2009-... modified by Truong Do (TruongDo). -// 2023- Included in PowerToys. -// -using MouseWithoutBorders.Class; -using MouseWithoutBorders.Core; - -using Thread = MouseWithoutBorders.Core.Thread; - -namespace MouseWithoutBorders -{ - // Desktops, and GetScreenConfig routines - internal partial class Common - { - private static MyRectangle newDesktopBounds; - private static MyRectangle newPrimaryScreenBounds; - private static string activeDesktop; - - internal static string ActiveDesktop => Common.activeDesktop; - - internal static void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) - { - GetScreenConfig(); - } - - internal static readonly List SensitivePoints = new(); - - private static bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref NativeMethods.RECT lprcMonitor, IntPtr dwData) - { - // lprcMonitor is wrong!!! => using GetMonitorInfo(...) - // Log(String.Format( CultureInfo.CurrentCulture,"MONITOR: l{0}, t{1}, r{2}, b{3}", lprcMonitor.Left, lprcMonitor.Top, lprcMonitor.Right, lprcMonitor.Bottom)); - NativeMethods.MonitorInfoEx mi = default; - mi.cbSize = Marshal.SizeOf(mi); - _ = NativeMethods.GetMonitorInfo(hMonitor, ref mi); - - try - { - // For logging only - _ = NativeMethods.GetDpiForMonitor(hMonitor, 0, out uint dpiX, out uint dpiY); - Logger.Log(string.Format(CultureInfo.CurrentCulture, "MONITOR: ({0}, {1}, {2}, {3}). DPI: ({4}, {5})", mi.rcMonitor.Left, mi.rcMonitor.Top, mi.rcMonitor.Right, mi.rcMonitor.Bottom, dpiX, dpiY)); - } - catch (DllNotFoundException) - { - Logger.Log("GetDpiForMonitor is unsupported in Windows 7 and lower."); - } - catch (EntryPointNotFoundException) - { - Logger.Log("GetDpiForMonitor is unsupported in Windows 7 and lower."); - } - catch (Exception e) - { - Logger.Log(e); - } - - if (mi.rcMonitor.Left == 0 && mi.rcMonitor.Top == 0 && mi.rcMonitor.Right != 0 && mi.rcMonitor.Bottom != 0) - { - // Primary screen - _ = Interlocked.Exchange(ref screenWidth, mi.rcMonitor.Right - mi.rcMonitor.Left); - _ = Interlocked.Exchange(ref screenHeight, mi.rcMonitor.Bottom - mi.rcMonitor.Top); - - newPrimaryScreenBounds.Left = mi.rcMonitor.Left; - newPrimaryScreenBounds.Top = mi.rcMonitor.Top; - newPrimaryScreenBounds.Right = mi.rcMonitor.Right; - newPrimaryScreenBounds.Bottom = mi.rcMonitor.Bottom; - } - else - { - if (mi.rcMonitor.Left < newDesktopBounds.Left) - { - newDesktopBounds.Left = mi.rcMonitor.Left; - } - - if (mi.rcMonitor.Top < newDesktopBounds.Top) - { - newDesktopBounds.Top = mi.rcMonitor.Top; - } - - if (mi.rcMonitor.Right > newDesktopBounds.Right) - { - newDesktopBounds.Right = mi.rcMonitor.Right; - } - - if (mi.rcMonitor.Bottom > newDesktopBounds.Bottom) - { - newDesktopBounds.Bottom = mi.rcMonitor.Bottom; - } - } - - lock (SensitivePoints) - { - SensitivePoints.Add(new Point(mi.rcMonitor.Left, mi.rcMonitor.Top)); - SensitivePoints.Add(new Point(mi.rcMonitor.Right, mi.rcMonitor.Top)); - SensitivePoints.Add(new Point(mi.rcMonitor.Right, mi.rcMonitor.Bottom)); - SensitivePoints.Add(new Point(mi.rcMonitor.Left, mi.rcMonitor.Bottom)); - } - - return true; - } - - internal static void GetScreenConfig() - { - try - { - Logger.LogDebug("==================== GetScreenConfig started"); - newDesktopBounds = new MyRectangle(); - newPrimaryScreenBounds = new MyRectangle(); - newDesktopBounds.Left = newPrimaryScreenBounds.Left = Screen.PrimaryScreen.Bounds.Left; - newDesktopBounds.Top = newPrimaryScreenBounds.Top = Screen.PrimaryScreen.Bounds.Top; - newDesktopBounds.Right = newPrimaryScreenBounds.Right = Screen.PrimaryScreen.Bounds.Right; - newDesktopBounds.Bottom = newPrimaryScreenBounds.Bottom = Screen.PrimaryScreen.Bounds.Bottom; - - Logger.Log(string.Format( - CultureInfo.CurrentCulture, - "logon = {0} PrimaryScreenBounds = {1},{2},{3},{4} desktopBounds = {5},{6},{7},{8}", - Common.RunOnLogonDesktop, - Common.newPrimaryScreenBounds.Left, - Common.newPrimaryScreenBounds.Top, - Common.newPrimaryScreenBounds.Right, - Common.newPrimaryScreenBounds.Bottom, - Common.newDesktopBounds.Left, - Common.newDesktopBounds.Top, - Common.newDesktopBounds.Right, - Common.newDesktopBounds.Bottom)); - -#if USE_MANAGED_ROUTINES - // Managed routines do not work well when running on secure desktop:( - screenWidth = Screen.PrimaryScreen.Bounds.Width; - screenHeight = Screen.PrimaryScreen.Bounds.Height; - screenCount = Screen.AllScreens.Length; - for (int i = 0; i < Screen.AllScreens.Length; i++) - { - if (Screen.AllScreens[i].Bounds.Left < desktopBounds.Left) desktopBounds.Left = Screen.AllScreens[i].Bounds.Left; - if (Screen.AllScreens[i].Bounds.Top < desktopBounds.Top) desktopBounds.Top = Screen.AllScreens[i].Bounds.Top; - if (Screen.AllScreens[i].Bounds.Right > desktopBounds.Right) desktopBounds.Right = Screen.AllScreens[i].Bounds.Right; - if (Screen.AllScreens[i].Bounds.Bottom > desktopBounds.Bottom) desktopBounds.Bottom = Screen.AllScreens[i].Bounds.Bottom; - } -#else - lock (SensitivePoints) - { - SensitivePoints.Clear(); - } - - NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, MonitorEnumProc, IntPtr.Zero); - - // 1000 calls to EnumDisplayMonitors cost a dozen of milliseconds -#endif - Interlocked.Exchange(ref MachineStuff.desktopBounds, newDesktopBounds); - Interlocked.Exchange(ref MachineStuff.primaryScreenBounds, newPrimaryScreenBounds); - - Logger.Log(string.Format( - CultureInfo.CurrentCulture, - "logon = {0} PrimaryScreenBounds = {1},{2},{3},{4} desktopBounds = {5},{6},{7},{8}", - Common.RunOnLogonDesktop, - MachineStuff.PrimaryScreenBounds.Left, - MachineStuff.PrimaryScreenBounds.Top, - MachineStuff.PrimaryScreenBounds.Right, - MachineStuff.PrimaryScreenBounds.Bottom, - MachineStuff.DesktopBounds.Left, - MachineStuff.DesktopBounds.Top, - MachineStuff.DesktopBounds.Right, - MachineStuff.DesktopBounds.Bottom)); - - Logger.Log("==================== GetScreenConfig ended"); - } - catch (Exception e) - { - Logger.Log(e); - } - } - -#if USING_SCREEN_SAVER_ROUTINES - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern int PostMessage(IntPtr hWnd, int wMsg, int wParam, int lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern IntPtr OpenDesktop(string hDesktop, int Flags, bool Inherit, UInt32 DesiredAccess); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern bool CloseDesktop(IntPtr hDesktop); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern bool EnumDesktopWindows( IntPtr hDesktop, EnumDesktopWindowsProc callback, IntPtr lParam); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern bool SystemParametersInfo(int uAction, int uParam, ref int pvParam, int flags); - - private delegate bool EnumDesktopWindowsProc(IntPtr hDesktop, IntPtr lParam); - private const int WM_CLOSE = 16; - private const int SPI_GETSCREENSAVERRUNNING = 114; - - internal static bool IsScreenSaverRunning() - { - int isRunning = 0; - SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0,ref isRunning, 0); - return (isRunning != 0); - } - - internal static void CloseScreenSaver() - { - IntPtr hDesktop = OpenDesktop("Screen-saver", 0, false, DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS); - if (hDesktop != IntPtr.Zero) - { - LogDebug("Closing screen saver..."); - EnumDesktopWindows(hDesktop, new EnumDesktopWindowsProc(CloseScreenSaverFunc), IntPtr.Zero); - CloseDesktop(hDesktop); - } - } - - private static bool CloseScreenSaverFunc(IntPtr hWnd, IntPtr lParam) - { - if (IsWindowVisible(hWnd)) - { - LogDebug("Posting WM_CLOSE to " + hWnd.ToString(CultureInfo.InvariantCulture)); - PostMessage(hWnd, WM_CLOSE, 0, 0); - } - return true; - } -#endif - - internal static string GetMyDesktop() - { - byte[] arThreadDesktop = new byte[256]; - IntPtr hD = NativeMethods.GetThreadDesktop(NativeMethods.GetCurrentThreadId()); - if (hD != IntPtr.Zero) - { - _ = NativeMethods.GetUserObjectInformation(hD, NativeMethods.UOI_NAME, arThreadDesktop, arThreadDesktop.Length, out _); - return GetString(arThreadDesktop).Replace("\0", string.Empty); - } - - return string.Empty; - } - - internal static string GetInputDesktop() - { - byte[] arInputDesktop = new byte[256]; - IntPtr hD = NativeMethods.OpenInputDesktop(0, false, NativeMethods.DESKTOP_READOBJECTS); - if (hD != IntPtr.Zero) - { - _ = NativeMethods.GetUserObjectInformation(hD, NativeMethods.UOI_NAME, arInputDesktop, arInputDesktop.Length, out _); - return GetString(arInputDesktop).Replace("\0", string.Empty); - } - - return string.Empty; - } - - internal static void StartMMService(string desktopToRunMouseWithoutBordersOn) - { - if (!Common.RunWithNoAdminRight) - { - Logger.LogDebug("*** Starting on active Desktop: " + desktopToRunMouseWithoutBordersOn); - Service.StartMouseWithoutBordersService(desktopToRunMouseWithoutBordersOn); - } - } - - internal static void CheckForDesktopSwitchEvent(bool cleanupIfExit) - { - try - { - if (!IsMyDesktopActive() || Common.CurrentProcess.SessionId != NativeMethods.WTSGetActiveConsoleSessionId()) - { - Helper.RunDDHelper(true); - int waitCount = 20; - - while (NativeMethods.WTSGetActiveConsoleSessionId() == 0xFFFFFFFF && waitCount > 0) - { - waitCount--; - Logger.LogDebug("The session is detached/attached."); - Thread.Sleep(500); - } - - string myDesktop = GetMyDesktop(); - activeDesktop = GetInputDesktop(); - - Logger.LogDebug("*** Active Desktop = " + activeDesktop); - Logger.LogDebug("*** My Desktop = " + myDesktop); - - if (myDesktop.Equals(activeDesktop, StringComparison.OrdinalIgnoreCase)) - { - Logger.LogDebug("*** Active Desktop == My Desktop (TS session)"); - } - - if (!activeDesktop.Equals("winlogon", StringComparison.OrdinalIgnoreCase) && - !activeDesktop.Equals("default", StringComparison.OrdinalIgnoreCase) && - !activeDesktop.Equals("disconnect", StringComparison.OrdinalIgnoreCase)) - { - try - { - StartMMService(activeDesktop); - } - catch (Exception e) - { - Logger.Log($"{nameof(CheckForDesktopSwitchEvent)}: {e}"); - } - } - else - { - if (!myDesktop.Equals(activeDesktop, StringComparison.OrdinalIgnoreCase)) - { - Logger.Log("*** Active Desktop <> My Desktop"); - } - - uint sid = NativeMethods.WTSGetActiveConsoleSessionId(); - - if (Process.GetProcessesByName(Common.BinaryName).Any(p => (uint)p.SessionId == sid)) - { - Logger.Log("Found MouseWithoutBorders on the active session!"); - } - else - { - Logger.Log("MouseWithoutBorders not found on the active session!"); - StartMMService(null); - } - } - - if (!myDesktop.Equals("winlogon", StringComparison.OrdinalIgnoreCase) && - !myDesktop.Equals("default", StringComparison.OrdinalIgnoreCase)) - { - Logger.LogDebug("*** Desktop inactive, exiting: " + myDesktop); - Setting.Values.LastX = JUST_GOT_BACK_FROM_SCREEN_SAVER; - if (cleanupIfExit) - { - InitAndCleanup.Cleanup(); - } - - Process.GetCurrentProcess().KillProcess(); - } - } - } - catch (Exception e) - { - Logger.Log(e); - } - } - - private static Point p; - - internal static bool IsMyDesktopActive() - { - return NativeMethods.GetCursorPos(ref p); - } - } -} diff --git a/src/modules/MouseWithoutBorders/App/Class/Common.cs b/src/modules/MouseWithoutBorders/App/Class/Common.cs index ba5a1655e0..9a34500b52 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Common.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Common.cs @@ -315,7 +315,7 @@ namespace MouseWithoutBorders if (!acquireMutex) { Process[] ps = Process.GetProcessesByName(Common.BinaryName); - Logger.TelemetryLogTrace($"Balance: {socketMutexBalance}, Active: {IsMyDesktopActive()}, Sid/Console: {Process.GetCurrentProcess().SessionId}/{NativeMethods.WTSGetActiveConsoleSessionId()}, Desktop/Input: {GetMyDesktop()}/{GetInputDesktop()}, count: {ps?.Length}.", SeverityLevel.Warning); + Logger.TelemetryLogTrace($"Balance: {socketMutexBalance}, Active: {WinAPI.IsMyDesktopActive()}, Sid/Console: {Process.GetCurrentProcess().SessionId}/{NativeMethods.WTSGetActiveConsoleSessionId()}, Desktop/Input: {WinAPI.GetMyDesktop()}/{WinAPI.GetInputDesktop()}, count: {ps?.Length}.", SeverityLevel.Warning); } Logger.LogDebug("SOCKET MUTEX ENDED."); @@ -358,7 +358,7 @@ namespace MouseWithoutBorders Logger.TelemetryLogTrace($"[{actionName}] took more than {(long)timeout.TotalSeconds}, restarting the process.", SeverityLevel.Warning, true); - string desktop = Common.GetMyDesktop(); + string desktop = WinAPI.GetMyDesktop(); MachineStuff.oneInstanceCheck?.Close(); _ = Process.Start(Application.ExecutablePath, desktop); Logger.LogDebug($"Started on desktop {desktop}"); @@ -514,7 +514,7 @@ namespace MouseWithoutBorders internal static void SendHeartBeat(bool initial = false) { - SendPackage(ID.ALL, initial && Common.GeneratedKey ? PackageType.Heartbeat_ex : PackageType.Heartbeat); + SendPackage(ID.ALL, initial && Encryption.GeneratedKey ? PackageType.Heartbeat_ex : PackageType.Heartbeat); } private static long lastSendNextMachine; @@ -550,7 +550,7 @@ namespace MouseWithoutBorders internal static void SendAwakeBeat() { - if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && Common.IsMyDesktopActive() && + if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && WinAPI.IsMyDesktopActive() && Setting.Values.BlockScreenSaver && lastRealInputEventCount != Event.RealInputEventCount) { SendPackage(ID.ALL, PackageType.Awake); @@ -568,7 +568,7 @@ namespace MouseWithoutBorders { if (lastInputEventCount == Event.InputEventCount) { - if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && Common.IsMyDesktopActive()) + if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && WinAPI.IsMyDesktopActive()) { PokeMyself(); } @@ -577,13 +577,13 @@ namespace MouseWithoutBorders lastInputEventCount = Event.InputEventCount; } - private static void PokeMyself() + internal static void PokeMyself() { int x, y = 0; for (int i = 0; i < 10; i++) { - x = Ran.Next(-9, 10); + x = Encryption.Ran.Next(-9, 10); InputSimulation.MoveMouseRelative(x, y); Thread.Sleep(50); InputSimulation.MoveMouseRelative(-x, -y); @@ -677,7 +677,7 @@ namespace MouseWithoutBorders { Common.MMSleep(0.2); InputSimulation.SendKey(new KEYBDDATA() { wVk = (int)VK.SNAPSHOT }); - InputSimulation.SendKey(new KEYBDDATA() { dwFlags = (int)Common.LLKHF.UP, wVk = (int)VK.SNAPSHOT }); + InputSimulation.SendKey(new KEYBDDATA() { dwFlags = (int)WM.LLKHF.UP, wVk = (int)VK.SNAPSHOT }); Logger.LogDebug("PrepareScreenCapture: SNAPSHOT simulated."); @@ -710,7 +710,7 @@ namespace MouseWithoutBorders "\"" + Environment.ExpandEnvironmentVariables(@"%SystemRoot%\System32\Mspaint.exe") + "\"", "\"" + file + "\"", - GetInputDesktop(), + WinAPI.GetInputDesktop(), 1); // CreateNormalIntegrityProcess(Environment.ExpandEnvironmentVariables(@"%SystemRoot%\System32\Mspaint.exe") + @@ -919,7 +919,7 @@ namespace MouseWithoutBorders try { - data.Id = Interlocked.Increment(ref PackageID); + data.Id = Interlocked.Increment(ref Package.PackageID); bool updateClientSockets = false; @@ -999,7 +999,7 @@ namespace MouseWithoutBorders } else { - PackageSent.Nil++; + Package.PackageSent.Nil++; } } @@ -1379,7 +1379,7 @@ namespace MouseWithoutBorders if (string.IsNullOrEmpty(machine_Name)) { - machine_Name = "RANDOM" + Ran.Next().ToString(CultureInfo.CurrentCulture); + machine_Name = "RANDOM" + Encryption.Ran.Next().ToString(CultureInfo.CurrentCulture); } } @@ -1533,13 +1533,13 @@ namespace MouseWithoutBorders internal static void SendOrReceiveARandomDataBlockPerInitialIV(Stream st, bool send = true) { - byte[] ranData = new byte[SymAlBlockSize]; + byte[] ranData = new byte[Encryption.SymAlBlockSize]; try { if (send) { - ranData = RandomNumberGenerator.GetBytes(SymAlBlockSize); + ranData = RandomNumberGenerator.GetBytes(Encryption.SymAlBlockSize); st.Write(ranData, 0, ranData.Length); } else diff --git a/src/modules/MouseWithoutBorders/App/Class/InputHook.cs b/src/modules/MouseWithoutBorders/App/Class/InputHook.cs index d68b1a1584..53e815c1a0 100644 --- a/src/modules/MouseWithoutBorders/App/Class/InputHook.cs +++ b/src/modules/MouseWithoutBorders/App/Class/InputHook.cs @@ -109,7 +109,7 @@ namespace MouseWithoutBorders.Class // Install Mouse Hook mouseHookProcedure = new NativeMethods.HookProc(MouseHookProc); hMouseHook = NativeMethods.SetWindowsHookEx( - Common.WH_MOUSE_LL, + WM.WH_MOUSE_LL, mouseHookProcedure, Marshal.GetHINSTANCE( Assembly.GetExecutingAssembly().GetModules()[0]), @@ -126,7 +126,7 @@ namespace MouseWithoutBorders.Class // Install Keyboard Hook keyboardHookProcedure = new NativeMethods.HookProc(KeyboardHookProc); hKeyboardHook = NativeMethods.SetWindowsHookEx( - Common.WH_KEYBOARD_LL, + WM.WH_KEYBOARD_LL, keyboardHookProcedure, Marshal.GetHINSTANCE( Assembly.GetExecutingAssembly().GetModules()[0]), @@ -233,7 +233,7 @@ namespace MouseWithoutBorders.Class if (nCode >= 0 && MouseEvent != null) { - if (wParam == Common.WM_LBUTTONUP && SkipMouseUpCount > 0) + if (wParam == WM.WM_LBUTTONUP && SkipMouseUpCount > 0) { Logger.LogDebug($"{nameof(SkipMouseUpCount)}: {SkipMouseUpCount}."); SkipMouseUpCount--; @@ -241,7 +241,7 @@ namespace MouseWithoutBorders.Class return rv; } - if ((wParam == Common.WM_LBUTTONUP || wParam == Common.WM_LBUTTONDOWN) && SkipMouseUpDown) + if ((wParam == WM.WM_LBUTTONUP || wParam == WM.WM_LBUTTONDOWN) && SkipMouseUpDown) { rv = NativeMethods.CallNextHookEx(hMouseHook, nCode, wParam, lParam); return rv; @@ -370,7 +370,7 @@ namespace MouseWithoutBorders.Class private bool ProcessKeyEx(int vkCode, int flags, KEYBDDATA hookCallbackKeybdData) { - if ((flags & (int)Common.LLKHF.UP) == (int)Common.LLKHF.UP) + if ((flags & (int)WM.LLKHF.UP) == (int)WM.LLKHF.UP) { EasyMouseKeyDown = false; @@ -553,7 +553,7 @@ namespace MouseWithoutBorders.Class KeyboardEvent(hookCallbackKeybdData); } - hookCallbackKeybdData.dwFlags |= (int)Common.LLKHF.UP; + hookCallbackKeybdData.dwFlags |= (int)WM.LLKHF.UP; foreach (var code in codes) { diff --git a/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs b/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs index e735db814c..156f69597d 100644 --- a/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs +++ b/src/modules/MouseWithoutBorders/App/Class/InputSimulation.cs @@ -112,12 +112,12 @@ namespace MouseWithoutBorders.Class uint scanCode = 0; // http://msdn.microsoft.com/en-us/library/ms644967(VS.85).aspx - if ((kd.dwFlags & (int)Common.LLKHF.UP) == (int)Common.LLKHF.UP) + if ((kd.dwFlags & (int)WM.LLKHF.UP) == (int)WM.LLKHF.UP) { dwFlags = NativeMethods.KEYEVENTF.KEYUP; } - if ((kd.dwFlags & (int)Common.LLKHF.EXTENDED) == (int)Common.LLKHF.EXTENDED) + if ((kd.dwFlags & (int)WM.LLKHF.EXTENDED) == (int)WM.LLKHF.EXTENDED) { dwFlags |= NativeMethods.KEYEVENTF.EXTENDEDKEY; } @@ -173,44 +173,44 @@ namespace MouseWithoutBorders.Class mouse_input.mi.dy = (int)dy; mouse_input.mi.mouseData = md.WheelDelta; - if (md.dwFlags != Common.WM_MOUSEMOVE) + if (md.dwFlags != WM.WM_MOUSEMOVE) { Logger.LogDebug($"InputSimulation.SendMouse: x = {md.X}, y = {md.Y}, WheelDelta = {md.WheelDelta}, dwFlags = {md.dwFlags}."); } switch (md.dwFlags) { - case Common.WM_MOUSEMOVE: + case WM.WM_MOUSEMOVE: mouse_input.mi.dwFlags |= (int)(NativeMethods.MOUSEEVENTF.MOVE | NativeMethods.MOUSEEVENTF.ABSOLUTE); break; - case Common.WM_LBUTTONDOWN: + case WM.WM_LBUTTONDOWN: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.LEFTDOWN; break; - case Common.WM_LBUTTONUP: + case WM.WM_LBUTTONUP: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.LEFTUP; break; - case Common.WM_RBUTTONDOWN: + case WM.WM_RBUTTONDOWN: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.RIGHTDOWN; break; - case Common.WM_RBUTTONUP: + case WM.WM_RBUTTONUP: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.RIGHTUP; break; - case Common.WM_MBUTTONDOWN: + case WM.WM_MBUTTONDOWN: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.MIDDLEDOWN; break; - case Common.WM_MBUTTONUP: + case WM.WM_MBUTTONUP: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.MIDDLEUP; break; - case Common.WM_MOUSEWHEEL: + case WM.WM_MOUSEWHEEL: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.WHEEL; break; - case Common.WM_MOUSEHWHEEL: + case WM.WM_MOUSEHWHEEL: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.HWHEEL; break; - case Common.WM_XBUTTONUP: + case WM.WM_XBUTTONUP: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XUP; break; - case Common.WM_XBUTTONDOWN: + case WM.WM_XBUTTONDOWN: mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XDOWN; break; @@ -373,7 +373,7 @@ namespace MouseWithoutBorders.Class { eatKey = false; - if ((flags & (int)Common.LLKHF.UP) == (int)Common.LLKHF.UP) + if ((flags & (int)WM.LLKHF.UP) == (int)WM.LLKHF.UP) { switch ((VK)vkCode) { diff --git a/src/modules/MouseWithoutBorders/App/Class/Program.cs b/src/modules/MouseWithoutBorders/App/Class/Program.cs index c139da46e9..23513e1515 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Program.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Program.cs @@ -143,7 +143,7 @@ namespace MouseWithoutBorders.Class return; } - string myDesktop = Common.GetMyDesktop(); + string myDesktop = WinAPI.GetMyDesktop(); if (firstArg.Equals("winlogon", StringComparison.OrdinalIgnoreCase)) { @@ -305,8 +305,8 @@ namespace MouseWithoutBorders.Class MachineStuff.ClearComputerMatrix(); Setting.Values.MyKey = securityKey; - Common.MyKey = securityKey; - Common.MagicNumber = Common.Get24BitHash(Common.MyKey); + Encryption.MyKey = securityKey; + Encryption.MagicNumber = Encryption.Get24BitHash(Encryption.MyKey); MachineStuff.MachineMatrix = new string[MachineStuff.MAX_MACHINE] { pcName.Trim().ToUpper(CultureInfo.CurrentCulture), Common.MachineName.Trim(), string.Empty, string.Empty }; string[] machines = MachineStuff.MachineMatrix; @@ -328,8 +328,8 @@ namespace MouseWithoutBorders.Class Setting.Values.EasyMouse = (int)EasyMouseOption.Enable; MachineStuff.ClearComputerMatrix(); - Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey(); - Common.GeneratedKey = true; + Setting.Values.MyKey = Encryption.MyKey = Encryption.CreateRandomKey(); + Encryption.GeneratedKey = true; Setting.Values.PauseInstantSaving = false; Setting.Values.SaveSettings(); diff --git a/src/modules/MouseWithoutBorders/App/Class/Setting.cs b/src/modules/MouseWithoutBorders/App/Class/Setting.cs index 623571f6ce..c526c70976 100644 --- a/src/modules/MouseWithoutBorders/App/Class/Setting.cs +++ b/src/modules/MouseWithoutBorders/App/Class/Setting.cs @@ -109,9 +109,9 @@ namespace MouseWithoutBorders.Class var shouldReopenSockets = false; - if (Common.MyKey != _properties.SecurityKey.Value) + if (Encryption.MyKey != _properties.SecurityKey.Value) { - Common.MyKey = _properties.SecurityKey.Value; + Encryption.MyKey = _properties.SecurityKey.Value; shouldReopenSockets = true; } @@ -489,7 +489,7 @@ namespace MouseWithoutBorders.Class } else { - string randomKey = Common.CreateDefaultKey(); + string randomKey = Encryption.CreateDefaultKey(); _properties.SecurityKey.Value = randomKey; return randomKey; @@ -1055,7 +1055,7 @@ namespace MouseWithoutBorders.Class if (machineId == 0) { - var newMachineId = Common.Ran.Next(); + var newMachineId = Encryption.Ran.Next(); _properties.MachineID.Value = newMachineId; machineId = newMachineId; if (!PauseInstantSaving) diff --git a/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs b/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs index c5241beddf..575c9582df 100644 --- a/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs +++ b/src/modules/MouseWithoutBorders/App/Class/SocketStuff.cs @@ -101,7 +101,7 @@ namespace MouseWithoutBorders.Class { if (encryptedStream == null && BackingSocket.Connected) { - encryptedStream = Common.GetEncryptedStream(new NetworkStream(BackingSocket)); + encryptedStream = Encryption.GetEncryptedStream(new NetworkStream(BackingSocket)); Common.SendOrReceiveARandomDataBlockPerInitialIV(encryptedStream); } @@ -115,7 +115,7 @@ namespace MouseWithoutBorders.Class { if (decryptedStream == null && BackingSocket.Connected) { - decryptedStream = Common.GetDecryptedStream(new NetworkStream(BackingSocket)); + decryptedStream = Encryption.GetDecryptedStream(new NetworkStream(BackingSocket)); Common.SendOrReceiveARandomDataBlockPerInitialIV(decryptedStream, false); } @@ -181,7 +181,7 @@ namespace MouseWithoutBorders.Class Logger.LogDebug("SocketStuff started."); bASE_PORT = port; - Common.Ran = new Random(); + Encryption.Ran = new Random(); Logger.LogDebug("Validating session..."); @@ -221,11 +221,11 @@ namespace MouseWithoutBorders.Class if (Setting.Values.IsMyKeyRandom) { - Setting.Values.MyKey = Common.MyKey; + Setting.Values.MyKey = Encryption.MyKey; } - Common.MagicNumber = Common.Get24BitHash(Common.MyKey); - Common.PackageID = Setting.Values.PackageID; + Encryption.MagicNumber = Encryption.Get24BitHash(Encryption.MyKey); + Package.PackageID = Setting.Values.PackageID; TcpPort = bASE_PORT; @@ -242,7 +242,7 @@ namespace MouseWithoutBorders.Class Logger.TelemetryLogTrace($"{nameof(SocketStuff)}: {e.Message}", SeverityLevel.Warning); } - Common.GetScreenConfig(); + WinAPI.GetScreenConfig(); if (firstRun && Common.RunOnScrSaverDesktop) { @@ -305,7 +305,7 @@ namespace MouseWithoutBorders.Class sleepSecs = 10; // It is reasonable to give a try on restarting MwB processes in other sessions. - if (restartCount++ < 5 && Common.IsMyDesktopActive() && !Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop) + if (restartCount++ < 5 && WinAPI.IsMyDesktopActive() && !Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop) { Logger.TelemetryLogTrace("Restarting the service dues to WSAEADDRINUSE.", SeverityLevel.Warning); Program.StartService(); @@ -361,7 +361,7 @@ namespace MouseWithoutBorders.Class { Setting.Values.LastX = Common.LastX; Setting.Values.LastY = Common.LastY; - Setting.Values.PackageID = Common.PackageID; + Setting.Values.PackageID = Package.PackageID; // Common.Log("Saving IP: " + Setting.Values.DesMachineID.ToString(CultureInfo.CurrentCulture)); Setting.Values.DesMachineID = (uint)Common.DesMachineID; @@ -505,10 +505,10 @@ namespace MouseWithoutBorders.Class throw new ExpectedSocketException(log); } - bytes[3] = (byte)((Common.MagicNumber >> 24) & 0xFF); - bytes[2] = (byte)((Common.MagicNumber >> 16) & 0xFF); + bytes[3] = (byte)((Encryption.MagicNumber >> 24) & 0xFF); + bytes[2] = (byte)((Encryption.MagicNumber >> 16) & 0xFF); bytes[1] = 0; - for (int i = 2; i < Common.PACKAGE_SIZE; i++) + for (int i = 2; i < Package.PACKAGE_SIZE; i++) { bytes[1] = (byte)(bytes[1] + bytes[i]); } @@ -535,13 +535,13 @@ namespace MouseWithoutBorders.Class magic = (buf[3] << 24) + (buf[2] << 16); - if (magic != (Common.MagicNumber & 0xFFFF0000)) + if (magic != (Encryption.MagicNumber & 0xFFFF0000)) { Logger.Log("Magic number invalid!"); buf[0] = (byte)PackageType.Invalid; } - for (int i = 2; i < Common.PACKAGE_SIZE; i++) + for (int i = 2; i < Package.PACKAGE_SIZE; i++) { checksum = (byte)(checksum + buf[i]); } @@ -557,7 +557,7 @@ namespace MouseWithoutBorders.Class internal static DATA TcpReceiveData(TcpSk tcp, out int bytesReceived) { - byte[] buf = new byte[Common.PACKAGE_SIZE_EX]; + byte[] buf = new byte[Package.PACKAGE_SIZE_EX]; Stream decryptedStream = tcp.DecryptedStream; if (tcp.BackingSocket == null || !tcp.BackingSocket.Connected || decryptedStream == null) @@ -571,9 +571,9 @@ namespace MouseWithoutBorders.Class try { - bytesReceived = decryptedStream.ReadEx(buf, 0, Common.PACKAGE_SIZE); + bytesReceived = decryptedStream.ReadEx(buf, 0, Package.PACKAGE_SIZE); - if (bytesReceived != Common.PACKAGE_SIZE) + if (bytesReceived != Package.PACKAGE_SIZE) { buf[0] = bytesReceived == 0 ? (byte)PackageType.Error : (byte)PackageType.Invalid; } @@ -586,9 +586,9 @@ namespace MouseWithoutBorders.Class if (package.IsBigPackage) { - bytesReceived = decryptedStream.ReadEx(buf, Common.PACKAGE_SIZE, Common.PACKAGE_SIZE); + bytesReceived = decryptedStream.ReadEx(buf, Package.PACKAGE_SIZE, Package.PACKAGE_SIZE); - if (bytesReceived != Common.PACKAGE_SIZE) + if (bytesReceived != Package.PACKAGE_SIZE) { buf[0] = bytesReceived == 0 ? (byte)PackageType.Error : (byte)PackageType.Invalid; } @@ -614,28 +614,28 @@ namespace MouseWithoutBorders.Class switch (type) { case PackageType.Keyboard: - Common.PackageSent.Keyboard++; + Package.PackageSent.Keyboard++; break; case PackageType.Mouse: - Common.PackageSent.Mouse++; + Package.PackageSent.Mouse++; break; case PackageType.Heartbeat: case PackageType.Heartbeat_ex: - Common.PackageSent.Heartbeat++; + Package.PackageSent.Heartbeat++; break; case PackageType.Hello: - Common.PackageSent.Hello++; + Package.PackageSent.Hello++; break; case PackageType.ByeBye: - Common.PackageSent.ByeBye++; + Package.PackageSent.ByeBye++; break; case PackageType.Matrix: - Common.PackageSent.Matrix++; + Package.PackageSent.Matrix++; break; default: @@ -643,11 +643,11 @@ namespace MouseWithoutBorders.Class switch (subtype) { case (byte)PackageType.ClipboardText: - Common.PackageSent.ClipboardText++; + Package.PackageSent.ClipboardText++; break; case (byte)PackageType.ClipboardImage: - Common.PackageSent.ClipboardImage++; + Package.PackageSent.ClipboardImage++; break; default: @@ -1266,7 +1266,7 @@ namespace MouseWithoutBorders.Class string strIP = string.Empty; ID remoteID = ID.NONE; - byte[] buf = RandomNumberGenerator.GetBytes(Common.PACKAGE_SIZE_EX); + byte[] buf = RandomNumberGenerator.GetBytes(Package.PACKAGE_SIZE_EX); d = new DATA(buf); TcpSk currentTcp = tcp; @@ -1280,8 +1280,8 @@ namespace MouseWithoutBorders.Class try { - currentSocket.SendBufferSize = Common.PACKAGE_SIZE * 10000; - currentSocket.ReceiveBufferSize = Common.PACKAGE_SIZE * 10000; + currentSocket.SendBufferSize = Package.PACKAGE_SIZE * 10000; + currentSocket.ReceiveBufferSize = Package.PACKAGE_SIZE * 10000; currentSocket.NoDelay = true; // This is very interesting to know:( currentSocket.SendTimeout = 500; d.MachineName = Common.MachineName; @@ -1829,7 +1829,7 @@ namespace MouseWithoutBorders.Class } while (rv > 0); - if ((rv = Common.PACKAGE_SIZE - (sentCount % Common.PACKAGE_SIZE)) > 0) + if ((rv = Package.PACKAGE_SIZE - (sentCount % Package.PACKAGE_SIZE)) > 0) { Array.Clear(buf, 0, buf.Length); ecStream.Write(buf, 0, rv); @@ -1900,7 +1900,7 @@ namespace MouseWithoutBorders.Class } while (rv > 0); - if ((rv = sentCount % Common.PACKAGE_SIZE) > 0) + if ((rv = sentCount % Package.PACKAGE_SIZE) > 0) { Array.Clear(buf, 0, buf.Length); ecStream.Write(buf, 0, rv); @@ -1984,7 +1984,7 @@ namespace MouseWithoutBorders.Class if (tcp.MachineId == Setting.Values.MachineId) { tcp = null; - Setting.Values.MachineId = Common.Ran.Next(); + Setting.Values.MachineId = Encryption.Ran.Next(); InitAndCleanup.UpdateMachineTimeAndID(); InitAndCleanup.PleaseReopenSocket = InitAndCleanup.REOPEN_WHEN_HOTKEY; diff --git a/src/modules/MouseWithoutBorders/App/Class/TcpServer.cs b/src/modules/MouseWithoutBorders/App/Class/TcpServer.cs index f915ae94e0..cc98381483 100644 --- a/src/modules/MouseWithoutBorders/App/Class/TcpServer.cs +++ b/src/modules/MouseWithoutBorders/App/Class/TcpServer.cs @@ -70,7 +70,7 @@ namespace MouseWithoutBorders.Class continue; } - if (!Common.IsMyDesktopActive()) + if (!WinAPI.IsMyDesktopActive()) { // We can just throw the SocketException but to avoid a redundant log entry: throw new ExpectedSocketException($"{nameof(StartServer)}: The desktop is no longer active."); diff --git a/src/modules/MouseWithoutBorders/App/Core/Clipboard.cs b/src/modules/MouseWithoutBorders/App/Core/Clipboard.cs index 5840325941..e557ff4a37 100644 --- a/src/modules/MouseWithoutBorders/App/Core/Clipboard.cs +++ b/src/modules/MouseWithoutBorders/App/Core/Clipboard.cs @@ -270,15 +270,15 @@ internal static class Clipboard int index = 0; int len; DATA package = new(); - byte[] buf = new byte[Common.PACKAGE_SIZE_EX]; - int dataStart = Common.PACKAGE_SIZE_EX - DATA_SIZE; + byte[] buf = new byte[Package.PACKAGE_SIZE_EX]; + int dataStart = Package.PACKAGE_SIZE_EX - DATA_SIZE; while (true) { if ((index + DATA_SIZE) > l) { len = l - index; - Array.Clear(buf, 0, Common.PACKAGE_SIZE_EX); + Array.Clear(buf, 0, Package.PACKAGE_SIZE_EX); } else { @@ -315,7 +315,7 @@ internal static class Clipboard } MemoryStream m = new(); - int dataStart = Common.PACKAGE_SIZE_EX - DATA_SIZE; + int dataStart = Package.PACKAGE_SIZE_EX - DATA_SIZE; m.Write(data.Bytes, dataStart, DATA_SIZE); int unexpectedCount = 0; @@ -809,27 +809,27 @@ internal static class Clipboard MachineName = Common.MachineName, }; - byte[] buf = new byte[Common.PACKAGE_SIZE_EX]; + byte[] buf = new byte[Package.PACKAGE_SIZE_EX]; NetworkStream ns = new(s); - enStream = Common.GetEncryptedStream(ns); + enStream = Encryption.GetEncryptedStream(ns); Common.SendOrReceiveARandomDataBlockPerInitialIV(enStream); Logger.LogDebug($"{nameof(ShakeHand)}: Writing header package."); - enStream.Write(package.Bytes, 0, Common.PACKAGE_SIZE_EX); + enStream.Write(package.Bytes, 0, Package.PACKAGE_SIZE_EX); Logger.LogDebug($"{nameof(ShakeHand)}: Sent: clientPush={clientPushData}, postAction={postAction}."); - deStream = Common.GetDecryptedStream(ns); + deStream = Encryption.GetDecryptedStream(ns); Common.SendOrReceiveARandomDataBlockPerInitialIV(deStream, false); Logger.LogDebug($"{nameof(ShakeHand)}: Reading header package."); - int bytesReceived = deStream.ReadEx(buf, 0, Common.PACKAGE_SIZE_EX); + int bytesReceived = deStream.ReadEx(buf, 0, Package.PACKAGE_SIZE_EX); package.Bytes = buf; string name = "Unknown"; - if (bytesReceived == Common.PACKAGE_SIZE_EX) + if (bytesReceived == Package.PACKAGE_SIZE_EX) { if (package.Type is PackageType.Clipboard or PackageType.ClipboardPush) { diff --git a/src/modules/MouseWithoutBorders/App/Core/ClipboardPostAction.cs b/src/modules/MouseWithoutBorders/App/Core/ClipboardPostAction.cs new file mode 100644 index 0000000000..c07a8bb91a --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/ClipboardPostAction.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal enum ClipboardPostAction : uint +{ + Other = 0, + Desktop = 1, + Mspaint = 2, +} diff --git a/src/modules/MouseWithoutBorders/App/Core/DATA.cs b/src/modules/MouseWithoutBorders/App/Core/DATA.cs new file mode 100644 index 0000000000..4085483bd9 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/DATA.cs @@ -0,0 +1,150 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +// In X64, we are WOW +[module: SuppressMessage("Microsoft.Portability", "CA1900:ValueTypeFieldsShouldBePortable", Scope = "type", Target = "MouseWithoutBorders.Core.DATA", Justification = "Dotnet port with style preservation")] + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +// The beauty of "union" in C# +[StructLayout(LayoutKind.Explicit)] +internal sealed class DATA +{ + [FieldOffset(0)] + internal PackageType Type; // 4 (first byte = package type, 1 = checksum, 2+3 = magic no.) + + [FieldOffset(sizeof(PackageType))] + internal int Id; // 4 + + [FieldOffset(sizeof(PackageType) + sizeof(uint))] + internal ID Src; // 4 + + [FieldOffset(sizeof(PackageType) + (2 * sizeof(uint)))] + internal ID Des; // 4 + + [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] + internal long DateTime; + + [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)) + sizeof(long))] + internal KEYBDDATA Kd; + + [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] + internal MOUSEDATA Md; + + [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] + internal ID Machine1; + + [FieldOffset(sizeof(PackageType) + (4 * sizeof(uint)))] + internal ID Machine2; + + [FieldOffset(sizeof(PackageType) + (5 * sizeof(uint)))] + internal ID Machine3; + + [FieldOffset(sizeof(PackageType) + (6 * sizeof(uint)))] + internal ID Machine4; + + [FieldOffset(sizeof(PackageType) + (3 * sizeof(uint)))] + internal ClipboardPostAction PostAction; + + [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)))] + private long machineNameP1; + + [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)) + sizeof(long))] + private long machineNameP2; + + [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)) + (2 * sizeof(long)))] + private long machineNameP3; + + [FieldOffset(sizeof(PackageType) + (7 * sizeof(uint)) + (3 * sizeof(long)))] + private long machineNameP4; + + internal string MachineName + { + get + { + string name = Common.GetString(BitConverter.GetBytes(machineNameP1)) + + Common.GetString(BitConverter.GetBytes(machineNameP2)) + + Common.GetString(BitConverter.GetBytes(machineNameP3)) + + Common.GetString(BitConverter.GetBytes(machineNameP4)); + return name.Trim(); + } + + set + { + byte[] machineName = Common.GetBytes(value.PadRight(32, ' ')); + machineNameP1 = BitConverter.ToInt64(machineName, 0); + machineNameP2 = BitConverter.ToInt64(machineName, 8); + machineNameP3 = BitConverter.ToInt64(machineName, 16); + machineNameP4 = BitConverter.ToInt64(machineName, 24); + } + } + + public DATA() + { + } + + public DATA(byte[] initialData) + { + Bytes = initialData; + } + + internal byte[] Bytes + { + get + { + byte[] buf = new byte[IsBigPackage ? Package.PACKAGE_SIZE_EX : Package.PACKAGE_SIZE]; + Array.Copy(StructToBytes(this), buf, IsBigPackage ? Package.PACKAGE_SIZE_EX : Package.PACKAGE_SIZE); + + return buf; + } + + set + { + Debug.Assert(value.Length <= Package.PACKAGE_SIZE_EX, "Length > package size"); + byte[] buf = new byte[Package.PACKAGE_SIZE_EX]; + Array.Copy(value, buf, value.Length); + BytesToStruct(buf, this); + } + } + + internal bool IsBigPackage + { + get => Type == 0 + ? throw new InvalidOperationException("Package type not set.") + : Type switch + { + PackageType.Hello or PackageType.Awake or PackageType.Heartbeat or PackageType.Heartbeat_ex or PackageType.Handshake or PackageType.HandshakeAck or PackageType.ClipboardPush or PackageType.Clipboard or PackageType.ClipboardAsk or PackageType.ClipboardImage or PackageType.ClipboardText or PackageType.ClipboardDataEnd => true, + _ => (Type & PackageType.Matrix) == PackageType.Matrix, + }; + } + + private byte[] StructToBytes(object structObject) + { + byte[] bytes = new byte[Package.PACKAGE_SIZE_EX]; + GCHandle bHandle = GCHandle.Alloc(bytes, GCHandleType.Pinned); + Marshal.StructureToPtr(structObject, Marshal.UnsafeAddrOfPinnedArrayElement(bytes, 0), false); + bHandle.Free(); + return bytes; + } + + private void BytesToStruct(byte[] value, object structObject) + { + GCHandle bHandle = GCHandle.Alloc(value, GCHandleType.Pinned); + Marshal.PtrToStructure(Marshal.UnsafeAddrOfPinnedArrayElement(value, 0), structObject); + bHandle.Free(); + } +} diff --git a/src/modules/MouseWithoutBorders/App/Core/DragDrop.cs b/src/modules/MouseWithoutBorders/App/Core/DragDrop.cs index 2decb83261..d262e48f24 100644 --- a/src/modules/MouseWithoutBorders/App/Core/DragDrop.cs +++ b/src/modules/MouseWithoutBorders/App/Core/DragDrop.cs @@ -67,20 +67,20 @@ internal static class DragDrop return; } - if (wParam == Common.WM_LBUTTONDOWN) + if (wParam == WM.WM_LBUTTONDOWN) { MouseDown = true; DragMachine = MachineStuff.desMachineID; MachineStuff.dropMachineID = ID.NONE; Logger.LogDebug("DragDropStep01: MouseDown"); } - else if (wParam == Common.WM_LBUTTONUP) + else if (wParam == WM.WM_LBUTTONUP) { MouseDown = false; Logger.LogDebug("DragDropStep01: MouseUp"); } - if (wParam == Common.WM_RBUTTONUP && IsDropping) + if (wParam == WM.WM_RBUTTONUP && IsDropping) { IsDropping = false; Clipboard.LastIDWithClipboardData = ID.NONE; @@ -252,7 +252,7 @@ internal static class DragDrop internal static void DragDropStep09(int wParam) { - if (wParam == Common.WM_MOUSEMOVE && IsDropping) + if (wParam == WM.WM_MOUSEMOVE && IsDropping) { // Show/Move form Common.DoSomethingInUIThread(() => @@ -260,7 +260,7 @@ internal static class DragDrop _ = NativeMethods.PostMessage(Common.MainForm.Handle, NativeMethods.WM_SHOW_DRAG_DROP, (IntPtr)0, (IntPtr)0); }); } - else if (wParam == Common.WM_LBUTTONUP && (IsDropping || IsDragging)) + else if (wParam == WM.WM_LBUTTONUP && (IsDropping || IsDragging)) { if (IsDropping) { diff --git a/src/modules/MouseWithoutBorders/App/Core/Encryption.cs b/src/modules/MouseWithoutBorders/App/Core/Encryption.cs new file mode 100644 index 0000000000..9d00b6bb40 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/Encryption.cs @@ -0,0 +1,245 @@ +// 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.Concurrent; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; + +// +// Encrypt/decrypt implementation. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal static class Encryption +{ +#pragma warning disable SYSLIB0021 + private static AesCryptoServiceProvider symAl; +#pragma warning restore SYSLIB0021 +#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter + internal static string myKey; +#pragma warning restore SA1307 + private static uint magicNumber; + private static Random ran = new(); // Used for non encryption related functionality. + internal const int SymAlBlockSize = 16; + + /// + /// This is used for the first encryption block, the following blocks will be combined with the cipher text of the previous block. + /// Thus identical blocks in the socket stream would be encrypted to different cipher text blocks. + /// The first block is a handshake one containing random data. + /// Related Unit Test: TestEncryptDecrypt + /// + private static readonly string InitialIV = ulong.MaxValue.ToString(CultureInfo.InvariantCulture); + + internal static Random Ran + { + get => Encryption.ran ??= new Random(); + set => Encryption.ran = value; + } + + internal static uint MagicNumber + { + get => Encryption.magicNumber; + set => Encryption.magicNumber = value; + } + + internal static string MyKey + { + get => Encryption.myKey; + + set + { + if (Encryption.myKey != value) + { + Encryption.myKey = value; + _ = Task.Factory.StartNew( + () => Encryption.GenLegalKey(), + System.Threading.CancellationToken.None, + TaskCreationOptions.None, + TaskScheduler.Default); // Cache the key to improve UX. + } + } + } + + private static string KeyDisplayedText(string key) + { + string displayedValue = string.Empty; + int i = 0; + + do + { + int length = Math.Min(4, key.Length - i); + displayedValue += string.Concat(key.AsSpan(i, length), " "); + i += 4; + } + while (i < key.Length - 1); + + return displayedValue.Trim(); + } + + internal static bool GeneratedKey { get; set; } + + internal static bool KeyCorrupted { get; set; } + + internal static void InitEncryption() + { + try + { + if (symAl == null) + { +#pragma warning disable SYSLIB0021 // No proper replacement for now + symAl = new AesCryptoServiceProvider(); +#pragma warning restore SYSLIB0021 + symAl.KeySize = 256; + symAl.BlockSize = SymAlBlockSize * 8; + symAl.Padding = PaddingMode.Zeros; + symAl.Mode = CipherMode.CBC; + symAl.GenerateIV(); + } + } + catch (Exception e) + { + Logger.Log(e); + } + } + + private static readonly ConcurrentDictionary LegalKeyDictionary = new(StringComparer.OrdinalIgnoreCase); + + private static byte[] GenLegalKey() + { + byte[] rv; + string myKey = Encryption.MyKey; + + if (!LegalKeyDictionary.TryGetValue(myKey, out byte[] value)) + { + Rfc2898DeriveBytes key = new( + myKey, + Common.GetBytesU(InitialIV), + 50000, + HashAlgorithmName.SHA512); + rv = key.GetBytes(32); + _ = LegalKeyDictionary.AddOrUpdate(myKey, rv, (k, v) => rv); + } + else + { + rv = value; + } + + return rv; + } + + private static byte[] GenLegalIV() + { + string st = InitialIV; + int ivLength = symAl.IV.Length; + if (st.Length > ivLength) + { + st = st[..ivLength]; + } + else if (st.Length < ivLength) + { + st = st.PadRight(ivLength, ' '); + } + + return Common.GetBytes(st); + } + + internal static Stream GetEncryptedStream(Stream encryptedStream) + { + ICryptoTransform encryptor; + encryptor = symAl.CreateEncryptor(GenLegalKey(), GenLegalIV()); + return new CryptoStream(encryptedStream, encryptor, CryptoStreamMode.Write); + } + + internal static Stream GetDecryptedStream(Stream encryptedStream) + { + ICryptoTransform decryptor; + decryptor = symAl.CreateDecryptor(GenLegalKey(), GenLegalIV()); + return new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read); + } + + internal static uint Get24BitHash(string st) + { + if (string.IsNullOrEmpty(st)) + { + return 0; + } + + byte[] bytes = new byte[Package.PACKAGE_SIZE]; + for (int i = 0; i < Package.PACKAGE_SIZE; i++) + { + if (i < st.Length) + { + bytes[i] = (byte)st[i]; + } + } + + var hash = SHA512.Create(); + byte[] hashValue = hash.ComputeHash(bytes); + + for (int i = 0; i < 50000; i++) + { + hashValue = hash.ComputeHash(hashValue); + } + + Logger.LogDebug(string.Format(CultureInfo.CurrentCulture, "magic: {0},{1},{2}", hashValue[0], hashValue[1], hashValue[^1])); + hash.Clear(); + return (uint)((hashValue[0] << 23) + (hashValue[1] << 16) + (hashValue[^1] << 8) + hashValue[2]); + } + + internal static string GetDebugInfo(string st) + { + return string.IsNullOrEmpty(st) ? st : ((byte)(Common.GetBytesU(st).Sum(value => value) % 256)).ToString(CultureInfo.InvariantCulture); + } + + internal static string CreateDefaultKey() + { + return CreateRandomKey(); + } + + private const int PW_LENGTH = 16; + + internal static string CreateRandomKey() + { + // Not including characters like "'`O0& since they are confusing to users. + string[] chars = new[] { "abcdefghjkmnpqrstuvxyz", "ABCDEFGHJKMNPQRSTUVXYZ", "123456789", "~!@#$%^*()_-+=:;<,>.?/\\|[]" }; + char[][] charactersUsedForKey = chars.Select(charset => Enumerable.Range(0, charset.Length - 1).Select(i => charset[i]).ToArray()).ToArray(); + byte[] randomData = new byte[1]; + string key = string.Empty; + + do + { + foreach (string set in chars) + { + randomData = RandomNumberGenerator.GetBytes(1); + key += set[randomData[0] % set.Length]; + + if (key.Length >= PW_LENGTH) + { + break; + } + } + } + while (key.Length < PW_LENGTH); + + return key; + } + + internal static bool IsKeyValid(string key, out string error) + { + error = string.IsNullOrEmpty(key) || key.Length < 16 + ? "Key must have at least 16 characters in length (spaces are discarded). Key must be auto generated in one of the machines." + : null; + + return error == null; + } +} diff --git a/src/modules/MouseWithoutBorders/App/Core/Event.cs b/src/modules/MouseWithoutBorders/App/Core/Event.cs index 1e6ee3e371..659a15526e 100644 --- a/src/modules/MouseWithoutBorders/App/Core/Event.cs +++ b/src/modules/MouseWithoutBorders/App/Core/Event.cs @@ -70,7 +70,7 @@ internal static class Event // Check if easy mouse setting is enabled. bool isEasyMouseEnabled = IsSwitchingByMouseEnabled(); - if (isEasyMouseEnabled && Common.Sk != null && (Common.DesMachineID == Common.MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == Common.WM_MOUSEMOVE) + if (isEasyMouseEnabled && Common.Sk != null && (Common.DesMachineID == Common.MachineID || !Setting.Values.MoveMouseRelatively) && e.dwFlags == WM.WM_MOUSEMOVE) { Point p = MachineStuff.MoveToMyNeighbourIfNeeded(e.X, e.Y, MachineStuff.desMachineID); @@ -115,7 +115,7 @@ internal static class Event Common.SkSend(MousePackage, null, false); - if (MousePackage.Md.dwFlags is Common.WM_LBUTTONUP or Common.WM_RBUTTONUP) + if (MousePackage.Md.dwFlags is WM.WM_LBUTTONUP or WM.WM_RBUTTONUP) { Thread.Sleep(10); } @@ -265,7 +265,7 @@ internal static class Event KeybdPackage.Kd = e; KeybdPackage.DateTime = Common.GetTick(); Common.SkSend(KeybdPackage, null, false); - if (KeybdPackage.Kd.dwFlags is Common.WM_KEYUP or Common.WM_SYSKEYUP) + if (KeybdPackage.Kd.dwFlags is WM.WM_KEYUP or WM.WM_SYSKEYUP) { Thread.Sleep(10); } diff --git a/src/modules/MouseWithoutBorders/App/Core/Helper.cs b/src/modules/MouseWithoutBorders/App/Core/Helper.cs index bd66c9a83f..8c291fb417 100644 --- a/src/modules/MouseWithoutBorders/App/Core/Helper.cs +++ b/src/modules/MouseWithoutBorders/App/Core/Helper.cs @@ -290,7 +290,7 @@ internal static class Helper return; } - if (!Common.IsMyDesktopActive()) + if (!WinAPI.IsMyDesktopActive()) { return; } @@ -314,7 +314,7 @@ internal static class Helper _ = Launch.CreateProcessInInputDesktopSession( $"\"{Path.GetDirectoryName(Application.ExecutablePath)}\\{HelperProcessName}.exe\"", string.Empty, - Common.GetInputDesktop(), + WinAPI.GetInputDesktop(), 0); Clipboard.HasSwitchedMachineSinceLastCopy = true; @@ -379,7 +379,7 @@ internal static class Helper log += "=============================================================================================================================\r\n"; log += $"{Application.ProductName} version {Application.ProductVersion}\r\n"; - log += $"{Setting.Values.Username}/{Common.GetDebugInfo(Common.MyKey)}\r\n"; + log += $"{Setting.Values.Username}/{Encryption.GetDebugInfo(Encryption.MyKey)}\r\n"; log += $"{Common.MachineName}/{Common.MachineID}/{Common.DesMachineID}\r\n"; log += $"Id: {Setting.Values.DeviceId}\r\n"; log += $"Matrix: {string.Join(",", MachineStuff.MachineMatrix)}\r\n"; diff --git a/src/modules/MouseWithoutBorders/App/Core/ID.cs b/src/modules/MouseWithoutBorders/App/Core/ID.cs new file mode 100644 index 0000000000..11dfcc22c8 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/ID.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal enum ID : uint +{ + NONE = 0, + ALL = 255, +} diff --git a/src/modules/MouseWithoutBorders/App/Core/InitAndCleanup.cs b/src/modules/MouseWithoutBorders/App/Core/InitAndCleanup.cs index 963775cbca..510671ee95 100644 --- a/src/modules/MouseWithoutBorders/App/Core/InitAndCleanup.cs +++ b/src/modules/MouseWithoutBorders/App/Core/InitAndCleanup.cs @@ -89,23 +89,23 @@ internal static class InitAndCleanup internal static void Init() { _ = Helper.GetUserName(); - Common.GeneratedKey = true; + Encryption.GeneratedKey = true; try { - Common.MyKey = Setting.Values.MyKey; + Encryption.MyKey = Setting.Values.MyKey; int tmp = Setting.Values.MyKeyDaysToExpire; } catch (FormatException e) { - Common.KeyCorrupted = true; - Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey(); + Encryption.KeyCorrupted = true; + Setting.Values.MyKey = Encryption.MyKey = Encryption.CreateRandomKey(); Logger.Log(e.Message); } catch (CryptographicException e) { - Common.KeyCorrupted = true; - Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey(); + Encryption.KeyCorrupted = true; + Setting.Values.MyKey = Encryption.MyKey = Encryption.CreateRandomKey(); Logger.Log(e.Message); } @@ -127,14 +127,14 @@ internal static class InitAndCleanup bool dummy = Setting.Values.DrawMouseEx; Common.Is64bitOS = IntPtr.Size == 8; Common.tcpPort = Setting.Values.TcpPort; - Common.GetScreenConfig(); - Common.PackageSent = new PackageMonitor(0); - Common.PackageReceived = new PackageMonitor(0); + WinAPI.GetScreenConfig(); + Package.PackageSent = new PackageMonitor(0); + Package.PackageReceived = new PackageMonitor(0); SetupMachineNameAndID(); - Common.InitEncryption(); + Encryption.InitEncryption(); CreateHelperThreads(); - SystemEvents.DisplaySettingsChanged += new EventHandler(Common.SystemEvents_DisplaySettingsChanged); + SystemEvents.DisplaySettingsChanged += new EventHandler(WinAPI.SystemEvents_DisplaySettingsChanged); NetworkChange.NetworkAvailabilityChanged += new NetworkAvailabilityChangedEventHandler(NetworkChange_NetworkAvailabilityChanged); SystemEvents.PowerModeChanged += new PowerModeChangedEventHandler(SystemEvents_PowerModeChanged); PleaseReopenSocket = 9; @@ -220,7 +220,7 @@ internal static class InitAndCleanup lastReleaseAllKeysCall = Common.GetTick(); KEYBDDATA kd; - kd.dwFlags = (int)Common.LLKHF.UP; + kd.dwFlags = (int)WM.LLKHF.UP; VK[] keys = new VK[] { @@ -266,7 +266,7 @@ internal static class InitAndCleanup true); } - if (!Common.IsMyDesktopActive()) + if (!WinAPI.IsMyDesktopActive()) { PleaseReopenSocket = 0; } diff --git a/src/modules/MouseWithoutBorders/App/Core/KEYBDDATA.cs b/src/modules/MouseWithoutBorders/App/Core/KEYBDDATA.cs new file mode 100644 index 0000000000..244c069c98 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/KEYBDDATA.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +[StructLayout(LayoutKind.Sequential)] +internal struct KEYBDDATA +{ + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Same name as in winAPI")] + internal int wVk; + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Same name as in winAPI")] + internal int dwFlags; +} diff --git a/src/modules/MouseWithoutBorders/App/Core/Logger.cs b/src/modules/MouseWithoutBorders/App/Core/Logger.cs index 86ce7605b5..4d39983c35 100644 --- a/src/modules/MouseWithoutBorders/App/Core/Logger.cs +++ b/src/modules/MouseWithoutBorders/App/Core/Logger.cs @@ -121,52 +121,52 @@ internal static class Logger { string log; - if (!lastPackageSent.Equals(Common.PackageSent)) + if (!lastPackageSent.Equals(Package.PackageSent)) { log = string.Format( CultureInfo.CurrentCulture, "SENT:" + HeaderSENT, - Common.PackageSent.Heartbeat, - Common.PackageSent.Keyboard, - Common.PackageSent.Mouse, - Common.PackageSent.Hello, - Common.PackageSent.Matrix, - Common.PackageSent.ClipboardText, - Common.PackageSent.ClipboardImage, - Common.PackageSent.ByeBye, - Common.PackageSent.Clipboard, - Common.PackageSent.ClipboardDragDrop, - Common.PackageSent.ClipboardDragDropEnd, - Common.PackageSent.ExplorerDragDrop, + Package.PackageSent.Heartbeat, + Package.PackageSent.Keyboard, + Package.PackageSent.Mouse, + Package.PackageSent.Hello, + Package.PackageSent.Matrix, + Package.PackageSent.ClipboardText, + Package.PackageSent.ClipboardImage, + Package.PackageSent.ByeBye, + Package.PackageSent.Clipboard, + Package.PackageSent.ClipboardDragDrop, + Package.PackageSent.ClipboardDragDropEnd, + Package.PackageSent.ExplorerDragDrop, Event.inputEventCount, - Common.PackageSent.Nil); + Package.PackageSent.Nil); Log(log); - lastPackageSent = Common.PackageSent; // Copy data + lastPackageSent = Package.PackageSent; // Copy data } - if (!lastPackageReceived.Equals(Common.PackageReceived)) + if (!lastPackageReceived.Equals(Package.PackageReceived)) { log = string.Format( CultureInfo.CurrentCulture, "RECEIVED:" + HeaderRECEIVED, - Common.PackageReceived.Heartbeat, - Common.PackageReceived.Keyboard, - Common.PackageReceived.Mouse, - Common.PackageReceived.Hello, - Common.PackageReceived.Matrix, - Common.PackageReceived.ClipboardText, - Common.PackageReceived.ClipboardImage, - Common.PackageReceived.ByeBye, - Common.PackageReceived.Clipboard, - Common.PackageReceived.ClipboardDragDrop, - Common.PackageReceived.ClipboardDragDropEnd, - Common.PackageReceived.ExplorerDragDrop, + Package.PackageReceived.Heartbeat, + Package.PackageReceived.Keyboard, + Package.PackageReceived.Mouse, + Package.PackageReceived.Hello, + Package.PackageReceived.Matrix, + Package.PackageReceived.ClipboardText, + Package.PackageReceived.ClipboardImage, + Package.PackageReceived.ByeBye, + Package.PackageReceived.Clipboard, + Package.PackageReceived.ClipboardDragDrop, + Package.PackageReceived.ClipboardDragDropEnd, + Package.PackageReceived.ExplorerDragDrop, Event.invalidPackageCount, - Common.PackageReceived.Nil, + Package.PackageReceived.Nil, Receiver.processedPackageCount, Receiver.skippedPackageCount); Log(log); - lastPackageReceived = Common.PackageReceived; + lastPackageReceived = Package.PackageReceived; } } @@ -209,9 +209,9 @@ internal static class Logger "Private Mem: " + (Process.GetCurrentProcess().PrivateMemorySize64 / 1024).ToString(CultureInfo.CurrentCulture) + "KB", sb.ToString()); - if (!string.IsNullOrEmpty(Common.myKey)) + if (!string.IsNullOrEmpty(Encryption.myKey)) { - log = log.Replace(Common.MyKey, Common.GetDebugInfo(Common.MyKey)); + log = log.Replace(Encryption.MyKey, Encryption.GetDebugInfo(Encryption.MyKey)); } log += Thread.DumpThreadsStack(); @@ -251,14 +251,18 @@ internal static class Logger { typeof(Clipboard), typeof(DragDrop), + typeof(Encryption), typeof(Event), typeof(InitAndCleanup), typeof(Helper), typeof(Launch), typeof(Logger), typeof(MachineStuff), + typeof(Package), typeof(Receiver), typeof(Service), + typeof(WinAPI), + typeof(WM), }; foreach (var staticType in staticTypes) { @@ -294,7 +298,7 @@ internal static class Logger // strArr[3] = t.FullName; strArr[4] = " = "; strArr[5] = objName.Equals("myKey", StringComparison.OrdinalIgnoreCase) - ? Common.GetDebugInfo(objString) + ? Encryption.GetDebugInfo(objString) : objName.Equals("lastClipboardObject", StringComparison.OrdinalIgnoreCase) ? string.Empty : objString diff --git a/src/modules/MouseWithoutBorders/App/Core/MOUSEDATA.cs b/src/modules/MouseWithoutBorders/App/Core/MOUSEDATA.cs new file mode 100644 index 0000000000..8f8e0f4267 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/MOUSEDATA.cs @@ -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.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +[StructLayout(LayoutKind.Sequential)] +internal struct MOUSEDATA +{ + internal int X; + internal int Y; + internal int WheelDelta; + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Same name as in winAPI")] + internal int dwFlags; +} diff --git a/src/modules/MouseWithoutBorders/App/Core/MachineStuff.cs b/src/modules/MouseWithoutBorders/App/Core/MachineStuff.cs index e5263aa788..add9a03b04 100644 --- a/src/modules/MouseWithoutBorders/App/Core/MachineStuff.cs +++ b/src/modules/MouseWithoutBorders/App/Core/MachineStuff.cs @@ -221,9 +221,9 @@ internal static class MachineStuff if (Setting.Values.BlockMouseAtCorners) { - lock (Common.SensitivePoints) + lock (WinAPI.SensitivePoints) { - foreach (Point p in Common.SensitivePoints) + foreach (Point p in WinAPI.SensitivePoints) { if (Math.Abs(p.X - x) < 100 && Math.Abs(p.Y - y) < 100) { @@ -793,8 +793,8 @@ internal static class MachineStuff internal static void ShowSetupForm(bool reopenSockets = false) { Logger.LogDebug("========== BEGIN THE SETUP EXPERIENCE ==========", true); - Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey(); - Common.GeneratedKey = true; + Setting.Values.MyKey = Encryption.MyKey = Encryption.CreateRandomKey(); + Encryption.GeneratedKey = true; if (Process.GetCurrentProcess().SessionId != NativeMethods.WTSGetActiveConsoleSessionId()) { @@ -1067,7 +1067,7 @@ internal static class MachineStuff internal static void AssertOneInstancePerDesktopSession() { - string eventName = $"Global\\{Application.ProductName}-{FrmAbout.AssemblyVersion}-{Common.GetMyDesktop()}-{Common.CurrentProcess.SessionId}"; + string eventName = $"Global\\{Application.ProductName}-{FrmAbout.AssemblyVersion}-{WinAPI.GetMyDesktop()}-{Common.CurrentProcess.SessionId}"; oneInstanceCheck = new EventWaitHandle(false, EventResetMode.ManualReset, eventName, out bool created); if (!created) diff --git a/src/modules/MouseWithoutBorders/App/Core/Package.cs b/src/modules/MouseWithoutBorders/App/Core/Package.cs new file mode 100644 index 0000000000..54b5e3e467 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/Package.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal static class Package +{ + internal const byte PACKAGE_SIZE = 32; + internal const byte PACKAGE_SIZE_EX = 64; + private const byte WP_PACKAGE_SIZE = 6; + internal static PackageMonitor PackageSent; + internal static PackageMonitor PackageReceived; + internal static int PackageID; +} diff --git a/src/modules/MouseWithoutBorders/App/Core/PackageMonitor.cs b/src/modules/MouseWithoutBorders/App/Core/PackageMonitor.cs new file mode 100644 index 0000000000..e0ccf9ef75 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/PackageMonitor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal struct PackageMonitor +{ + internal ulong Keyboard; + internal ulong Mouse; + internal ulong Heartbeat; + internal ulong ByeBye; + internal ulong Hello; + internal ulong Matrix; + internal ulong ClipboardText; + internal ulong ClipboardImage; + internal ulong Clipboard; + internal ulong ClipboardDragDrop; + internal ulong ClipboardDragDropEnd; + internal ulong ClipboardAsk; + internal ulong ExplorerDragDrop; + internal ulong Nil; + + internal PackageMonitor(ulong value) + { + ClipboardDragDrop = ClipboardDragDropEnd = ExplorerDragDrop = + Keyboard = Mouse = Heartbeat = ByeBye = Hello = Clipboard = + Matrix = ClipboardImage = ClipboardText = Nil = ClipboardAsk = value; + } +} diff --git a/src/modules/MouseWithoutBorders/App/Core/PackageType.cs b/src/modules/MouseWithoutBorders/App/Core/PackageType.cs new file mode 100644 index 0000000000..9b7b48fc12 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/PackageType.cs @@ -0,0 +1,57 @@ +// 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. + +// +// Package format/conversion. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal enum PackageType // : int +{ + // Search for PACKAGE_TYPE_RELATED before changing these! + Invalid = 0xFF, + + Error = 0xFE, + + Hi = 2, + Hello = 3, + ByeBye = 4, + + Heartbeat = 20, + Awake = 21, + HideMouse = 50, + Heartbeat_ex = 51, + Heartbeat_ex_l2 = 52, + Heartbeat_ex_l3 = 53, + + Clipboard = 69, + ClipboardDragDrop = 70, + ClipboardDragDropEnd = 71, + ExplorerDragDrop = 72, + ClipboardCapture = 73, + CaptureScreenCommand = 74, + ClipboardDragDropOperation = 75, + ClipboardDataEnd = 76, + MachineSwitched = 77, + ClipboardAsk = 78, + ClipboardPush = 79, + + NextMachine = 121, + Keyboard = 122, + Mouse = 123, + ClipboardText = 124, + ClipboardImage = 125, + + Handshake = 126, + HandshakeAck = 127, + + Matrix = 128, + MatrixSwapFlag = 2, + MatrixTwoRowFlag = 4, +} diff --git a/src/modules/MouseWithoutBorders/App/Core/Receiver.cs b/src/modules/MouseWithoutBorders/App/Core/Receiver.cs index 1b1e0730b0..0a6aaad2ee 100644 --- a/src/modules/MouseWithoutBorders/App/Core/Receiver.cs +++ b/src/modules/MouseWithoutBorders/App/Core/Receiver.cs @@ -93,7 +93,7 @@ internal static class Receiver switch (package.Type) { case PackageType.Keyboard: - Common.PackageReceived.Keyboard++; + Package.PackageReceived.Keyboard++; if (package.Des == Common.MachineID || package.Des == ID.ALL) { JustGotAKey = Common.GetTick(); @@ -102,7 +102,7 @@ internal static class Receiver bool nonElevated = Common.RunWithNoAdminRight && false; if (nonElevated && Setting.Values.OneWayControlMode) { - if ((package.Kd.dwFlags & (int)Common.LLKHF.UP) == (int)Common.LLKHF.UP) + if ((package.Kd.dwFlags & (int)WM.LLKHF.UP) == (int)WM.LLKHF.UP) { Helper.ShowOneWayModeMessage(); } @@ -116,7 +116,7 @@ internal static class Receiver break; case PackageType.Mouse: - Common.PackageReceived.Mouse++; + Package.PackageReceived.Mouse++; if (package.Des == Common.MachineID || package.Des == ID.ALL) { @@ -127,16 +127,16 @@ internal static class Receiver // NOTE(@yuyoyuppe): disabled to drop elevation requirement bool nonElevated = Common.RunWithNoAdminRight && false; - if (nonElevated && Setting.Values.OneWayControlMode && package.Md.dwFlags != Common.WM_MOUSEMOVE) + if (nonElevated && Setting.Values.OneWayControlMode && package.Md.dwFlags != WM.WM_MOUSEMOVE) { if (!DragDrop.IsDropping) { - if (package.Md.dwFlags is Common.WM_LBUTTONDOWN or Common.WM_RBUTTONDOWN) + if (package.Md.dwFlags is WM.WM_LBUTTONDOWN or WM.WM_RBUTTONDOWN) { Helper.ShowOneWayModeMessage(); } } - else if (package.Md.dwFlags is Common.WM_LBUTTONUP or Common.WM_RBUTTONUP) + else if (package.Md.dwFlags is WM.WM_LBUTTONUP or WM.WM_RBUTTONUP) { DragDrop.IsDropping = false; } @@ -146,7 +146,7 @@ internal static class Receiver if (Math.Abs(package.Md.X) >= Event.MOVE_MOUSE_RELATIVE && Math.Abs(package.Md.Y) >= Event.MOVE_MOUSE_RELATIVE) { - if (package.Md.dwFlags == Common.WM_MOUSEMOVE) + if (package.Md.dwFlags == WM.WM_MOUSEMOVE) { InputSimulation.MoveMouseRelative( package.Md.X < 0 ? package.Md.X + Event.MOVE_MOUSE_RELATIVE : package.Md.X - Event.MOVE_MOUSE_RELATIVE, @@ -203,19 +203,19 @@ internal static class Receiver break; case PackageType.ExplorerDragDrop: - Common.PackageReceived.ExplorerDragDrop++; + Package.PackageReceived.ExplorerDragDrop++; DragDrop.DragDropStep03(package); break; case PackageType.Heartbeat: case PackageType.Heartbeat_ex: - Common.PackageReceived.Heartbeat++; + Package.PackageReceived.Heartbeat++; - Common.GeneratedKey = Common.GeneratedKey || package.Type == PackageType.Heartbeat_ex; + Encryption.GeneratedKey = Encryption.GeneratedKey || package.Type == PackageType.Heartbeat_ex; - if (Common.GeneratedKey) + if (Encryption.GeneratedKey) { - Setting.Values.MyKey = Common.MyKey; + Setting.Values.MyKey = Encryption.MyKey; Common.SendPackage(ID.ALL, PackageType.Heartbeat_ex_l2); } @@ -230,26 +230,26 @@ internal static class Receiver break; case PackageType.Heartbeat_ex_l2: - Common.GeneratedKey = true; - Setting.Values.MyKey = Common.MyKey; + Encryption.GeneratedKey = true; + Setting.Values.MyKey = Encryption.MyKey; Common.SendPackage(ID.ALL, PackageType.Heartbeat_ex_l3); break; case PackageType.Heartbeat_ex_l3: - Common.GeneratedKey = true; - Setting.Values.MyKey = Common.MyKey; + Encryption.GeneratedKey = true; + Setting.Values.MyKey = Encryption.MyKey; break; case PackageType.Awake: - Common.PackageReceived.Heartbeat++; + Package.PackageReceived.Heartbeat++; _ = MachineStuff.AddToMachinePool(package); Common.HumanBeingDetected(); break; case PackageType.Hello: - Common.PackageReceived.Hello++; + Package.PackageReceived.Hello++; Common.SendHeartBeat(); string newMachine = MachineStuff.AddToMachinePool(package); if (Setting.Values.MachineMatrixString == null) @@ -262,16 +262,16 @@ internal static class Receiver break; case PackageType.Hi: - Common.PackageReceived.Hello++; + Package.PackageReceived.Hello++; break; case PackageType.ByeBye: - Common.PackageReceived.ByeBye++; + Package.PackageReceived.ByeBye++; Common.ProcessByeByeMessage(package); break; case PackageType.Clipboard: - Common.PackageReceived.Clipboard++; + Package.PackageReceived.Clipboard++; if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop) { Clipboard.clipboardCopiedTime = Common.GetTick(); @@ -291,7 +291,7 @@ internal static class Receiver break; case PackageType.ClipboardCapture: - Common.PackageReceived.Clipboard++; + Package.PackageReceived.Clipboard++; if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop) { if (package.Des == Common.MachineID || package.Des == ID.ALL) @@ -304,7 +304,7 @@ internal static class Receiver break; case PackageType.CaptureScreenCommand: - Common.PackageReceived.Clipboard++; + Package.PackageReceived.Clipboard++; if (package.Des == Common.MachineID || package.Des == ID.ALL) { Common.SendImage(package.Src, Common.CaptureScreen()); @@ -313,7 +313,7 @@ internal static class Receiver break; case PackageType.ClipboardAsk: - Common.PackageReceived.ClipboardAsk++; + Package.PackageReceived.ClipboardAsk++; if (package.Des == Common.MachineID) { @@ -344,17 +344,17 @@ internal static class Receiver break; case PackageType.ClipboardDragDrop: - Common.PackageReceived.ClipboardDragDrop++; + Package.PackageReceived.ClipboardDragDrop++; DragDrop.DragDropStep08(package); break; case PackageType.ClipboardDragDropOperation: - Common.PackageReceived.ClipboardDragDrop++; + Package.PackageReceived.ClipboardDragDrop++; DragDrop.DragDropStep08_2(package); break; case PackageType.ClipboardDragDropEnd: - Common.PackageReceived.ClipboardDragDropEnd++; + Package.PackageReceived.ClipboardDragDropEnd++; DragDrop.DragDropStep12(); break; @@ -363,11 +363,11 @@ internal static class Receiver Clipboard.clipboardCopiedTime = 0; if (package.Type == PackageType.ClipboardImage) { - Common.PackageReceived.ClipboardImage++; + Package.PackageReceived.ClipboardImage++; } else { - Common.PackageReceived.ClipboardText++; + Package.PackageReceived.ClipboardText++; } if (tcp != null) @@ -390,7 +390,7 @@ internal static class Receiver default: if ((package.Type & PackageType.Matrix) == PackageType.Matrix) { - Common.PackageReceived.Matrix++; + Package.PackageReceived.Matrix++; MachineStuff.UpdateMachineMatrix(package); break; } diff --git a/src/modules/MouseWithoutBorders/App/Core/ShutdownWithPowerToys.cs b/src/modules/MouseWithoutBorders/App/Core/ShutdownWithPowerToys.cs new file mode 100644 index 0000000000..a221aa1733 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/ShutdownWithPowerToys.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +using ManagedCommon; +using Microsoft.PowerToys.Telemetry; + +namespace MouseWithoutBorders.Core; + +internal static class ShutdownWithPowerToys +{ + internal static void WaitForPowerToysRunner(ETWTrace etwTrace) + { + try + { + RunnerHelper.WaitForPowerToysRunnerExitFallback(() => + { + etwTrace?.Dispose(); + Common.MainForm.Quit(true, false); + }); + } + catch (Exception e) + { + Logger.Log(e); + } + } +} diff --git a/src/modules/MouseWithoutBorders/App/Core/VK.cs b/src/modules/MouseWithoutBorders/App/Core/VK.cs new file mode 100644 index 0000000000..239692abed --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/VK.cs @@ -0,0 +1,88 @@ +// 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. + +// +// Virtual key constants. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal enum VK : ushort +{ + CAPITAL = 0x14, + NUMLOCK = 0x90, + SHIFT = 0x10, + CONTROL = 0x11, + MENU = 0x12, + ESCAPE = 0x1B, + BACK = 0x08, + TAB = 0x09, + RETURN = 0x0D, + PRIOR = 0x21, + NEXT = 0x22, + END = 0x23, + HOME = 0x24, + LEFT = 0x25, + UP = 0x26, + RIGHT = 0x27, + DOWN = 0x28, + SELECT = 0x29, + PRINT = 0x2A, + EXECUTE = 0x2B, + SNAPSHOT = 0x2C, + INSERT = 0x2D, + DELETE = 0x2E, + HELP = 0x2F, + NUMPAD0 = 0x60, + NUMPAD1 = 0x61, + NUMPAD2 = 0x62, + NUMPAD3 = 0x63, + NUMPAD4 = 0x64, + NUMPAD5 = 0x65, + NUMPAD6 = 0x66, + NUMPAD7 = 0x67, + NUMPAD8 = 0x68, + NUMPAD9 = 0x69, + MULTIPLY = 0x6A, + ADD = 0x6B, + SEPARATOR = 0x6C, + SUBTRACT = 0x6D, + DECIMAL = 0x6E, + DIVIDE = 0x6F, + F1 = 0x70, + F2 = 0x71, + F3 = 0x72, + F4 = 0x73, + F5 = 0x74, + F6 = 0x75, + F7 = 0x76, + F8 = 0x77, + F9 = 0x78, + F10 = 0x79, + F11 = 0x7A, + F12 = 0x7B, + OEM_1 = 0xBA, + OEM_PLUS = 0xBB, + OEM_COMMA = 0xBC, + OEM_MINUS = 0xBD, + OEM_PERIOD = 0xBE, + OEM_2 = 0xBF, + OEM_3 = 0xC0, + MEDIA_NEXT_TRACK = 0xB0, + MEDIA_PREV_TRACK = 0xB1, + MEDIA_STOP = 0xB2, + MEDIA_PLAY_PAUSE = 0xB3, + LWIN = 0x5B, + RWIN = 0x5C, + LSHIFT = 0xA0, + RSHIFT = 0xA1, + LCONTROL = 0xA2, + RCONTROL = 0xA3, + LMENU = 0xA4, + RMENU = 0xA5, +} diff --git a/src/modules/MouseWithoutBorders/App/Core/WM.cs b/src/modules/MouseWithoutBorders/App/Core/WM.cs new file mode 100644 index 0000000000..e93897e93b --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/WM.cs @@ -0,0 +1,55 @@ +// 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; + +// +// Virtual key constants. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +internal partial class WM +{ + internal const ushort KEYEVENTF_KEYDOWN = 0x0001; + internal const ushort KEYEVENTF_KEYUP = 0x0002; + + internal const int WH_MOUSE = 7; + internal const int WH_KEYBOARD = 2; + internal const int WH_MOUSE_LL = 14; + internal const int WH_KEYBOARD_LL = 13; + + internal const int WM_MOUSEMOVE = 0x200; + internal const int WM_LBUTTONDOWN = 0x201; + internal const int WM_RBUTTONDOWN = 0x204; + internal const int WM_MBUTTONDOWN = 0x207; + internal const int WM_XBUTTONDOWN = 0x20B; + internal const int WM_LBUTTONUP = 0x202; + internal const int WM_RBUTTONUP = 0x205; + internal const int WM_MBUTTONUP = 0x208; + internal const int WM_XBUTTONUP = 0x20C; + internal const int WM_LBUTTONDBLCLK = 0x203; + internal const int WM_RBUTTONDBLCLK = 0x206; + internal const int WM_MBUTTONDBLCLK = 0x209; + internal const int WM_MOUSEWHEEL = 0x020A; + internal const int WM_MOUSEHWHEEL = 0x020E; + + internal const int WM_KEYDOWN = 0x100; + internal const int WM_KEYUP = 0x101; + internal const int WM_SYSKEYDOWN = 0x104; + internal const int WM_SYSKEYUP = 0x105; + + [Flags] + internal enum LLKHF + { + EXTENDED = 0x01, + INJECTED = 0x10, + ALTDOWN = 0x20, + UP = 0x80, + } +} diff --git a/src/modules/MouseWithoutBorders/App/Core/WinAPI.cs b/src/modules/MouseWithoutBorders/App/Core/WinAPI.cs new file mode 100644 index 0000000000..4d14dcb973 --- /dev/null +++ b/src/modules/MouseWithoutBorders/App/Core/WinAPI.cs @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Forms; + +using MouseWithoutBorders.Class; + +// +// Screen/Desktop helper functions. +// +// +// 2008 created by Truong Do (ductdo). +// 2009-... modified by Truong Do (TruongDo). +// 2023- Included in PowerToys. +// +namespace MouseWithoutBorders.Core; + +// Desktops, and GetScreenConfig routines +internal static class WinAPI +{ + private static MyRectangle newDesktopBounds; + private static MyRectangle newPrimaryScreenBounds; + private static string activeDesktop; + + private static string ActiveDesktop => WinAPI.activeDesktop; + + internal static void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) + { + GetScreenConfig(); + } + + internal static readonly List SensitivePoints = new(); + + private static bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref NativeMethods.RECT lprcMonitor, IntPtr dwData) + { + // lprcMonitor is wrong!!! => using GetMonitorInfo(...) + // Log(String.Format( CultureInfo.CurrentCulture,"MONITOR: l{0}, t{1}, r{2}, b{3}", lprcMonitor.Left, lprcMonitor.Top, lprcMonitor.Right, lprcMonitor.Bottom)); + NativeMethods.MonitorInfoEx mi = default; + mi.cbSize = Marshal.SizeOf(mi); + _ = NativeMethods.GetMonitorInfo(hMonitor, ref mi); + + try + { + // For logging only + _ = NativeMethods.GetDpiForMonitor(hMonitor, 0, out uint dpiX, out uint dpiY); + Logger.Log(string.Format(CultureInfo.CurrentCulture, "MONITOR: ({0}, {1}, {2}, {3}). DPI: ({4}, {5})", mi.rcMonitor.Left, mi.rcMonitor.Top, mi.rcMonitor.Right, mi.rcMonitor.Bottom, dpiX, dpiY)); + } + catch (DllNotFoundException) + { + Logger.Log("GetDpiForMonitor is unsupported in Windows 7 and lower."); + } + catch (EntryPointNotFoundException) + { + Logger.Log("GetDpiForMonitor is unsupported in Windows 7 and lower."); + } + catch (Exception e) + { + Logger.Log(e); + } + + if (mi.rcMonitor.Left == 0 && mi.rcMonitor.Top == 0 && mi.rcMonitor.Right != 0 && mi.rcMonitor.Bottom != 0) + { + // Primary screen + _ = Interlocked.Exchange(ref Common.screenWidth, mi.rcMonitor.Right - mi.rcMonitor.Left); + _ = Interlocked.Exchange(ref Common.screenHeight, mi.rcMonitor.Bottom - mi.rcMonitor.Top); + + newPrimaryScreenBounds.Left = mi.rcMonitor.Left; + newPrimaryScreenBounds.Top = mi.rcMonitor.Top; + newPrimaryScreenBounds.Right = mi.rcMonitor.Right; + newPrimaryScreenBounds.Bottom = mi.rcMonitor.Bottom; + } + else + { + if (mi.rcMonitor.Left < newDesktopBounds.Left) + { + newDesktopBounds.Left = mi.rcMonitor.Left; + } + + if (mi.rcMonitor.Top < newDesktopBounds.Top) + { + newDesktopBounds.Top = mi.rcMonitor.Top; + } + + if (mi.rcMonitor.Right > newDesktopBounds.Right) + { + newDesktopBounds.Right = mi.rcMonitor.Right; + } + + if (mi.rcMonitor.Bottom > newDesktopBounds.Bottom) + { + newDesktopBounds.Bottom = mi.rcMonitor.Bottom; + } + } + + lock (SensitivePoints) + { + SensitivePoints.Add(new Point(mi.rcMonitor.Left, mi.rcMonitor.Top)); + SensitivePoints.Add(new Point(mi.rcMonitor.Right, mi.rcMonitor.Top)); + SensitivePoints.Add(new Point(mi.rcMonitor.Right, mi.rcMonitor.Bottom)); + SensitivePoints.Add(new Point(mi.rcMonitor.Left, mi.rcMonitor.Bottom)); + } + + return true; + } + + internal static void GetScreenConfig() + { + try + { + Logger.LogDebug("==================== GetScreenConfig started"); + newDesktopBounds = new MyRectangle(); + newPrimaryScreenBounds = new MyRectangle(); + newDesktopBounds.Left = newPrimaryScreenBounds.Left = Screen.PrimaryScreen.Bounds.Left; + newDesktopBounds.Top = newPrimaryScreenBounds.Top = Screen.PrimaryScreen.Bounds.Top; + newDesktopBounds.Right = newPrimaryScreenBounds.Right = Screen.PrimaryScreen.Bounds.Right; + newDesktopBounds.Bottom = newPrimaryScreenBounds.Bottom = Screen.PrimaryScreen.Bounds.Bottom; + + Logger.Log(string.Format( + CultureInfo.CurrentCulture, + "logon = {0} PrimaryScreenBounds = {1},{2},{3},{4} desktopBounds = {5},{6},{7},{8}", + Common.RunOnLogonDesktop, + WinAPI.newPrimaryScreenBounds.Left, + WinAPI.newPrimaryScreenBounds.Top, + WinAPI.newPrimaryScreenBounds.Right, + WinAPI.newPrimaryScreenBounds.Bottom, + WinAPI.newDesktopBounds.Left, + WinAPI.newDesktopBounds.Top, + WinAPI.newDesktopBounds.Right, + WinAPI.newDesktopBounds.Bottom)); + +#if USE_MANAGED_ROUTINES + // Managed routines do not work well when running on secure desktop:( + screenWidth = Screen.PrimaryScreen.Bounds.Width; + screenHeight = Screen.PrimaryScreen.Bounds.Height; + screenCount = Screen.AllScreens.Length; + for (int i = 0; i < Screen.AllScreens.Length; i++) + { + if (Screen.AllScreens[i].Bounds.Left < desktopBounds.Left) desktopBounds.Left = Screen.AllScreens[i].Bounds.Left; + if (Screen.AllScreens[i].Bounds.Top < desktopBounds.Top) desktopBounds.Top = Screen.AllScreens[i].Bounds.Top; + if (Screen.AllScreens[i].Bounds.Right > desktopBounds.Right) desktopBounds.Right = Screen.AllScreens[i].Bounds.Right; + if (Screen.AllScreens[i].Bounds.Bottom > desktopBounds.Bottom) desktopBounds.Bottom = Screen.AllScreens[i].Bounds.Bottom; + } +#else + lock (SensitivePoints) + { + SensitivePoints.Clear(); + } + + NativeMethods.EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, MonitorEnumProc, IntPtr.Zero); + + // 1000 calls to EnumDisplayMonitors cost a dozen of milliseconds +#endif + Interlocked.Exchange(ref MachineStuff.desktopBounds, newDesktopBounds); + Interlocked.Exchange(ref MachineStuff.primaryScreenBounds, newPrimaryScreenBounds); + + Logger.Log(string.Format( + CultureInfo.CurrentCulture, + "logon = {0} PrimaryScreenBounds = {1},{2},{3},{4} desktopBounds = {5},{6},{7},{8}", + Common.RunOnLogonDesktop, + MachineStuff.PrimaryScreenBounds.Left, + MachineStuff.PrimaryScreenBounds.Top, + MachineStuff.PrimaryScreenBounds.Right, + MachineStuff.PrimaryScreenBounds.Bottom, + MachineStuff.DesktopBounds.Left, + MachineStuff.DesktopBounds.Top, + MachineStuff.DesktopBounds.Right, + MachineStuff.DesktopBounds.Bottom)); + + Logger.Log("==================== GetScreenConfig ended"); + } + catch (Exception e) + { + Logger.Log(e); + } + } + +#if USING_SCREEN_SAVER_ROUTINES + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int PostMessage(IntPtr hWnd, int wMsg, int wParam, int lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern IntPtr OpenDesktop(string hDesktop, int Flags, bool Inherit, UInt32 DesiredAccess); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool CloseDesktop(IntPtr hDesktop); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool EnumDesktopWindows( IntPtr hDesktop, EnumDesktopWindowsProc callback, IntPtr lParam); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern bool SystemParametersInfo(int uAction, int uParam, ref int pvParam, int flags); + + private delegate bool EnumDesktopWindowsProc(IntPtr hDesktop, IntPtr lParam); + private const int WM_CLOSE = 16; + private const int SPI_GETSCREENSAVERRUNNING = 114; + + internal static bool IsScreenSaverRunning() + { + int isRunning = 0; + SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0,ref isRunning, 0); + return (isRunning != 0); + } + + internal static void CloseScreenSaver() + { + IntPtr hDesktop = OpenDesktop("Screen-saver", 0, false, DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS); + if (hDesktop != IntPtr.Zero) + { + LogDebug("Closing screen saver..."); + EnumDesktopWindows(hDesktop, new EnumDesktopWindowsProc(CloseScreenSaverFunc), IntPtr.Zero); + CloseDesktop(hDesktop); + } + } + + private static bool CloseScreenSaverFunc(IntPtr hWnd, IntPtr lParam) + { + if (IsWindowVisible(hWnd)) + { + LogDebug("Posting WM_CLOSE to " + hWnd.ToString(CultureInfo.InvariantCulture)); + PostMessage(hWnd, WM_CLOSE, 0, 0); + } + return true; + } +#endif + + internal static string GetMyDesktop() + { + byte[] arThreadDesktop = new byte[256]; + IntPtr hD = NativeMethods.GetThreadDesktop(NativeMethods.GetCurrentThreadId()); + if (hD != IntPtr.Zero) + { + _ = NativeMethods.GetUserObjectInformation(hD, NativeMethods.UOI_NAME, arThreadDesktop, arThreadDesktop.Length, out _); + return Common.GetString(arThreadDesktop).Replace("\0", string.Empty); + } + + return string.Empty; + } + + internal static string GetInputDesktop() + { + byte[] arInputDesktop = new byte[256]; + IntPtr hD = NativeMethods.OpenInputDesktop(0, false, NativeMethods.DESKTOP_READOBJECTS); + if (hD != IntPtr.Zero) + { + _ = NativeMethods.GetUserObjectInformation(hD, NativeMethods.UOI_NAME, arInputDesktop, arInputDesktop.Length, out _); + return Common.GetString(arInputDesktop).Replace("\0", string.Empty); + } + + return string.Empty; + } + + private static void StartMMService(string desktopToRunMouseWithoutBordersOn) + { + if (!Common.RunWithNoAdminRight) + { + Logger.LogDebug("*** Starting on active Desktop: " + desktopToRunMouseWithoutBordersOn); + Service.StartMouseWithoutBordersService(desktopToRunMouseWithoutBordersOn); + } + } + + internal static void CheckForDesktopSwitchEvent(bool cleanupIfExit) + { + try + { + if (!IsMyDesktopActive() || Common.CurrentProcess.SessionId != NativeMethods.WTSGetActiveConsoleSessionId()) + { + Helper.RunDDHelper(true); + int waitCount = 20; + + while (NativeMethods.WTSGetActiveConsoleSessionId() == 0xFFFFFFFF && waitCount > 0) + { + waitCount--; + Logger.LogDebug("The session is detached/attached."); + Thread.Sleep(500); + } + + string myDesktop = GetMyDesktop(); + activeDesktop = GetInputDesktop(); + + Logger.LogDebug("*** Active Desktop = " + activeDesktop); + Logger.LogDebug("*** My Desktop = " + myDesktop); + + if (myDesktop.Equals(activeDesktop, StringComparison.OrdinalIgnoreCase)) + { + Logger.LogDebug("*** Active Desktop == My Desktop (TS session)"); + } + + if (!activeDesktop.Equals("winlogon", StringComparison.OrdinalIgnoreCase) && + !activeDesktop.Equals("default", StringComparison.OrdinalIgnoreCase) && + !activeDesktop.Equals("disconnect", StringComparison.OrdinalIgnoreCase)) + { + try + { + StartMMService(activeDesktop); + } + catch (Exception e) + { + Logger.Log($"{nameof(CheckForDesktopSwitchEvent)}: {e}"); + } + } + else + { + if (!myDesktop.Equals(activeDesktop, StringComparison.OrdinalIgnoreCase)) + { + Logger.Log("*** Active Desktop <> My Desktop"); + } + + uint sid = NativeMethods.WTSGetActiveConsoleSessionId(); + + if (Process.GetProcessesByName(Common.BinaryName).Any(p => (uint)p.SessionId == sid)) + { + Logger.Log("Found MouseWithoutBorders on the active session!"); + } + else + { + Logger.Log("MouseWithoutBorders not found on the active session!"); + StartMMService(null); + } + } + + if (!myDesktop.Equals("winlogon", StringComparison.OrdinalIgnoreCase) && + !myDesktop.Equals("default", StringComparison.OrdinalIgnoreCase)) + { + Logger.LogDebug("*** Desktop inactive, exiting: " + myDesktop); + Setting.Values.LastX = Common.JUST_GOT_BACK_FROM_SCREEN_SAVER; + if (cleanupIfExit) + { + InitAndCleanup.Cleanup(); + } + + Process.GetCurrentProcess().KillProcess(); + } + } + } + catch (Exception e) + { + Logger.Log(e); + } + } + + private static Point p; + + internal static bool IsMyDesktopActive() + { + return NativeMethods.GetCursorPos(ref p); + } +} diff --git a/src/modules/MouseWithoutBorders/App/Form/Settings/SettingsFormPage.cs b/src/modules/MouseWithoutBorders/App/Form/Settings/SettingsFormPage.cs index 39574ac8fe..81c04982a9 100644 --- a/src/modules/MouseWithoutBorders/App/Form/Settings/SettingsFormPage.cs +++ b/src/modules/MouseWithoutBorders/App/Form/Settings/SettingsFormPage.cs @@ -42,7 +42,7 @@ namespace MouseWithoutBorders protected string GetSecureKey() { - return Common.MyKey; + return Encryption.MyKey; } private void BackButton_Click(object sender, EventArgs e) diff --git a/src/modules/MouseWithoutBorders/App/Form/Settings/SetupPage2a.cs b/src/modules/MouseWithoutBorders/App/Form/Settings/SetupPage2a.cs index c86df58143..5fcee5dc54 100644 --- a/src/modules/MouseWithoutBorders/App/Form/Settings/SetupPage2a.cs +++ b/src/modules/MouseWithoutBorders/App/Form/Settings/SetupPage2a.cs @@ -89,8 +89,8 @@ namespace MouseWithoutBorders { if (GetSecureKey() != SecurityCodeField.Text) { - Common.MyKey = Regex.Replace(SecurityCodeField.Text, @"\s+", string.Empty); - SecurityCode = Common.MyKey; + Encryption.MyKey = Regex.Replace(SecurityCodeField.Text, @"\s+", string.Empty); + SecurityCode = Encryption.MyKey; } MachineStuff.MachineMatrix = new string[MachineStuff.MAX_MACHINE] { ComputerNameField.Text.Trim().ToUpper(CultureInfo.CurrentCulture), Common.MachineName.Trim(), string.Empty, string.Empty }; diff --git a/src/modules/MouseWithoutBorders/App/Form/frmMatrix.cs b/src/modules/MouseWithoutBorders/App/Form/frmMatrix.cs index 66301c52cb..703ad8ef91 100644 --- a/src/modules/MouseWithoutBorders/App/Form/frmMatrix.cs +++ b/src/modules/MouseWithoutBorders/App/Form/frmMatrix.cs @@ -135,7 +135,7 @@ namespace MouseWithoutBorders internal void UpdateKeyTextBox() { _ = Helper.GetUserName(); - textBoxEnc.Text = Common.MyKey; + textBoxEnc.Text = Encryption.MyKey; } private void InitAll() @@ -505,19 +505,19 @@ namespace MouseWithoutBorders private bool UpdateKey(string newKey) { - if (!Common.IsKeyValid(newKey, out string rv)) + if (!Encryption.IsKeyValid(newKey, out string rv)) { ShowKeyErrorMsg(rv); return false; } - if (!newKey.Equals(Common.MyKey, StringComparison.OrdinalIgnoreCase)) + if (!newKey.Equals(Encryption.MyKey, StringComparison.OrdinalIgnoreCase)) { - Common.MyKey = newKey; - Common.GeneratedKey = false; + Encryption.MyKey = newKey; + Encryption.GeneratedKey = false; } - Common.MagicNumber = Common.Get24BitHash(Common.MyKey); + Encryption.MagicNumber = Encryption.Get24BitHash(Encryption.MyKey); return true; } @@ -1116,10 +1116,10 @@ namespace MouseWithoutBorders if (MessageBox.Show(message, Application.ProductName, MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2) == DialogResult.Yes) { - Setting.Values.MyKey = Common.MyKey = Common.CreateRandomKey(); - textBoxEnc.Text = Common.MyKey; + Setting.Values.MyKey = Encryption.MyKey = Encryption.CreateRandomKey(); + textBoxEnc.Text = Encryption.MyKey; checkBoxShowKey.Checked = true; - Common.GeneratedKey = true; + Encryption.GeneratedKey = true; ButtonOK_Click(null, null); Common.ShowToolTip("New security key was generated, update other machines to the same key.", 10000, ToolTipIcon.Info, false); } diff --git a/src/modules/MouseWithoutBorders/App/Form/frmScreen.cs b/src/modules/MouseWithoutBorders/App/Form/frmScreen.cs index 1ab0ce8cc7..ca123ec850 100644 --- a/src/modules/MouseWithoutBorders/App/Form/frmScreen.cs +++ b/src/modules/MouseWithoutBorders/App/Form/frmScreen.cs @@ -318,7 +318,7 @@ namespace MouseWithoutBorders try { - if (!Common.IsMyDesktopActive() || Common.CurrentProcess.SessionId != NativeMethods.WTSGetActiveConsoleSessionId()) + if (!WinAPI.IsMyDesktopActive() || Common.CurrentProcess.SessionId != NativeMethods.WTSGetActiveConsoleSessionId()) { myDesktopNotActive = true; @@ -348,7 +348,7 @@ namespace MouseWithoutBorders Common.Hook?.ResetLastSwitchKeys(); }); - Common.CheckForDesktopSwitchEvent(true); + WinAPI.CheckForDesktopSwitchEvent(true); } } else @@ -369,21 +369,21 @@ namespace MouseWithoutBorders if (myDesktopNotActive) { myDesktopNotActive = false; - Common.MyKey = Setting.Values.MyKey; + Encryption.MyKey = Setting.Values.MyKey; } MachineStuff.UpdateMachinePoolStringSetting(); - if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && (Setting.Values.FirstRun || Common.KeyCorrupted)) + if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && (Setting.Values.FirstRun || Encryption.KeyCorrupted)) { if (!shownSetupFormOneTime) { shownSetupFormOneTime = true; MachineStuff.ShowMachineMatrix(); - if (Common.KeyCorrupted && !Setting.Values.FirstRun) + if (Encryption.KeyCorrupted && !Setting.Values.FirstRun) { - Common.KeyCorrupted = false; + Encryption.KeyCorrupted = false; string msg = "The security key is corrupted for some reason, please re-setup."; MessageBox.Show(msg, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Warning); } @@ -490,9 +490,9 @@ namespace MouseWithoutBorders if (count == 600) { - if (!Common.GeneratedKey) + if (!Encryption.GeneratedKey) { - Common.MyKey = Setting.Values.MyKey; + Encryption.MyKey = Setting.Values.MyKey; if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop) { @@ -505,7 +505,7 @@ namespace MouseWithoutBorders Common.ShowToolTip("The security key must be auto generated in one of the machines.", 10000); } } - else if (!Common.KeyCorrupted && !Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && !Setting.Values.FirstRun && Common.AtLeastOneSocketConnected()) + else if (!Encryption.KeyCorrupted && !Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && !Setting.Values.FirstRun && Common.AtLeastOneSocketConnected()) { int myKeyDaysToExpire = Setting.Values.MyKeyDaysToExpire; @@ -531,7 +531,7 @@ namespace MouseWithoutBorders #if SHOW_ON_WINLOGON // if (Common.RunOnLogonDesktop) ShowMouseWithoutBordersUiOnWinLogonDesktop(false); #endif - Common.CheckForDesktopSwitchEvent(true); + WinAPI.CheckForDesktopSwitchEvent(true); MachineStuff.UpdateClientSockets("helperTimer_Tick"); // Sockets may be closed by the remote host when both machines switch desktop at the same time. } @@ -582,7 +582,7 @@ namespace MouseWithoutBorders int rv = 0; - if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && Common.IsMyDesktopActive() && (rv = Helper.SendMessageToHelper(0x400, IntPtr.Zero, IntPtr.Zero)) <= 0) + if (!Common.RunOnLogonDesktop && !Common.RunOnScrSaverDesktop && WinAPI.IsMyDesktopActive() && (rv = Helper.SendMessageToHelper(0x400, IntPtr.Zero, IntPtr.Zero)) <= 0) { Logger.TelemetryLogTrace($"{Helper.HELPER_FORM_TEXT} not found: {rv}", SeverityLevel.Warning); } diff --git a/src/modules/MouseWithoutBorders/MouseWithoutBorders.UnitTests/Core/Logger.PrivateDump.expected.txt b/src/modules/MouseWithoutBorders/MouseWithoutBorders.UnitTests/Core/Logger.PrivateDump.expected.txt index 1bbd8ba49c..34a83830cd 100644 --- a/src/modules/MouseWithoutBorders/MouseWithoutBorders.UnitTests/Core/Logger.PrivateDump.expected.txt +++ b/src/modules/MouseWithoutBorders/MouseWithoutBorders.UnitTests/Core/Logger.PrivateDump.expected.txt @@ -46,79 +46,6 @@ avgSendTime = 0 maxSendTime = 0 totalSendCount = 0 totalSendTime = 0 -magicNumber = 0 -ran = System.Random ---_impl = System.Random+XoshiroImpl -----_s0 = ???????????? -----_s1 = ???????????? -----_s2 = ???????????? -----_s3 = ???????????? ---k__BackingField = System.Random+ThreadSafeRandom -InitialIV = ???????????? -k__BackingField = False -k__BackingField = False -LegalKeyDictionary = Concurrent.ConcurrentDictionary`2[System.String,System.Byte[]] ---_tables = Concurrent.ConcurrentDictionary`2+Tables[System.String,System.Byte[]] -----_comparer = Generic.NonRandomizedStringEqualityComparer+OrdinalIgnoreCaseComparer -----_buckets = Concurrent.ConcurrentDictionary`2+VolatileNode[System.String,System.Byte[]][] -------System.Collections.Concurrent.ConcurrentDictionary`2+VolatileNode[System.String,System.Byte[]][] = Concurrent.ConcurrentDictionary`2+VolatileNode[System.String,System.Byte[]][]: N/A -----_fastModBucketsMultiplier = 498560650640798693 -----_locks = O[] -------System.Object[] = O[]: N/A -----_countPerLock = 32[] -------[0] = 0 -------[1] = 0 -------[2] = 0 -------[3] = 0 -------[4] = 0 -------[5] = 0 -------[6] = 0 -------[7] = 0 ---_budget = ???????????? ---_growLockArray = True ---_comparerIsDefaultForClasses = False -PackageSent = MouseWithoutBorders.PackageMonitor ---Keyboard = 0 ---Mouse = 0 ---Heartbeat = 0 ---ByeBye = 0 ---Hello = 0 ---Matrix = 0 ---ClipboardText = 0 ---ClipboardImage = 0 ---Clipboard = 0 ---ClipboardDragDrop = 0 ---ClipboardDragDropEnd = 0 ---ClipboardAsk = 0 ---ExplorerDragDrop = 0 ---Nil = 0 -PackageReceived = MouseWithoutBorders.PackageMonitor ---Keyboard = 0 ---Mouse = 0 ---Heartbeat = 0 ---ByeBye = 0 ---Hello = 0 ---Matrix = 0 ---ClipboardText = 0 ---ClipboardImage = 0 ---Clipboard = 0 ---ClipboardDragDrop = 0 ---ClipboardDragDropEnd = 0 ---ClipboardAsk = 0 ---ExplorerDragDrop = 0 ---Nil = 0 -PackageID = 0 -SensitivePoints = Generic.List`1[Point] ---_items = Point[] -----System.Drawing.Point[] = Point[]: N/A ---_size = 0 ---_version = 0 ---s_emptyArray = Point[] -----System.Drawing.Point[] = Point[]: N/A -p = {X=0,Y=0} ---x = 0 ---y = 0 ---Empty = {X=0,Y=0} k__BackingField = False TOGGLE_ICONS_SIZE = 4 ICON_ONE = 0 @@ -128,34 +55,6 @@ ICON_BIG_CLIPBOARD = 3 ICON_ERROR = 4 JUST_GOT_BACK_FROM_SCREEN_SAVER = 9999 NETWORK_STREAM_BUF_SIZE = 1048576 -SymAlBlockSize = 16 -PW_LENGTH = 16 -PACKAGE_SIZE = 32 -PACKAGE_SIZE_EX = 64 -WP_PACKAGE_SIZE = 6 -KEYEVENTF_KEYDOWN = 1 -KEYEVENTF_KEYUP = 2 -WH_MOUSE = 7 -WH_KEYBOARD = 2 -WH_MOUSE_LL = 14 -WH_KEYBOARD_LL = 13 -WM_MOUSEMOVE = 512 -WM_LBUTTONDOWN = 513 -WM_RBUTTONDOWN = 516 -WM_MBUTTONDOWN = 519 -WM_XBUTTONDOWN = 523 -WM_LBUTTONUP = 514 -WM_RBUTTONUP = 517 -WM_MBUTTONUP = 520 -WM_XBUTTONUP = 524 -WM_LBUTTONDBLCLK = 515 -WM_RBUTTONDBLCLK = 518 -WM_MBUTTONDBLCLK = 521 -WM_MOUSEWHEEL = 522 -WM_KEYDOWN = 256 -WM_KEYUP = 257 -WM_SYSKEYDOWN = 260 -WM_SYSKEYUP = 261 [Clipboard] =============== Comma = System.Char[] @@ -193,16 +92,51 @@ dragDropStep05ExCalledByIpc = 0 isDropping = False dragMachine = NONE k__BackingField = False +[Encryption] +=============== +magicNumber = 0 +ran = System.Random +--_impl = System.Random+XoshiroImpl +----_s0 = ???????????? +----_s1 = ???????????? +----_s2 = ???????????? +----_s3 = ???????????? +--k__BackingField = System.Random+ThreadSafeRandom +InitialIV = ???????????? +k__BackingField = False +k__BackingField = False +LegalKeyDictionary = Concurrent.ConcurrentDictionary`2[System.String,System.Byte[]] +--_tables = Concurrent.ConcurrentDictionary`2+Tables[System.String,System.Byte[]] +----_comparer = Generic.NonRandomizedStringEqualityComparer+OrdinalIgnoreCaseComparer +----_buckets = Concurrent.ConcurrentDictionary`2+VolatileNode[System.String,System.Byte[]][] +------System.Collections.Concurrent.ConcurrentDictionary`2+VolatileNode[System.String,System.Byte[]][] = Concurrent.ConcurrentDictionary`2+VolatileNode[System.String,System.Byte[]][]: N/A +----_fastModBucketsMultiplier = 498560650640798693 +----_locks = O[] +------System.Object[] = O[]: N/A +----_countPerLock = 32[] +------[0] = 0 +------[1] = 0 +------[2] = 0 +------[3] = 0 +------[4] = 0 +------[5] = 0 +------[6] = 0 +------[7] = 0 +--_budget = ???????????? +--_growLockArray = True +--_comparerIsDefaultForClasses = False +SymAlBlockSize = 16 +PW_LENGTH = 16 [Event] =============== -KeybdPackage = MouseWithoutBorders.DATA +KeybdPackage = MouseWithoutBorders.Core.DATA --Type = 0 --Id = 0 --Src = NONE --Des = NONE --DateTime = 0 ---Kd = MouseWithoutBorders.KEYBDDATA ---Md = MouseWithoutBorders.MOUSEDATA +--Kd = MouseWithoutBorders.Core.KEYBDDATA +--Md = MouseWithoutBorders.Core.MOUSEDATA --Machine1 = NONE --Machine2 = NONE --Machine3 = NONE @@ -212,14 +146,14 @@ KeybdPackage = MouseWithoutBorders.DATA --machineNameP2 = 0 --machineNameP3 = 0 --machineNameP4 = 0 -MousePackage = MouseWithoutBorders.DATA +MousePackage = MouseWithoutBorders.Core.DATA --Type = 0 --Id = 0 --Src = NONE --Des = NONE --DateTime = 0 ---Kd = MouseWithoutBorders.KEYBDDATA ---Md = MouseWithoutBorders.MOUSEDATA +--Kd = MouseWithoutBorders.Core.KEYBDDATA +--Md = MouseWithoutBorders.Core.MOUSEDATA --Machine1 = NONE --Machine2 = NONE --Machine3 = NONE @@ -296,7 +230,7 @@ LogCounter = Concurrent.ConcurrentDictionary`2[System.String,32] allLogsIndex = 0 lastHour = 0 exceptionCount = 0 -lastPackageSent = MouseWithoutBorders.PackageMonitor +lastPackageSent = MouseWithoutBorders.Core.PackageMonitor --Keyboard = 0 --Mouse = 0 --Heartbeat = 0 @@ -311,7 +245,7 @@ lastPackageSent = MouseWithoutBorders.PackageMonitor --ClipboardAsk = 0 --ExplorerDragDrop = 0 --Nil = 0 -lastPackageReceived = MouseWithoutBorders.PackageMonitor +lastPackageReceived = MouseWithoutBorders.Core.PackageMonitor --Keyboard = 0 --Mouse = 0 --Heartbeat = 0 @@ -366,6 +300,42 @@ MAX_SOCKET = 8 HEARTBEAT_TIMEOUT = 1500000 SKIP_PIXELS = 1 JUMP_PIXELS = 2 +[Package] +=============== +PackageSent = MouseWithoutBorders.Core.PackageMonitor +--Keyboard = 0 +--Mouse = 0 +--Heartbeat = 0 +--ByeBye = 0 +--Hello = 0 +--Matrix = 0 +--ClipboardText = 0 +--ClipboardImage = 0 +--Clipboard = 0 +--ClipboardDragDrop = 0 +--ClipboardDragDropEnd = 0 +--ClipboardAsk = 0 +--ExplorerDragDrop = 0 +--Nil = 0 +PackageReceived = MouseWithoutBorders.Core.PackageMonitor +--Keyboard = 0 +--Mouse = 0 +--Heartbeat = 0 +--ByeBye = 0 +--Hello = 0 +--Matrix = 0 +--ClipboardText = 0 +--ClipboardImage = 0 +--Clipboard = 0 +--ClipboardDragDrop = 0 +--ClipboardDragDropEnd = 0 +--ClipboardAsk = 0 +--ExplorerDragDrop = 0 +--Nil = 0 +PackageID = 0 +PACKAGE_SIZE = 32 +PACKAGE_SIZE_EX = 64 +WP_PACKAGE_SIZE = 6 [Receiver] =============== QUEUE_SIZE = 50 @@ -436,3 +406,41 @@ lastStartServiceTime = ???????????? --MinValue = 01/01/0001 00:00:00 --MaxValue = 31/12/9999 23:59:59 --UnixEpoch = 01/01/1970 00:00:00 +[WinAPI] +=============== +SensitivePoints = Generic.List`1[Point] +--_items = Point[] +----System.Drawing.Point[] = Point[]: N/A +--_size = 0 +--_version = 0 +--s_emptyArray = Point[] +----System.Drawing.Point[] = Point[]: N/A +p = {X=0,Y=0} +--x = 0 +--y = 0 +--Empty = {X=0,Y=0} +[WM] +=============== +KEYEVENTF_KEYDOWN = 1 +KEYEVENTF_KEYUP = 2 +WH_MOUSE = 7 +WH_KEYBOARD = 2 +WH_MOUSE_LL = 14 +WH_KEYBOARD_LL = 13 +WM_MOUSEMOVE = 512 +WM_LBUTTONDOWN = 513 +WM_RBUTTONDOWN = 516 +WM_MBUTTONDOWN = 519 +WM_XBUTTONDOWN = 523 +WM_LBUTTONUP = 514 +WM_RBUTTONUP = 517 +WM_MBUTTONUP = 520 +WM_XBUTTONUP = 524 +WM_LBUTTONDBLCLK = 515 +WM_RBUTTONDBLCLK = 518 +WM_MBUTTONDBLCLK = 521 +WM_MOUSEWHEEL = 522 +WM_KEYDOWN = 256 +WM_KEYUP = 257 +WM_SYSKEYDOWN = 260 +WM_SYSKEYUP = 261 diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj index 16c6b4efbc..ea6f5ab7ba 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/NewPlus.ShellExtension.win10.vcxproj @@ -46,7 +46,7 @@ _DEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true Use - ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu + ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory) false stdcpplatest @@ -67,7 +67,7 @@ NDEBUG;NEWPLUSSHELLEXTENSIONWIN10_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) true Use - ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu + ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories);..\NewShellExtensionContextMenu;$(MSBuildThisFileDirectory) false stdcpplatest @@ -92,6 +92,7 @@ + @@ -99,7 +100,7 @@ - + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h index 5018653070..711063d0cb 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu.win10/pch.h @@ -3,6 +3,7 @@ #pragma once #define WIN32_LEAN_AND_MEAN +#define NOMINMAX #define NOMCX #define NOHELP #define NOCOMM diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp new file mode 100644 index 0000000000..945a516ca4 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.cpp @@ -0,0 +1,118 @@ +// 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. + +#include "pch.h" +#include "Helpers.h" +#include + +// Minimal subset of PowerRename Helpers used by NewPlus +// This is a copy from PowerRename main branch to avoid cross-module dependencies + +HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime) +{ + std::locale::global(std::locale("")); + HRESULT hr = E_INVALIDARG; + if (source && wcslen(source) > 0) + { + std::wstring res(source); + wchar_t replaceTerm[MAX_PATH] = { 0 }; + wchar_t formattedDate[MAX_PATH] = { 0 }; + + wchar_t localeName[LOCALE_NAME_MAX_LENGTH]; + if (GetUserDefaultLocaleName(localeName, LOCALE_NAME_MAX_LENGTH) == 0) + { + StringCchCopy(localeName, LOCALE_NAME_MAX_LENGTH, L"en_US"); + } + + int hour12 = (fileTime.wHour % 12); + if (hour12 == 0) + { + hour12 = 12; + } + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100)); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10)); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm); + + GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL); + formattedDate[0] = towupper(formattedDate[0]); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMMM"), replaceTerm); + + GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL); + formattedDate[0] = towupper(formattedDate[0]); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm); + + GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL); + formattedDate[0] = towupper(formattedDate[0]); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDDD"), replaceTerm); + + GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL); + formattedDate[0] = towupper(formattedDate[0]); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM"); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"am" : L"pm"); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm); + + hr = StringCchCopy(result, cchMax, res.c_str()); + } + + return hr; +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h new file mode 100644 index 0000000000..540478856e --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/Helpers.h @@ -0,0 +1,9 @@ +// 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. + +#pragma once + +// Minimal subset of PowerRename Helpers used by NewPlus +// This is a copy from PowerRename's main branch to avoid cross-module dependencies +HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime); diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj index 90058a503e..7dade586e7 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj @@ -114,6 +114,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv + @@ -131,7 +132,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv - + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h index 63f23c3e86..1d511f2afe 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/helpers_variables.h @@ -1,7 +1,7 @@ #pragma once #include -#include "..\..\powerrename\lib\Helpers.h" +#include "Helpers.h" #include "helpers_filesystem.h" #pragma comment(lib, "Pathcch.lib") diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h index 50f92562d2..02874ab8f3 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h @@ -302,9 +302,9 @@ namespace newplus::utilities POINT mouse_position; GetCursorPos(&mouse_position); mouse_position.x -= GetSystemMetrics(SM_CXMENUSIZE); - mouse_position.x = max(mouse_position.x, 20); + mouse_position.x = (std::max)(mouse_position.x, 20L); mouse_position.y -= GetSystemMetrics(SM_CXMENUSIZE)/2; - mouse_position.y = max(mouse_position.y, 20); + mouse_position.y = (std::max)(mouse_position.y, 20L); POINT position[] = { mouse_position }; folder_view->SelectAndPositionItems(1, shell_item_to_select_and_position, position, common_select_flags | SVSI_POSITIONITEM); } diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h index b766a837d5..13093e1d08 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h @@ -3,6 +3,7 @@ #pragma once #define WIN32_LEAN_AND_MEAN +#define NOMINMAX #define NOMCX #define NOHELP #define NOCOMM @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp index a7ddfe835f..a178e00195 100644 --- a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp @@ -60,8 +60,8 @@ std::wstring template_item::get_target_filename(const bool include_starting_digi std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const { - filename.erase(0, min(filename.find_first_not_of(L"0123456789"), filename.size())); - filename.erase(0, min(filename.find_first_not_of(L" ."), filename.size())); + filename.erase(0, std::min(filename.find_first_not_of(L"0123456789"), filename.size())); + filename.erase(0, std::min(filename.find_first_not_of(L" ."), filename.size())); return filename; } diff --git a/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs index 926729542a..18702eaaf6 100644 --- a/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs +++ b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs @@ -2,8 +2,10 @@ // 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.PowerToys.UITest; using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium.Interactions; using static Microsoft.PowerToys.UITest.UITestBase; namespace PowerOCR.UITests; @@ -19,41 +21,274 @@ public class PowerOCRTests : UITestBase [TestInitialize] public void TestInitialize() { - if (FindAll("Text Extractor").Count == 0) + if (FindAll(By.AccessibilityId("TextExtractorNavItem")).Count == 0) { - // Expand Advanced list-group if needed - Find("System Tools").Click(); + // Expand System Tools list-group if needed + Find(By.AccessibilityId("SystemToolsNavItem")).Click(); } - Find("Text Extractor").Click(); + Find(By.AccessibilityId("TextExtractorNavItem")).Click(); - Find("Enable Text Extractor").Toggle(true); + Find(By.AccessibilityId("EnableTextExtractorToggleSwitch")).Toggle(true); - SendKeys(Key.Win, Key.D); + // Reset activation shortcut to default (Win+Shift+T) + var shortcutControl = Find(By.AccessibilityId("TextExtractorActivationShortcut"), 5000); + if (shortcutControl != null) + { + shortcutControl.Click(); + Thread.Sleep(500); + + // Set default shortcut Win+Shift+T + SendKeys(Key.Win, Key.Shift, Key.T); + Thread.Sleep(1000); + + // Click Save to confirm + var saveButton = Find public partial class App : Application { + private readonly GlobalErrorHandler _globalErrorHandler = new(); + /// /// Gets the current instance in use. /// @@ -61,6 +64,10 @@ public partial class App : Application /// public App() { +#if !CMDPAL_DISABLE_GLOBAL_ERROR_HANDLER + _globalErrorHandler.Register(this); +#endif + Services = ConfigureServices(); this.InitializeComponent(); @@ -128,8 +135,9 @@ public partial class App : Application try { var winget = new WinGetExtensionCommandsProvider(); - var callback = allApps.LookupApp; - winget.SetAllLookup(callback); + winget.SetAllLookup( + query => allApps.LookupAppByPackageFamilyName(query, requireSingleMatch: true), + query => allApps.LookupAppByProductCode(query, requireSingleMatch: true)); services.AddSingleton(winget); } catch (Exception ex) @@ -145,6 +153,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Models services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg new file mode 100644 index 0000000000..ec3cb933d8 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.dark.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.light.svg b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.light.svg new file mode 100644 index 0000000000..9d74683716 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/StoreLogo.light.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg new file mode 100644 index 0000000000..0bf3ebbaff --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Assets/WinGetLogo.svg @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml new file mode 100644 index 0000000000..e354f0519f --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml.cs new file mode 100644 index 0000000000..1659f32d32 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/DevRibbon.xaml.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; + +namespace Microsoft.CmdPal.UI.Controls; + +internal sealed partial class DevRibbon : UserControl +{ + public ViewModels.DevRibbonViewModel ViewModel { get; } + + public DevRibbon() + { + InitializeComponent(); + ViewModel = new ViewModels.DevRibbonViewModel(); + + if (FlyoutContent != null) + { + FlyoutContent.DataContext = ViewModel; + } + } + + private void DevRibbonButton_PointerEntered(object sender, PointerRoutedEventArgs e) + { + VisualStateManager.GoToState(this, "PointerOver", true); + } + + private void DevRibbonButton_PointerExited(object sender, PointerRoutedEventArgs e) + { + VisualStateManager.GoToState(this, "Normal", true); + } + + private Visibility VisibleIfGreaterThanZero(int value) + { + return value > 0 ? Visibility.Visible : Visibility.Collapsed; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml index 36a14965a1..f8c888e8f8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml @@ -77,7 +77,7 @@ SelectedValue="{x:Bind ViewModel.CurrentFilter, Mode=OneWay}" SelectionChanged="FiltersComboBox_SelectionChanged" Style="{StaticResource ComboBoxStyle}" - Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"> + Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}, FallbackValue=Collapsed}"> + + + + + Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" /> + ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}"> - - + Padding="8" + AutomationProperties.Name="{x:Bind Title, Mode=OneWay}" + CornerRadius="{StaticResource MediumGridViewItemCornerRadius}" + ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}"> + + + + - - + Visibility="{x:Bind LayoutShowsTitle, Mode=OneWay}" /> + @@ -193,11 +316,11 @@ Padding="0" HorizontalAlignment="Center" VerticalAlignment="Center" - AutomationProperties.Name="{x:Bind Title}" + AutomationProperties.Name="{x:Bind Title, Mode=OneWay}" BorderThickness="0" - CornerRadius="4" + CornerRadius="{StaticResource GalleryGridViewItemRadius}" Orientation="Vertical" - ToolTipService.ToolTip="{x:Bind Title}"> + ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}"> - - - + CornerRadius="{StaticResource GalleryGridViewItemRadius}"> @@ -222,35 +341,39 @@ - + + TextWrapping="NoWrap" + Visibility="{x:Bind ShowTitle, Mode=OneWay}" /> + TextWrapping="NoWrap" + Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" /> @@ -295,6 +418,7 @@ IsDoubleTapEnabled="True" IsItemClickEnabled="True" ItemClick="Items_ItemClick" + ItemContainerStyleSelector="{StaticResource GridItemContainerStyleSelector}" ItemTemplateSelector="{StaticResource GridItemTemplateSelector}" ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}" RightTapped="Items_RightTapped" @@ -302,6 +426,7 @@ + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs index 55b2f368ba..a28ae3e133 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml.cs @@ -18,6 +18,7 @@ using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Navigation; +using Windows.Foundation; using Windows.System; namespace Microsoft.CmdPal.UI; @@ -25,6 +26,8 @@ namespace Microsoft.CmdPal.UI; public sealed partial class ListPage : Page, IRecipient, IRecipient, + IRecipient, + IRecipient, IRecipient, IRecipient { @@ -82,6 +85,8 @@ public sealed partial class ListPage : Page, // RegisterAll isn't AOT compatible WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -94,6 +99,8 @@ public sealed partial class ListPage : Page, WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); @@ -181,9 +188,9 @@ public sealed partial class ListPage : Page, var notificationText = li.Title; UIHelper.AnnounceActionForAccessibility( - ItemsList, - notificationText, - "CommandPaletteSelectedItemChanged"); + ItemsList, + notificationText, + "CommandPaletteSelectedItemChanged"); } } } @@ -296,6 +303,142 @@ public sealed partial class ListPage : Page, } } + public void Receive(NavigatePageDownCommand message) + { + var indexes = CalculateTargetIndexPageUpDownScrollTo(true); + if (indexes is null) + { + return; + } + + if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex) + { + ItemView.SelectedIndex = indexes.Value.TargetIndex; + ItemView.ScrollIntoView(ItemView.SelectedItem); + } + } + + public void Receive(NavigatePageUpCommand message) + { + var indexes = CalculateTargetIndexPageUpDownScrollTo(false); + if (indexes is null) + { + return; + } + + if (indexes.Value.CurrentIndex != indexes.Value.TargetIndex) + { + ItemView.SelectedIndex = indexes.Value.TargetIndex; + ItemView.ScrollIntoView(ItemView.SelectedItem); + } + } + + /// + /// Calculates the item index to target when performing a page up or page down + /// navigation. The calculation attempts to estimate how many items fit into + /// the visible viewport by measuring actual container heights currently visible + /// within the internal ScrollViewer. If measurements are not available a + /// fallback estimate is used. + /// + /// True to calculate a page-down target, false for page-up. + /// + /// A tuple containing the current index and the calculated target index, or null + /// if a valid calculation could not be performed (for example, missing ScrollViewer). + /// + private (int CurrentIndex, int TargetIndex)? CalculateTargetIndexPageUpDownScrollTo(bool isPageDown) + { + var scroll = FindScrollViewer(ItemView); + if (scroll is null) + { + return null; + } + + var viewportHeight = scroll.ViewportHeight; + if (viewportHeight <= 0) + { + return null; + } + + var currentIndex = ItemView.SelectedIndex < 0 ? 0 : ItemView.SelectedIndex; + var itemCount = ItemView.Items.Count; + + // Compute visible item heights within the ScrollViewer viewport + const int firstVisibleIndexNotFound = -1; + var firstVisibleIndex = firstVisibleIndexNotFound; + var visibleHeights = new List(itemCount); + + for (var i = 0; i < itemCount; i++) + { + if (ItemView.ContainerFromIndex(i) is FrameworkElement container) + { + try + { + var transform = container.TransformToVisual(scroll); + var topLeft = transform.TransformPoint(new Point(0, 0)); + var bottom = topLeft.Y + container.ActualHeight; + + // If any part of the container is inside the viewport, consider it visible + if (topLeft.Y >= 0 && bottom <= viewportHeight) + { + if (firstVisibleIndex == firstVisibleIndexNotFound) + { + firstVisibleIndex = i; + } + + visibleHeights.Add(container.ActualHeight > 0 ? container.ActualHeight : 0); + } + } + catch + { + // ignore transform errors and continue + } + } + } + + var itemsPerPage = 0; + + // Calculate how many items fit in the viewport based on their actual heights + if (visibleHeights.Count > 0) + { + double accumulated = 0; + for (var i = 0; i < visibleHeights.Count; i++) + { + accumulated += visibleHeights[i] <= 0 ? 1 : visibleHeights[i]; + itemsPerPage++; + if (accumulated >= viewportHeight) + { + break; + } + } + } + else + { + // fallback: estimate using first measured container height + double itemHeight = 0; + for (var i = currentIndex; i < itemCount; i++) + { + if (ItemView.ContainerFromIndex(i) is FrameworkElement { ActualHeight: > 0 } c) + { + itemHeight = c.ActualHeight; + break; + } + } + + if (itemHeight <= 0) + { + itemHeight = 1; + } + + itemsPerPage = Math.Max(1, (int)Math.Floor(viewportHeight / itemHeight)); + } + + var targetIndex = isPageDown + ? Math.Min(itemCount - 1, currentIndex + Math.Max(1, itemsPerPage)) + : Math.Max(0, currentIndex - Math.Max(1, itemsPerPage)); + + return (currentIndex, targetIndex); + } + private static void OnViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is ListPage @this) @@ -351,11 +494,11 @@ public sealed partial class ListPage : Page, } } - private ScrollViewer? FindScrollViewer(DependencyObject parent) + private static ScrollViewer? FindScrollViewer(DependencyObject parent) { - if (parent is ScrollViewer) + if (parent is ScrollViewer viewer) { - return (ScrollViewer)parent; + return viewer; } for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs index 24d2ef47a6..012e8dc789 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BindTransformers.cs @@ -15,4 +15,7 @@ internal static class BindTransformers public static Visibility EmptyOrWhitespaceToCollapsed(string? input) => string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible; + + public static Visibility VisibleWhenAny(bool value1, bool value2) + => (value1 || value2) ? Visibility.Visible : Visibility.Collapsed; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BuildInfo.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BuildInfo.cs new file mode 100644 index 0000000000..7f129d8b06 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/BuildInfo.cs @@ -0,0 +1,36 @@ +// 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.Reflection; +using System.Runtime.CompilerServices; + +namespace Microsoft.CmdPal.UI.Helpers; + +internal static class BuildInfo +{ +#if DEBUG + public const string Configuration = "Debug"; +#else + public const string Configuration = "Release"; +#endif + + // Runtime AOT detection + public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported; + + // From assembly metadata (build-time values) + public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false); + + // From assembly metadata (build-time values) + public static bool PublishAot => GetBoolMetadata("PublishAot", false); + + public static bool IsCiBuild => GetBoolMetadata("CIBuild", false); + + private static string? GetMetadata(string key) => + Assembly.GetExecutingAssembly() + .GetCustomAttributes() + .FirstOrDefault(a => a.Key == key)?.Value; + + private static bool GetBoolMetadata(string key, bool defaultValue) => + bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue; +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs new file mode 100644 index 0000000000..9b5ac21364 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/GlobalErrorHandler.cs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using ManagedCommon; +using Microsoft.CmdPal.Core.Common.Helpers; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.UI.WindowsAndMessaging; +using SystemUnhandledExceptionEventArgs = System.UnhandledExceptionEventArgs; +using XamlUnhandledExceptionEventArgs = Microsoft.UI.Xaml.UnhandledExceptionEventArgs; + +namespace Microsoft.CmdPal.UI.Helpers; + +/// +/// Global error handler for Command Palette. +/// +internal sealed partial class GlobalErrorHandler +{ + // GlobalErrorHandler is designed to be self-contained; it can be registered and invoked before a service provider is available. + internal void Register(App app) + { + ArgumentNullException.ThrowIfNull(app); + + app.UnhandledException += App_UnhandledException; + TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + } + + private void App_UnhandledException(object sender, XamlUnhandledExceptionEventArgs e) + { + // Exceptions thrown on the main UI thread are handled here. + if (e.Exception != null) + { + HandleException(e.Exception, Context.MainThreadException); + } + } + + private void CurrentDomain_UnhandledException(object sender, SystemUnhandledExceptionEventArgs e) + { + // Exceptions thrown on background threads are handled here. + if (e.ExceptionObject is Exception ex) + { + HandleException(ex, Context.AppDomainUnhandledException); + } + } + + private void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + // This event is raised only when a faulted Task is garbage-collected + // without its exception being observed. It is NOT raised immediately + // when the Task faults; timing depends on GC finalization. + e.SetObserved(); + HandleException(e.Exception, Context.UnobservedTaskException); + } + + private static void HandleException(Exception ex, Context context) + { + Logger.LogError($"Unhandled exception detected ({context})", ex); + + if (context == Context.MainThreadException) + { + var error = DiagnosticsHelper.BuildExceptionMessage(ex, null); + var report = $""" + This is an error report generated by Windows Command Palette. + If you are seeing this message, it means the application has encountered an unexpected issue. + You can help us fix it by filing a report at https://aka.ms/powerToysReportBug. + {error} + """; + + StoreReport(report, storeOnDesktop: false); + + string message; + string caption; + try + { + message = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Message"); + caption = ResourceLoaderInstance.GetString("GlobalErrorHandler_CrashMessageBox_Caption"); + } + catch + { + // The resource loader may not be available if the exception occurred during startup. + // Fall back to hardcoded strings in that case. + message = "Command Palette has encountered a fatal error and must close."; + caption = "Command Palette - Fatal error"; + } + + PInvoke.MessageBox( + HWND.Null, + message, + caption, + MESSAGEBOX_STYLE.MB_ICONERROR); + } + } + + private static string? StoreReport(string report, bool storeOnDesktop) + { + // Generate a unique name for the report file; include timestamp and a random zero-padded number to avoid collisions + // in case of crash storm. + var name = FormattableString.Invariant($"CmdPal_ErrorReport_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{Random.Shared.Next(100000):D5}.log"); + + // Always store a copy in log directory, this way it is available for Bug Report Tool + string? reportPath = null; + if (Logger.CurrentVersionLogDirectoryPath != null) + { + reportPath = Save(report, name, static () => Logger.CurrentVersionLogDirectoryPath); + } + + // Optionally store a copy on the desktop for user (in)convenience + if (storeOnDesktop) + { + var path = Save(report, name, static () => Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)); + + // show the desktop copy if both succeeded + if (path != null) + { + reportPath = path; + } + } + + return reportPath; + + static string? Save(string reportContent, string reportFileName, Func directory) + { + try + { + var logDirectory = directory(); + Directory.CreateDirectory(logDirectory); + var reportFilePath = Path.Combine(logDirectory, reportFileName); + File.WriteAllText(reportFilePath, reportContent); + return reportFilePath; + } + catch (Exception ex) + { + Logger.LogError("Failed to store exception report", ex); + return null; + } + } + } + + private enum Context + { + Unknown = 0, + MainThreadException, + BackgroundThreadException, + UnobservedTaskException, + AppDomainUnhandledException, + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowExtensions.cs index 0866a57589..ee782766bc 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowExtensions.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowExtensions.cs @@ -22,7 +22,7 @@ internal static class WindowExtensions appWindow.SetIcon(@"Assets\icon.ico"); } - private static HWND GetWindowHwnd(this Window window) + public static HWND GetWindowHwnd(this Window window) { return window is null ? throw new ArgumentNullException(nameof(window)) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/HiddenOwnerWindowBehavior.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/HiddenOwnerWindowBehavior.cs new file mode 100644 index 0000000000..7b01afd20a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/HiddenOwnerWindowBehavior.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.UI.Helpers; +using Microsoft.UI.Xaml; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Dwm; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace Microsoft.CmdPal.UI; + +/// +/// Provides behavior to control taskbar and Alt+Tab presence by assigning a hidden owner +/// and toggling extended window styles for a target window. +/// +internal sealed class HiddenOwnerWindowBehavior +{ + private HWND _hiddenOwnerHwnd; + private Window? _hiddenWindow; + + /// + /// Shows or hides a window in the taskbar (and Alt+Tab) by updating ownership and extended window styles. + /// + /// The to update. + /// True to show the window in the taskbar (and Alt+Tab); false to hide it from both. + /// + /// When hiding the window, a hidden owner is assigned and + /// is enabled to keep it out of the taskbar and Alt+Tab. When showing, the owner is cleared and + /// is enabled to ensure taskbar presence. Since tool + /// windows use smaller corner radii, the normal rounded corners are enforced via + /// . + /// + /// + public void ShowInTaskbar(Window target, bool isVisibleInTaskbar) + { + /* + * There are the three main ways to control whether a window appears on the taskbar: + * https://learn.microsoft.com/en-us/windows/win32/shell/taskbar#managing-taskbar-buttons + * + * 1. Set the window's owner. Owned windows do not appear on the taskbar: + * Turns out this is the most reliable way to hide a window from the taskbar and ALT+TAB. WinForms and WPF uses this method + * to back their ShowInTaskbar property as well. + * + * 2. Use the WS_EX_TOOLWINDOW extended window style: + * This mostly works, with some reports that it silently fails in some cases. The biggest issue + * is that for certain Windows settings (like Multitasking -> Show taskbar buttons on all displays = On all desktops), + * the taskbar button is always shown even for tool windows. + * + * 3. Using ITaskbarList: + * This is what AppWindow.IsShownInSwitchers uses, but it's COM-based and more complex, and can + * fail if Explorer isn't running or responding. It could be a good backup, if needed. + */ + + var visibleHwnd = target.GetWindowHwnd(); + + if (isVisibleInTaskbar) + { + // remove any owner window + PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, HWND.Null); + } + else + { + // Set the hidden window as the owner of the target window + var hiddenHwnd = EnsureHiddenOwner(); + PInvoke.SetWindowLongPtr(visibleHwnd, WINDOW_LONG_PTR_INDEX.GWLP_HWNDPARENT, hiddenHwnd); + } + + // Tool windows don't show up in ALT+TAB, and don't show up in the taskbar + // Tool window and app window styles are mutually exclusive, change both just to be safe + target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !isVisibleInTaskbar); + target.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_APPWINDOW, isVisibleInTaskbar); + + // Since tool windows have smaller corner radii, we need to force the normal ones + target.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND); + } + + private HWND EnsureHiddenOwner() + { + if (_hiddenOwnerHwnd.IsNull) + { + _hiddenWindow = new Window(); + _hiddenOwnerHwnd = _hiddenWindow.GetWindowHwnd(); + } + + return _hiddenOwnerHwnd; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml index d06932fd59..c0c0ab811f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml @@ -14,5 +14,7 @@ Activated="MainWindow_Activated" Closed="MainWindow_Closed" mc:Ignorable="d"> - + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index b33feedc28..1655626714 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -11,6 +11,7 @@ using Microsoft.CmdPal.Core.Common.Helpers; using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Ext.ClipboardHistory.Messages; +using Microsoft.CmdPal.UI.Controls; using Microsoft.CmdPal.UI.Events; using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Messages; @@ -18,6 +19,7 @@ using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerToys.Telemetry; +using Microsoft.UI; using Microsoft.UI.Composition; using Microsoft.UI.Composition.SystemBackdrops; using Microsoft.UI.Input; @@ -33,6 +35,8 @@ using Windows.UI.WindowManagement; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.Graphics.Dwm; +using Windows.Win32.Graphics.Gdi; +using Windows.Win32.UI.HiDpi; using Windows.Win32.UI.Input.KeyboardAndMouse; using Windows.Win32.UI.WindowsAndMessaging; using WinRT; @@ -48,24 +52,35 @@ public sealed partial class MainWindow : WindowEx, IRecipient, IDisposable { + private const int DefaultWidth = 800; + private const int DefaultHeight = 480; + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")] [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")] private readonly uint WM_TASKBAR_RESTART; private readonly HWND _hwnd; + private readonly DispatcherTimer _autoGoHomeTimer; private readonly WNDPROC? _hotkeyWndProc; private readonly WNDPROC? _originalWndProc; private readonly List _hotkeys = []; private readonly KeyboardListener _keyboardListener; private readonly LocalKeyboardListener _localKeyboardListener; + private readonly HiddenOwnerWindowBehavior _hiddenOwnerBehavior = new(); private bool _ignoreHotKeyWhenFullScreen = true; private DesktopAcrylicController? _acrylicController; private SystemBackdropConfiguration? _configurationSource; + private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan; + + private WindowPosition _currentWindowPosition = new(); public MainWindow() { InitializeComponent(); + _autoGoHomeTimer = new DispatcherTimer(); + _autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick; + _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); unsafe @@ -73,6 +88,8 @@ public sealed partial class MainWindow : WindowEx, CommandPaletteHost.SetHostHwnd((ulong)_hwnd.Value); } + _hiddenOwnerBehavior.ShowInTaskbar(this, Debugger.IsAttached); + _keyboardListener = new KeyboardListener(); _keyboardListener.Start(); @@ -80,7 +97,9 @@ public sealed partial class MainWindow : WindowEx, this.SetIcon(); AppWindow.Title = RS_.GetString("AppName"); - PositionCentered(); + RestoreWindowPosition(); + UpdateWindowPositionInMemory(); + SetAcrylic(); WeakReferenceMessenger.Default.Register(this); @@ -95,7 +114,7 @@ public sealed partial class MainWindow : WindowEx, ExtendsContentIntoTitleBar = true; AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed; SizeChanged += WindowSizeChanged; - RootShellPage.Loaded += RootShellPage_Loaded; + RootElement.Loaded += RootElementLoaded; WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated"); @@ -112,7 +131,7 @@ public sealed partial class MainWindow : WindowEx, App.Current.Services.GetService()!.SettingsChanged += SettingsChangedHandler; // Make sure that we update the acrylic theme when the OS theme changes - RootShellPage.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic); + RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic); // Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () => @@ -126,16 +145,15 @@ public sealed partial class MainWindow : WindowEx, // Force window to be created, and then cloaked. This will offset initial animation when the window is shown. HideWindow(); - - ApplyWindowStyle(); } - private void ApplyWindowStyle() + private void OnAutoGoHomeTimerOnTick(object? s, object e) { - // Tool windows don't show up in ALT+TAB, and don't show up in the taskbar - // Since tool windows have smaller corner radii, we need to force the normal ones - this.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, !Debugger.IsAttached); - this.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND); + _autoGoHomeTimer.Stop(); + + // BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon) + // and prevent the user from opening its context menu. + WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false)); } private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e) @@ -148,11 +166,18 @@ public sealed partial class MainWindow : WindowEx, private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(); - private void RootShellPage_Loaded(object sender, RoutedEventArgs e) => - + private void RootElementLoaded(object sender, RoutedEventArgs e) + { // Now that our content has loaded, we can update our draggable regions UpdateRegionsForCustomTitleBar(); + // Add dev ribbon if enabled + if (!BuildInfo.IsCiBuild) + { + RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) }); + } + } + private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar(); private void PositionCentered() @@ -161,6 +186,25 @@ public sealed partial class MainWindow : WindowEx, PositionCentered(displayArea); } + private void RestoreWindowPosition() + { + var settings = App.Current.Services.GetService(); + if (settings?.LastWindowPosition is not WindowPosition savedPosition) + { + PositionCentered(); + return; + } + + if (savedPosition.Width <= 0 || savedPosition.Height <= 0) + { + PositionCentered(); + return; + } + + var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi); + AppWindow.MoveAndResize(newRect); + } + private void PositionCentered(DisplayArea displayArea) { if (displayArea is not null) @@ -175,6 +219,21 @@ public sealed partial class MainWindow : WindowEx, } } + private void UpdateWindowPositionInMemory() + { + var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary; + _currentWindowPosition = new WindowPosition + { + X = AppWindow.Position.X, + Y = AppWindow.Position.Y, + Width = AppWindow.Size.Width, + Height = AppWindow.Size.Height, + Dpi = (int)this.GetDpiForWindow(), + ScreenWidth = displayArea.WorkArea.Width, + ScreenHeight = displayArea.WorkArea.Height, + }; + } + private void HotReloadSettings() { var settings = App.Current.Services.GetService()!; @@ -183,6 +242,9 @@ public sealed partial class MainWindow : WindowEx, App.Current.Services.GetService()!.SetupTrayIcon(settings.ShowSystemTrayIcon); _ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen; + + _autoGoHomeInterval = settings.AutoGoHomeInterval; + _autoGoHomeTimer.Interval = _autoGoHomeInterval; } // We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material @@ -242,6 +304,8 @@ public sealed partial class MainWindow : WindowEx, private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target) { + StopAutoGoHome(); + var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd); // Remember, IsIconic == "minimized", which is entirely different state @@ -257,14 +321,22 @@ public sealed partial class MainWindow : WindowEx, PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE); } - var display = GetScreen(hwnd, target); - PositionCentered(display); + if (target == MonitorBehavior.ToLast) + { + var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi); + AppWindow.MoveAndResize(newRect); + } + else + { + var display = GetScreen(hwnd, target); + PositionCentered(display); + } // Check if the debugger is attached. If it is, we don't want to apply the tool window style, // because that would make it hard to debug the app if (Debugger.IsAttached) { - ApplyWindowStyle(); + _hiddenOwnerBehavior.ShowInTaskbar(this, true); } // Just to be sure, SHOW our hwnd. @@ -281,6 +353,114 @@ public sealed partial class MainWindow : WindowEx, PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE); } + /// + /// Ensures that the window rectangle is visible on-screen. + /// + /// The window rectangle in physical pixels. + /// The desktop area the window was positioned on. + /// The window's original DPI. + /// + /// A window rectangle in physical pixels, moved to the nearest display and resized + /// if the DPI has changed. + /// + private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi) + { + var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest); + if (displayArea is null) + { + return windowRect; + } + + var workArea = displayArea.WorkArea; + if (workArea.Width <= 0 || workArea.Height <= 0) + { + // Fallback, nothing reasonable to do + return windowRect; + } + + var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea); + if (originalDpi <= 0) + { + originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed) + } + + var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0; + if (hasInvalidSize) + { + windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight); + } + + // If we have a DPI change, scale the window rectangle accordingly + if (effectiveDpi != originalDpi) + { + var scalingFactor = effectiveDpi / (double)originalDpi; + windowRect = new RectInt32( + (int)Math.Round(windowRect.X * scalingFactor), + (int)Math.Round(windowRect.Y * scalingFactor), + (int)Math.Round(windowRect.Width * scalingFactor), + (int)Math.Round(windowRect.Height * scalingFactor)); + } + + var targetWidth = Math.Min(windowRect.Width, workArea.Width); + var targetHeight = Math.Min(windowRect.Height, workArea.Height); + + // Ensure at least some minimum visible area (e.g., 100 pixels) + // This helps prevent the window from being entirely offscreen, regardless of display scaling. + const int minimumVisibleSize = 100; + var isOffscreen = + windowRect.X + minimumVisibleSize > workArea.X + workArea.Width || + windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X || + windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height || + windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y; + + // if the work area size has changed, re-center the window + var workAreaSizeChanged = + originalScreen.Width != workArea.Width || + originalScreen.Height != workArea.Height; + + int targetX; + int targetY; + var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize; + if (recenter) + { + targetX = workArea.X + ((workArea.Width - targetWidth) / 2); + targetY = workArea.Y + ((workArea.Height - targetHeight) / 2); + } + else + { + targetX = windowRect.X; + targetY = windowRect.Y; + } + + return new RectInt32(targetX, targetY, targetWidth, targetHeight); + } + + private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea) + { + var effectiveDpi = 96; + + var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId); + if (!hMonitor.IsNull) + { + var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _); + if (hr == 0) + { + effectiveDpi = (int)dpiX; + } + else + { + Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}"); + } + } + + if (effectiveDpi <= 0) + { + effectiveDpi = 96; + } + + return effectiveDpi; + } + private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target) { // Leaving a note here, in case we ever need it: @@ -350,6 +530,11 @@ public sealed partial class MainWindow : WindowEx, public void Receive(DismissMessage message) { + if (message.ForceGoHome) + { + WeakReferenceMessenger.Default.Send(new GoHomeMessage(false, false)); + } + // This might come in off the UI thread. Make sure to hop back. DispatcherQueue.TryEnqueue(() => { @@ -380,6 +565,25 @@ public sealed partial class MainWindow : WindowEx, // If the window was not cloaked, then leave it hidden. // Sure, it's not ideal, but at least it's not visible. } + + // Start auto-go-home timer + RestartAutoGoHome(); + } + + private void StopAutoGoHome() + { + _autoGoHomeTimer.Stop(); + } + + private void RestartAutoGoHome() + { + if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan) + { + return; + } + + _autoGoHomeTimer.Stop(); + _autoGoHomeTimer.Start(); } private bool Cloak() @@ -419,6 +623,25 @@ public sealed partial class MainWindow : WindowEx, internal void MainWindow_Closed(object sender, WindowEventArgs args) { var serviceProvider = App.Current.Services; + UpdateWindowPositionInMemory(); + + var settings = serviceProvider.GetService(); + if (settings is not null) + { + settings.LastWindowPosition = new WindowPosition + { + X = _currentWindowPosition.X, + Y = _currentWindowPosition.Y, + Width = _currentWindowPosition.Width, + Height = _currentWindowPosition.Height, + Dpi = _currentWindowPosition.Dpi, + ScreenWidth = _currentWindowPosition.ScreenWidth, + ScreenHeight = _currentWindowPosition.ScreenHeight, + }; + + SettingsModel.SaveSettings(settings); + } + var extensionService = serviceProvider.GetService()!; extensionService.SignalStopExtensionsAsync(); @@ -448,28 +671,28 @@ public sealed partial class MainWindow : WindowEx, private void UpdateRegionsForCustomTitleBar() { // Specify the interactive regions of the title bar. - var scaleAdjustment = RootShellPage.XamlRoot.RasterizationScale; + var scaleAdjustment = RootElement.XamlRoot.RasterizationScale; // Get the rectangle around our XAML content. We're going to mark this // rectangle as "Passthrough", so that the normal window operations // (resizing, dragging) don't apply in this space. - var transform = RootShellPage.TransformToVisual(null); + var transform = RootElement.TransformToVisual(null); // Reserve 16px of space at the top for dragging. var topHeight = 16; var bounds = transform.TransformBounds(new Rect( 0, topHeight, - RootShellPage.ActualWidth, - RootShellPage.ActualHeight)); + RootElement.ActualWidth, + RootElement.ActualHeight)); var contentRect = GetRect(bounds, scaleAdjustment); var rectArray = new RectInt32[] { contentRect }; var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id); nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray); // Add a drag-able region on top - var w = RootShellPage.ActualWidth; - _ = RootShellPage.ActualHeight; + var w = RootElement.ActualWidth; + _ = RootElement.ActualHeight; var dragSides = new RectInt32[] { GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall @@ -490,6 +713,9 @@ public sealed partial class MainWindow : WindowEx, { if (args.WindowActivationState == WindowActivationState.Deactivated) { + // Save the current window position before hiding the window + UpdateWindowPositionInMemory(); + // If there's a debugger attached... if (System.Diagnostics.Debugger.IsAttached) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index f577f13898..eac3643847 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -15,6 +15,7 @@ enable enable true + preview $(CmdPalVersion) @@ -25,10 +26,10 @@ - + true + --> true @@ -37,7 +38,7 @@ true - + true Never $(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\ @@ -66,6 +67,7 @@ + @@ -111,9 +113,14 @@ + + + + + @@ -163,6 +170,9 @@ MSBuild:Compile + + MSBuild:Compile + $(DefaultXamlRuntime) @@ -181,6 +191,15 @@ PreserveNewest + + Never + + + Never + + + Never + @@ -221,4 +240,24 @@ + + + + <_Parameter1>PublishTrimmed + <_Parameter2>$(PublishTrimmed) + + + <_Parameter1>PublishAot + <_Parameter2>$(PublishAot) + + + <_Parameter1>CIBuild + <_Parameter2>$(CIBuild) + + + <_Parameter1>CommandPaletteBranding + <_Parameter2>$(CommandPaletteBranding) + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt index a653eb726a..fc5a608199 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt @@ -58,4 +58,9 @@ GetModuleHandle GetWindowLong SetWindowLong -WINDOW_EX_STYLE \ No newline at end of file +WINDOW_EX_STYLE +CreateWindowEx +WNDCLASSEXW +RegisterClassEx +GetStockObject +GetModuleHandle \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs index 6ec7f23a59..7e8dc9eebd 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs @@ -21,7 +21,6 @@ using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Dispatching; using Microsoft.UI.Input; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media.Animation; @@ -48,7 +47,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, IRecipient, IRecipient, IRecipient, - INotifyPropertyChanged + INotifyPropertyChanged, + IDisposable { private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread(); @@ -65,6 +65,9 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, private SettingsWindow? _settingsWindow; + private CancellationTokenSource? _focusAfterLoadedCts; + private WeakReference? _lastNavigatedPageRef; + public ShellViewModel ViewModel { get; private set; } = App.Current.Services.GetService()!; public event PropertyChangedEventHandler? PropertyChanged; @@ -130,7 +133,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, if (!message.FromBackspace) { // If we can't go back then we must be at the top and thus escape again should quit. - WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(new DismissMessage()); PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnEsc()); } @@ -156,7 +159,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, new AsyncNavigationRequest(message.Page, message.CancellationToken), message.WithAnimation ? DefaultPageAnimation : _noAnimation); - PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth)); + PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id)); if (!ViewModel.IsNested) { @@ -342,7 +345,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, // Depending on the settings, either // * Go home, or // * Select the search text (if we should remain open on this page) - if (settings.HotkeyGoesHome) + if (settings.AutoGoHomeInterval == TimeSpan.Zero) { GoHome(false); } @@ -447,7 +450,15 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, { while (RootFrame.CanGoBack) { - GoBack(withAnimation, focusSearch); + // don't focus on each step, just at the end + GoBack(withAnimation, focusSearch: false); + } + + // focus search box, even if we were already home + if (focusSearch) + { + SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic); + SearchBox.SelectSearch(); } } @@ -488,6 +499,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, if (e.Content is Page element) { + _lastNavigatedPageRef = new WeakReference(element); element.Loaded += FocusAfterLoaded; } } @@ -497,6 +509,18 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, var page = (Page)sender; page.Loaded -= FocusAfterLoaded; + // Only handle focus for the latest navigated page + if (_lastNavigatedPageRef is null || !_lastNavigatedPageRef.TryGetTarget(out var last) || !ReferenceEquals(page, last)) + { + return; + } + + // Cancel any previous pending focus work + _focusAfterLoadedCts?.Cancel(); + _focusAfterLoadedCts?.Dispose(); + _focusAfterLoadedCts = new CancellationTokenSource(); + var token = _focusAfterLoadedCts.Token; + AnnounceNavigationToPage(page); var shouldSearchBoxBeVisible = ViewModel.CurrentPage?.HasSearchBox ?? false; @@ -509,34 +533,57 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, } else { - _ = Task.Run(async () => - { - await page.DispatcherQueue.EnqueueAsync(async () => + _ = Task.Run( + async () => { - // I hate this so much, but it can take a while for the page to be ready to accept focus; - // focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts) - for (var i = 0; i < 10; i++) + if (token.IsCancellationRequested) { - if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement) - { - var set = frameworkElement.Focus(FocusState.Programmatic); - if (set) - { - break; - } - } - - await Task.Delay(100); + return; } - // Update the search box visibility based on the current page: - // - We do this here after navigation so the focus is not jumping around too much, - // it messes with screen readers if we do it too early - // - Since this should hide the search box on content pages, it's not a problem if we - // wait for the code above to finish trying to focus the content - ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false; - }); - }); + try + { + await page.DispatcherQueue.EnqueueAsync( + async () => + { + // I hate this so much, but it can take a while for the page to be ready to accept focus; + // focusing page with MarkdownTextBlock takes up to 5 attempts (* 100ms delay between attempts) + for (var i = 0; i < 10; i++) + { + token.ThrowIfCancellationRequested(); + + if (FocusManager.FindFirstFocusableElement(page) is FrameworkElement frameworkElement) + { + var set = frameworkElement.Focus(FocusState.Programmatic); + if (set) + { + break; + } + } + + await Task.Delay(100, token); + } + + token.ThrowIfCancellationRequested(); + + // Update the search box visibility based on the current page: + // - We do this here after navigation so the focus is not jumping around too much, + // it messes with screen readers if we do it too early + // - Since this should hide the search box on content pages, it's not a problem if we + // wait for the code above to finish trying to focus the content + ViewModel.IsSearchBoxVisible = ViewModel.CurrentPage?.HasSearchBox ?? false; + }); + } + catch (OperationCanceledException) + { + // Swallow cancellation - another FocusAfterLoaded invocation superseded this one + } + catch (Exception ex) + { + Logger.LogError("Error during FocusAfterLoaded async focus work", ex); + } + }, + token); } } @@ -591,24 +638,31 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down); var onlyAlt = altPressed && !ctrlPressed && !shiftPressed && !winPressed; - if (e.Key == VirtualKey.Left && onlyAlt) + var onlyCtrl = !altPressed && ctrlPressed && !shiftPressed && !winPressed; + switch (e.Key) { - WeakReferenceMessenger.Default.Send(new()); - e.Handled = true; - } - else if (e.Key == VirtualKey.Home && onlyAlt) - { - WeakReferenceMessenger.Default.Send(new(WithAnimation: false)); - e.Handled = true; - } - else - { - // 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; + case VirtualKey.Left when onlyAlt: // Alt+Left arrow + WeakReferenceMessenger.Default.Send(new()); + e.Handled = true; + break; + case VirtualKey.Home when onlyAlt: // Alt+Home + WeakReferenceMessenger.Default.Send(new(WithAnimation: false)); + e.Handled = true; + break; + case (VirtualKey)188 when onlyCtrl: // Ctrl+, + WeakReferenceMessenger.Default.Send(new()); + e.Handled = true; + break; + default: + { + // 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; + break; + } } } @@ -658,4 +712,11 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, Logger.LogError("Error handling mouse button press event", ex); } } + + public void Dispose() + { + _focusAfterLoadedCts?.Cancel(); + _focusAfterLoadedCts?.Dispose(); + _focusAfterLoadedCts = null; + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/launchSettings.json b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/launchSettings.json index febacfc92e..4631e9aeaf 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/launchSettings.json +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/launchSettings.json @@ -5,6 +5,11 @@ "nativeDebugging": false, "doNotLaunchApp": false }, + "Microsoft.CmdPal.UI (Package) + Native debugging": { + "commandName": "MsixPackage", + "nativeDebugging": true, + "doNotLaunchApp": false + }, "Microsoft.CmdPal.UI (Unpackaged)": { "commandName": "Project" } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml index f798134c10..9e9600bd6e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml @@ -14,18 +14,204 @@ xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels" mc:Ignorable="d"> + + + + + + + + + + ms-appx:///Assets/StoreLogo.dark.svg + + + + + + + + + + + + + + + + ms-appx:///Assets/StoreLogo.light.svg + + + + + ms-appx:///Assets/StoreLogo.dark.svg + + + + + + + - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs index e164638ebb..e99296bad9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/ExtensionsPage.xaml.cs @@ -8,6 +8,7 @@ using Microsoft.CmdPal.UI.ViewModels; using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; namespace Microsoft.CmdPal.UI.Settings; @@ -35,4 +36,10 @@ public sealed partial class ExtensionsPage : Page } } } + + private void OnFindInvoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args) + { + SearchBox?.Focus(FocusState.Keyboard); + args.Handled = true; + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml index 5c52526f04..040d6eb2ea 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml @@ -54,8 +54,18 @@ - - + + + + + + + + + + + + @@ -66,6 +76,7 @@ + @@ -81,6 +92,15 @@ + + + + + + + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml index 7eb6b28f88..dc3cf9fd3b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml @@ -20,12 +20,17 @@ - + - + @@ -87,7 +94,10 @@ - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs index 5d042a09e3..09000545d2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/SettingsWindow.xaml.cs @@ -4,14 +4,19 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.Messaging; +using ManagedCommon; using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.UI.Input; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Navigation; +using Windows.System; +using Windows.UI.Core; using WinUIEx; using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance; using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar; @@ -19,9 +24,12 @@ using TitleBar = Microsoft.UI.Xaml.Controls.TitleBar; namespace Microsoft.CmdPal.UI.Settings; public sealed partial class SettingsWindow : WindowEx, + IDisposable, IRecipient, IRecipient { + private readonly LocalKeyboardListener _localKeyboardListener; + public ObservableCollection BreadCrumbs { get; } = []; // Gets or sets optional action invoked after NavigationView is loaded. @@ -40,6 +48,17 @@ public sealed partial class SettingsWindow : WindowEx, WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + + _localKeyboardListener = new LocalKeyboardListener(); + _localKeyboardListener.KeyPressed += LocalKeyboardListener_OnKeyPressed; + _localKeyboardListener.Start(); + Closed += SettingsWindow_Closed; + RootElement.AddHandler(UIElement.PointerPressedEvent, new PointerEventHandler(RootElement_OnPointerPressed), true); + } + + private void SettingsWindow_Closed(object sender, WindowEventArgs args) + { + Dispose(); } // Handles NavigationView loaded event. @@ -85,10 +104,9 @@ public sealed partial class SettingsWindow : WindowEx, "Extensions" => typeof(ExtensionsPage), _ => null, }; + if (pageType is not null) { - BreadCrumbs.Clear(); - BreadCrumbs.Add(new(page, page)); NavFrame.Navigate(pageType); } } @@ -96,7 +114,6 @@ public sealed partial class SettingsWindow : WindowEx, private void Navigate(ProviderSettingsViewModel extension) { NavFrame.Navigate(typeof(ExtensionPage), extension); - BreadCrumbs.Add(new(extension.DisplayName, string.Empty)); } private void PositionCentered() @@ -127,9 +144,9 @@ public sealed partial class SettingsWindow : WindowEx, } } - private void Window_Activated(object sender, WindowActivatedEventArgs args) + private void Window_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args) { - WeakReferenceMessenger.Default.Send(args); + WeakReferenceMessenger.Default.Send(args); } private void Window_Closed(object sender, WindowEventArgs args) @@ -141,7 +158,7 @@ public sealed partial class SettingsWindow : WindowEx, private void NavView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args) { - if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal) + if (args.DisplayMode is NavigationViewDisplayMode.Compact or NavigationViewDisplayMode.Minimal) { AppTitleBar.IsPaneToggleButtonVisible = true; WorkAroundIcon.Margin = new Thickness(8, 0, 16, 0); // Required for workaround, see XAML comment @@ -149,7 +166,7 @@ public sealed partial class SettingsWindow : WindowEx, else { AppTitleBar.IsPaneToggleButtonVisible = false; - WorkAroundIcon.Margin = new Thickness(16, 0, 0, 0); // Required for workaround, see XAML comment + WorkAroundIcon.Margin = new Thickness(16, 0, 8, 0); // Required for workaround, see XAML comment } } @@ -163,6 +180,93 @@ public sealed partial class SettingsWindow : WindowEx, { NavView.IsPaneOpen = !NavView.IsPaneOpen; } + + private void TryGoBack() + { + if (NavFrame.CanGoBack) + { + NavFrame.GoBack(); + } + } + + private void TitleBar_BackRequested(TitleBar sender, object args) + { + TryGoBack(); + } + + private void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e) + { + switch (e.Key) + { + case VirtualKey.GoBack: + case VirtualKey.XButton1: + TryGoBack(); + break; + + case VirtualKey.Left: + var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down); + if (altPressed) + { + TryGoBack(); + } + + break; + } + } + + private void RootElement_OnPointerPressed(object sender, PointerRoutedEventArgs e) + { + try + { + if (e.Pointer.PointerDeviceType == PointerDeviceType.Mouse) + { + var ptrPt = e.GetCurrentPoint(RootElement); + if (ptrPt.Properties.IsXButton1Pressed) + { + TryGoBack(); + } + } + } + catch (Exception ex) + { + Logger.LogError("Error handling mouse button press event", ex); + } + } + + public void Dispose() + { + _localKeyboardListener?.Dispose(); + } + + private void NavFrame_OnNavigated(object sender, NavigationEventArgs e) + { + BreadCrumbs.Clear(); + + if (e.SourcePageType == typeof(GeneralPage)) + { + NavView.SelectedItem = GeneralPageNavItem; + var pageType = RS_.GetString("Settings_PageTitles_GeneralPage"); + BreadCrumbs.Add(new(pageType, pageType)); + } + else if (e.SourcePageType == typeof(ExtensionsPage)) + { + NavView.SelectedItem = ExtensionPageNavItem; + var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage"); + BreadCrumbs.Add(new(pageType, pageType)); + } + else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm) + { + NavView.SelectedItem = ExtensionPageNavItem; + var extensionsPageType = RS_.GetString("Settings_PageTitles_ExtensionsPage"); + BreadCrumbs.Add(new(extensionsPageType, extensionsPageType)); + BreadCrumbs.Add(new(vm.DisplayName, vm)); + } + else + { + BreadCrumbs.Add(new($"[{e.SourcePageType?.Name}]", string.Empty)); + Logger.LogError($"Unknown breadcrumb for page type '{e.SourcePageType}'"); + } + } } public readonly struct Crumb diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw index c7d6fb6417..930fe6e55a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw @@ -350,12 +350,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Search Chinese contents with PinYin - - Go home when activated - - - Automatically opens the home page upon activation - Highlight search on activate @@ -434,6 +428,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut. More + + Last position + Reopen the window where it was last closed + Settings @@ -477,4 +475,109 @@ Right-click to remove the key combination, thereby deactivating the shortcut. Navigated to {0} page + + Settings (Ctrl+,) + + + No extensions found + + + Try a different search term + + + More options + + + Reload extensions + + + Reloading extensions.. + + + Discover more extensions + + + Find more extensions on the Microsoft Store or WinGet. + + + Learn how to create your own extensions + + + Find extensions on the Microsoft Store + + + Microsoft Store + + + Find extensions on WinGet + + + Microsoft Store + + + Search extensions + + + Command Palette has encountered a fatal error and must close. + + + Command Palette - Fatal error + + + Never + + + Immediately + + + 10 seconds + + + 20 seconds + + + 30 seconds + + + 60 seconds + + + 90 seconds + + + 2 minutes + + + 3 minutes + + + Automatically return home + + + Automatically returns to home page after a period of inactivity when Command Palette is closed + + + General + + + Extensions + + + Clear search first, then go back + + + Go back + + + Hide window and go home + + + Hide window + + + Escape key behavior + + + Choose how Escape key behaves + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ToastWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ToastWindow.xaml.cs index 9f0e63edcc..1a49fec8e0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ToastWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ToastWindow.xaml.cs @@ -25,11 +25,11 @@ public sealed partial class ToastWindow : WindowEx, IRecipient { private readonly HWND _hwnd; + private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); + private readonly HiddenOwnerWindowBehavior _hiddenOwnerWindowBehavior = new(); public ToastViewModel ViewModel { get; } = new(); - private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer(); - public ToastWindow() { this.InitializeComponent(); @@ -39,12 +39,7 @@ public sealed partial class ToastWindow : WindowEx, this.SetIcon(); AppWindow.Title = RS_.GetString("ToastWindowTitle"); AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed; - - // Tool windows don't show up in ALT+TAB, and don't show up in the taskbar - // Since tool windows have smaller corner radii, we need to force the normal ones - // to visually match system toasts. - this.ToggleExtendedWindowStyle(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW, true); - this.SetCornerPreference(DWM_WINDOW_CORNER_PREFERENCE.DWMWCP_ROUND); + _hiddenOwnerWindowBehavior.ShowInTaskbar(this, false); _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); PInvoke.EnableWindow(_hwnd, false); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ViewModels/DevRibbonViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ViewModels/DevRibbonViewModel.cs new file mode 100644 index 0000000000..1876d3f82a --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ViewModels/DevRibbonViewModel.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ManagedCommon; +using Microsoft.CmdPal.UI.Helpers; +using Microsoft.UI; +using Windows.System; +using Windows.UI; +using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; + +namespace Microsoft.CmdPal.UI.ViewModels; + +internal sealed partial class DevRibbonViewModel : ObservableObject +{ + private const int MaxLogEntries = 2; + private const string Release = "Release"; + private const string Debug = "Debug"; + + private static readonly Color ReleaseAotColor = ColorHelper.FromArgb(255, 124, 58, 237); + private static readonly Color ReleaseColor = ColorHelper.FromArgb(255, 51, 65, 85); + private static readonly Color DebugAotColor = ColorHelper.FromArgb(255, 99, 102, 241); + private static readonly Color DebugColor = ColorHelper.FromArgb(255, 107, 114, 128); + + private readonly DispatcherQueue _dispatcherQueue; + + public DevRibbonViewModel() + { + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + Trace.Listeners.Add(new DevRibbonTraceListener(this)); + + var configLabel = BuildConfiguration == Release ? "RLS" : "DBG"; /* #no-spell-check-line */ + var aotLabel = BuildInfo.IsNativeAot ? "âš¡AOT" : "NO AOT"; + Tag = $"{configLabel} | {aotLabel}"; + + TagColor = (BuildConfiguration, BuildInfo.IsNativeAot) switch + { + (Release, true) => ReleaseAotColor, + (Release, false) => ReleaseColor, + (Debug, true) => DebugAotColor, + (Debug, false) => DebugColor, + _ => Colors.Fuchsia, + }; + } + + public string BuildConfiguration => BuildInfo.Configuration; + + public bool IsAotReleaseConfiguration => BuildConfiguration == Release && BuildInfo.IsNativeAot; + + public bool IsAot => BuildInfo.IsNativeAot; + + public bool IsPublishTrimmed => BuildInfo.PublishTrimmed; + + public ObservableCollection LatestLogs { get; } = []; + + [ObservableProperty] + public partial int WarningCount { get; private set; } + + [ObservableProperty] + public partial int ErrorCount { get; private set; } + + [ObservableProperty] + public partial string Tag { get; private set; } + + [ObservableProperty] + public partial Color TagColor { get; private set; } + + [RelayCommand] + private async Task OpenLogFileAsync() + { + var logPath = Logger.CurrentLogFile; + if (File.Exists(logPath)) + { + await Launcher.LaunchUriAsync(new Uri(logPath)); + } + } + + [RelayCommand] + private async Task OpenLogFolderAsync() + { + var logFolderPath = Logger.CurrentVersionLogDirectoryPath; + if (Directory.Exists(logFolderPath)) + { + await Launcher.LaunchFolderPathAsync(logFolderPath); + } + } + + [RelayCommand] + private void ResetErrorCounters() + { + WarningCount = 0; + ErrorCount = 0; + LatestLogs.Clear(); + } + + private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener + { + private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff"; + + [GeneratedRegex(@"^\[(?.*?)\] \[(?.*?)\] (?.*)")] + private static partial Regex LogRegex(); + + private readonly Lock _lock = new(); + private LogEntryViewModel? _latestLogEntry; + + public override void Write(string? message) + { + // Not required for this scenario. + } + + public override void WriteLine(string? message) + { + if (message is null) + { + return; + } + + lock (_lock) + { + var match = LogRegex().Match(message); + if (match.Success) + { + var severity = match.Groups["severity"].Value; + var isWarning = severity.Equals("Warning", StringComparison.OrdinalIgnoreCase); + var isError = severity.Equals("Error", StringComparison.OrdinalIgnoreCase); + + if (isWarning || isError) + { + var timestampStr = match.Groups["timestamp"].Value; + var timestamp = DateTimeOffset.TryParseExact( + timestampStr, + TimestampFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeLocal, + out var parsed) + ? parsed + : DateTimeOffset.Now; + + var logEntry = new LogEntryViewModel( + timestamp, + severity, + match.Groups["message"].Value, + string.Empty); + + _latestLogEntry = logEntry; + + viewModel._dispatcherQueue.TryEnqueue(() => + { + if (isWarning) + { + viewModel.WarningCount++; + } + else + { + viewModel.ErrorCount++; + } + + viewModel.LatestLogs.Insert(0, logEntry); + + while (viewModel.LatestLogs.Count > MaxLogEntries) + { + viewModel.LatestLogs.RemoveAt(viewModel.LatestLogs.Count - 1); + } + }); + } + else + { + _latestLogEntry = null; + } + + return; + } + + if (IndentLevel > 0 && _latestLogEntry is { } latest) + { + viewModel._dispatcherQueue.TryEnqueue(() => + { + latest.AppendDetails(message); + }); + } + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ViewModels/LogEntryViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/ViewModels/LogEntryViewModel.cs new file mode 100644 index 0000000000..5f9ed8db68 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ViewModels/LogEntryViewModel.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Microsoft.CmdPal.UI.ViewModels; + +internal sealed partial class LogEntryViewModel : ObservableObject +{ + private const int HeaderMaxLength = 80; + private const string WarningGlyph = "\uE7BA"; + private const string ErrorGlyph = "\uEA39"; + private const string TimestampFormat = "HH:mm:ss"; + + private DateTimeOffset Timestamp { get; } + + private string Severity { get; } + + private string Message { get; } + + private string FormattedTimestamp { get; } + + public string SeverityGlyph { get; } + + [ObservableProperty] + public partial string Header { get; private set; } + + [ObservableProperty] + public partial string Description { get; private set; } + + [ObservableProperty] + public partial string Details { get; private set; } + + public LogEntryViewModel(DateTimeOffset timestamp, string severity, string message, string details) + { + Timestamp = timestamp; + Severity = severity; + Message = message; + Details = details; + + SeverityGlyph = severity.ToUpperInvariant() switch + { + "WARNING" => WarningGlyph, + "ERROR" => ErrorGlyph, + _ => string.Empty, + }; + + FormattedTimestamp = timestamp.ToString(TimestampFormat, CultureInfo.CurrentCulture); + Description = $"{FormattedTimestamp} • {Message}"; + Header = Message; + } + + public void AppendDetails(string? message) + { + if (string.IsNullOrEmpty(message)) + { + return; + } + + Details += Environment.NewLine + message; + + // Make header the second line of details (because that's actually the message itself): + var detailsLines = Details.Split([Environment.NewLine], StringSplitOptions.None); + if (detailsLines.Length < 2) + { + return; + } + + Header = detailsLines[1].Trim(); + if (Header.Length > HeaderMaxLength) + { + Header = Header[..(HeaderMaxLength - 1)] + "…"; + } + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs index e7fbc6859d..cc24433931 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Apps.UnitTests/AllAppsCommandProviderTests.cs @@ -58,7 +58,7 @@ public class AllAppsCommandProviderTests : AppsTestBase var provider = new AllAppsCommandProvider(page); // Act - var result = provider.LookupApp(string.Empty); + var result = provider.LookupAppByDisplayName(string.Empty); // Assert Assert.IsNotNull(result); @@ -77,7 +77,7 @@ public class AllAppsCommandProviderTests : AppsTestBase await WaitForPageInitializationAsync(); // Act - var result = provider.LookupApp("TestApp"); + var result = provider.LookupAppByDisplayName("TestApp"); // Assert Assert.IsNotNull(result); @@ -97,7 +97,7 @@ public class AllAppsCommandProviderTests : AppsTestBase await WaitForPageInitializationAsync(); // Act - var result = provider.LookupApp("NonExistentApp"); + var result = provider.LookupAppByDisplayName("NonExistentApp"); // Assert Assert.IsNull(result); diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkManagerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkManagerTests.cs index 0751b5afe3..b4e533d66d 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkManagerTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkManagerTests.cs @@ -186,4 +186,48 @@ public class BookmarkManagerTests Assert.AreEqual("D:\\UpdatedPath", updatedBookmark.Bookmark); Assert.IsTrue(bookmarkUpdatedEventFired); } + + [TestMethod] + public void BookmarkManager_LegacyData_IdsArePersistedAcrossLoads() + { + // Arrange + const string json = """ + { + "Data": + [ + { "Name": "C:\\","Bookmark": "C:\\" }, + { "Name": "Bing.com","Bookmark": "https://bing.com" } + ] + } + """; + + var dataSource = new MockBookmarkDataSource(json); + + // First load: IDs should be generated for legacy entries + var manager1 = new BookmarksManager(dataSource); + var firstLoad = manager1.Bookmarks.ToList(); + Assert.AreEqual(2, firstLoad.Count); + Assert.AreNotEqual(Guid.Empty, firstLoad[0].Id); + Assert.AreNotEqual(Guid.Empty, firstLoad[1].Id); + + // Keep a name->id map to be insensitive to ordering + var firstIdsByName = firstLoad.ToDictionary(b => b.Name, b => b.Id); + + // Wait deterministically for async persistence to complete + var wasSaved = dataSource.WaitForSave(1, 5000); + Assert.IsTrue(wasSaved, "Data was not saved within the expected time."); + + // Second load: should read back the same IDs from persisted data + var manager2 = new BookmarksManager(dataSource); + var secondLoad = manager2.Bookmarks.ToList(); + Assert.AreEqual(2, secondLoad.Count); + + var secondIdsByName = secondLoad.ToDictionary(b => b.Name, b => b.Id); + + foreach (var kvp in firstIdsByName) + { + Assert.IsTrue(secondIdsByName.ContainsKey(kvp.Key), $"Missing bookmark '{kvp.Key}' after reload."); + Assert.AreEqual(kvp.Value, secondIdsByName[kvp.Key], $"Bookmark '{kvp.Key}' upgraded ID was not persisted across loads."); + } + } } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkResolverTests.Common.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkResolverTests.Common.cs index 2fa7b81b08..9be4a187e8 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkResolverTests.Common.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/BookmarkResolverTests.Common.cs @@ -110,7 +110,7 @@ public partial class BookmarkResolverTests [ new PlaceholderClassificationCase( Name: "Drive", - Input: "C:", + Input: "C:\\.", ExpectSuccess: true, ExpectedKind: CommandKind.Directory, ExpectedTarget: "C:\\", diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs index 3980ac13c6..02d71d6d77 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Bookmarks.UnitTests/MockBookmarkDataSource.cs @@ -2,6 +2,8 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Threading; using Microsoft.CmdPal.Ext.Bookmarks.Persistence; namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; @@ -9,6 +11,7 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests; internal sealed class MockBookmarkDataSource : IBookmarkDataSource { private string _jsonData; + private int _saveCount; public MockBookmarkDataSource(string initialJsonData = "[]") { @@ -23,5 +26,26 @@ internal sealed class MockBookmarkDataSource : IBookmarkDataSource public void SaveBookmarkData(string jsonData) { _jsonData = jsonData; + Interlocked.Increment(ref _saveCount); + } + + public int SaveCount => Volatile.Read(ref _saveCount); + + // Waits until at least expectedMinSaves have occurred or the timeout elapses. + // Returns true if the condition was met, false on timeout. + public bool WaitForSave(int expectedMinSaves = 1, int timeoutMs = 2000) + { + var start = Environment.TickCount; + while (Volatile.Read(ref _saveCount) < expectedMinSaves) + { + if (Environment.TickCount - start > timeoutMs) + { + return false; + } + + Thread.Sleep(50); + } + + return true; } } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs new file mode 100644 index 0000000000..f42520e6de --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/FallbackRemoteDesktopItemTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Reflection; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +[TestClass] +public class FallbackRemoteDesktopItemTests +{ + private static readonly CompositeFormat OpenHostCompositeFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host); + + [TestMethod] + public void UpdateQuery_WhenMatchingConnectionExists_UsesConnectionName() + { + var connectionName = "my-rdp-server"; + + // Arrange + var setup = CreateFallback(connectionName); + var fallback = setup.Fallback; + + // Act + fallback.UpdateQuery("my-rdp-server"); + + // Assert + Assert.AreEqual(connectionName, fallback.Title); + var expectedSubtitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, connectionName); + Assert.AreEqual(expectedSubtitle, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNotNull(command); + Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name); + Assert.AreEqual(connectionName, GetCommandHost(command)); + } + + [TestMethod] + public void UpdateQuery_WhenQueryIsValidHostWithoutExistingConnection_UsesQuery() + { + // Arrange + var setup = CreateFallback(); + var fallback = setup.Fallback; + const string hostname = "test.corp"; + + // Act + fallback.UpdateQuery(hostname); + + // Assert + var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname); + Assert.AreEqual(expectedTitle, fallback.Title); + Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNotNull(command); + Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name); + Assert.AreEqual(hostname, GetCommandHost(command)); + } + + [TestMethod] + public void UpdateQuery_WhenQueryIsWhitespace_ResetsCommand() + { + // Arrange + var setup = CreateFallback("rdp-server-two"); + var fallback = setup.Fallback; + + // Act + fallback.UpdateQuery(" "); + + // Assert + Assert.AreEqual(string.Empty, fallback.Title); + Assert.AreEqual(string.Empty, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNull(command); + } + + [TestMethod] + public void UpdateQuery_WhenQueryIsInvalidHost_ClearsCommand() + { + // Arrange + var setup = CreateFallback("rdp-server-three"); + var fallback = setup.Fallback; + + // Act + fallback.UpdateQuery("not a valid host"); + + // Assert + Assert.AreEqual(string.Empty, fallback.Title); + Assert.AreEqual(string.Empty, fallback.Subtitle); + + var command = fallback.Command as OpenRemoteDesktopCommand; + Assert.IsNull(command); + } + + private static string GetCommandHost(OpenRemoteDesktopCommand command) + { + var field = typeof(OpenRemoteDesktopCommand).GetField("_rdpHost", BindingFlags.NonPublic | BindingFlags.Instance); + if (field is null) + { + return string.Empty; + } + + return field.GetValue(command) as string ?? string.Empty; + } + + private static (FallbackRemoteDesktopItem Fallback, IRdpConnectionsManager Manager) CreateFallback(params string[] connectionNames) + { + var settingsManager = new MockSettingsManager(connectionNames); + var connectionsManager = new MockRdpConnectionsManager(settingsManager); + + var fallback = new FallbackRemoteDesktopItem(connectionsManager); + + return (fallback, connectionsManager); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj new file mode 100644 index 0000000000..0b998ec4ad --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj @@ -0,0 +1,24 @@ + + + + + + false + true + Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\ + false + false + enable + + + + + + + + + + + + diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRdpConnectionsManager.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRdpConnectionsManager.cs new file mode 100644 index 0000000000..be1c961523 --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockRdpConnectionsManager.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +internal sealed class MockRdpConnectionsManager : IRdpConnectionsManager +{ + private readonly List _connections = new(); + + public IReadOnlyCollection Connections => _connections.AsReadOnly(); + + public MockRdpConnectionsManager(ISettingsInterface settingsManager) + { + _connections.AddRange(settingsManager.PredefinedConnections.Select(ConnectionHelpers.MapToResult)); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs new file mode 100644 index 0000000000..1a81dcc7ee --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/MockSettingsManager.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; +using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +internal sealed class MockSettingsManager : ISettingsInterface +{ + private readonly List _connections; + + public IReadOnlyCollection PredefinedConnections => _connections; + + public ToolkitSettings Settings { get; } = new(); + + public MockSettingsManager(params string[] predefinedConnections) + { + _connections = new(predefinedConnections); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RdpConnectionsManagerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RdpConnectionsManagerTests.cs new file mode 100644 index 0000000000..a8a48ba79c --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RdpConnectionsManagerTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +[TestClass] +public class RdpConnectionsManagerTests +{ + [TestMethod] + public void Constructor_AddsOpenCommandItem() + { + // Act + var manager = new RdpConnectionsManager(new MockSettingsManager(["test.local"])); + + // Assert + Assert.IsTrue(manager.Connections.Any(item => string.IsNullOrEmpty(item.ConnectionName))); + } + + [TestMethod] + public void FindConnection_ReturnsExactMatch() + { + // Arrange + var connectionName = "rdp-test"; + var connection = new ConnectionListItem(connectionName); + + // Act + var result = ConnectionHelpers.FindConnection(connectionName, new[] { connection }); + + // Assert + Assert.IsNotNull(result); + Assert.AreEqual(connectionName, result.ConnectionName); + } + + [TestMethod] + public void FindConnection_ReturnsNullForWhitespaceQuery() + { + // Arrange + var connection = new ConnectionListItem("rdp-test"); + + // Act + var result = ConnectionHelpers.FindConnection(" ", new[] { connection }); + + // Assert + Assert.IsNull(result); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs new file mode 100644 index 0000000000..54698997ee --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests/RemoteDesktopCommandProviderTests.cs @@ -0,0 +1,101 @@ +// 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.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Pages; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests; + +[TestClass] +public class RemoteDesktopCommandProviderTests +{ + [TestMethod] + public void ProviderHasCorrectId() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Assert + Assert.AreEqual("com.microsoft.cmdpal.builtin.remotedesktop", provider.Id); + } + + [TestMethod] + public void ProviderHasDisplayName() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Assert + Assert.IsNotNull(provider.DisplayName); + Assert.IsTrue(provider.DisplayName.Length > 0); + } + + [TestMethod] + public void ProviderHasIcon() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Assert + Assert.IsNotNull(provider.Icon); + } + + [TestMethod] + public void TopLevelCommandsNotEmpty() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.IsNotNull(commands); + Assert.IsTrue(commands.Length > 0); + } + + [TestMethod] + public void FallbackCommandsNotEmpty() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.FallbackCommands(); + + // Assert + Assert.IsNotNull(commands); + Assert.IsTrue(commands.Length > 0); + } + + [TestMethod] + public void TopLevelCommandsContainListPageCommand() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.TopLevelCommands(); + + // Assert + Assert.AreEqual(1, commands.Length); + Assert.IsInstanceOfType(commands.Single().Command, typeof(RemoteDesktopListPage)); + } + + [TestMethod] + public void FallbackCommandsContainFallbackItem() + { + // Setup + var provider = new RemoteDesktopCommandProvider(); + + // Act + var commands = provider.FallbackCommands(); + + // Assert + Assert.AreEqual(1, commands.Length); + Assert.IsInstanceOfType(commands.Single(), typeof(FallbackRemoteDesktopItem)); + } +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.System.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.System.UnitTests/QueryTests.cs index 5b11ba6e05..af17ad8ec3 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.System.UnitTests/QueryTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.System.UnitTests/QueryTests.cs @@ -25,7 +25,7 @@ public class QueryTests : CommandPaletteUnitTestBase [DataRow("hibernate", "Hibernate")] [DataRow("open recycle", "Open Recycle Bin")] [DataRow("empty recycle", "Empty Recycle Bin")] - [DataRow("uefi", "UEFI Firmware Settings")] + [DataRow("uefi", "UEFI firmware settings")] public void TopLevelPageQueryTest(string input, string matchedTitle) { var settings = new Settings(); @@ -143,6 +143,6 @@ public class QueryTests : CommandPaletteUnitTestBase Assert.IsNotNull(result); var firstItem = result.FirstOrDefault(); var firstItemIsUefiCommand = firstItem?.Title.Contains("UEFI", StringComparison.OrdinalIgnoreCase) ?? false; - Assert.AreEqual(hasCommand, firstItemIsUefiCommand, $"Expected to match (or not match) 'UEFI Firmware Settings' but got '{firstItem?.Title}'"); + Assert.AreEqual(hasCommand, firstItemIsUefiCommand, $"Expected to match (or not match) 'UEFI firmware settings' but got '{firstItem?.Title}'"); } } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.TimeDate.UnitTests/TimeDateCommandsProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.TimeDate.UnitTests/TimeDateCommandsProviderTests.cs index 7553ca8321..00f1af30a8 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.TimeDate.UnitTests/TimeDateCommandsProviderTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.TimeDate.UnitTests/TimeDateCommandsProviderTests.cs @@ -41,7 +41,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests // Assert Assert.IsNotNull(provider); Assert.IsNotNull(provider.DisplayName); - Assert.AreEqual("DateTime", provider.Id); + Assert.AreEqual("com.microsoft.cmdpal.builtin.datetime", provider.Id); Assert.IsNotNull(provider.Icon); Assert.IsNotNull(provider.Settings); } @@ -103,7 +103,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests // Assert Assert.IsFalse(string.IsNullOrEmpty(subtitle)); - Assert.IsTrue(subtitle.Contains("Provides time and date values in different formats")); + Assert.IsTrue(subtitle.Contains("Show time and date values in different formats")); } } } diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockBrowserInfoService.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockBrowserInfoService.cs new file mode 100644 index 0000000000..ee27aa737e --- /dev/null +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockBrowserInfoService.cs @@ -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 Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; + +namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests; + +public class MockBrowserInfoService : IBrowserInfoService +{ + public BrowserInfo GetDefaultBrowser() => new() { Name = "mocked browser", Path = "C:\\mockery\\mock.exe" }; +} diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockSettingsInterface.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockSettingsInterface.cs index a51db9165d..1e5f0533c7 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockSettingsInterface.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockSettingsInterface.cs @@ -18,6 +18,8 @@ public class MockSettingsInterface : ISettingsInterface public int HistoryItemCount { get; set; } + public string CustomSearchUri { get; } + public IReadOnlyList HistoryItems => _historyItems; public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List mockHistory = null) diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs index 00f1235c0e..63e35314cd 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs @@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase { // Setup var settings = new MockSettingsInterface(); + var browserInfoService = new MockBrowserInfoService(); - var page = new WebSearchListPage(settings); + var page = new WebSearchListPage(settings, browserInfoService); // Act page.UpdateSearchText(string.Empty, query); @@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase }; var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5); + var browserInfoService = new MockBrowserInfoService(); - var page = new WebSearchListPage(settings); + var page = new WebSearchListPage(settings, browserInfoService); // Act page.UpdateSearchText("abcdef", string.Empty); @@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase }; var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5); + var browserInfoService = new MockBrowserInfoService(); - var page = new WebSearchListPage(settings); + var page = new WebSearchListPage(settings, browserInfoService); mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture))); @@ -123,8 +126,9 @@ public class QueryTests : CommandPaletteUnitTestBase }; var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0); + var browserInfoService = new MockBrowserInfoService(); - var page = new WebSearchListPage(settings); + var page = new WebSearchListPage(settings, browserInfoService); // Act page.UpdateSearchText("abcdef", string.Empty); diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs index fd19427ca1..2ec5546daa 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/SettingsManagerTests.cs @@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase { // Setup var settings = new MockSettingsInterface(historyItemCount: 5); - var page = new WebSearchListPage(settings); + var browserInfoService = new MockBrowserInfoService(); + + var page = new WebSearchListPage(settings, browserInfoService); var eventRaised = false; diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/WebSearchCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/WebSearchCommandProviderTests.cs index c141d28d6e..ef8b56a1b8 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/WebSearchCommandProviderTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/WebSearchCommandProviderTests.cs @@ -16,7 +16,7 @@ public class WebSearchCommandProviderTests var provider = new WebSearchCommandsProvider(); // Assert - Assert.AreEqual("WebSearch", provider.Id); + Assert.AreEqual("com.microsoft.cmdpal.builtin.websearch", provider.Id); } [TestMethod] diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/BasicTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/BasicTests.cs index 78cd82062a..7fe4e5281d 100644 --- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/BasicTests.cs +++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/BasicTests.cs @@ -49,8 +49,8 @@ public class BasicTests : CommandPaletteTestBase { SetSearchBox("time and date"); - var searchFileItem = this.Find("Time and Date"); - Assert.AreEqual(searchFileItem.Name, "Time and Date"); + var searchFileItem = this.Find("Time and date"); + Assert.AreEqual(searchFileItem.Name, "Time and date"); searchFileItem.DoubleClick(); SetTimeAndDaterExtensionSearchBox("year"); @@ -63,8 +63,8 @@ public class BasicTests : CommandPaletteTestBase { SetSearchBox("Windows Terminal"); - var searchFileItem = this.Find("Open Windows Terminal Profiles"); - Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal Profiles"); + var searchFileItem = this.Find("Open Windows Terminal profiles"); + Assert.AreEqual(searchFileItem.Name, "Open Windows Terminal profiles"); searchFileItem.DoubleClick(); // SetSearchBox("PowerShell"); @@ -74,10 +74,10 @@ public class BasicTests : CommandPaletteTestBase [TestMethod] public void BasicWindowsSettingsTest() { - SetSearchBox("Windows Settings"); + SetSearchBox("Windows settings"); - var searchFileItem = this.Find("Windows Settings"); - Assert.AreEqual(searchFileItem.Name, "Windows Settings"); + var searchFileItem = this.Find("Windows settings"); + Assert.AreEqual(searchFileItem.Name, "Windows settings"); searchFileItem.DoubleClick(); SetSearchBox("power"); diff --git a/src/modules/cmdpal/custom.props b/src/modules/cmdpal/custom.props index 86541e31cc..2cfa0bbf4b 100644 --- a/src/modules/cmdpal/custom.props +++ b/src/modules/cmdpal/custom.props @@ -5,7 +5,7 @@ true 2025 0 - 6 + 7 Microsoft Command Palette diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs index b5ce5522bb..317087847e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; +using Microsoft.CmdPal.Ext.Apps.Helpers; +using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CommandPalette.Extensions; @@ -35,7 +37,6 @@ public partial class AllAppsCommandProvider : CommandProvider _listItem = new(_page) { - Subtitle = Resources.search_installed_apps, MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], }; @@ -67,7 +68,71 @@ public partial class AllAppsCommandProvider : CommandProvider public override ICommandItem[] TopLevelCommands() => [_listItem, .. _page.GetPinnedApps()]; - public ICommandItem? LookupApp(string displayName) + public ICommandItem? LookupAppByPackageFamilyName(string packageFamilyName, bool requireSingleMatch) + { + if (string.IsNullOrEmpty(packageFamilyName)) + { + return null; + } + + var items = _page.GetItems(); + List matches = []; + + foreach (var item in items) + { + if (item is AppListItem appItem && string.Equals(packageFamilyName, appItem.App.PackageFamilyName, StringComparison.OrdinalIgnoreCase)) + { + matches.Add(item); + if (!requireSingleMatch) + { + // Return early if we don't require uniqueness. + return item; + } + } + } + + return requireSingleMatch && matches.Count == 1 ? matches[0] : null; + } + + public ICommandItem? LookupAppByProductCode(string productCode, bool requireSingleMatch) + { + if (string.IsNullOrEmpty(productCode)) + { + return null; + } + + if (!UninstallRegistryAppLocator.TryGetInstallInfo(productCode, out _, out var candidates) || candidates.Count <= 0) + { + return null; + } + + var items = _page.GetItems(); + List matches = []; + + foreach (var item in items) + { + if (item is not AppListItem appListItem || string.IsNullOrEmpty(appListItem.App.FullExecutablePath)) + { + continue; + } + + foreach (var candidate in candidates) + { + if (string.Equals(appListItem.App.FullExecutablePath, candidate, StringComparison.OrdinalIgnoreCase)) + { + matches.Add(item); + if (!requireSingleMatch) + { + return item; + } + } + } + } + + return requireSingleMatch && matches.Count == 1 ? matches[0] : null; + } + + public ICommandItem? LookupAppByDisplayName(string displayName) { var items = _page.GetItems(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs index b7d01593cb..14f9597418 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs @@ -29,6 +29,10 @@ public sealed class AppItem public string AppIdentifier { get; set; } = string.Empty; + public string? PackageFamilyName { get; set; } + + public string? FullExecutablePath { get; set; } + public AppItem() { } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs index d97bde7037..5689b70698 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs @@ -15,8 +15,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Programs; public sealed partial class AppListItem : ListItem { - private static readonly Tag _appTag = new("App"); - private readonly AppCommand _appCommand; private readonly AppItem _app; private readonly Lazy
_details; @@ -42,13 +40,14 @@ public sealed partial class AppListItem : ListItem public string AppIdentifier => _app.AppIdentifier; + public AppItem App => _app; + public AppListItem(AppItem app, bool useThumbnails, bool isPinned) { Command = _appCommand = new AppCommand(app); _app = app; Title = app.Name; Subtitle = app.Subtitle; - Tags = [_appTag]; Icon = Icons.GenericAppIcon; MoreCommands = AddPinCommands(_app.Commands!, isPinned); @@ -85,6 +84,12 @@ public sealed partial class AppListItem : ListItem metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } }); } +#if DEBUG + metadata.Add(new DetailsElement() { Key = "[DEBUG] AppIdentifier", Data = new DetailsLink() { Text = _app.AppIdentifier } }); + metadata.Add(new DetailsElement() { Key = "[DEBUG] ExePath", Data = new DetailsLink() { Text = _app.ExePath } }); + metadata.Add(new DetailsElement() { Key = "[DEBUG] IcoPath", Data = new DetailsLink() { Text = _app.IcoPath } }); +#endif + // Icon IconInfo? heroImage = null; if (_app.IsPackaged) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/UninstallRegistryAppLocator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/UninstallRegistryAppLocator.cs new file mode 100644 index 0000000000..8e59a26395 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/UninstallRegistryAppLocator.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.Apps.Helpers; + +internal static class UninstallRegistryAppLocator +{ + private static readonly string[] UninstallBaseKeys = + [ + @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", + @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall", + ]; + + /// + /// Tries to find install directory and a list of plausible main EXEs from an uninstall key + /// (e.g. Inno Setup keys like "{guid}_is1"). + /// may be empty if we couldn't pick any safe EXEs. + /// + /// + /// Returns true if the uninstall key is found and an install directory is resolved. + /// + public static bool TryGetInstallInfo( + string uninstallKeyName, + out string? installDir, + out IReadOnlyList exeCandidates, + string? expectedExeName = null) + { + installDir = null; + exeCandidates = []; + + if (string.IsNullOrWhiteSpace(uninstallKeyName)) + { + throw new ArgumentException("Key name must not be null or empty.", nameof(uninstallKeyName)); + } + + uninstallKeyName = uninstallKeyName.Trim(); + + foreach (var baseKeyPath in UninstallBaseKeys) + { + // HKLM + using (var key = Registry.LocalMachine.OpenSubKey($"{baseKeyPath}\\{uninstallKeyName}")) + { + if (TryFromUninstallKey(key, expectedExeName, out installDir, out exeCandidates)) + { + return true; + } + } + + // HKCU + using (var key = Registry.CurrentUser.OpenSubKey($"{baseKeyPath}\\{uninstallKeyName}")) + { + if (TryFromUninstallKey(key, expectedExeName, out installDir, out exeCandidates)) + { + return true; + } + } + } + + return false; + } + + private static bool TryFromUninstallKey( + RegistryKey? key, + string? expectedExeName, + out string? installDir, + out IReadOnlyList exeCandidates) + { + installDir = null; + exeCandidates = []; + + if (key is null) + { + return false; + } + + var location = (key.GetValue("InstallLocation") as string)?.Trim('"', ' ', '\t'); + if (string.IsNullOrEmpty(location)) + { + location = (key.GetValue("Inno Setup: App Path") as string)?.Trim('"', ' ', '\t'); + } + + if (string.IsNullOrEmpty(location)) + { + var uninstall = key.GetValue("UninstallString") as string; + var uninsExe = ExtractFirstPath(uninstall); + if (!string.IsNullOrEmpty(uninsExe)) + { + var dir = Path.GetDirectoryName(uninsExe); + if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) + { + location = dir; + } + } + } + + if (string.IsNullOrEmpty(location) || !Directory.Exists(location)) + { + return false; + } + + installDir = location; + + // Collect safe EXE candidates; may be empty if ambiguous or only uninstall exes exist. + exeCandidates = GetExeCandidates(location, expectedExeName); + return true; + } + + private static IReadOnlyList GetExeCandidates(string root, string? expectedExeName) + { + // Look at root and a "bin" subfolder (very common pattern) + var allExes = Directory.EnumerateFiles(root, "*.exe", SearchOption.TopDirectoryOnly) + .Concat(GetBinExes(root)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (allExes.Length == 0) + { + return []; + } + + var result = new List(); + + // 1) Exact match on expected exe name (if provided), ignoring case, and not uninstall/setup-like. + if (!string.IsNullOrWhiteSpace(expectedExeName)) + { + foreach (var exe in allExes) + { + if (string.Equals(Path.GetFileName(exe), expectedExeName, StringComparison.OrdinalIgnoreCase) && + !LooksLikeUninstallerOrSetup(exe)) + { + result.Add(exe); + } + } + } + + // 2) All other non-uninstall/setup exes + foreach (var exe in allExes) + { + if (LooksLikeUninstallerOrSetup(exe)) + { + continue; + } + + // Skip ones already added as expectedExeName matches + if (result.Contains(exe, StringComparer.OrdinalIgnoreCase)) + { + continue; + } + + result.Add(exe); + } + + // 3) We intentionally do NOT add uninstall/setup/update exes here. + // If you ever want them, you can add a separate API to expose them. + return result; + } + + private static IEnumerable GetBinExes(string root) + { + var bin = Path.Combine(root, "bin"); + return !Directory.Exists(bin) + ? [] + : Directory.EnumerateFiles(bin, "*.exe", SearchOption.TopDirectoryOnly); + } + + private static bool LooksLikeUninstallerOrSetup(string path) + { + var name = Path.GetFileName(path); + return name.StartsWith("unins", StringComparison.OrdinalIgnoreCase) // e.g. Inno: unins000.exe + || name.Contains("setup", StringComparison.OrdinalIgnoreCase) // setup.exe + || name.Contains("installer", StringComparison.OrdinalIgnoreCase) // installer.exe / MyAppInstaller.exe + || name.Contains("update", StringComparison.OrdinalIgnoreCase); // updater/updater.exe + } + + private static string? ExtractFirstPath(string? commandLine) + { + if (string.IsNullOrWhiteSpace(commandLine)) + { + return null; + } + + commandLine = commandLine.Trim(); + + if (commandLine.StartsWith('"')) + { + var endQuote = commandLine.IndexOf('"', 1); + if (endQuote > 1) + { + return commandLine[1..endQuote]; + } + } + + var firstSpace = commandLine.IndexOf(' '); + var candidate = firstSpace > 0 ? commandLine[..firstSpace] : commandLine; + candidate = candidate.Trim('"'); + return candidate.Length > 0 ? candidate : null; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs index 37698d972d..4ec9598483 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs @@ -558,6 +558,7 @@ public class UWPApplication : IUWPApplication IsPackaged = true, Commands = app.GetCommands(), AppIdentifier = app.GetAppIdentifier(), + PackageFamilyName = app.Package.FamilyName, }; return item; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs index 9b89afc425..fda8e2c697 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs @@ -1065,6 +1065,7 @@ public class Win32Program : IProgram DirPath = app.Location, Commands = app.GetCommands(), AppIdentifier = app.GetAppIdentifier(), + FullExecutablePath = app.FullPath, }; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs index 15cc4d338e..9fdf833b0a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -61,7 +61,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties { } /// - /// Looks up a localized string similar to All Apps. + /// Looks up a localized string similar to Search apps. /// internal static string all_apps { get { @@ -313,16 +313,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties { } /// - /// Looks up a localized string similar to Search installed apps. - /// - internal static string search_installed_apps { - get { - return ResourceManager.GetString("search_installed_apps", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Search installed apps.... + /// Looks up a localized string similar to Search apps.... /// internal static string search_installed_apps_placeholder { get { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx index dae13838e6..758c61e20f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx @@ -124,14 +124,11 @@ Installed apps - - Search installed apps - - All Apps + Search apps - Search installed apps... + Search apps... Open path in console diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksManager.cs index 1eb57fb7eb..fde574360f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksManager.cs @@ -103,7 +103,33 @@ internal sealed partial class BookmarksManager : IDisposable, IBookmarksManager try { var jsonData = _dataSource.GetBookmarkData(); - _bookmarksData = _parser.ParseBookmarks(jsonData); + var bookmarksData = _parser.ParseBookmarks(jsonData); + + // Upgrade old bookmarks if necessary + // Pre .95 versions did not assign IDs to bookmarks + var upgraded = false; + for (var index = 0; index < bookmarksData.Data.Count; index++) + { + var bookmark = bookmarksData.Data[index]; + if (bookmark.Id == Guid.Empty) + { + bookmarksData.Data[index] = bookmark with { Id = Guid.NewGuid() }; + upgraded = true; + } + } + + lock (_lock) + { + _bookmarksData = bookmarksData; + } + + // LOAD BEARING: Save upgraded data back to file + // This ensures that old bookmarks are not repeatedly upgraded on each load, + // as the hotkeys and aliases are tied to the generated bookmark IDs. + if (upgraded) + { + _ = SaveChangesAsync(); + } } catch (Exception ex) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Pages/AddBookmarkForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Pages/AddBookmarkForm.cs index 6931064a90..e165bfd0d4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Pages/AddBookmarkForm.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Pages/AddBookmarkForm.cs @@ -30,25 +30,72 @@ internal sealed partial class AddBookmarkForm : FormContent "type": "Input.Text", "style": "text", "id": "bookmark", - "value": {{JsonSerializer.Serialize(url, BookmarkSerializationContext.Default.String)}}, - "label": "{{Resources.bookmarks_form_bookmark_label}}", + "value": {{EncodeString(url)}}, + "label": {{EncodeString(Resources.bookmarks_form_bookmark_label)}}, "isRequired": true, - "errorMessage": "{{Resources.bookmarks_form_bookmark_required}}" + "errorMessage": {{EncodeString(Resources.bookmarks_form_bookmark_required)}}, + "placeholder": {{EncodeString(Resources.bookmarks_form_bookmark_placeholder)}} }, { "type": "Input.Text", "style": "text", "id": "name", - "label": "{{Resources.bookmarks_form_name_label}}", - "value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}}, - "isRequired": false, - "errorMessage": "{{Resources.bookmarks_form_name_required}}" + "label": {{EncodeString(Resources.bookmarks_form_name_label)}}, + "value": {{EncodeString(name)}}, + "isRequired": false + }, + { + "type": "RichTextBlock", + "inlines": [ + { + "type": "TextRun", + "text": {{EncodeString(Resources.bookmarks_form_hint_text1)}}, + "isSubtle": true, + "size": "Small" + }, + { + "type": "TextRun", + "text": " ", + "isSubtle": true, + "size": "Small" + }, + { + "type": "TextRun", + "text": {{EncodeString(Resources.bookmarks_form_hint_text2)}}, + "fontType": "Monospace", + "size": "Small" + }, + { + "type": "TextRun", + "text": " ", + "isSubtle": true, + "size": "Small" + }, + { + "type": "TextRun", + "text": {{EncodeString(Resources.bookmarks_form_hint_text3)}}, + "isSubtle": true, + "size": "Small" + }, + { + "type": "TextRun", + "text": " ", + "isSubtle": true, + "size": "Small" + }, + { + "type": "TextRun", + "text": {{EncodeString(Resources.bookmarks_form_hint_text4)}}, + "fontType": "Monospace", + "size": "Small" + } + ] } ], "actions": [ { "type": "Action.Submit", - "title": "{{Resources.bookmarks_form_save}}", + "title": {{EncodeString(Resources.bookmarks_form_save)}}, "data": { "name": "name", "bookmark": "bookmark" @@ -59,6 +106,8 @@ internal sealed partial class AddBookmarkForm : FormContent """; } + private static string EncodeString(string s) => JsonSerializer.Serialize(s, BookmarkSerializationContext.Default.String); + public override CommandResult SubmitForm(string payload) { var formInput = JsonNode.Parse(payload); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Persistence/BookmarkData.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Persistence/BookmarkData.cs index 3129e1b578..b577f9cb35 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Persistence/BookmarkData.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Persistence/BookmarkData.cs @@ -19,7 +19,7 @@ public sealed record BookmarkData [SetsRequiredMembers] public BookmarkData(Guid id, string? name, string? bookmark) { - Id = id == Guid.Empty ? Guid.NewGuid() : id; + Id = id; Name = name ?? string.Empty; Bookmark = bookmark ?? string.Empty; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs index e5a65f2db3..02f95cf479 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -177,6 +177,15 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties { } } + /// + /// Looks up a localized string similar to Enter URL or file path, you can use {placeholders}, e.g. https://www.bing.com/search?q={Query}. + /// + public static string bookmarks_form_bookmark_placeholder { + get { + return ResourceManager.GetString("bookmarks_form_bookmark_placeholder", resourceCulture); + } + } + /// /// Looks up a localized string similar to URL or file path is required. /// @@ -187,7 +196,44 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties { } /// - /// Looks up a localized string similar to Name. + /// Looks up a localized string similar to You can add placeholders to bookmarks, and Command Palette will prompt you to enter their values when you open the bookmark. + ///A placeholder looks like this:. + /// + public static string bookmarks_form_hint_text1 { + get { + return ResourceManager.GetString("bookmarks_form_hint_text1", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {placeholder}. + /// + public static string bookmarks_form_hint_text2 { + get { + return ResourceManager.GetString("bookmarks_form_hint_text2", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to — for example:. + /// + public static string bookmarks_form_hint_text3 { + get { + return ResourceManager.GetString("bookmarks_form_hint_text3", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to https://www.bing.com/search?q={Query}. + /// + public static string bookmarks_form_hint_text4 { + get { + return ResourceManager.GetString("bookmarks_form_hint_text4", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name (optional). /// public static string bookmarks_form_name_label { get { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx index 763c697f2e..45f57c0d77 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx @@ -140,7 +140,7 @@ "Terminal" should be the localized name of the Windows Terminal - Name + Name (optional) Save @@ -185,4 +185,20 @@ Failed to open {0} + + You can add placeholders to bookmarks, and Command Palette will prompt you to enter their values when you open the bookmark. +A placeholder looks like this: + + + {placeholder} + + + — for example: + + + https://www.bing.com/search?q={Query} + + + Enter URL or file path, you can use {placeholders}, e.g. https://www.bing.com/search?q={Query} + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs index 2b408f24dc..5e044247ba 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/FallbackOpenFileItem.cs @@ -120,7 +120,6 @@ internal sealed partial class FallbackOpenFileItem : FallbackCommandItem, System var indexerPage = new IndexerPage(query, _searchEngine, _queryCookie, results); Title = string.Format(CultureInfo.CurrentCulture, fallbackItemSearchPageTitleCompositeFormat, query); Icon = Icons.FileExplorerIcon; - Subtitle = Resources.Indexer_Subtitle; Command = indexerPage; return; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Icons.cs index f57b8a2d07..bd9759514c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Icons.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Icons.cs @@ -6,7 +6,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.Indexer; -internal sealed class Icons +internal static class Icons { internal static IconInfo FileExplorerSegoeIcon { get; } = new("\uEC50"); @@ -19,4 +19,8 @@ internal sealed class Icons internal static IconInfo DocumentIcon { get; } = new("\uE8A5"); // Document internal static IconInfo FolderOpenIcon { get; } = new("\uE838"); // FolderOpen + + internal static IconInfo FilesIcon { get; } = new("\uF571"); // PrintAllPages + + internal static IconInfo FilterIcon { get; } = new("\uE71C"); // Filter } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchFilters.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchFilters.cs new file mode 100644 index 0000000000..bf2e5dc451 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Indexer/SearchFilters.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Indexer.Indexer; + +internal sealed partial class SearchFilters : Filters +{ + public SearchFilters() + { + CurrentFilterId = "all"; + } + + public override IFilterItem[] GetFilters() + { + return [ + new Filter() { Id = "all", Name = Resources.Indexer_Filter_All, Icon = Icons.FilterIcon }, + new Separator(), + new Filter() { Id = "folders", Name = Resources.Indexer_Filter_Folders_Only, Icon = Icons.FolderOpenIcon }, + new Filter() { Id = "files", Name = Resources.Indexer_Filter_Files_Only, Icon = Icons.FilesIcon }, + ]; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs index d2ea9b9b0c..ca1f215d4c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/IndexerCommandsProvider.cs @@ -33,7 +33,6 @@ public partial class IndexerCommandsProvider : CommandProvider new CommandItem(new IndexerPage()) { Title = Resources.Indexer_Title, - Subtitle = Resources.Indexer_Subtitle, } ]; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs index 3b09fcf149..41abc0b018 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Pages/IndexerPage.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Globalization; using System.Text.Encodings.Web; using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Indexer.Indexer; using Microsoft.CmdPal.Ext.Indexer.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -36,6 +37,11 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable PlaceholderText = Resources.Indexer_PlaceholderText; _searchEngine = new(); _queryCookie = 10; + + var filters = new SearchFilters(); + filters.PropChanged += Filters_PropChanged; + Filters = filters; + CreateEmptyContent(); } @@ -49,6 +55,11 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable initialQuery = query; SearchText = query; disposeSearchEngine = false; + + var filters = new SearchFilters(); + filters.PropChanged += Filters_PropChanged; + Filters = filters; + CreateEmptyContent(); } @@ -57,7 +68,6 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable _noSearchEmptyContent = new CommandItem(new NoOpCommand()) { Icon = Icon, - Title = Resources.Indexer_Subtitle, Subtitle = Resources.Indexer_NoSearchQueryMessageTip, }; @@ -79,30 +89,56 @@ internal sealed partial class IndexerPage : DynamicListPage, IDisposable { // {20D04FE0-3AEA-1069-A2D8-08002B30309D} is CLSID for "This PC" const string template = "search-ms:query={0}&crumb=location:::{{20D04FE0-3AEA-1069-A2D8-08002B30309D}}"; - var encodedSearchText = UrlEncoder.Default.Encode(SearchText); + var fullSearchText = FullSearchString(SearchText); + var encodedSearchText = UrlEncoder.Default.Encode(fullSearchText); var command = string.Format(CultureInfo.CurrentCulture, template, encodedSearchText); ShellHelpers.OpenInShell(command); } public override ICommandItem EmptyContent => _isEmptyQuery ? _noSearchEmptyContent : _nothingFoundEmptyContent; + private void Filters_PropChanged(object sender, IPropChangedEventArgs args) + { + PerformSearch(SearchText); + } + public override void UpdateSearchText(string oldSearch, string newSearch) { if (oldSearch != newSearch && newSearch != initialQuery) { - _ = Task.Run(() => - { - _isEmptyQuery = string.IsNullOrWhiteSpace(newSearch); - Query(newSearch); - LoadMore(); - OnPropertyChanged(nameof(EmptyContent)); - initialQuery = null; - }); + PerformSearch(newSearch); } } + private void PerformSearch(string newSearch) + { + var actualSearch = FullSearchString(newSearch); + _ = Task.Run(() => + { + _isEmptyQuery = string.IsNullOrWhiteSpace(actualSearch); + Query(actualSearch); + LoadMore(); + OnPropertyChanged(nameof(EmptyContent)); + initialQuery = null; + }); + } + public override IListItem[] GetItems() => [.. _indexerListItems]; + private string FullSearchString(string query) + { + switch (Filters.CurrentFilterId) + { + case "folders": + return $"{query} kind:folders"; + case "files": + return $"{query} kind:NOT folders"; + case "all": + default: + return query; + } + } + public override void LoadMore() { IsLoading = true; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs index 44b87b05e2..4394cd6697 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.Designer.cs @@ -177,6 +177,33 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties { } } + /// + /// Looks up a localized string similar to Files and folders. + /// + internal static string Indexer_Filter_All { + get { + return ResourceManager.GetString("Indexer_Filter_All", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Files. + /// + internal static string Indexer_Filter_Files_Only { + get { + return ResourceManager.GetString("Indexer_Filter_Files_Only", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Folders. + /// + internal static string Indexer_Filter_Folders_Only { + get { + return ResourceManager.GetString("Indexer_Filter_Folders_Only", resourceCulture); + } + } + /// /// Looks up a localized string similar to Find file from path. /// @@ -268,15 +295,6 @@ namespace Microsoft.CmdPal.Ext.Indexer.Properties { } } - /// - /// Looks up a localized string similar to Search files on this device. - /// - internal static string Indexer_Subtitle { - get { - return ResourceManager.GetString("Indexer_Subtitle", resourceCulture); - } - } - /// /// Looks up a localized string similar to Search files. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx index 66504abed1..9ea1c4563e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Properties/Resources.resx @@ -171,9 +171,6 @@ Only when file path exist - - Search files on this device - Search files @@ -196,4 +193,13 @@ You can try searching all files on this PC or adjust your indexing settings. Search all files + + Files and folders + + + Folders + + + Files + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs index 22eca4cc3f..5d68f013a4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/RegistryCommandsProvider.cs @@ -25,8 +25,7 @@ public partial class RegistryCommandsProvider : CommandProvider return [ new CommandItem(new RegistryListPage(_settingsManager)) { - Title = "Registry", - Subtitle = "Navigate the Windows registry", + Title = "Browse the Windows registry", } ]; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png new file mode 100644 index 0000000000..52d97dbfe9 Binary files /dev/null and b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.png differ diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.svg b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.svg new file mode 100644 index 0000000000..e683f4d040 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Assets/RemoteDesktop.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs new file mode 100644 index 0000000000..888a1d2f71 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/ConnectionListItem.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +internal sealed partial class ConnectionListItem : ListItem +{ + public ConnectionListItem(string connectionName) + { + ConnectionName = connectionName; + + if (string.IsNullOrEmpty(connectionName)) + { + Title = Resources.remotedesktop_open_rdp; + Subtitle = Resources.remotedesktop_subtitle; + } + else + { + Title = connectionName; + CompositeFormat remoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host); + Subtitle = string.Format(CultureInfo.CurrentCulture, remoteDesktopOpenHostFormat, connectionName); + } + + Icon = Icons.RDPIcon; + Command = new OpenRemoteDesktopCommand(connectionName); + } + + public string ConnectionName { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs new file mode 100644 index 0000000000..e3692a763b --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/FallbackRemoteDesktopItem.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem +{ + private const string _id = "com.microsoft.cmdpal.builtin.remotedesktop.fallback"; + + private static readonly UriHostNameType[] ValidUriHostNameTypes = [ + UriHostNameType.IPv6, + UriHostNameType.IPv4, + UriHostNameType.Dns + ]; + + private static readonly CompositeFormat RemoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host); + + private readonly IRdpConnectionsManager _rdpConnectionsManager; + private readonly NoOpCommand _emptyCommand = new NoOpCommand(); + + public FallbackRemoteDesktopItem(IRdpConnectionsManager rdpConnectionsManager) + : base(Resources.remotedesktop_title) + { + _rdpConnectionsManager = rdpConnectionsManager; + + Command = _emptyCommand; + Title = string.Empty; + Subtitle = string.Empty; + Icon = Icons.RDPIcon; + } + + public override void UpdateQuery(string query) + { + if (string.IsNullOrWhiteSpace(query)) + { + Title = string.Empty; + Subtitle = string.Empty; + Command = _emptyCommand; + return; + } + + var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName)); + + var queryConnection = ConnectionHelpers.FindConnection(query, connections); + + if (queryConnection is not null && !string.IsNullOrWhiteSpace(queryConnection.ConnectionName)) + { + var connectionName = queryConnection.ConnectionName; + + Command = new OpenRemoteDesktopCommand(connectionName); + Title = connectionName; + Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName); + } + else if (ValidUriHostNameTypes.Contains(Uri.CheckHostName(query))) + { + var connectionName = query.Trim(); + Command = new OpenRemoteDesktopCommand(connectionName); + Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName); + Subtitle = Resources.remotedesktop_title; + } + else + { + Title = string.Empty; + Subtitle = string.Empty; + Command = _emptyCommand; + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs new file mode 100644 index 0000000000..39c53fe9d2 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Commands/OpenRemoteDesktopCommand.cs @@ -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; +using System.Diagnostics; +using System.Text; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +internal sealed partial class OpenRemoteDesktopCommand : BaseObservable, IInvokableCommand +{ + private static readonly CompositeFormat ProcessErrorFormat = + CompositeFormat.Parse(Resources.remotedesktop_log_mstsc_error); + + private static readonly CompositeFormat InvalidHostnameFormat = + CompositeFormat.Parse(Resources.remotedesktop_log_invalid_hostname); + + public string Name { get; } + + public string Id { get; } = "com.microsoft.cmdpal.builtin.remotedesktop.openrdp"; + + public IIconInfo Icon => Icons.RDPIcon; + + private readonly string _rdpHost; + + public OpenRemoteDesktopCommand(string rdpHost) + { + _rdpHost = rdpHost; + + Name = string.IsNullOrWhiteSpace(_rdpHost) ? + Resources.remotedesktop_command_open : + Resources.remotedesktop_command_connect; + } + + public ICommandResult Invoke(object sender) + { + using var process = new Process(); + process.StartInfo.UseShellExecute = false; + process.StartInfo.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + process.StartInfo.FileName = "mstsc"; + + if (!string.IsNullOrWhiteSpace(_rdpHost)) + { + // validate that _rdpHost is a proper hostname or IP address + if (Uri.CheckHostName(_rdpHost) == UriHostNameType.Unknown) + { + return CommandResult.ShowToast(new ToastArgs() + { + Message = string.Format( + System.Globalization.CultureInfo.CurrentCulture, + InvalidHostnameFormat, + _rdpHost), + Result = CommandResult.KeepOpen(), + }); + } + + process.StartInfo.Arguments = $"/v:{_rdpHost}"; + } + + try + { + process.Start(); + + return CommandResult.Dismiss(); + } + catch (Exception ex) + { + return CommandResult.ShowToast(new ToastArgs() + { + Message = string.Format( + System.Globalization.CultureInfo.CurrentCulture, + ProcessErrorFormat, + ex.Message), + Result = CommandResult.KeepOpen(), + }); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs new file mode 100644 index 0000000000..5fac986169 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/ConnectionHelpers.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper; + +internal static class ConnectionHelpers +{ + public static ConnectionListItem MapToResult(string item) => new(item); + + public static ConnectionListItem? FindConnection(string query, IEnumerable connections) + { + if (string.IsNullOrWhiteSpace(query)) + { + return null; + } + + var matchedConnection = ListHelpers.FilterList( + connections, + query, + (s, i) => ListHelpers.ScoreListItem(s, i)) + .FirstOrDefault(); + return matchedConnection; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRdpConnectionsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRdpConnectionsManager.cs new file mode 100644 index 0000000000..2968e15c9c --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/IRdpConnectionsManager.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper; + +internal interface IRdpConnectionsManager +{ + IReadOnlyCollection Connections { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RdpConnectionsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RdpConnectionsManager.cs new file mode 100644 index 0000000000..6e357e27d9 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Helper/RdpConnectionsManager.cs @@ -0,0 +1,89 @@ +// 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.Threading; +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; +using Microsoft.Win32; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper; + +internal class RdpConnectionsManager : IRdpConnectionsManager +{ + private readonly ISettingsInterface _settingsManager; + private readonly ConnectionListItem _openRdpCommandListItem = new(string.Empty); + + private ReadOnlyCollection _connections = new(Array.Empty()); + + private const int MinutesToCache = 1; + private DateTime? _connectionsLastLoaded; + + public RdpConnectionsManager(ISettingsInterface settingsManager) + { + _settingsManager = settingsManager; + _settingsManager.Settings.SettingsChanged += (s, e) => + { + _connectionsLastLoaded = null; + }; + } + + public IReadOnlyCollection Connections + { + get + { + if (!_connectionsLastLoaded.HasValue || + (DateTime.Now - _connectionsLastLoaded.Value).TotalMinutes >= MinutesToCache) + { + var registryConnections = GetRdpConnectionsFromRegistry(); + var predefinedConnections = GetPredefinedConnectionsFromSettings(); + _connectionsLastLoaded = DateTime.Now; + + var newConnections = new List(registryConnections.Count + predefinedConnections.Count + 1); + newConnections.AddRange(registryConnections); + newConnections.AddRange(predefinedConnections); + newConnections.Insert(0, _openRdpCommandListItem); + + Interlocked.Exchange(ref _connections, new ReadOnlyCollection(newConnections)); + } + + return _connections; + } + } + + private List GetRdpConnectionsFromRegistry() + { + using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Terminal Server Client\Default"); + + var validConnections = new List(); + + if (key is not null) + { + validConnections = key.GetValueNames() + .Select(name => key.GetValue(name)) + .OfType() // Keep only string values + .Select(v => v.Trim()) // Normalize + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Distinct() // Remove dupes if any + .Select(ConnectionHelpers.MapToResult) + .ToList(); + } + + return validConnections; + } + + private List GetPredefinedConnectionsFromSettings() + { + var validConnections = _settingsManager.PredefinedConnections + .Select(s => s.Trim()) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(ConnectionHelpers.MapToResult) + .ToList(); + + return validConnections; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs new file mode 100644 index 0000000000..eec9e48e24 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Icons.cs @@ -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 Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop; + +internal static class Icons +{ + internal static IconInfo RDPIcon { get; } = IconHelpers.FromRelativePath("Assets\\RemoteDesktop.svg"); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj new file mode 100644 index 0000000000..2a561b9b9e --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Microsoft.CmdPal.Ext.RemoteDesktop.csproj @@ -0,0 +1,44 @@ + + + + + + Microsoft.CmdPal.Ext.RemoteDesktop + $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal + false + false + + Microsoft.CmdPal.Ext.RemoteDesktop.pri + enable + + + + + + + + + Resources.resx + True + True + + + + + Resources.Designer.cs + PublicResXFileCodeGenerator + + + + + + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs new file mode 100644 index 0000000000..c6ba2b3187 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Pages/RemoteDesktopListPage.cs @@ -0,0 +1,27 @@ +// 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.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages; + +internal sealed partial class RemoteDesktopListPage : ListPage +{ + private readonly IRdpConnectionsManager _rdpConnectionsManager; + + public RemoteDesktopListPage(IRdpConnectionsManager rdpConnectionsManager) + { + Icon = Icons.RDPIcon; + Name = Resources.remotedesktop_title; + Id = "com.microsoft.cmdpal.builtin.remotedesktop"; + + _rdpConnectionsManager = rdpConnectionsManager; + } + + public override IListItem[] GetItems() => _rdpConnectionsManager.Connections.ToArray(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..4a6c84ddea --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests")] diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..de0b924c33 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.RemoteDesktop.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Connect. + /// + public static string remotedesktop_command_connect { + get { + return ResourceManager.GetString("remotedesktop_command_connect", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open. + /// + public static string remotedesktop_command_open { + get { + return ResourceManager.GetString("remotedesktop_command_open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address.. + /// + public static string remotedesktop_log_invalid_hostname { + get { + return ResourceManager.GetString("remotedesktop_log_invalid_hostname", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}. + /// + public static string remotedesktop_log_mstsc_error { + get { + return ResourceManager.GetString("remotedesktop_log_mstsc_error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connect to {0}. + /// + public static string remotedesktop_open_host { + get { + return ResourceManager.GetString("remotedesktop_open_host", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open Remote Desktop Client. + /// + public static string remotedesktop_open_rdp { + get { + return ResourceManager.GetString("remotedesktop_open_rdp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A list of connections to include in the query results by default. + /// + public static string remotedesktop_settings_predefined_connections_description { + get { + return ResourceManager.GetString("remotedesktop_settings_predefined_connections_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Predefined connections. + /// + public static string remotedesktop_settings_predefined_connections_title { + get { + return ResourceManager.GetString("remotedesktop_settings_predefined_connections_title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Establish Remote Desktop connections. + /// + public static string remotedesktop_subtitle { + get { + return ResourceManager.GetString("remotedesktop_subtitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remote Desktop. + /// + public static string remotedesktop_title { + get { + return ResourceManager.GetString("remotedesktop_title", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx new file mode 100644 index 0000000000..bfbf1d3ac5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Properties/Resources.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Remote Desktop + + + Establish Remote Desktop connections + + + Open + + + Connect to {0} + + + Connect + + + Open Remote Desktop Client + + + Predefined connections + + + A list of connections to include in the query results by default + + + Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0} + + + The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address. + + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs new file mode 100644 index 0000000000..1ce307b301 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/RemoteDesktopCommandProvider.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CmdPal.Ext.RemoteDesktop.Commands; +using Microsoft.CmdPal.Ext.RemoteDesktop.Helper; +using Microsoft.CmdPal.Ext.RemoteDesktop.Pages; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CmdPal.Ext.RemoteDesktop.Settings; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop; + +public partial class RemoteDesktopCommandProvider : CommandProvider +{ + private readonly CommandItem listPageCommand; + private readonly FallbackRemoteDesktopItem fallback; + + public RemoteDesktopCommandProvider() + { + Id = "com.microsoft.cmdpal.builtin.remotedesktop"; + DisplayName = Resources.remotedesktop_title; + Icon = Icons.RDPIcon; + + var settingsManager = new SettingsManager(); + var rdpConnectionsManager = new RdpConnectionsManager(settingsManager); + var listPage = new RemoteDesktopListPage(rdpConnectionsManager); + + fallback = new FallbackRemoteDesktopItem(rdpConnectionsManager); + + listPageCommand = new CommandItem(listPage) + { + Subtitle = Resources.remotedesktop_subtitle, + Icon = Icons.RDPIcon, + MoreCommands = [ + new CommandContextItem(settingsManager.Settings.SettingsPage), + ], + }; + } + + public override ICommandItem[] TopLevelCommands() => [listPageCommand]; + + public override IFallbackCommandItem[] FallbackCommands() => [fallback]; +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs new file mode 100644 index 0000000000..dbca0d3833 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/ISettingsInterface.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings; + +internal interface ISettingsInterface +{ + public IReadOnlyCollection PredefinedConnections { get; } + + public ToolkitSettings Settings { get; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs new file mode 100644 index 0000000000..1469e448d7 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.RemoteDesktop/Settings/SettingsManager.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CmdPal.Ext.RemoteDesktop.Properties; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings; + +internal class SettingsManager : JsonSettingsManager, ISettingsInterface +{ + // Line break character used in WinUI3 TextBox and TextBlock. + private const char TEXTBOXNEWLINE = '\r'; + + private static readonly string _namespace = "com.microsoft.cmdpal.builtin.remotedesktop"; + + private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}"; + + private readonly TextSetting _predefinedConnections = new( + Namespaced(nameof(PredefinedConnections)), + Resources.remotedesktop_settings_predefined_connections_title, + Resources.remotedesktop_settings_predefined_connections_description, + string.Empty) + { + Multiline = true, + Placeholder = $"server1.domain.com{TEXTBOXNEWLINE}server2.domain.com{TEXTBOXNEWLINE}192.168.1.1", + }; + + public IReadOnlyCollection PredefinedConnections => _predefinedConnections.Value?.Split(TEXTBOXNEWLINE).ToList() ?? []; + + internal static string SettingsJsonPath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + + return Path.Combine(directory, "settings.json"); + } + + public SettingsManager() + { + FilePath = SettingsJsonPath(); + + Settings.Add(_predefinedConnections); + + // Load settings from file upon initialization + LoadSettings(); + + Settings.SettingsChanged += (s, a) => this.SaveSettings(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs index 4200c3050a..e5949f8a02 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs @@ -97,7 +97,7 @@ namespace Microsoft.CmdPal.Ext.Shell.Properties { } /// - /// Looks up a localized string similar to Executes commands (e.g. 'ping', 'cmd'). + /// Looks up a localized string similar to Execute system commands like 'ping' and 'cmd'. /// public static string cmd_plugin_description { get { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.resx index a2f4cfb64f..991869b8da 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Properties/Resources.resx @@ -121,7 +121,7 @@ Run commands - Executes commands (e.g. 'ping', 'cmd') + Execute system commands like 'ping' and 'cmd' this command has been executed {0} times diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs index f2ec8c1218..0b1e1fe121 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs @@ -160,7 +160,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to Adapter Details. + /// Looks up a localized string similar to Adapter details. /// public static string Microsoft_plugin_ext_adapter_details { get { @@ -169,7 +169,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to Connection Details. + /// Looks up a localized string similar to Connection details. /// public static string Microsoft_plugin_ext_connection_details { get { @@ -187,7 +187,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to Open System Command. + /// Looks up a localized string similar to Open system command. /// public static string Microsoft_plugin_ext_fallback_display_title { get { @@ -205,7 +205,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to System Commands. + /// Looks up a localized string similar to System commands. /// public static string Microsoft_plugin_ext_system_page_name { get { @@ -214,7 +214,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to Windows System Commands. + /// Looks up a localized string similar to Windows system commands. /// public static string Microsoft_plugin_ext_system_page_title { get { @@ -628,7 +628,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to You are about to restart this computer, are you sure?. + /// Looks up a localized string similar to You are about to restart this computer. Are you sure?. /// public static string Microsoft_plugin_sys_restart_computer_confirmation { get { @@ -790,7 +790,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to DNS Suffix. + /// Looks up a localized string similar to DNS suffix. /// public static string Microsoft_plugin_sys_Suffix { get { @@ -817,7 +817,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to UEFI Firmware Settings. + /// Looks up a localized string similar to UEFI firmware settings. /// public static string Microsoft_plugin_sys_uefi { get { @@ -826,7 +826,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to You are about to reboot this computer into UEFI Firmware Settings menu, are you sure?. + /// Looks up a localized string similar to You are about to reboot this computer into UEFI firmware settings menu, are you sure?. /// public static string Microsoft_plugin_sys_uefi_confirmation { get { @@ -835,7 +835,7 @@ namespace Microsoft.CmdPal.Ext.System { } /// - /// Looks up a localized string similar to Reboot computer into UEFI Firmware Settings (Requires administrative permissions.). + /// Looks up a localized string similar to Reboot computer into UEFI firmware Settings (requires administrative permissions.). /// public static string Microsoft_plugin_sys_uefi_description { get { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx index bf7ab04da0..bc8ea5ec38 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx @@ -149,10 +149,10 @@ Shutdown - Connection Details + Connection details - Adapter Details + Adapter details Copy to clipboard @@ -161,10 +161,10 @@ Hide disconnected network info - Windows System Commands + Windows system commands - System Commands + System commands Adapter name @@ -327,7 +327,7 @@ This should align to the action in Windows of a restarting your computer. - You are about to restart this computer, are you sure? + You are about to restart this computer. Are you sure? This should align to the action in Windows of a restarting your computer. @@ -381,7 +381,7 @@ State - DNS Suffix + DNS suffix Tunnel @@ -391,15 +391,15 @@ Means type like category. Here it means network interface type (ethernet, wifi, ...). - UEFI Firmware Settings + UEFI firmware settings This should align to the action in Windows Recovery Environment that restart into uefi settings. - You are about to reboot this computer into UEFI Firmware Settings menu, are you sure? + You are about to reboot this computer into UEFI firmware settings menu, are you sure? This should align to the action in Windows Recovery Environment that restart into uefi settings. - Reboot computer into UEFI Firmware Settings (Requires administrative permissions.) + Reboot computer into UEFI firmware Settings (requires administrative permissions.) This should align to the action in Windows Recovery Environment that restart into uefi settings. @@ -415,7 +415,7 @@ Sleep - Open System Command + Execute system commands Restart Windows Explorer diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs index 54ec578dfa..4bc86c209d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/SystemCommandExtensionProvider.cs @@ -9,7 +9,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.System; -public partial class SystemCommandExtensionProvider : CommandProvider +public sealed partial class SystemCommandExtensionProvider : CommandProvider { private readonly ICommandItem[] _commands; private static readonly SettingsManager _settingsManager = new(); @@ -19,7 +19,7 @@ public partial class SystemCommandExtensionProvider : CommandProvider public SystemCommandExtensionProvider() { DisplayName = Resources.Microsoft_plugin_ext_system_page_name; - Id = "System"; + Id = "com.microsoft.cmdpal.builtin.system"; _commands = [ new CommandItem(Page) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs index 9b820bbff8..2884cbbad2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs @@ -17,11 +17,6 @@ public sealed partial class TimeDateCalculator /// private const string InputDelimiter = "::"; - /// - /// A list of conjunctions that we ignore on search - /// - private static readonly string[] _conjunctionList = Resources.Microsoft_plugin_timedate_Search_ConjunctionList.Split("; "); - /// /// Searches for results /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs index ef4f2ede35..e3443c3ee6 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs @@ -349,7 +349,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate { } /// - /// Looks up a localized string similar to Time and Date. + /// Looks up a localized string similar to Time and date. /// public static string Microsoft_plugin_timedate_main_page_title { get { @@ -448,7 +448,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate { } /// - /// Looks up a localized string similar to Provides time and date values in different formats. + /// Looks up a localized string similar to Show time and date values in different formats. /// public static string Microsoft_plugin_timedate_plugin_description { get { @@ -484,7 +484,7 @@ namespace Microsoft.CmdPal.Ext.TimeDate { } /// - /// Looks up a localized string similar to Time and Date. + /// Looks up a localized string similar to Time and date. /// public static string Microsoft_plugin_timedate_plugin_name { get { @@ -501,15 +501,6 @@ namespace Microsoft.CmdPal.Ext.TimeDate { } } - /// - /// Looks up a localized string similar to "for; and; nor; but; or; so". - /// - public static string Microsoft_plugin_timedate_Search_ConjunctionList { - get { - return ResourceManager.GetString("Microsoft_plugin_timedate_Search_ConjunctionList", resourceCulture); - } - } - /// /// Looks up a localized string similar to Date and time; Time and Date; Custom format. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx index 35862592ca..eb248e3b1a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx @@ -202,7 +202,7 @@ 'UTC' means here 'Universal Time Convention' - Provides time and date values in different formats + Show time and date values in different formats Do not translate the placeholders like '{0}' because it will be replaced in code. @@ -215,7 +215,7 @@ Time - Time and Date + Time and date RFC1123 @@ -252,10 +252,6 @@ Current Time; Now Don't change order - - for; and; nor; but; or; so - List of conjunctions. We don't add 'yet' because this can be a synonym of 'now' which might be problematic on localized searches. - Second @@ -358,7 +354,7 @@ Error: Invalid input - Time and Date + Time and date A {0}format name{0}, a {0}valid date or time value{0}, or a {0}prefixed number{0}. To search for a format in a specific date/time please use the syntax {0}format::date/time/number{0}.{1}Supported prefixes:{2}'{0}u{0}' for Unix Timestamp{2}'{0}ums{0}' for Unix Timestamp in milliseconds{2}'{0}ft{0}' for Windows file time{2}'{0}oa{0}' for OLE Automation Date{2}'{0}exc{0}' for Excel's 1900 date value{2}'{0}exf{0}' for Excel's 1904 date value diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs index 26bd4d8453..0ad8c339ff 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs @@ -12,7 +12,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.TimeDate; -public partial class TimeDateCommandsProvider : CommandProvider +public sealed partial class TimeDateCommandsProvider : CommandProvider { private readonly CommandItem _command; private static readonly SettingsManager _settingsManager = new SettingsManager(); @@ -23,7 +23,7 @@ public partial class TimeDateCommandsProvider : CommandProvider public TimeDateCommandsProvider() { DisplayName = Resources.Microsoft_plugin_timedate_plugin_name; - Id = "DateTime"; + Id = "com.microsoft.cmdpal.builtin.datetime"; _command = new CommandItem(_timeDateExtensionPage) { Icon = _timeDateExtensionPage.Icon, diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs index 08d0a114f5..937be16ac2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/OpenURLCommand.cs @@ -2,32 +2,28 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; using Microsoft.CommandPalette.Extensions.Toolkit; -using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo; - namespace Microsoft.CmdPal.Ext.WebSearch.Commands; internal sealed partial class OpenURLCommand : InvokableCommand { + private readonly IBrowserInfoService _browserInfoService; + public string Url { get; internal set; } = string.Empty; - internal OpenURLCommand(string url) + internal OpenURLCommand(string url, IBrowserInfoService browserInfoService) { + _browserInfoService = browserInfoService; Url = url; - BrowserInfo.UpdateIfTimePassed(); Icon = Icons.WebSearch; Name = string.Empty; } public override CommandResult Invoke() { - if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}")) - { - // TODO GH# 138 --> actually display feedback from the extension somewhere. - return CommandResult.KeepOpen(); - } - - return CommandResult.Dismiss(); + // TODO GH# 138 --> actually display feedback from the extension somewhere. + return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen(); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs index 2cc8953048..1f5fdb8598 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Commands/SearchWebCommand.cs @@ -4,36 +4,39 @@ using System; using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; -using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo; - namespace Microsoft.CmdPal.Ext.WebSearch.Commands; internal sealed partial class SearchWebCommand : InvokableCommand { private readonly ISettingsInterface _settingsManager; + private readonly IBrowserInfoService _browserInfoService; - public string Arguments { get; internal set; } = string.Empty; + public string Arguments { get; internal set; } - internal SearchWebCommand(string arguments, ISettingsInterface settingsManager) + internal SearchWebCommand(string arguments, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService) { Arguments = arguments; - BrowserInfo.UpdateIfTimePassed(); Icon = Icons.WebSearch; - Name = Properties.Resources.open_in_default_browser; + Name = Resources.open_in_default_browser; _settingsManager = settingsManager; + _browserInfoService = browserInfoService; } public override CommandResult Invoke() { - if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"? {Arguments}")) + var uri = BuildUri(); + + if (!_browserInfoService.Open(uri)) { // TODO GH# 138 --> actually display feedback from the extension somewhere. return CommandResult.KeepOpen(); } + // remember only the query, not the full URI if (_settingsManager.HistoryItemCount != 0) { _settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now)); @@ -41,4 +44,28 @@ internal sealed partial class SearchWebCommand : InvokableCommand return CommandResult.Dismiss(); } + + private string BuildUri() + { + if (string.IsNullOrWhiteSpace(_settingsManager.CustomSearchUri)) + { + return $"? " + Arguments; + } + + // if the custom search URI contains query placeholder, replace it with the actual query + // otherwise append the query to the end of the URI + // support {query}, %query% or %s as placeholder + var placeholderVariants = new[] { "{query}", "%query%", "%s" }; + foreach (var placeholder in placeholderVariants) + { + if (_settingsManager.CustomSearchUri.Contains(placeholder, StringComparison.OrdinalIgnoreCase)) + { + return _settingsManager.CustomSearchUri.Replace(placeholder, Uri.EscapeDataString(Arguments), StringComparison.OrdinalIgnoreCase); + } + } + + // is this too smart? + var separator = _settingsManager.CustomSearchUri.Contains('?') ? '&' : '?'; + return $"{_settingsManager.CustomSearchUri}{separator}q={Uri.EscapeDataString(Arguments)}"; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs index c942e668d3..61557d996a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackExecuteSearchItem.cs @@ -5,9 +5,9 @@ using System.Globalization; using System.Text; using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; -using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo; namespace Microsoft.CmdPal.Ext.WebSearch.Commands; @@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem private readonly SearchWebCommand _executeItem; private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle); - private string _title; - public FallbackExecuteSearchItem(SettingsManager settings) - : base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title) + private readonly IBrowserInfoService _browserInfoService; + + public FallbackExecuteSearchItem(ISettingsInterface settings, IBrowserInfoService browserInfoService) + : base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title) { - _executeItem = (SearchWebCommand)this.Command!; + _executeItem = (SearchWebCommand)Command!; + _browserInfoService = browserInfoService; Title = string.Empty; Subtitle = string.Empty; _executeItem.Name = string.Empty; - _title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName); Icon = Icons.WebSearch; } + private static string UpdateBrowserName(IBrowserInfoService browserInfoService) + { + var browserName = browserInfoService.GetDefaultBrowser()?.Name; + return string.IsNullOrWhiteSpace(browserName) + ? Resources.open_in_default_browser + : string.Format(CultureInfo.CurrentCulture, PluginOpen, browserName); + } + public override void UpdateQuery(string query) { _executeItem.Arguments = query; var isEmpty = string.IsNullOrEmpty(query); - _executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser; - Title = isEmpty ? string.Empty : _title; + _executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser; + Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService); Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs index 9f5d9d86ca..7feb53b1de 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs @@ -7,21 +7,26 @@ using System.Globalization; using System.Text; using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; +using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; -using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo; namespace Microsoft.CmdPal.Ext.WebSearch; internal sealed partial class FallbackOpenURLItem : FallbackCommandItem { + private readonly IBrowserInfoService _browserInfoService; private readonly OpenURLCommand _executeItem; private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url); private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser); - public FallbackOpenURLItem(SettingsManager settings) - : base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title) + public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService) + : base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title) { - _executeItem = (OpenURLCommand)this.Command!; + ArgumentNullException.ThrowIfNull(browserInfoService); + + _browserInfoService = browserInfoService; + _executeItem = (OpenURLCommand)Command!; Title = string.Empty; _executeItem.Name = string.Empty; Subtitle = string.Empty; @@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem return; } - var success = Uri.TryCreate(query, UriKind.Absolute, out var uri); + var success = Uri.TryCreate(query, UriKind.Absolute, out _); // if url not contain schema, add http:// by default. if (!success) @@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem } _executeItem.Url = query; - _executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.open_in_default_browser; + _executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser; Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query); - Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, BrowserInfo.Name ?? BrowserInfo.MSEdgeName); + + var browserName = _browserInfoService.GetDefaultBrowser()?.Name; + Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName); } - public static bool IsValidUrl(string url) + private static bool IsValidUrl(string url) { if (string.IsNullOrWhiteSpace(url)) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfo.cs new file mode 100644 index 0000000000..9da978f481 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfo.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; + +public record BrowserInfo +{ + public required string Path { get; init; } + + public required string Name { get; init; } + + public string? ArgumentsPattern { get; init; } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfoServiceExtensions.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfoServiceExtensions.cs new file mode 100644 index 0000000000..1614273d83 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/BrowserInfoServiceExtensions.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; + +/// +/// Extension methods for . +/// +/// +internal static class BrowserInfoServiceExtensions +{ + /// + /// Opens the specified URL in the system's default web browser. + /// + /// The browser information service used to resolve the system's default browser. + /// The URL to open. + /// + /// if a default browser is found and the URL launch command is issued successfully; + /// otherwise, . + /// + /// + /// Returns if the default browser cannot be determined. + /// + public static bool Open(this IBrowserInfoService browserInfoService, string url) + { + var defaultBrowser = browserInfoService.GetDefaultBrowser(); + return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/DefaultBrowserInfoService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/DefaultBrowserInfoService.cs new file mode 100644 index 0000000000..51312fe4c0 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/DefaultBrowserInfoService.cs @@ -0,0 +1,99 @@ +// 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.Threading; +using ManagedCommon; +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; + +/// +/// Service to get information about the default browser. +/// +internal class DefaultBrowserInfoService : IBrowserInfoService +{ + private static readonly IDefaultBrowserProvider[] Providers = + [ + new ShellAssociationProvider(), + new LegacyRegistryAssociationProvider(), + new FallbackMsEdgeBrowserProvider(), + ]; + + private readonly Lock _updateLock = new(); + + private readonly Dictionary _lastLoggedErrors = []; + + private const long UpdateTimeout = 3000; + private long _lastUpdateTickCount = -UpdateTimeout; + + private BrowserInfo? _defaultBrowser; + + public BrowserInfo? GetDefaultBrowser() + { + try + { + UpdateIfTimePassed(); + } + catch (Exception) + { + // exception is already logged at this point + } + + return _defaultBrowser; + } + + /// + /// Updates only if at least more than 3000ms has passed since the last update, to avoid multiple calls to . + /// (because of multiple plugins calling update at the same time.) + /// + private void UpdateIfTimePassed() + { + lock (_updateLock) + { + var curTickCount = Environment.TickCount64; + if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null) + { + return; + } + + var newDefaultBrowser = UpdateCore(); + _defaultBrowser = newDefaultBrowser; + _lastUpdateTickCount = curTickCount; + } + } + + /// + /// Consider using to avoid updating multiple times. + /// (because of multiple plugins calling update at the same time.) + /// + private BrowserInfo UpdateCore() + { + foreach (var provider in Providers) + { + try + { + var result = provider.GetDefaultBrowserInfo(); +#if DEBUG + result = result with { Name = result.Name + " (" + provider.GetType().Name + ")" }; +#endif + return result; + } + catch (Exception ex) + { + // since we run this fairly often, avoid logging the same error multiple times + var lastLoggedError = _lastLoggedErrors.GetValueOrDefault(provider.GetType()); + var error = ex.ToString(); + if (error != lastLoggedError) + { + _lastLoggedErrors[provider.GetType()] = error; + Logger.LogError($"Exception when retrieving browser using provider {provider.GetType()}", ex); + } + } + } + + throw new InvalidOperationException("Unable to determine default browser"); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/IBrowserInfoService.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/IBrowserInfoService.cs new file mode 100644 index 0000000000..5d82193e5d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/IBrowserInfoService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; + +/// +/// Provides functionality to retrieve information about the system's default web browser. +/// +public interface IBrowserInfoService +{ + /// + /// Gets information about the system's default web browser. + /// + /// + BrowserInfo? GetDefaultBrowser(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociatedApp.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociatedApp.cs new file mode 100644 index 0000000000..3c6ba74d67 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociatedApp.cs @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers; + +internal record AssociatedApp(string? Command, string? FriendlyName); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociationProviderBase.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociationProviderBase.cs new file mode 100644 index 0000000000..43ed130401 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/AssociationProviderBase.cs @@ -0,0 +1,154 @@ +// 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.IO; +using Windows.Win32; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers; + +/// +/// Base class for providers that determine the default browser via application associations. +/// +internal abstract class AssociationProviderBase : IDefaultBrowserProvider +{ + protected abstract AssociatedApp? FindAssociation(); + + public BrowserInfo GetDefaultBrowserInfo() + { + var appAssociation = FindAssociation(); + if (appAssociation is null) + { + throw new ArgumentNullException(nameof(appAssociation), "Could not determine default browser application."); + } + + var commandPattern = appAssociation.Command; + var appAndArgs = SplitAppAndArgs(commandPattern); + + if (string.IsNullOrEmpty(appAndArgs.Path)) + { + throw new ArgumentOutOfRangeException(nameof(appAndArgs.Path), "Default browser program path could not be determined."); + } + + // Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App + if (!Path.Exists(appAndArgs.Path) && !Uri.TryCreate(appAndArgs.Path, UriKind.Absolute, out _)) + { + throw new ArgumentException($"Command validation failed: {commandPattern}", nameof(commandPattern)); + } + + return new BrowserInfo + { + Path = appAndArgs.Path, + Name = appAssociation.FriendlyName ?? Path.GetFileNameWithoutExtension(appAndArgs.Path), + ArgumentsPattern = appAndArgs.Arguments, + }; + } + + private static (string? Path, string? Arguments) SplitAppAndArgs(string? commandPattern) + { + if (string.IsNullOrEmpty(commandPattern)) + { + throw new ArgumentOutOfRangeException(nameof(commandPattern), "Default browser program command is not specified."); + } + + commandPattern = GetIndirectString(commandPattern); + + // HACK: for firefox installed through Microsoft store + // When installed through Microsoft Firefox the commandPattern does not have + // quotes for the path. As the Program Files does have a space + // the extracted path would be invalid, here we add the quotes to fix it + const string FirefoxExecutableName = "firefox.exe"; + if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") && + !commandPattern.StartsWith('\"')) + { + var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) + + FirefoxExecutableName.Length; + commandPattern = commandPattern.Insert(pathEndIndex, "\""); + commandPattern = commandPattern.Insert(0, "\""); + } + + if (commandPattern.StartsWith('\"')) + { + var endQuoteIndex = commandPattern.IndexOf('\"', 1); + if (endQuoteIndex != -1) + { + return (commandPattern[1..endQuoteIndex], commandPattern[(endQuoteIndex + 1)..].Trim()); + } + } + else + { + var spaceIndex = commandPattern.IndexOf(' '); + if (spaceIndex != -1) + { + return (commandPattern[..spaceIndex], commandPattern[(spaceIndex + 1)..].Trim()); + } + } + + return (null, null); + } + + protected static string GetIndirectString(string str) + { + if (string.IsNullOrEmpty(str) || str[0] != '@') + { + return str; + } + + const int initialCapacity = 128; + const int maxCapacity = 8192; // Reasonable upper limit + int hresult; + + unsafe + { + // Try with stack allocation first for common cases + var stackBuffer = stackalloc char[initialCapacity]; + + fixed (char* pszSource = str) + { + hresult = PInvoke.SHLoadIndirectString( + pszSource, + stackBuffer, + initialCapacity, + null); + + // S_OK (0) means success + if (hresult == 0) + { + return new string(stackBuffer); + } + + // STRSAFE_E_INSUFFICIENT_BUFFER (0x8007007A) means buffer too small + // Try with progressively larger heap buffers + if (unchecked((uint)hresult) == 0x8007007A) + { + for (var capacity = initialCapacity * 2; capacity <= maxCapacity; capacity *= 2) + { + var heapBuffer = new char[capacity]; + fixed (char* pBuffer = heapBuffer) + { + hresult = PInvoke.SHLoadIndirectString( + pszSource, + pBuffer, + (uint)capacity, + null); + + if (hresult == 0) + { + return new string(pBuffer); + } + + if (unchecked((uint)hresult) != 0x8007007A) + { + break; // Different error, stop retrying + } + } + } + } + } + } + + throw new InvalidOperationException( + $"Could not load indirect string. HRESULT: 0x{unchecked((uint)hresult):X8}"); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/FallbackMsEdgeBrowserProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/FallbackMsEdgeBrowserProvider.cs new file mode 100644 index 0000000000..8489362004 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/FallbackMsEdgeBrowserProvider.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers; + +/// +/// Provides a fallback implementation of the default browser provider that returns information for Microsoft Edge. +/// +/// This class is used when no other default browser provider is available. It supplies the path, +/// arguments pattern, and name for Microsoft Edge as the default browser information. +internal sealed class FallbackMsEdgeBrowserProvider : IDefaultBrowserProvider +{ + private const string MsEdgeArgumentsPattern = "--single-argument %1"; + + private const string MsEdgeName = "Microsoft Edge"; + + private static string MsEdgePath => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), + @"Microsoft\Edge\Application\msedge.exe"); + + public BrowserInfo GetDefaultBrowserInfo() => new() + { + Path = MsEdgePath, + ArgumentsPattern = MsEdgeArgumentsPattern, + Name = MsEdgeName, + }; +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/IDefaultBrowserProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/IDefaultBrowserProvider.cs new file mode 100644 index 0000000000..82a0b679fb --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/IDefaultBrowserProvider.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers; + +/// +/// Retrieves information about the default browser. +/// +internal interface IDefaultBrowserProvider +{ + BrowserInfo GetDefaultBrowserInfo(); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/LegacyRegistryAssociationProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/LegacyRegistryAssociationProvider.cs new file mode 100644 index 0000000000..28fe40f995 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/LegacyRegistryAssociationProvider.cs @@ -0,0 +1,46 @@ +// 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.Win32; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers; + +/// +/// Provides the default web browser by reading registry keys. This is a legacy method and may not work on all systems. +/// +internal sealed class LegacyRegistryAssociationProvider : AssociationProviderBase +{ + protected override AssociatedApp? FindAssociation() + { + var progId = GetRegistryValue( + @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId", + "ProgId") + ?? GetRegistryValue( + @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", + "ProgId"); + var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName") + ?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName"); + + if (appName is not null) + { + appName = GetIndirectString(appName); + appName = appName + .Replace("URL", null, StringComparison.OrdinalIgnoreCase) + .Replace("HTML", null, StringComparison.OrdinalIgnoreCase) + .Replace("Document", null, StringComparison.OrdinalIgnoreCase) + .Replace("Web", null, StringComparison.OrdinalIgnoreCase) + .TrimEnd(); + } + + var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null); + + return commandPattern is null ? null : new AssociatedApp(commandPattern, appName); + + static string? GetRegistryValue(string registryLocation, string? valueName) + { + return Registry.GetValue(registryLocation, valueName, null) as string; + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/ShellAssociationProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/ShellAssociationProvider.cs new file mode 100644 index 0000000000..a70c3476d4 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/Browser/Providers/ShellAssociationProvider.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers; + +/// +/// Retrieves the default web browser using the system shell functions. +/// +internal sealed class ShellAssociationProvider : AssociationProviderBase +{ + private static readonly string[] Protocols = ["https", "http"]; + + protected override AssociatedApp FindAssociation() + { + foreach (var protocol in Protocols) + { + var command = AssocQueryStringSafe(NativeMethods.AssocStr.Command, protocol); + if (string.IsNullOrWhiteSpace(command)) + { + continue; + } + + var appName = AssocQueryStringSafe(NativeMethods.AssocStr.FriendlyAppName, protocol); + + return new AssociatedApp(command, appName); + } + + return new AssociatedApp(null, null); + } + + private static unsafe string? AssocQueryStringSafe(NativeMethods.AssocStr what, string protocol) + { + uint cch = 0; + + // First call: get required length (incl. null) + _ = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, null, ref cch); + if (cch == 0) + { + return null; + } + + // Small buffers on stack; large on heap + var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch]; + + fixed (char* p = span) + { + var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch); + if (hr != 0 || cch == 0) + { + return null; + } + + // cch includes the null terminator; slice it off + var len = (int)cch - 1; + if (len < 0) + { + len = 0; + } + + return new string(span[..len]); + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs deleted file mode 100644 index f6b82ecfbb..0000000000 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/DefaultBrowserInfo.cs +++ /dev/null @@ -1,215 +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.Text; -using System.Threading; -using ManagedCommon; - -namespace Microsoft.CmdPal.Ext.WebSearch.Helpers; - -/// -/// Contains information (e.g. path to executable, name...) about the default browser. -/// -public static class DefaultBrowserInfo -{ - private static readonly Lock _updateLock = new(); - - /// Gets the path to the MS Edge browser executable. - public static string MSEdgePath => System.IO.Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), - @"Microsoft\Edge\Application\msedge.exe"); - - /// Gets the command line pattern of the MS Edge. - public const string MSEdgeArgumentsPattern = "--single-argument %1"; - - public const string MSEdgeName = "Microsoft Edge"; - - /// Gets the path to default browser's executable. - public static string? Path { get; private set; } - - /// Gets since the icon is embedded in the executable. - public static string? IconPath => Path; - - /// Gets the user-friendly name of the default browser. - public static string? Name { get; private set; } - - /// Gets the command line pattern of the default browser. - public static string? ArgumentsPattern { get; private set; } - - public static bool IsDefaultBrowserSet => !string.IsNullOrEmpty(Path); - - public const long UpdateTimeout = 300; - - private static long _lastUpdateTickCount = -UpdateTimeout; - - private static bool _updatedOnce; - private static bool _errorLogged; - - /// - /// Updates only if at least more than 300ms has passed since the last update, to avoid multiple calls to . - /// (because of multiple plugins calling update at the same time.) - /// - public static void UpdateIfTimePassed() - { - var curTickCount = Environment.TickCount64; - if (curTickCount - _lastUpdateTickCount >= UpdateTimeout) - { - _lastUpdateTickCount = curTickCount; - Update(); - } - } - - /// - /// Consider using to avoid updating multiple times. - /// (because of multiple plugins calling update at the same time.) - /// - public static void Update() - { - lock (_updateLock) - { - if (!_updatedOnce) - { - // Log.Info("I've tried updating the chosen Web Browser info at least once.", typeof(DefaultBrowserInfo)); - _updatedOnce = true; - } - - try - { - var progId = GetRegistryValue( - @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId", - "ProgId") - ?? GetRegistryValue( - @"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice", - "ProgId"); - var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName") - ?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName"); - - if (appName is not null) - { - // Handle indirect strings: - if (appName.StartsWith('@')) - { - appName = GetIndirectString(appName); - } - - appName = appName - .Replace("URL", null, StringComparison.OrdinalIgnoreCase) - .Replace("HTML", null, StringComparison.OrdinalIgnoreCase) - .Replace("Document", null, StringComparison.OrdinalIgnoreCase) - .Replace("Web", null, StringComparison.OrdinalIgnoreCase) - .TrimEnd(); - } - - Name = appName; - - var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null); - - if (string.IsNullOrEmpty(commandPattern)) - { - throw new ArgumentOutOfRangeException( - nameof(commandPattern), - "Default browser program command is not specified."); - } - - if (commandPattern.StartsWith('@')) - { - commandPattern = GetIndirectString(commandPattern); - } - - // HACK: for firefox installed through Microsoft store - // When installed through Microsoft Firefox the commandPattern does not have - // quotes for the path. As the Program Files does have a space - // the extracted path would be invalid, here we add the quotes to fix it - const string FirefoxExecutableName = "firefox.exe"; - if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") && (!commandPattern.StartsWith('\"'))) - { - var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) + FirefoxExecutableName.Length; - commandPattern = commandPattern.Insert(pathEndIndex, "\""); - commandPattern = commandPattern.Insert(0, "\""); - } - - if (commandPattern.StartsWith('\"')) - { - var endQuoteIndex = commandPattern.IndexOf('\"', 1); - if (endQuoteIndex != -1) - { - Path = commandPattern.Substring(1, endQuoteIndex - 1); - ArgumentsPattern = commandPattern.Substring(endQuoteIndex + 1).Trim(); - } - } - else - { - var spaceIndex = commandPattern.IndexOf(' '); - if (spaceIndex != -1) - { - Path = commandPattern.Substring(0, spaceIndex); - ArgumentsPattern = commandPattern.Substring(spaceIndex + 1).Trim(); - } - } - - // Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App - if (!System.IO.Path.Exists(Path) && !Uri.TryCreate(Path, UriKind.Absolute, out _)) - { - throw new ArgumentException( - $"Command validation failed: {commandPattern}", - nameof(commandPattern)); - } - - if (string.IsNullOrEmpty(Path)) - { - throw new ArgumentOutOfRangeException( - nameof(Path), - "Default browser program path could not be determined."); - } - } - catch (Exception) - { - // Fallback to MS Edge - Path = MSEdgePath; - Name = MSEdgeName; - ArgumentsPattern = MSEdgeArgumentsPattern; - - if (!_errorLogged) - { - // Log.Exception("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.", e, typeof(DefaultBrowserInfo)); - Logger.LogError("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge."); - _errorLogged = true; - } - } - - string? GetRegistryValue(string registryLocation, string? valueName) - { - return Microsoft.Win32.Registry.GetValue(registryLocation, valueName, null) as string; - } - - string GetIndirectString(string str) - { - var stringBuilder = new StringBuilder(128); - unsafe - { - var buffer = stackalloc char[128]; - var capacity = 128; - var firstChar = str[0]; - var strPtr = &firstChar; - - // S_OK == 0 - fixed (char* pszSourceLocal = str) - { - if (global::Windows.Win32.PInvoke.SHLoadIndirectString( - pszSourceLocal, - buffer, - (uint)capacity, - default) == 0) - { - return new string(buffer); - } - } - } - - throw new ArgumentNullException(nameof(str), "Could not load indirect string."); - } - } - } -} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/ISettingsInterface.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/ISettingsInterface.cs index cbbb86bbd2..cff6f8919d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/ISettingsInterface.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/ISettingsInterface.cs @@ -18,5 +18,7 @@ public interface ISettingsInterface public IReadOnlyList HistoryItems { get; } + string CustomSearchUri { get; } + public void AddHistoryItem(HistoryItem historyItem); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/NativeMethods.cs new file mode 100644 index 0000000000..dee5b33fc5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/NativeMethods.cs @@ -0,0 +1,54 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.CmdPal.Ext.WebSearch.Helpers; + +internal static partial class NativeMethods +{ + [LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = false)] + internal static unsafe partial int AssocQueryStringW( + AssocF flags, + AssocStr str, + string pszAssoc, + string? pszExtra, + char* pszOut, + ref uint pcchOut); + + [Flags] + public enum AssocF : uint + { + None = 0, + IsProtocol = 0x00001000, + } + + public enum AssocStr + { + Command = 1, + Executable, + FriendlyDocName, + FriendlyAppName, + NoOpen, + ShellNewValue, + DDECommand, + DDEIfExec, + DDEApplication, + DDETopic, + InfoTip, + QuickTip, + TileInfo, + ContentType, + DefaultIcon, + ShellExtension, + DropTarget, + DelegateExecute, + SupportedUriProtocols, + ProgId, + AppId, + AppPublisher, + AppIconReference, // sometimes present, but DefaultIcon is most common + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs index 8cc7734368..0af19e14c2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs @@ -41,6 +41,15 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface Resources.plugin_global_if_uri, false); + private readonly TextSetting _customSearchUri = new( + Namespaced(nameof(CustomSearchUri)), + Resources.plugin_custom_search_uri, + Resources.plugin_custom_search_uri, + string.Empty) + { + Placeholder = Resources.plugin_custom_search_uri_placeholder, + }; + private readonly ChoiceSetSetting _historyItemCount = new( Namespaced(HistoryItemCountLegacySettingsKey), Resources.plugin_history_item_count, @@ -51,6 +60,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0; + public string CustomSearchUri => _customSearchUri.Value ?? string.Empty; + public IReadOnlyList HistoryItems => _history.HistoryItems; public SettingsManager() @@ -59,6 +70,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface Settings.Add(_globalIfURI); Settings.Add(_historyItemCount); + Settings.Add(_customSearchUri); LoadSettings(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs index 641d5f6135..bf21f7c912 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs @@ -9,23 +9,24 @@ using System.Text; using System.Threading; using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo; namespace Microsoft.CmdPal.Ext.WebSearch.Pages; internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable { private readonly ISettingsInterface _settingsManager; + private readonly IBrowserInfoService _browserInfoService; private readonly Lock _sync = new(); private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name); private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); private IListItem[] _allItems = []; private List _historyItems = []; - public WebSearchListPage(ISettingsInterface settingsManager) + public WebSearchListPage(ISettingsInterface settingsManager, IBrowserInfoService browserInfoService) { ArgumentNullException.ThrowIfNull(settingsManager); @@ -35,6 +36,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable Id = "com.microsoft.cmdpal.websearch"; _settingsManager = settingsManager; + _browserInfoService = browserInfoService; _settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged; // It just looks viewer to have string twice on the page, and default placeholder is good enough @@ -43,8 +45,8 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable EmptyContent = new CommandItem(new NoOpCommand()) { Icon = Icon, - Title = Properties.Resources.plugin_description, - Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), + Title = Resources.plugin_description, + Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser), }; UpdateHistory(); @@ -67,7 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable for (var index = items.Count - 1; index >= 0; index--) { var historyItem = items[index]; - history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager)) + history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager, _browserInfoService)) { Icon = Icons.History, Title = historyItem.SearchString, @@ -82,7 +84,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable } } - private static IListItem[] Query(string query, List historySnapshot, ISettingsInterface settingsManager) + private static IListItem[] Query(string query, List historySnapshot, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService) { ArgumentNullException.ThrowIfNull(query); @@ -95,10 +97,10 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable if (!string.IsNullOrEmpty(query)) { var searchTerm = query; - var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager)) + var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager, browserInfoService)) { Title = searchTerm, - Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), + Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser), Icon = Icons.Search, }; results.Add(result); @@ -117,7 +119,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable historySnapshot = _historyItems; } - var items = Query(search ?? string.Empty, historySnapshot, _settingsManager); + var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _browserInfoService); lock (_sync) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs index 39ebd6bf2b..9db0a40cac 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Resources { @@ -69,6 +69,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties { } } + /// + /// Looks up a localized string similar to default browser. + /// + public static string default_browser { + get { + return ResourceManager.GetString("default_browser", resourceCulture); + } + } + /// /// Looks up a localized string similar to Web Search. /// @@ -150,6 +159,24 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties { } } + /// + /// Looks up a localized string similar to Custom search engine URL. + /// + public static string plugin_custom_search_uri { + get { + return ResourceManager.GetString("plugin_custom_search_uri", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query}. + /// + public static string plugin_custom_search_uri_placeholder { + get { + return ResourceManager.GetString("plugin_custom_search_uri_placeholder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Searches the web with your default search engine. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx index 5a406eca60..c7f424c6f9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Properties/Resources.resx @@ -184,4 +184,13 @@ Open URL + + default browser + + + Custom search engine URL + + + Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query} + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs index 9087ce0ee1..89cfe5a183 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchCommandsProvider.cs @@ -5,13 +5,14 @@ using System; using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.WebSearch; -public partial class WebSearchCommandsProvider : CommandProvider +public sealed partial class WebSearchCommandsProvider : CommandProvider { private readonly SettingsManager _settingsManager = new(); private readonly FallbackExecuteSearchItem _fallbackItem; @@ -19,18 +20,19 @@ public partial class WebSearchCommandsProvider : CommandProvider private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem; private readonly ICommandItem[] _topLevelItems; private readonly IFallbackCommandItem[] _fallbackCommands; + private readonly IBrowserInfoService _browserInfoService = new DefaultBrowserInfoService(); public WebSearchCommandsProvider() { - Id = "WebSearch"; + Id = "com.microsoft.cmdpal.builtin.websearch"; DisplayName = Resources.extension_name; Icon = Icons.WebSearch; Settings = _settingsManager.Settings; - _fallbackItem = new FallbackExecuteSearchItem(_settingsManager); - _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager); + _fallbackItem = new FallbackExecuteSearchItem(_settingsManager, _browserInfoService); + _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager, _browserInfoService); - _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager) + _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager, _browserInfoService) { MoreCommands = [ diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs index bc161991ca..b2aaa95f5e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/WebSearchTopLevelCommandItem.cs @@ -5,6 +5,7 @@ using System; using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Helpers; +using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser; using Microsoft.CmdPal.Ext.WebSearch.Pages; using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CommandPalette.Extensions; @@ -15,13 +16,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch; public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable { private readonly SettingsManager _settingsManager; + private readonly IBrowserInfoService _browserInfoService; - public WebSearchTopLevelCommandItem(SettingsManager settingsManager) - : base(new WebSearchListPage(settingsManager)) + public WebSearchTopLevelCommandItem(SettingsManager settingsManager, IBrowserInfoService browserInfoService) + : base(new WebSearchListPage(settingsManager, browserInfoService)) { Icon = Icons.WebSearch; SetDefaultTitle(); _settingsManager = settingsManager; + _browserInfoService = browserInfoService; } private void SetDefaultTitle() => Title = Resources.command_item_title; @@ -37,12 +40,12 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle if (string.IsNullOrEmpty(query)) { SetDefaultTitle(); - ReplaceCommand(new WebSearchListPage(_settingsManager)); + ReplaceCommand(new WebSearchListPage(_settingsManager, _browserInfoService)); } else { Title = query; - ReplaceCommand(new SearchWebCommand(query, _settingsManager)); + ReplaceCommand(new SearchWebCommand(query, _settingsManager, _browserInfoService)); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs index d2c1ea7283..7dbe740d95 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs @@ -62,7 +62,7 @@ public partial class InstallPackageCommand : InvokableCommand { PackageInstallCommandState.Install => Icons.DownloadIcon, PackageInstallCommandState.Update => Icons.UpdateIcon, - PackageInstallCommandState.Uninstall => Icons.CompletedIcon, + PackageInstallCommandState.Uninstall => Icons.DeleteIcon, _ => throw new NotImplementedException(), }; Name = InstallCommandState switch diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs index a8eda1bae9..1e1f337944 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs @@ -194,46 +194,95 @@ public partial class InstallPackageListItem : ListItem var isInstalled = _package.InstalledVersion is not null; var installedState = isInstalled ? - (_package.IsUpdateAvailable ? - PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) : + (_package.IsUpdateAvailable ? PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) : PackageInstallCommandState.Install; // might be an uninstall command InstallPackageCommand installCommand = new(_package, installedState); - if (isInstalled) + if (_package.InstalledVersion is not null) { - this.Icon = installCommand.Icon; - this.Command = new NoOpCommand(); +#if DEBUG + var installerType = _package.InstalledVersion.GetMetadata(PackageVersionMetadataField.InstallerType); + Subtitle = installerType + " | " + Subtitle; +#endif + List contextMenu = []; - CommandContextItem uninstallContextItem = new(installCommand) + Command = installCommand; + Icon = installedState switch { - IsCritical = true, - Icon = Icons.DeleteIcon, + PackageInstallCommandState.Install => Icons.DownloadIcon, + PackageInstallCommandState.Update => Icons.UpdateIcon, + PackageInstallCommandState.Uninstall => Icons.CompletedIcon, + _ => Icons.DownloadIcon, }; - if (WinGetStatics.AppSearchCallback is not null) + TryLocateAndAppendActionForApp(contextMenu); + + MoreCommands = contextMenu.ToArray(); + } + else + { + _installCommand = new InstallPackageCommand(_package, installedState); + _installCommand.InstallStateChanged += InstallStateChangedHandler; + Command = _installCommand; + Icon = _installCommand.Icon; + } + } + + private void TryLocateAndAppendActionForApp(List contextMenu) + { + try + { + // Let's try to connect it to an installed app if possible + // This is a bit of dark magic, since there's no direct link between + // WinGet packages and installed apps. + var lookupByPackageName = WinGetStatics.AppSearchByPackageFamilyNameCallback; + if (lookupByPackageName is not null) { - var callback = WinGetStatics.AppSearchCallback; - var installedApp = callback(_package.DefaultInstallVersion is null ? _package.Name : _package.DefaultInstallVersion.DisplayName); - if (installedApp is not null) + var names = _package.InstalledVersion.PackageFamilyNames; + for (var i = 0; i < names.Count; i++) { - this.Command = installedApp.Command; - contextMenu = [.. installedApp.MoreCommands]; + var installedAppByPfn = lookupByPackageName(names[i]); + if (installedAppByPfn is not null) + { + contextMenu.Add(new Separator()); + contextMenu.Add(new CommandContextItem(installedAppByPfn.Command)); + foreach (var item in installedAppByPfn.MoreCommands) + { + contextMenu.Add(item); + } + + return; + } } } - contextMenu.Add(uninstallContextItem); - this.MoreCommands = contextMenu.ToArray(); - return; + var lookupByProductCode = WinGetStatics.AppSearchByProductCodeCallback; + if (lookupByProductCode is not null) + { + var productCodes = _package.InstalledVersion.ProductCodes; + for (var i = 0; i < productCodes.Count; i++) + { + var installedAppByProductCode = lookupByProductCode(productCodes[i]); + if (installedAppByProductCode is not null) + { + contextMenu.Add(new Separator()); + contextMenu.Add(new CommandContextItem(installedAppByProductCode.Command)); + foreach (var item in installedAppByProductCode.MoreCommands) + { + contextMenu.Add(item); + } + + return; + } + } + } + } + catch (Exception ex) + { + Logger.LogError($"Failed to retrieve app context menu items for package '{_package?.Name ?? "Unknown"}'", ex); } - - // didn't find the app - _installCommand = new InstallPackageCommand(_package, installedState); - this.Command = _installCommand; - - Icon = _installCommand.Icon; - _installCommand.InstallStateChanged += InstallStateChangedHandler; } private void InstallStateChangedHandler(object? sender, InstallPackageCommand e) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs index 1d7758769a..e84802b8fa 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/WinGetExtensionPage.cs @@ -28,9 +28,10 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable public bool HasTag => !string.IsNullOrEmpty(_tag); private readonly Lock _resultsLock = new(); + private readonly Lock _taskLock = new(); - private CancellationTokenSource? _cancellationTokenSource; - private Task>? _currentSearchTask; + private string? _nextSearchQuery; + private bool _isTaskRunning; private List? _results; @@ -85,7 +86,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable stopwatch.Stop(); Logger.LogDebug($"Building ListItems took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(GetItems)); - IsLoading = false; return results; } } @@ -98,8 +98,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable Properties.Resources.winget_no_packages_found, }; - IsLoading = false; - return []; } @@ -117,64 +115,70 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable private void DoUpdateSearchText(string newSearch) { - // Cancel any ongoing search - if (_cancellationTokenSource is not null) + lock (_taskLock) { - Logger.LogDebug("Cancelling old search", memberName: nameof(DoUpdateSearchText)); - _cancellationTokenSource.Cancel(); - } - - _cancellationTokenSource = new CancellationTokenSource(); - - var cancellationToken = _cancellationTokenSource.Token; - - IsLoading = true; - - try - { - // Save the latest search task - _currentSearchTask = DoSearchAsync(newSearch, cancellationToken); - } - catch (OperationCanceledException) - { - // DO NOTHING HERE - return; - } - catch (Exception ex) - { - // Handle other exceptions - ExtensionHost.LogMessage($"[WinGet] DoUpdateSearchText throw exception: {ex.Message}"); - return; - } - - // Await the task to ensure only the latest one gets processed - _ = ProcessSearchResultsAsync(_currentSearchTask, newSearch); - } - - private async Task ProcessSearchResultsAsync( - Task> searchTask, - string newSearch) - { - try - { - var results = await searchTask; - - // Ensure this is still the latest task - if (_currentSearchTask == searchTask) + if (_isTaskRunning) { - // Process the results (e.g., update UI) - UpdateWithResults(results, newSearch); + // If a task is running, queue the next search query + // Keep IsLoading = true since we still have work to do + Logger.LogDebug($"Task is running, queueing next search: '{newSearch}'", memberName: nameof(DoUpdateSearchText)); + _nextSearchQuery = newSearch; + } + else + { + // No task is running, start a new search + Logger.LogDebug($"Starting new search: '{newSearch}'", memberName: nameof(DoUpdateSearchText)); + _isTaskRunning = true; + _nextSearchQuery = null; + IsLoading = true; + + _ = ExecuteSearchChainAsync(newSearch); } } - catch (OperationCanceledException) + } + + private async Task ExecuteSearchChainAsync(string query) + { + while (true) { - // Handle cancellation gracefully (e.g., log or ignore) - Logger.LogDebug($" Cancelled search for '{newSearch}'"); - } - catch (Exception ex) - { - // Handle other exceptions - Logger.LogError("Unexpected error while processing results", ex); + try + { + Logger.LogDebug($"Executing search for '{query}'", memberName: nameof(ExecuteSearchChainAsync)); + + var results = await DoSearchAsync(query); + + // Update UI with results + UpdateWithResults(results, query); + } + catch (Exception ex) + { + Logger.LogError($"Unexpected error while searching for '{query}'", ex); + } + + // Check if there's a next query to process + string? nextQuery; + lock (_taskLock) + { + if (_nextSearchQuery is not null) + { + // There's a queued search, execute it + nextQuery = _nextSearchQuery; + _nextSearchQuery = null; + + Logger.LogDebug($"Found queued search, continuing with: '{nextQuery}'", memberName: nameof(ExecuteSearchChainAsync)); + } + else + { + // No more searches queued, mark task as completed + _isTaskRunning = false; + IsLoading = false; + Logger.LogDebug("No more queued searches, task chain completed", memberName: nameof(ExecuteSearchChainAsync)); + break; + } + } + + // Continue with the next query + query = nextQuery; } } @@ -189,11 +193,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable RaiseItemsChanged(); } - private async Task> DoSearchAsync(string query, CancellationToken ct) + private async Task> DoSearchAsync(string query) { - // Were we already canceled? - ct.ThrowIfCancellationRequested(); - Stopwatch stopwatch = new(); stopwatch.Start(); @@ -230,9 +231,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable opts.Filters.Add(tagFilter); } - // Clean up here, then... - ct.ThrowIfCancellationRequested(); - var catalogTask = HasTag ? WinGetStatics.CompositeWingetCatalog : WinGetStatics.CompositeAllCatalog; // Both these catalogs should have been instantiated by the @@ -251,13 +249,11 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable findPackages_stopwatch.Start(); Logger.LogDebug($" Searching {catalog.Info.Name} ({query})", memberName: nameof(DoSearchAsync)); - ct.ThrowIfCancellationRequested(); - Logger.LogDebug($"Preface for \"{searchDebugText}\" took {stopwatch.ElapsedMilliseconds}ms", memberName: nameof(DoSearchAsync)); // BODGY, re: microsoft/winget-cli#5151 // FindPackagesAsync isn't actually async. - var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct); + var internalSearchTask = Task.Run(() => catalog.FindPackages(opts)); var searchResults = await internalSearchTask; findPackages_stopwatch.Stop(); @@ -271,8 +267,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable return []; } - ct.ThrowIfCancellationRequested(); - Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync)); // FYI Using .ToArray or any other kind of enumerable loop @@ -282,8 +276,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable { var match = searchResults.Matches[i]; - ct.ThrowIfCancellationRequested(); - var package = match.CatalogPackage; results.Add(package); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs index 418cccf58c..e4567a30d4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs @@ -124,16 +124,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties { } /// - /// Looks up a localized string similar to Search for extensions on WinGet. - /// - public static string winget_install_extensions_subtitle { - get { - return ResourceManager.GetString("winget_install_extensions_subtitle", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Install Command Palette extensions. + /// Looks up a localized string similar to Add Command Palette extensions from WinGet. /// public static string winget_install_extensions_title { get { @@ -205,7 +196,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties { } /// - /// Looks up a localized string similar to Search WinGet. + /// Looks up a localized string similar to Find apps on WinGet. /// public static string winget_page_name { get { @@ -268,7 +259,7 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties { } /// - /// Looks up a localized string similar to Search for extensions in the Store. + /// Looks up a localized string similar to Add Command Palette extensions from the Microsoft Store. /// public static string winget_search_store_title { get { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx index ac2128430d..4a6df0b518 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx @@ -127,19 +127,15 @@ - Install Command Palette extensions - - - - Search for extensions on WinGet + Add Command Palette extensions from WinGet - Search for extensions in the Store + Add Command Palette extensions from the Microsoft Store - Search WinGet + Find apps on WinGet diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs index 44b6da1f17..a2608ef8a8 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetExtensionCommandsProvider.cs @@ -27,7 +27,6 @@ public partial class WinGetExtensionCommandsProvider : CommandProvider new WinGetExtensionPage(WinGetExtensionPage.ExtensionsTag) { Title = Properties.Resources.winget_install_extensions_title }) { Title = Properties.Resources.winget_install_extensions_title, - Subtitle = Properties.Resources.winget_install_extensions_subtitle, }, new ListItem( @@ -42,5 +41,9 @@ public partial class WinGetExtensionCommandsProvider : CommandProvider public override void InitializeWithHost(IExtensionHost host) => WinGetExtensionHost.Instance.Initialize(host); - public void SetAllLookup(Func callback) => WinGetStatics.AppSearchCallback = callback; + public void SetAllLookup(Func lookupByPackageName, Func lookupByProductCode) + { + WinGetStatics.AppSearchByPackageFamilyNameCallback = lookupByPackageName; + WinGetStatics.AppSearchByProductCodeCallback = lookupByProductCode; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs index da591c566c..001ba5539d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/WinGetStatics.cs @@ -34,7 +34,9 @@ internal static class WinGetStatics private static readonly StatusMessage _errorMessage = new() { State = MessageState.Error }; - public static Func? AppSearchCallback { get; set; } + public static Func? AppSearchByPackageFamilyNameCallback { get; set; } + + public static Func? AppSearchByProductCodeCallback { get; set; } private static readonly CompositeFormat CreateCatalogErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.winget_create_catalog_error); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs index 1a8c5106d4..0d0cce13c6 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.Designer.cs @@ -70,7 +70,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties { } /// - /// Looks up a localized string similar to On all Desktops. + /// Looks up a localized string similar to On all desktops. /// public static string VirtualDesktopHelper_AllDesktops { get { @@ -196,7 +196,7 @@ namespace Microsoft.CmdPal.Ext.WindowWalker.Properties { } /// - /// Looks up a localized string similar to Not Responding. + /// Looks up a localized string similar to Not responding. /// public static string windowwalker_NotResponding { get { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx index 3d61936a1d..cbfa9b69b0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Properties/Resources.resx @@ -209,7 +209,7 @@ When disabled, windows will be sorted by title - Not Responding + Not responding Switch between open windows @@ -218,7 +218,7 @@ Switch to - On all Desktops + On all desktops Desktop {0} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/WindowsServicesCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/WindowsServicesCommandsProvider.cs index 48ca848707..c48dcd99b2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/WindowsServicesCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/WindowsServicesCommandsProvider.cs @@ -23,8 +23,7 @@ public partial class WindowsServicesCommandsProvider : CommandProvider return [ new CommandItem(new ServicesListPage()) { - Title = "Windows Services", - Subtitle = "Manage Windows Services", + Title = "Manage Windows services", } ]; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettingsCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettingsCommandsProvider.cs index 600e621c99..dd13d5295a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettingsCommandsProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/WindowsSettingsCommandsProvider.cs @@ -10,7 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.WindowsSettings; -public partial class WindowsSettingsCommandsProvider : CommandProvider +public sealed partial class WindowsSettingsCommandsProvider : CommandProvider { private readonly CommandItem _searchSettingsListItem; @@ -22,7 +22,7 @@ public partial class WindowsSettingsCommandsProvider : CommandProvider public WindowsSettingsCommandsProvider() { - Id = "Windows.Settings"; + Id = "com.microsoft.cmdpal.builtin.windowssettings"; DisplayName = Resources.WindowsSettingsProvider_DisplayName; Icon = Icons.WindowsSettingsIcon; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs index 81d82638f0..46b0d185b6 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.Designer.cs @@ -97,7 +97,7 @@ namespace Microsoft.CmdPal.Ext.WindowsTerminal.Properties { } /// - /// Looks up a localized string similar to Open Windows Terminal Profiles. + /// Looks up a localized string similar to Open Windows Terminal profiles. /// internal static string list_item_title { get { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx index 23a1533fab..bff603bedc 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Properties/Resources.resx @@ -156,7 +156,7 @@ Settings - Open Windows Terminal Profiles + Open Windows Terminal profiles Preferred channel diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/IssueSpecificPages/AllIssueSamplesIndexPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/IssueSpecificPages/AllIssueSamplesIndexPage.cs new file mode 100644 index 0000000000..42cb326178 --- /dev/null +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/IssueSpecificPages/AllIssueSamplesIndexPage.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension.Pages.IssueSpecificPages; + +internal sealed partial class AllIssueSamplesIndexPage : ListPage +{ + public AllIssueSamplesIndexPage() + { + Icon = new IconInfo("ðŸ›"); + Name = "All Issue Samples Index Page"; + } + + public override IListItem[] GetItems() + { + return new IListItem[] + { + new ListItem(new SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage()) + { + Title = "Issue 42827 - Filter Drop Down Stays Visible After Switching From List To Content Page", + Subtitle = "Repro steps: Open this page, open the filter dropdown, select a filter, navigate to a content page, navigate back to this page. The filter dropdown should be closed but it remains open.", + }, + }; + } +} diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/IssueSpecificPages/SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/IssueSpecificPages/SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage.cs new file mode 100644 index 0000000000..4d29b56c83 --- /dev/null +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/IssueSpecificPages/SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage.cs @@ -0,0 +1,76 @@ +// 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.Linq; +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension.Pages.IssueSpecificPages; + +internal sealed partial class SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage : DynamicListPage +{ + public SamplePageForIssue42827_FilterDropDownStaysVisibleAfterSwitchingFromListToContentPage() + { + Icon = new IconInfo(string.Empty); + Name = "Issue 42827 - Filters not hiding when navigating between pages"; + IsLoading = true; + var filters = new SampleFilters(); + filters.PropChanged += Filters_PropChanged; + Filters = filters; + } + + private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged(); + + public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(); + + public override IListItem[] GetItems() + { + var items = SearchText.ToCharArray().Select(ch => new ListItem(new SampleContentPage()) { Title = ch.ToString() }).ToArray(); + if (items.Length == 0) + { + items = [ + new ListItem(new SampleContentPage()) { Title = "This List item will open a content page" }, + new ListItem(new SampleContentPage()) { Title = "This List item will open a content page too" }, + new ListItem(new SampleContentPage()) { Title = "Guess what this one will do?" }, + ]; + } + + if (!string.IsNullOrEmpty(Filters.CurrentFilterId)) + { + switch (Filters.CurrentFilterId) + { + case "mod2": + items = items.Where((item, index) => (index + 1) % 2 == 0).ToArray(); + break; + case "mod3": + items = items.Where((item, index) => (index + 1) % 3 == 0).ToArray(); + break; + case "all": + default: + // No filtering + break; + } + } + + foreach (var item in items) + { + item.Subtitle = "Filter drop-down should be hidden when navigating to a content page"; + } + + return items; + } + + internal sealed partial class SampleFilters : Filters + { + public override IFilterItem[] GetFilters() + { + return + [ + new Filter() { Id = "all", Name = "All" }, + new Filter() { Id = "mod2", Name = "Every 2nd", Icon = new IconInfo("2") }, + new Filter() { Id = "mod3", Name = "Every 3rd (and long name)", Icon = new IconInfo("3") }, + ]; + } + } +} diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleGalleryListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleGalleryListPage.cs index 3a80d180e0..2f6fba7089 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleGalleryListPage.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleGalleryListPage.cs @@ -9,13 +9,6 @@ namespace SamplePagesExtension; internal sealed partial class SampleGalleryListPage : ListPage { - public SampleGalleryListPage() - { - Icon = new IconInfo("\uE7C5"); - Name = "Sample Gallery List Page"; - GridProperties = new GalleryGridLayout(); - } - public override IListItem[] GetItems() { return [ diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleGridsListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleGridsListPage.cs new file mode 100644 index 0000000000..05b604c912 --- /dev/null +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleGridsListPage.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace SamplePagesExtension; + +internal sealed partial class SampleGridsListPage : ListPage +{ + private readonly IListItem[] _items = + [ + new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = true } }) + { + Title = "Gallery list page (title and subtitle)", + Subtitle = "A sample gallery list page with images", + Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"), + }, + new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = false } }) + { + Title = "Gallery list page (title, no subtitle)", + Subtitle = "A sample gallery list page with images", + Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"), + }, + new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = false, ShowSubtitle = false } }) + { + Title = "Gallery list page (no title, no subtitle)", + Subtitle = "A sample gallery list page with images", + Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"), + }, + new ListItem(new SampleGalleryListPage { GridProperties = new SmallGridLayout() }) + { + Title = "Small grid list page", + Subtitle = "A sample grid list page with text items", + Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"), + }, + new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = true } }) + { + Title = "Medium grid (with title)", + Subtitle = "A sample grid list page with text items", + Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"), + }, + new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = false } }) + { + Title = "Medium grid (hidden title)", + Subtitle = "A sample grid list page with text items", + Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"), + } + ]; + + public SampleGridsListPage() + { + Icon = new IconInfo("\uE7C5"); + Name = "Grid and gallery lists"; + } + + public override IListItem[] GetItems() => _items; +} diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs index 95f5eb84c0..2464724d50 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs @@ -190,7 +190,11 @@ internal sealed partial class SampleListPage : ListPage new CommandContextItem(new EverChangingCommand("Faces", "ðŸ˜", "🥺", "ðŸ˜")), new CommandContextItem(new EverChangingCommand("Hearts", "♥ï¸", "💚", "💜", "🧡", "💛", "💙")), ], - } + }, + new ListItemChangingCommandInTime() + { + Title = "I'm a list item that changes entire command in time", + }, ]; } @@ -248,6 +252,11 @@ internal sealed partial class SampleListPage : ListPage private int _currentIndex; public EverChangingCommand(string name, params string[] icons) + : this(name, TimeSpan.FromSeconds(5), icons) + { + } + + public EverChangingCommand(string name, TimeSpan interval, params string[] icons) { _icons = icons ?? throw new ArgumentNullException(nameof(icons)); if (_icons.Length == 0) @@ -260,7 +269,7 @@ internal sealed partial class SampleListPage : ListPage Icon = new IconInfo(_icons[_currentIndex]); // Start timer to change icon and name every 5 seconds - _timer = new Timer(OnTimerElapsed, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + _timer = new Timer(OnTimerElapsed, null, interval, interval); } private void OnTimerElapsed(object state) @@ -282,4 +291,29 @@ internal sealed partial class SampleListPage : ListPage _timer?.Dispose(); } } + + internal sealed partial class ListItemChangingCommandInTime : ListItem + { + private readonly EverChangingCommand[] _commands = + [ + new("Water", TimeSpan.FromSeconds(2), "ðŸ¬", "ðŸ³", "ðŸŸ", "🦈"), + new("Faces", TimeSpan.FromSeconds(2), "ðŸ˜", "🥺", "ðŸ˜"), + new("Hearts", TimeSpan.FromSeconds(2), "♥ï¸", "💚", "💜", "🧡", "💛", "💙"), + ]; + + private int _state; + + public ListItemChangingCommandInTime() + { + Subtitle = "I change my command every 10 seconds, and the command changes it's icon every 2 seconds"; + var timer = new Timer(OnTimerElapsed, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); + this.Command = _commands[0]; + } + + private void OnTimerElapsed(object state) + { + _state = (_state + 1) % _commands.Length; + this.Command = _commands[_state]; + } + } } diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs index 018c11720c..abf80e3d1e 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs @@ -63,7 +63,7 @@ internal sealed partial class SampleListPageWithDetails : ListPage Details = new Details() { Title = "Hero Image Example", - HeroImage = new IconInfo("https://m.media-amazon.com/images/M/MV5BNDBkMzVmNGQtYTM2OC00OWRjLTk5OWMtNzNkMDI4NjFjNTZmXkEyXkFqcGdeQXZ3ZXNsZXk@._V1_QL75_UX500_CR0,0,500,281_.jpg"), + HeroImage = new IconInfo("https://m.media-amazon.com/images/M/MV5BNDBkMzVmNGQtYTM2OC00OWRjLTk5OWMtNzNkMDI4NjFjNTZmXkEyXkFqcGdeQXZ3ZXNsZXk@._V1_QL75_UX500_CR0,0,500,281_.jpg"), /* #no-spell-check-line */ Body = "It is literally an image of a hero", }, }, diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/SamplePagesExtension.csproj b/src/modules/cmdpal/ext/SamplePagesExtension/SamplePagesExtension.csproj index 4007e6a986..964211ddff 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/SamplePagesExtension.csproj +++ b/src/modules/cmdpal/ext/SamplePagesExtension/SamplePagesExtension.csproj @@ -62,10 +62,18 @@ true - + + true true true + + + false + false + false + + diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs index ef3a027f4f..73ef1815d4 100644 --- a/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs +++ b/src/modules/cmdpal/ext/SamplePagesExtension/SamplesListPage.cs @@ -5,6 +5,7 @@ using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using SamplePagesExtension.Pages; +using SamplePagesExtension.Pages.IssueSpecificPages; namespace SamplePagesExtension; @@ -33,9 +34,9 @@ public partial class SamplesListPage : ListPage Title = "Dynamic List Page Command", Subtitle = "Changes the list of items in response to the typed query", }, - new ListItem(new SampleGalleryListPage()) + new ListItem(new SampleGridsListPage()) { - Title = "Gallery List Page Command", + Title = "Grid views and galleries", Subtitle = "Displays items as a gallery", }, new ListItem(new OnLoadPage()) @@ -106,6 +107,11 @@ public partial class SamplesListPage : ListPage { Title = "Evil samples", Subtitle = "Samples designed to break the palette in many different evil ways", + }, + new ListItem(new AllIssueSamplesIndexPage()) + { + Title = "Issue-specific samples", + Subtitle = "Samples designed to reproduce specific issues", } ]; diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs index 13740eb1a1..1c0b5c18d0 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/FallbackCommandItem.cs @@ -6,7 +6,12 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit; public partial class FallbackCommandItem : CommandItem, IFallbackCommandItem, IFallbackHandler { - private IFallbackHandler? _fallbackHandler; + private readonly IFallbackHandler? _fallbackHandler; + + public FallbackCommandItem(string displayTitle) + { + DisplayTitle = displayTitle; + } public FallbackCommandItem(ICommand command, string displayTitle) : base(command) diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs index 6a0dde88cc..98e2fae688 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs @@ -15,6 +15,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Dictionary")] +[JsonSerializable(typeof(List>))] [JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)] internal sealed partial class JsonSerializationContext : JsonSerializerContext { diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs index cdb7b72b25..87beb49075 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ToggleSetting.cs @@ -26,15 +26,69 @@ public sealed class ToggleSetting : Setting public override Dictionary ToDictionary() { - return new Dictionary + var items = new List>(); + + if (!string.IsNullOrEmpty(Label)) { - { "type", "Input.Toggle" }, - { "title", Label }, - { "id", Key }, - { "label", Description }, - { "value", JsonSerializer.Serialize(Value, JsonSerializationContext.Default.Boolean) }, - { "isRequired", IsRequired }, - { "errorMessage", ErrorMessage }, + items.Add( + new() + { + { "type", "TextBlock" }, + { "text", Label }, + { "wrap", true }, + }); + } + + if (!(string.IsNullOrEmpty(Description) || string.Equals(Description, Label, StringComparison.OrdinalIgnoreCase))) + { + items.Add( + new() + { + { "type", "TextBlock" }, + { "text", Description }, + { "isSubtle", true }, + { "size", "Small" }, + { "spacing", "Small" }, + { "wrap", true }, + }); + } + + return new() + { + { "type", "ColumnSet" }, + { + "columns", new List> + { + new() + { + { "type", "Column" }, + { "width", "20px" }, + { + "items", new List> + { + new() + { + { "type", "Input.Toggle" }, + { "title", " " }, + { "id", Key }, + { "value", JsonSerializer.Serialize(Value, JsonSerializationContext.Default.Boolean) }, + { "isRequired", IsRequired }, + { "errorMessage", ErrorMessage }, + }, + } + }, + { "verticalContentAlignment", "Center" }, + }, + new() + { + { "type", "Column" }, + { "width", "stretch" }, + { "items", items }, + { "verticalContentAlignment", "Center" }, + }, + } + }, + { "spacing", "Medium" }, }; } diff --git a/src/modules/cmdpal/extensionsdk/nuget/BuildSDKHelper.ps1 b/src/modules/cmdpal/extensionsdk/nuget/BuildSDKHelper.ps1 index 8270e7bb4a..4390f0120e 100644 --- a/src/modules/cmdpal/extensionsdk/nuget/BuildSDKHelper.ps1 +++ b/src/modules/cmdpal/extensionsdk/nuget/BuildSDKHelper.ps1 @@ -56,7 +56,7 @@ if ($IsAzurePipelineBuild) { } if (($BuildStep -ieq "all") -Or ($BuildStep -ieq "build")) { - & $nugetPath restore (Join-Path $PSScriptRoot "..\..\..\..\..\PowerToys.sln") + & $nugetPath restore (Join-Path $PSScriptRoot "..\..\..\..\..\PowerToys.slnx") Try { foreach ($config in $Configuration.Split(",")) { diff --git a/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs b/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs index 36a17ceb19..f45fc28e6a 100644 --- a/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs +++ b/src/modules/imageresizer/tests/Models/ResizeBatchTests.cs @@ -10,7 +10,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; - +using ImageResizer.Properties; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Moq.Protected; @@ -101,7 +101,9 @@ namespace ImageResizer.Models private static ResizeBatch CreateBatch(Action executeAction) { var mock = new Mock { CallBase = true }; - mock.Protected().Setup("Execute", ItExpr.IsAny()).Callback(executeAction); + mock.Protected() + .Setup("Execute", ItExpr.IsAny(), ItExpr.IsAny()) + .Callback((string file, Settings settings) => executeAction(file)); return mock.Object; } diff --git a/src/modules/imageresizer/ui/ImageResizerUI.csproj b/src/modules/imageresizer/ui/ImageResizerUI.csproj index b146db8435..3ce98d8386 100644 --- a/src/modules/imageresizer/ui/ImageResizerUI.csproj +++ b/src/modules/imageresizer/ui/ImageResizerUI.csproj @@ -24,13 +24,13 @@ Resources\ImageResizer.ico - + diff --git a/src/modules/imageresizer/ui/Models/ResizeBatch.cs b/src/modules/imageresizer/ui/Models/ResizeBatch.cs index 1181395c09..87e0b84e7b 100644 --- a/src/modules/imageresizer/ui/Models/ResizeBatch.cs +++ b/src/modules/imageresizer/ui/Models/ResizeBatch.cs @@ -87,9 +87,14 @@ namespace ImageResizer.Models public IEnumerable Process(Action reportProgress, CancellationToken cancellationToken) { double total = Files.Count; - var completed = 0; + int completed = 0; var errors = new ConcurrentBag(); + // NOTE: Settings.Default is captured once before parallel processing. + // Any changes to settings on disk during this batch will NOT be reflected until the next batch. + // This improves performance and predictability by avoiding repeated mutex acquisition and behaviour change results in a batch. + var settings = Settings.Default; + // TODO: If we ever switch to Windows.Graphics.Imaging, we can get a lot more throughput by using the async // APIs and a custom SynchronizationContext Parallel.ForEach( @@ -97,13 +102,12 @@ namespace ImageResizer.Models new ParallelOptions { CancellationToken = cancellationToken, - MaxDegreeOfParallelism = Environment.ProcessorCount, }, (file, state, i) => { try { - Execute(file); + Execute(file, settings); } catch (Exception ex) { @@ -111,14 +115,13 @@ namespace ImageResizer.Models } Interlocked.Increment(ref completed); - reportProgress(completed, total); }); return errors; } - protected virtual void Execute(string file) - => new ResizeOperation(file, DestinationDirectory, Settings.Default).Execute(); + protected virtual void Execute(string file, Settings settings) + => new ResizeOperation(file, DestinationDirectory, settings).Execute(); } } diff --git a/src/modules/imageresizer/ui/Properties/Settings.cs b/src/modules/imageresizer/ui/Properties/Settings.cs index debb26a191..0f8690dcbb 100644 --- a/src/modules/imageresizer/ui/Properties/Settings.cs +++ b/src/modules/imageresizer/ui/Properties/Settings.cs @@ -461,33 +461,42 @@ namespace ImageResizer.Properties { } - // Needs to be called on the App UI thread as the properties are bound to the UI. - App.Current.Dispatcher.Invoke(() => + if (App.Current?.Dispatcher != null) { - ShrinkOnly = jsonSettings.ShrinkOnly; - Replace = jsonSettings.Replace; - IgnoreOrientation = jsonSettings.IgnoreOrientation; - RemoveMetadata = jsonSettings.RemoveMetadata; - JpegQualityLevel = jsonSettings.JpegQualityLevel; - PngInterlaceOption = jsonSettings.PngInterlaceOption; - TiffCompressOption = jsonSettings.TiffCompressOption; - FileName = jsonSettings.FileName; - KeepDateModified = jsonSettings.KeepDateModified; - FallbackEncoder = jsonSettings.FallbackEncoder; - CustomSize = jsonSettings.CustomSize; - SelectedSizeIndex = jsonSettings.SelectedSizeIndex; - - if (jsonSettings.Sizes.Count > 0) - { - Sizes.Clear(); - Sizes.AddRange(jsonSettings.Sizes); - - // Ensure Ids are unique and handle missing Ids - IdRecoveryHelper.RecoverInvalidIds(Sizes); - } - }); + // Needs to be called on the App UI thread as the properties are bound to the UI. + App.Current.Dispatcher.Invoke(() => ReloadCore(jsonSettings)); + } + else + { + ReloadCore(jsonSettings); + } _jsonMutex.ReleaseMutex(); } + + private void ReloadCore(Settings jsonSettings) + { + ShrinkOnly = jsonSettings.ShrinkOnly; + Replace = jsonSettings.Replace; + IgnoreOrientation = jsonSettings.IgnoreOrientation; + RemoveMetadata = jsonSettings.RemoveMetadata; + JpegQualityLevel = jsonSettings.JpegQualityLevel; + PngInterlaceOption = jsonSettings.PngInterlaceOption; + TiffCompressOption = jsonSettings.TiffCompressOption; + FileName = jsonSettings.FileName; + KeepDateModified = jsonSettings.KeepDateModified; + FallbackEncoder = jsonSettings.FallbackEncoder; + CustomSize = jsonSettings.CustomSize; + SelectedSizeIndex = jsonSettings.SelectedSizeIndex; + + if (jsonSettings.Sizes.Count > 0) + { + Sizes.Clear(); + Sizes.AddRange(jsonSettings.Sizes); + + // Ensure Ids are unique and handle missing Ids + IdRecoveryHelper.RecoverInvalidIds(Sizes); + } + } } } diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png index 4228da1e88..7a96d92df1 100644 Binary files a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png and b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.dark.png differ diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png index 6c4dcc5ae5..580cf3f609 100644 Binary files a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png and b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.OneNote/Images/oneNote.light.png differ diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System.UnitTests/QueryTests.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System.UnitTests/QueryTests.cs index d08ec588d3..a6cec9880d 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System.UnitTests/QueryTests.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System.UnitTests/QueryTests.cs @@ -74,7 +74,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.UnitTests var result = main.Object.Query(expectedQuery).FirstOrDefault().SubTitle; // Assert - Assert.AreEqual("Reboot computer into UEFI Firmware Settings (Requires administrative permissions.)", result); + Assert.AreEqual("Reboot computer into UEFI firmware settings (Requires administrative permissions.)", result); } [TestMethod] diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.Designer.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.Designer.cs index 5d07afd41f..a530fbc580 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.Designer.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -682,7 +682,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties { } /// - /// Looks up a localized string similar to You are about to reboot this computer into UEFI Firmware Settings menu, are you sure?. + /// Looks up a localized string similar to You are about to reboot this computer into UEFI firmware settings menu, are you sure?. /// internal static string Microsoft_plugin_sys_uefi_confirmation { get { @@ -691,7 +691,7 @@ namespace Microsoft.PowerToys.Run.Plugin.System.Properties { } /// - /// Looks up a localized string similar to Reboot computer into UEFI Firmware Settings (Requires administrative permissions.). + /// Looks up a localized string similar to Reboot computer into UEFI firmware settings (Requires administrative permissions.). /// internal static string Microsoft_plugin_sys_uefi_description { get { diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.resx b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.resx index eeaa8a423b..f9d1deaab2 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.resx +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.System/Properties/Resources.resx @@ -362,15 +362,15 @@ Means type like category. Here it means network interface type (ethernet, wifi, ...). - UEFI Firmware Settings + UEFI firmware settings This should align to the action in Windows Recovery Environment that restart into uefi settings. - You are about to reboot this computer into UEFI Firmware Settings menu, are you sure? + You are about to reboot this computer into UEFI firmware settings menu, are you sure? This should align to the action in Windows Recovery Environment that restart into uefi settings. - Reboot computer into UEFI Firmware Settings (Requires administrative permissions.) + Reboot computer into UEFI firmware settings (Requires administrative permissions.) This should align to the action in Windows Recovery Environment that restart into uefi settings. diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Properties/Resources.resx b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Properties/Resources.resx index e1e3a68766..84d81c1b96 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Properties/Resources.resx +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.TimeDate/Properties/Resources.resx @@ -208,7 +208,7 @@ 'UTC' means here 'Universal Time Convention' - Provides time and date values for the system time or a custom time stamp (e.g.'{0}', '{1}', '{2}', '{3}') + Shows time and date values for the system time or a custom time stamp (e.g.'{0}', '{1}', '{2}', '{3}') Do not translate the placeholders like '{0}' because it will be replaced in code. diff --git a/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs b/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs index 5003a02cae..4ff1a08697 100644 --- a/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs +++ b/src/modules/launcher/PowerLauncher/Helper/ErrorReporting.cs @@ -61,7 +61,7 @@ namespace PowerLauncher.Helper // Many bug reports because users see the "Report problem UI" after "the" crash with System.Runtime.InteropServices.COMException 0xD0000701 or 0x80263001. // However, displaying this "Report problem UI" during WPF crashes, especially when DWM composition is changing, is not ideal; some users reported it hangs for up to a minute before the "Report problem UI" appears. // This change modifies the behavior to log the exception instead of showing the "Report problem UI". - if (IsDwmCompositionException(e as System.Runtime.InteropServices.COMException)) + if (ExceptionHelper.IsRecoverableDwmCompositionException(e as System.Runtime.InteropServices.COMException)) { var logger = LogManager.GetLogger(LoggerName); logger.Error($"From {(isNotUIThread ? "non" : string.Empty)} UI thread's exception: {ExceptionFormatter.FormatException(e)}"); @@ -91,22 +91,5 @@ namespace PowerLauncher.Helper } } } - - private static bool IsDwmCompositionException(System.Runtime.InteropServices.COMException comException) - { - if (comException == null) - { - return false; - } - - var stackTrace = comException.StackTrace; - if (string.IsNullOrEmpty(stackTrace)) - { - return false; - } - - // Check for common DWM composition changed patterns in the stack trace - return stackTrace.Contains("DwmCompositionChanged"); - } } } diff --git a/src/modules/launcher/PowerLauncher/Helper/ExceptionHelper.cs b/src/modules/launcher/PowerLauncher/Helper/ExceptionHelper.cs new file mode 100644 index 0000000000..15e7de4eac --- /dev/null +++ b/src/modules/launcher/PowerLauncher/Helper/ExceptionHelper.cs @@ -0,0 +1,46 @@ +// 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.Runtime.InteropServices; + +namespace PowerLauncher.Helper +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Win32 naming conventions")] + internal static class ExceptionHelper + { + private const string PresentationFrameworkExceptionSource = "PresentationFramework"; + + private const int DWM_E_COMPOSITIONDISABLED = unchecked((int)0x80263001); + + // HRESULT for NT STATUS STATUS_MESSAGE_LOST (0xC0000701 | 0x10000000 == 0xD0000701) + private const int STATUS_MESSAGE_LOST_HR = unchecked((int)0xD0000701); + + /// + /// Returns true if the exception is a recoverable DWM composition exception. + /// + internal static bool IsRecoverableDwmCompositionException(Exception exception) + { + if (exception is not COMException comException) + { + return false; + } + + if (comException.HResult is DWM_E_COMPOSITIONDISABLED) + { + return true; + } + + if (comException.HResult is STATUS_MESSAGE_LOST_HR && comException.Source == PresentationFrameworkExceptionSource) + { + return true; + } + + // Check for common DWM composition changed patterns in the stack trace + var stackTrace = comException.StackTrace; + return !string.IsNullOrEmpty(stackTrace) && + stackTrace.Contains("DwmCompositionChanged"); + } + } +} diff --git a/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs b/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs index 2a27494b30..53cc841b30 100644 --- a/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs +++ b/src/modules/launcher/PowerLauncher/Helper/ThemeManager.cs @@ -3,13 +3,16 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using ManagedCommon; using Microsoft.Win32; using Wox.Infrastructure.Image; using Wox.Infrastructure.UserSettings; +using Wox.Plugin.Logger; namespace PowerLauncher.Helper { @@ -20,6 +23,9 @@ namespace PowerLauncher.Helper private readonly ThemeHelper _themeHelper = new(); private bool _disposed; + private CancellationTokenSource _themeUpdateTokenSource; + private const int MaxRetries = 5; + private const int InitialDelayMs = 2000; public Theme CurrentTheme { get; private set; } @@ -108,10 +114,80 @@ namespace PowerLauncher.Helper { Theme newTheme = _themeHelper.DetermineTheme(_settings.Theme); - _mainWindow.Dispatcher.Invoke(() => + // Cancel any existing theme update operation + _themeUpdateTokenSource?.Cancel(); + _themeUpdateTokenSource?.Dispose(); + _themeUpdateTokenSource = new CancellationTokenSource(); + + // Start theme update with retry logic in the background + _ = UpdateThemeWithRetryAsync(newTheme, _themeUpdateTokenSource.Token); + } + + /// + /// Applies the theme with retry logic for desktop composition errors. + /// + /// The theme to apply. + /// Token to cancel the operation. + private async Task UpdateThemeWithRetryAsync(Theme theme, CancellationToken cancellationToken) + { + var delayMs = 0; + const int maxAttempts = MaxRetries + 1; + + for (var attempt = 1; attempt <= maxAttempts; attempt++) { - SetSystemTheme(newTheme); - }); + try + { + if (delayMs > 0) + { + await Task.Delay(delayMs, cancellationToken); + } + + if (cancellationToken.IsCancellationRequested) + { + Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager)); + return; + } + + await _mainWindow.Dispatcher.InvokeAsync(() => + { + SetSystemTheme(theme); + }); + + if (attempt > 1) + { + Log.Info($"Successfully applied theme after {attempt - 1} retry attempt(s).", typeof(ThemeManager)); + } + + return; + } + catch (COMException ex) when (ExceptionHelper.IsRecoverableDwmCompositionException(ex)) + { + switch (attempt) + { + case 1: + Log.Warn($"Desktop composition is disabled (HRESULT: 0x{ex.HResult:X}). Scheduling retries for theme update.", typeof(ThemeManager)); + delayMs = InitialDelayMs; + break; + case < maxAttempts: + Log.Warn($"Retry {attempt - 1}/{MaxRetries} failed: Desktop composition still disabled. Retrying in {delayMs * 2}ms...", typeof(ThemeManager)); + delayMs *= 2; + break; + default: + Log.Exception($"Failed to set theme after {MaxRetries} retry attempts. Desktop composition remains disabled.", ex, typeof(ThemeManager)); + break; + } + } + catch (OperationCanceledException) + { + Log.Debug("Theme update operation was cancelled.", typeof(ThemeManager)); + return; + } + catch (Exception ex) + { + Log.Exception($"Unexpected error during theme update (attempt {attempt}/{maxAttempts}): {ex.Message}", ex, typeof(ThemeManager)); + throw; + } + } } public void Dispose() @@ -130,6 +206,8 @@ namespace PowerLauncher.Helper if (disposing) { SystemEvents.UserPreferenceChanged -= OnUserPreferenceChanged; + _themeUpdateTokenSource?.Cancel(); + _themeUpdateTokenSource?.Dispose(); } _disposed = true; diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs index 586b92ed75..a3721a04ec 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs @@ -24,13 +24,15 @@ using Windows.Storage; namespace Peek.FilePreviewer.Previewers.MediaPreviewer { - public partial class AudioPreviewer : ObservableObject, IAudioPreviewer + public partial class AudioPreviewer : ObservableObject, IDisposable, IAudioPreviewer { + private MediaSource? _mediaSource; + [ObservableProperty] private PreviewState _state; [ObservableProperty] - private AudioPreviewData _preview; + private AudioPreviewData? _preview; private IFileSystemItem Item { get; } @@ -40,7 +42,6 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer { Item = file; Dispatcher = DispatcherQueue.GetForCurrentThread(); - Preview = new AudioPreviewData(); } public async Task CopyAsync() @@ -63,19 +64,23 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer { State = PreviewState.Loading; + Preview = new AudioPreviewData(); + var thumbnailTask = LoadThumbnailAsync(cancellationToken); var sourceTask = LoadSourceAsync(cancellationToken); var metadataTask = LoadMetadataAsync(cancellationToken); await Task.WhenAll(thumbnailTask, sourceTask, metadataTask); - if (!thumbnailTask.Result || !sourceTask.Result || !metadataTask.Result) + if (sourceTask.Result && metadataTask.Result) { - State = PreviewState.Error; + State = PreviewState.Loaded; } else { - State = PreviewState.Loaded; + // Release all resources on error. + Unload(); + State = PreviewState.Error; } } @@ -88,12 +93,15 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer { cancellationToken.ThrowIfCancellationRequested(); - var thumbnail = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken) - ?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken); + if (Preview != null) + { + var thumbnail = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken) + ?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken); - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - Preview.Thumbnail = thumbnail ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg")); + Preview.Thumbnail = thumbnail ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg")); + } }); }); } @@ -110,7 +118,11 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer { cancellationToken.ThrowIfCancellationRequested(); - Preview.MediaSource = MediaSource.CreateFromStorageFile(storageFile); + if (Preview != null) + { + _mediaSource = MediaSource.CreateFromStorageFile(storageFile); + Preview.MediaSource = _mediaSource; + } }); }); } @@ -123,6 +135,11 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer await Dispatcher.RunOnUiThread(() => { + if (Preview == null) + { + return; + } + cancellationToken.ThrowIfCancellationRequested(); Preview.Title = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicTitle) ?? Item.Name[..^Item.Extension.Length]; @@ -160,6 +177,22 @@ namespace Peek.FilePreviewer.Previewers.MediaPreviewer return _supportedFileTypes.Contains(item.Extension); } + public void Dispose() + { + Unload(); + GC.SuppressFinalize(this); + } + + /// + /// Explicitly unloads the preview and releases file resources. + /// + public void Unload() + { + _mediaSource?.Dispose(); + _mediaSource = null; + Preview = null; + } + private static readonly HashSet _supportedFileTypes = new() { ".aac", diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs index b9de53e87b..061d3eca47 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/VideoPreviewer.cs @@ -25,6 +25,8 @@ namespace Peek.FilePreviewer.Previewers { public partial class VideoPreviewer : ObservableObject, IVideoPreviewer, IDisposable { + private MediaSource? _mediaSource; + [ObservableProperty] private MediaSource? preview; @@ -56,6 +58,7 @@ namespace Peek.FilePreviewer.Previewers public void Dispose() { + Unload(); GC.SuppressFinalize(this); } @@ -145,7 +148,8 @@ namespace Peek.FilePreviewer.Previewers MissingCodecName = missingCodecName; } - Preview = MediaSource.CreateFromStorageFile(storageFile); + _mediaSource = MediaSource.CreateFromStorageFile(storageFile); + Preview = _mediaSource; }); }); } @@ -155,6 +159,16 @@ namespace Peek.FilePreviewer.Previewers return !(VideoTask?.Result ?? true); } + /// + /// Explicitly unloads the preview and releases file resources. + /// + public void Unload() + { + _mediaSource?.Dispose(); + _mediaSource = null; + Preview = null; + } + private static readonly HashSet _supportedFileTypes = new() { ".mp4", ".3g2", ".3gp", ".3gp2", ".3gpp", ".asf", ".avi", ".m2t", ".m2ts", diff --git a/src/modules/peek/Peek.UI/MainWindowViewModel.cs b/src/modules/peek/Peek.UI/MainWindowViewModel.cs index db54e346cc..775664c042 100644 --- a/src/modules/peek/Peek.UI/MainWindowViewModel.cs +++ b/src/modules/peek/Peek.UI/MainWindowViewModel.cs @@ -62,6 +62,12 @@ namespace Peek.UI [ObservableProperty] private IFileSystemItem? _currentItem; + /// + /// Work around missing navigation when peeking from CLI. + /// TODO: Implement navigation when peeking from CLI. + /// + private bool _isFromCli; + partial void OnCurrentItemChanged(IFileSystemItem? value) { WindowTitle = value != null @@ -129,7 +135,24 @@ namespace Peek.UI NavigationThrottleTimer.Interval = TimeSpan.FromMilliseconds(NavigationThrottleDelayMs); } - public void Initialize(HWND foregroundWindowHandle) + public void Initialize(SelectedItem selectedItem) + { + switch (selectedItem) + { + case SelectedItemByPath selectedItemByPath: + InitializeFromCli(selectedItemByPath.Path); + break; + + case SelectedItemByWindowHandle selectedItemByWindowHandle: + InitializeFromExplorer(selectedItemByWindowHandle.WindowHandle); + break; + + default: + throw new NotImplementedException($"Invalid type of selected item: '{selectedItem.GetType().FullName}'"); + } + } + + private void InitializeFromExplorer(HWND foregroundWindowHandle) { try { @@ -141,10 +164,20 @@ namespace Peek.UI } _currentIndex = DisplayIndex = 0; + _isFromCli = false; CurrentItem = (Items != null && Items.Count > 0) ? Items[0] : null; } + private void InitializeFromCli(string path) + { + // TODO: implement navigation + _isFromCli = true; + Items = null; + _currentIndex = DisplayIndex = 0; + CurrentItem = new FileItem(path, Path.GetFileName(path)); + } + public void Uninitialize() { _currentIndex = DisplayIndex = 0; @@ -153,6 +186,7 @@ namespace Peek.UI Items = null; _navigationDirection = NavigationDirection.Forwards; IsErrorVisible = false; + _isFromCli = false; } public void AttemptPreviousNavigation() => Navigate(NavigationDirection.Backwards); @@ -166,6 +200,12 @@ namespace Peek.UI return; } + // TODO: implement navigation. + if (_isFromCli) + { + return; + } + if (Items == null || Items.Count == _deletedItemIndexes.Count) { _currentIndex = DisplayIndex = 0; diff --git a/src/modules/peek/Peek.UI/Models/SelectedItem.cs b/src/modules/peek/Peek.UI/Models/SelectedItem.cs new file mode 100644 index 0000000000..9d0cc6568a --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/SelectedItem.cs @@ -0,0 +1,11 @@ +// 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 Peek.UI.Models +{ + public abstract class SelectedItem + { + public abstract bool Matches(string? path); + } +} diff --git a/src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs b/src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs new file mode 100644 index 0000000000..5f53865bfd --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/SelectedItemByPath.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Peek.UI.Models +{ + public class SelectedItemByPath : SelectedItem + { + public string Path { get; } + + public SelectedItemByPath(string path) + { + Path = path; + } + + public override bool Matches(string? path) + { + return string.Equals(Path, path, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/src/modules/peek/Peek.UI/Models/SelectedItemByWindowHandle.cs b/src/modules/peek/Peek.UI/Models/SelectedItemByWindowHandle.cs new file mode 100644 index 0000000000..e93b2f94ca --- /dev/null +++ b/src/modules/peek/Peek.UI/Models/SelectedItemByWindowHandle.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Peek.UI.Extensions; +using Peek.UI.Helpers; +using Windows.Win32.Foundation; + +namespace Peek.UI.Models +{ + public class SelectedItemByWindowHandle : SelectedItem + { + public HWND WindowHandle { get; } + + public SelectedItemByWindowHandle(HWND windowHandle) + { + WindowHandle = windowHandle; + } + + public override bool Matches(string? path) + { + var selectedItems = FileExplorerHelper.GetSelectedItems(WindowHandle); + var selectedItemsCount = selectedItems?.GetCount() ?? 0; + if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1) + { + return false; + } + + var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path; + var currentItemPath = path; + return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath; + } + } +} diff --git a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs index 9bd66e380f..b89e871a4d 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs @@ -3,16 +3,19 @@ // See the LICENSE file in the project root for more information. using System; - +using System.IO; +using System.Threading; using ManagedCommon; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls.Primitives; using Peek.Common; using Peek.FilePreviewer; using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Previewers; +using Peek.UI.Models; using Peek.UI.Native; using Peek.UI.Telemetry.Events; using Peek.UI.Views; @@ -23,7 +26,7 @@ namespace Peek.UI /// /// Provides application-specific behavior to supplement the default Application class. /// - public partial class App : Application, IApp + public partial class App : Application, IApp, IDisposable { public static int PowerToysPID { get; set; } @@ -36,6 +39,10 @@ namespace Peek.UI private MainWindow? Window { get; set; } + private bool _disposed; + private SelectedItem? _selectedItem; + private bool _launchedFromCli; + /// /// Initializes a new instance of the class. /// Initializes the singleton application object. This is the first line of authored code @@ -52,22 +59,22 @@ namespace Peek.UI InitializeComponent(); Logger.InitializeLogger("\\Peek\\Logs"); - Host = Microsoft.Extensions.Hosting.Host. - CreateDefaultBuilder(). - UseContentRoot(AppContext.BaseDirectory). - ConfigureServices((context, services) => - { - // Core Services - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); + Host = Microsoft.Extensions.Hosting.Host + .CreateDefaultBuilder() + .UseContentRoot(AppContext.BaseDirectory) + .ConfigureServices((context, services) => + { + // Core Services + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); - // Views and ViewModels - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - }). - Build(); + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + }) + .Build(); UnhandledException += App_UnhandledException; } @@ -99,6 +106,7 @@ namespace Peek.UI var cmdArgs = Environment.GetCommandLineArgs(); if (cmdArgs?.Length > 1) { + // Check if the last argument is a PowerToys Runner PID if (int.TryParse(cmdArgs[^1], out int powerToysRunnerPid)) { RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () => @@ -107,9 +115,25 @@ namespace Peek.UI Environment.Exit(0); }); } + else + { + // Command line argument is a file path - activate Peek with that file + string filePath = cmdArgs[^1]; + if (File.Exists(filePath) || Directory.Exists(filePath)) + { + _selectedItem = new SelectedItemByPath(filePath); + _launchedFromCli = true; + OnShowPeek(); + return; + } + else + { + Logger.LogError($"Command line argument is not a valid file or directory: {filePath}"); + } + } } - NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey); + NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnShowPeek); NativeEventWaiter.WaitForEventLoop(Constants.TerminatePeekEvent(), () => { ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); @@ -126,11 +150,16 @@ namespace Peek.UI /// /// Handle Peek hotkey /// - private void OnPeekHotkey() + private void OnShowPeek() { - // Need to read the foreground HWND before activating Peek to avoid focus stealing - // Foreground HWND must always be Explorer or Desktop - var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow(); + // null means explorer, not null means CLI + if (_selectedItem == null) + { + // Need to read the foreground HWND before activating Peek to avoid focus stealing + // Foreground HWND must always be Explorer or Desktop + var foregroundWindowHandle = Windows.Win32.PInvoke_PeekUI.GetForegroundWindow(); + _selectedItem = new SelectedItemByWindowHandle(foregroundWindowHandle); + } bool firstActivation = false; @@ -140,7 +169,38 @@ namespace Peek.UI Window = new MainWindow(); } - Window.Toggle(firstActivation, foregroundWindowHandle); + Window.Toggle(firstActivation, _selectedItem, _launchedFromCli); + _launchedFromCli = false; + _selectedItem = null; + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // dispose managed state (managed objects) + } + + // free unmanaged resources (unmanaged objects) and override finalizer + // set large fields to null + _disposed = true; + } + } + + /* // override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources + // ~App() + // { + // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + // Dispose(disposing: false); + // } */ + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } } } diff --git a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs index 4edad9a807..2c8983c634 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs @@ -18,6 +18,7 @@ using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Previewers; using Peek.UI.Extensions; using Peek.UI.Helpers; +using Peek.UI.Models; using Peek.UI.Telemetry.Events; using Windows.Foundation; using WinUIEx; @@ -38,6 +39,7 @@ namespace Peek.UI /// dialog is open at a time. /// private bool _isDeleteInProgress; + private bool _exitAfterClose; public MainWindow() { @@ -116,12 +118,17 @@ namespace Peek.UI /// /// Toggling the window visibility and querying files when necessary. /// - public void Toggle(bool firstActivation, Windows.Win32.Foundation.HWND foregroundWindowHandle) + public void Toggle(bool firstActivation, SelectedItem selectedItem, bool exitAfterClose) { + if (exitAfterClose) + { + _exitAfterClose = true; + } + if (firstActivation) { Activate(); - Initialize(foregroundWindowHandle); + Initialize(selectedItem); return; } @@ -132,9 +139,9 @@ namespace Peek.UI if (AppWindow.IsVisible) { - if (IsNewSingleSelectedItem(foregroundWindowHandle)) + if (IsNewSingleSelectedItem(selectedItem)) { - Initialize(foregroundWindowHandle); + Initialize(selectedItem); Activate(); // Brings existing window into focus in case it was previously minimized } else @@ -144,7 +151,7 @@ namespace Peek.UI } else { - Initialize(foregroundWindowHandle); + Initialize(selectedItem); } } @@ -182,12 +189,12 @@ namespace Peek.UI Uninitialize(); } - private void Initialize(Windows.Win32.Foundation.HWND foregroundWindowHandle) + private void Initialize(SelectedItem selectedItem) { var bootTime = new System.Diagnostics.Stopwatch(); bootTime.Start(); - ViewModel.Initialize(foregroundWindowHandle); + ViewModel.Initialize(selectedItem); ViewModel.ScalingFactor = this.GetMonitorScale(); this.Content.KeyUp += Content_KeyUp; @@ -207,6 +214,11 @@ namespace Peek.UI this.Content.KeyUp -= Content_KeyUp; ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); + + if (_exitAfterClose) + { + Environment.Exit(0); + } } /// @@ -272,20 +284,11 @@ namespace Peek.UI Uninitialize(); } - private bool IsNewSingleSelectedItem(Windows.Win32.Foundation.HWND foregroundWindowHandle) + private bool IsNewSingleSelectedItem(SelectedItem selectedItem) { try { - var selectedItems = FileExplorerHelper.GetSelectedItems(foregroundWindowHandle); - var selectedItemsCount = selectedItems?.GetCount() ?? 0; - if (selectedItems == null || selectedItemsCount == 0 || selectedItemsCount > 1) - { - return false; - } - - var fileExplorerSelectedItemPath = selectedItems.GetItemAt(0).ToIFileSystemItem().Path; - var currentItemPath = ViewModel.CurrentItem?.Path; - return fileExplorerSelectedItemPath != null && currentItemPath != null && fileExplorerSelectedItemPath != currentItemPath; + return selectedItem.Matches(ViewModel.CurrentItem?.Path); } catch (Exception ex) { diff --git a/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs b/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs index 36f2491fcf..fd57c444ca 100644 --- a/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs +++ b/src/modules/peek/Peek.UITests/PeekFilePreviewTests.cs @@ -9,6 +9,7 @@ using System.Drawing; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.PowerToys.UITest; @@ -35,6 +36,105 @@ public class PeekFilePreviewTests : UITestBase { } + static PeekFilePreviewTests() + { + FixSettingsFileBeforeTests(); + } + + private static readonly JsonSerializerOptions IndentedJsonOptions = new() { WriteIndented = true }; + + private static void FixSettingsFileBeforeTests() + { + try + { + // Default Peek settings + string peekSettingsContent = @"{ + ""name"": ""Peek"", + ""version"": ""1.0"", + ""properties"": { + ""ActivationShortcut"": { + ""win"": false, + ""ctrl"": true, + ""alt"": false, + ""shift"": false, + ""code"": 32, + ""key"": ""Space"" + }, + ""AlwaysRunNotElevated"": { + ""value"": true + }, + ""CloseAfterLosingFocus"": { + ""value"": false + }, + ""ConfirmFileDelete"": { + ""value"": true + }, + ""EnableSpaceToActivate"": { + ""value"": false + } + } + }"; + + // Update Peek module settings + SettingsConfigHelper.UpdateModuleSettings( + "Peek", + peekSettingsContent, + (settings) => + { + // Get or ensure properties section exists + Dictionary properties; + + if (settings.TryGetValue("properties", out var propertiesObj)) + { + if (propertiesObj is Dictionary dict) + { + properties = dict; + } + else if (propertiesObj is JsonElement jsonElem) + { + properties = JsonSerializer.Deserialize>(jsonElem.GetRawText()) + ?? throw new InvalidOperationException("Failed to deserialize properties"); + } + else + { + properties = new Dictionary(); + } + } + else + { + properties = new Dictionary(); + } + + // Update the required properties + properties["ActivationShortcut"] = new Dictionary + { + { "win", false }, + { "ctrl", true }, + { "alt", false }, + { "shift", false }, + { "code", 32 }, + { "key", "Space" }, + }; + + properties["EnableSpaceToActivate"] = new Dictionary + { + { "value", false }, + }; + + settings["properties"] = properties; + }); + + // Disable all modules except Peek in global settings + SettingsConfigHelper.ConfigureGlobalModuleSettings("Peek"); + + Debug.WriteLine("Successfully updated all settings - Peek shortcut configured and all modules except Peek disabled"); + } + catch (Exception ex) + { + Assert.Fail($"ERROR in FixSettingsFileBeforeTests: {ex.Message}"); + } + } + [TestInitialize] public void TestInitialize() { diff --git a/src/modules/poweraccent/PowerAccent.Core/Languages.cs b/src/modules/poweraccent/PowerAccent.Core/Languages.cs index 6e329ffe7f..2917feff91 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Languages.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Languages.cs @@ -212,7 +212,7 @@ namespace PowerAccent.Core LetterKey.VK_L => new[] { "ļ", "₺" }, // ₺ is in VK_T for other languages, but not VK_L, so we add it here. LetterKey.VK_M => new[] { "á¹" }, LetterKey.VK_N => new[] { "ņ", "á¹…", "â¿", "â„•", "â„–" }, - LetterKey.VK_O => new[] { "ȯ", "∅" }, + LetterKey.VK_O => new[] { "ȯ", "∅", "⌀" }, LetterKey.VK_P => new[] { "á¹—", "â„—", "âˆ", "¶" }, LetterKey.VK_Q => new[] { "ℚ" }, LetterKey.VK_R => new[] { "á¹™", "®", "â„" }, @@ -224,7 +224,7 @@ namespace PowerAccent.Core LetterKey.VK_X => new[] { "ẋ", "×" }, LetterKey.VK_Y => new[] { "áº", "ê¡" }, LetterKey.VK_Z => new[] { "Ê’", "ǯ", "ℤ" }, - LetterKey.VK_COMMA => new[] { "∙", "â‚‹", "â»", "–", "√" }, // – is in VK_MINUS for other languages, but not VK_COMMA, so we add it here. + LetterKey.VK_COMMA => new[] { "∙", "â‚‹", "â»", "–", "√", "‟", "《", "》", "‛", "〈", "〉", "″", "‴", "â—" }, // – is in VK_MINUS for other languages, but not VK_COMMA, so we add it here. LetterKey.VK_PERIOD => new[] { "…", "â", "\u0300", "\u0301", "\u0302", "\u0303", "\u0304", "\u0308", "\u030B", "\u030C" }, LetterKey.VK_MINUS => new[] { "~", "â€", "‑", "‒", "—", "―", "â“", "−", "⸺", "⸻", "∓" }, LetterKey.VK_SLASH_ => new[] { "÷", "√" }, @@ -302,6 +302,7 @@ namespace PowerAccent.Core LetterKey.VK_E => new[] { "€" }, LetterKey.VK_S => new[] { "Å¡" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "»", "«" }, _ => Array.Empty(), }; } @@ -317,6 +318,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ü" }, LetterKey.VK_Z => new[] { "ž" }, LetterKey.VK_S => new[] { "Å¡" }, + LetterKey.VK_COMMA => new[] { "„", "“", "«", "»" }, _ => Array.Empty(), }; } @@ -344,6 +346,7 @@ namespace PowerAccent.Core LetterKey.VK_A => new[] { "ä", "Ã¥" }, LetterKey.VK_E => new[] { "€" }, LetterKey.VK_O => new[] { "ö" }, + LetterKey.VK_COMMA => new[] { "â€", "’", "»" }, _ => Array.Empty(), }; } @@ -360,6 +363,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ô", "ö", "ó", "ò", "õ", "Å“" }, LetterKey.VK_U => new[] { "û", "ù", "ü", "ú" }, LetterKey.VK_Y => new[] { "ÿ", "ý" }, + LetterKey.VK_COMMA => new[] { "«", "»", "‹", "›", "“", "â€", "‘", "’" }, _ => Array.Empty(), }; } @@ -376,6 +380,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ú" }, LetterKey.VK_Y => new[] { "ý" }, LetterKey.VK_T => new[] { "þ" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘" }, _ => Array.Empty(), }; } @@ -393,7 +398,7 @@ namespace PowerAccent.Core LetterKey.VK_N => new[] { "ñ" }, LetterKey.VK_O => new[] { "ó" }, LetterKey.VK_U => new[] { "ú", "ü" }, - LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!" }, + LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!", "«", "»", "“", "â€", "‘", "’" }, _ => Array.Empty(), }; } @@ -411,7 +416,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ò", "ó" }, LetterKey.VK_U => new[] { "ù", "ú", "ü" }, LetterKey.VK_L => new[] { "·" }, - LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!" }, + LetterKey.VK_COMMA => new[] { "¿", "?", "¡", "!", "«", "»", "“", "â€", "‘", "’" }, _ => Array.Empty(), }; } @@ -427,6 +432,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "Å" }, LetterKey.VK_S => new[] { "$" }, LetterKey.VK_U => new[] { "Å«" }, + LetterKey.VK_COMMA => new[] { "“", "â€", "‘", "’" }, _ => Array.Empty(), }; } @@ -443,6 +449,7 @@ namespace PowerAccent.Core LetterKey.VK_N => new[] { "ñ" }, LetterKey.VK_O => new[] { "ó", "ö", "ô" }, LetterKey.VK_U => new[] { "ú", "ü", "û" }, + LetterKey.VK_COMMA => new[] { "“", "„", "â€", "‘", ",", "’" }, _ => Array.Empty(), }; } @@ -469,6 +476,7 @@ namespace PowerAccent.Core LetterKey.VK_V => new[] { "ü", "Ç–", "ǘ", "Çš", "Çœ" }, LetterKey.VK_Y => new[] { "Â¥" }, LetterKey.VK_Z => new[] { "ẑ" }, + LetterKey.VK_COMMA => new[] { "“", "â€", "‘", "’", "「", "ã€", "『", "ã€" }, _ => Array.Empty(), }; } @@ -505,6 +513,7 @@ namespace PowerAccent.Core LetterKey.VK_S => new[] { "ÅŸ" }, LetterKey.VK_T => new[] { "₺" }, LetterKey.VK_U => new[] { "ü", "û" }, + LetterKey.VK_COMMA => new[] { "“", "â€", "‘", "’", "«", "»", "‹", "›" }, _ => Array.Empty(), }; } @@ -522,6 +531,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ó" }, LetterKey.VK_S => new[] { "Å›" }, LetterKey.VK_Z => new[] { "ż", "ź" }, + LetterKey.VK_COMMA => new[] { "„", "â€", "‘", "’", "»", "«" }, _ => Array.Empty(), }; } @@ -539,7 +549,7 @@ namespace PowerAccent.Core LetterKey.VK_P => new[] { "Ï€" }, LetterKey.VK_S => new[] { "$" }, LetterKey.VK_U => new[] { "ú" }, - LetterKey.VK_COMMA => new[] { "≤", "≥", "≠", "≈", "≙", "±", "₊", "âº" }, + LetterKey.VK_COMMA => new[] { "≤", "≥", "≠", "≈", "≙", "±", "₊", "âº", "“", "â€", "‘", "’", "«", "»" }, _ => Array.Empty(), }; } @@ -594,6 +604,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ú" }, LetterKey.VK_Y => new[] { "ý" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -608,6 +619,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "í" }, LetterKey.VK_O => new[] { "ó" }, LetterKey.VK_U => new[] { "ú" }, + LetterKey.VK_COMMA => new[] { "“", "â€", "‘", "’" }, _ => Array.Empty(), }; } @@ -623,6 +635,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ò" }, LetterKey.VK_P => new[] { "£" }, LetterKey.VK_U => new[] { "ù" }, + LetterKey.VK_COMMA => new[] { "“", "â€", "‘", "’" }, _ => Array.Empty(), }; } @@ -645,6 +658,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ů", "ú" }, LetterKey.VK_Y => new[] { "ý" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -659,6 +673,7 @@ namespace PowerAccent.Core LetterKey.VK_O => new[] { "ö" }, LetterKey.VK_S => new[] { "ß" }, LetterKey.VK_U => new[] { "ü" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -689,6 +704,7 @@ namespace PowerAccent.Core LetterKey.VK_X => new string[] { "ξ" }, LetterKey.VK_Y => new string[] { "Ï…" }, LetterKey.VK_Z => new string[] { "ζ" }, + LetterKey.VK_COMMA => new[] { "“", "â€", "«", "»", }, _ => Array.Empty(), }; } @@ -710,7 +726,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "ï­‹", "וּ", "×°", "\u05b9" }, LetterKey.VK_X => new[] { "\u05b6", "\u05b1" }, LetterKey.VK_Y => new[] { "×±" }, - LetterKey.VK_COMMA => new[] { "â€", "’", "×´", "׳" }, + LetterKey.VK_COMMA => new[] { "â€", "’", "'", "×´", "׳" }, LetterKey.VK_PERIOD => new[] { "\u05ab", "\u05bd", "\u05bf" }, LetterKey.VK_MINUS => new[] { "–", "Ö¾" }, _ => Array.Empty(), @@ -727,6 +743,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "í" }, LetterKey.VK_O => new[] { "ó", "Å‘", "ö" }, LetterKey.VK_U => new[] { "ú", "ű", "ü" }, + LetterKey.VK_COMMA => new[] { "„", "â€", "»", "«" }, _ => Array.Empty(), }; } @@ -740,6 +757,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "î" }, LetterKey.VK_S => new[] { "È™" }, LetterKey.VK_T => new[] { "È›" }, + LetterKey.VK_COMMA => new[] { "„", "â€", "«", "»" }, _ => Array.Empty(), }; } @@ -754,6 +772,7 @@ namespace PowerAccent.Core LetterKey.VK_I => new[] { "ì", "í" }, LetterKey.VK_O => new[] { "ò", "ó" }, LetterKey.VK_U => new[] { "ù", "ú" }, + LetterKey.VK_COMMA => new[] { "«", "»", "“", "â€", "‘", "’" }, _ => Array.Empty(), }; } @@ -772,6 +791,7 @@ namespace PowerAccent.Core LetterKey.VK_R => new[] { "Å™" }, LetterKey.VK_S => new[] { "ÅŸ" }, LetterKey.VK_U => new[] { "û", "ü" }, + LetterKey.VK_COMMA => new[] { "«", "»", "“", "â€" }, _ => Array.Empty(), }; } @@ -789,6 +809,7 @@ namespace PowerAccent.Core LetterKey.VK_U => new[] { "û", "ü", "ù", "ú" }, LetterKey.VK_Y => new[] { "Å·", "ÿ", "ỳ", "ý" }, LetterKey.VK_W => new[] { "ŵ", "ẅ", "áº", "ẃ" }, + LetterKey.VK_COMMA => new[] { "‘", "’", "“", "“" }, _ => Array.Empty(), }; } @@ -801,6 +822,7 @@ namespace PowerAccent.Core LetterKey.VK_A => new[] { "Ã¥", "ä" }, LetterKey.VK_E => new[] { "é" }, LetterKey.VK_O => new[] { "ö" }, + LetterKey.VK_COMMA => new[] { "â€", "’", "»", "«" }, _ => Array.Empty(), }; } @@ -814,6 +836,7 @@ namespace PowerAccent.Core LetterKey.VK_D => new[] { "Ä‘" }, LetterKey.VK_S => new[] { "Å¡" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "’", "»", "«", "›", "‹" }, _ => Array.Empty(), }; } @@ -838,6 +861,7 @@ namespace PowerAccent.Core { LetterKey.VK_E => new[] { "Ñ" }, LetterKey.VK_I => new[] { "Ñ" }, + LetterKey.VK_COMMA => new[] { "„", "“", "’", "‘" }, _ => Array.Empty(), }; } @@ -869,6 +893,7 @@ namespace PowerAccent.Core LetterKey.VK_E => new[] { "€", "é" }, LetterKey.VK_O => new[] { "ø" }, LetterKey.VK_S => new[] { "$" }, + LetterKey.VK_COMMA => new[] { "«", "»", ",", "‘", "’", "„", "“" }, _ => Array.Empty(), }; } @@ -881,6 +906,7 @@ namespace PowerAccent.Core LetterKey.VK_A => new[] { "Ã¥", "æ" }, LetterKey.VK_E => new[] { "€" }, LetterKey.VK_O => new[] { "ø" }, + LetterKey.VK_COMMA => new[] { "»", "«", "“", "â€", "›", "‹", "‘", "’" }, _ => Array.Empty(), }; } @@ -897,6 +923,7 @@ namespace PowerAccent.Core LetterKey.VK_S => new[] { "Å¡" }, LetterKey.VK_U => new[] { "ų", "Å«" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "‚", "‘" }, _ => Array.Empty(), }; } @@ -910,6 +937,7 @@ namespace PowerAccent.Core LetterKey.VK_E => new[] { "€" }, LetterKey.VK_S => new[] { "Å¡" }, LetterKey.VK_Z => new[] { "ž" }, + LetterKey.VK_COMMA => new[] { "„", "“", "»", "«" }, _ => Array.Empty(), }; } diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs index fa020ee4fe..d538d5d139 100644 --- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs +++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs @@ -20,6 +20,7 @@ public partial class PowerAccent : IDisposable // Keys that show a description (like dashes) when ShowCharacterInfoSetting is 1 private readonly LetterKey[] _letterKeysShowingDescription = new LetterKey[] { LetterKey.VK_O }; + private const double ScreenMinPadding = 150; private bool _visible; private string[] _characters = Array.Empty(); @@ -323,13 +324,17 @@ public partial class PowerAccent : IDisposable public Point GetDisplayCoordinates(Size window) { (Point Location, Size Size, double Dpi) activeDisplay = WindowsFunctions.GetActiveDisplay(); - double primaryDPI = Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth; - Rect screen = new Rect(activeDisplay.Location, activeDisplay.Size) / primaryDPI; + Rect screen = new(activeDisplay.Location, activeDisplay.Size); Position position = _settingService.Position; /* Debug.WriteLine("Dpi: " + activeDisplay.Dpi); */ - return Calculation.GetRawCoordinatesFromPosition(position, screen, window); + return Calculation.GetRawCoordinatesFromPosition(position, screen, window, activeDisplay.Dpi) / activeDisplay.Dpi; + } + + public double GetDisplayMaxWidth() + { + return WindowsFunctions.GetActiveDisplay().Size.Width - ScreenMinPadding; } public Position GetToolbarPosition() diff --git a/src/modules/poweraccent/PowerAccent.Core/Tools/Calculation.cs b/src/modules/poweraccent/PowerAccent.Core/Tools/Calculation.cs index 4929af535b..0945bfc99f 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Tools/Calculation.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Tools/Calculation.cs @@ -18,18 +18,18 @@ namespace PowerAccent.Core.Tools top < screen.Y ? caret.Y + 20 : top); } - public static Point GetRawCoordinatesFromPosition(Position position, Rect screen, Size window) + public static Point GetRawCoordinatesFromPosition(Position position, Rect screen, Size window, double dpi) { int offset = 24; double pointX = position switch { Position.Top or Position.Bottom or Position.Center - => screen.X + (screen.Width / 2) - (window.Width / 2), + => screen.X + (screen.Width / 2) - (window.Width * dpi / 2), Position.TopLeft or Position.Left or Position.BottomLeft => screen.X + offset, Position.TopRight or Position.Right or Position.BottomRight - => screen.X + screen.Width - (window.Width + offset), + => screen.X + screen.Width - ((window.Width * dpi) + offset), _ => throw new NotImplementedException(), }; @@ -38,9 +38,9 @@ namespace PowerAccent.Core.Tools Position.TopLeft or Position.Top or Position.TopRight => screen.Y + offset, Position.Left or Position.Center or Position.Right - => screen.Y + (screen.Height / 2) - (window.Height / 2), + => screen.Y + (screen.Height / 2) - (window.Height * dpi / 2), Position.BottomLeft or Position.Bottom or Position.BottomRight - => screen.Y + screen.Height - (window.Height + offset), + => screen.Y + screen.Height - ((window.Height * dpi) + offset), _ => throw new NotImplementedException(), }; diff --git a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs index 311417851a..7eed6a9a1b 100644 --- a/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs +++ b/src/modules/poweraccent/PowerAccent.UI/Selector.xaml.cs @@ -59,6 +59,7 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange _selectedIndex = index; characters.SelectedIndex = _selectedIndex; characterName.Text = _powerAccent.CharacterDescriptions[_selectedIndex]; + characters.ScrollIntoView(character); } private void PowerAccent_OnChangeDisplay(bool isActive, string[] chars) @@ -73,6 +74,7 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange characters.ItemsSource = chars; characters.SelectedIndex = _selectedIndex; this.UpdateLayout(); // Required for filling the actual width/height before positioning. + SetWindowsSize(); SetWindowPosition(); Show(); Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new PowerAccent.Core.Telemetry.PowerAccentShowAccentMenuEvent()); @@ -96,6 +98,11 @@ public partial class Selector : FluentWindow, IDisposable, INotifyPropertyChange this.Top = position.Y; } + private void SetWindowsSize() + { + this.characters.MaxWidth = _powerAccent.GetDisplayMaxWidth(); + } + protected override void OnClosed(EventArgs e) { _powerAccent.SaveUsageInfo(); diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj index f9e245559c..16272dba69 100644 --- a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj +++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj @@ -53,7 +53,7 @@ true true true - legacy_stdio_definitions.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + legacy_stdio_definitions.lib;windowscodecs.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)" @@ -72,6 +72,7 @@ Console + windowscodecs.lib;%(AdditionalDependencies) diff --git a/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj b/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj index af3c71ad8e..a101c28ac9 100644 --- a/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj +++ b/src/modules/powerrename/PowerRenameContextMenu/PowerRenameContextMenu.vcxproj @@ -51,7 +51,7 @@ Windows true false - runtimeobject.lib;%(AdditionalDependencies) + runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies) Source.def @@ -75,7 +75,7 @@ MakeAppx.exe pack /d . /p $(OutDir)PowerRenameContextMenuPackage.msix /nvtrue true false - runtimeobject.lib;%(AdditionalDependencies) + runtimeobject.lib;windowscodecs.lib;%(AdditionalDependencies) Source.def diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj index df105e5815..eb21a94049 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj @@ -79,7 +79,7 @@ _DEBUG;%(PreprocessorDefinitions) - kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies) @@ -89,7 +89,7 @@ true true - kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;dwmapi.lib;Shcore.lib;windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies) diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp index 67f1834499..b46c5e548d 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp @@ -1,4 +1,4 @@ -#include "pch.h" +#include "pch.h" #include "App.xaml.h" #include "MainWindow.xaml.h" @@ -117,6 +117,9 @@ App::App() /// Details about the launch request and process. void App::OnLaunched(LaunchActivatedEventArgs const&) { + // WinUI3 framework automatically initializes COM as STA on the main thread + // No manual initialization needed for WIC operations + LoggerHelpers::init_logger(moduleName, L"", LogSettings::powerRenameLoggerName); if (powertoys_gpo::getConfiguredPowerRenameEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) @@ -237,7 +240,6 @@ void App::OnLaunched(LaunchActivatedEventArgs const&) } #else #define BUFSIZE 4096 * 4 - BOOL bSuccess; WCHAR chBuf[BUFSIZE]; DWORD dwRead; @@ -269,4 +271,4 @@ void App::OnLaunched(LaunchActivatedEventArgs const&) window = make(); window.Activate(); -} \ No newline at end of file +} diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl index 041e3d1921..bb02ec2e14 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl @@ -16,6 +16,7 @@ namespace PowerRenameUI Windows.Foundation.Collections.IObservableVector DateTimeShortcuts { get; }; Windows.Foundation.Collections.IObservableVector CounterShortcuts { get; }; Windows.Foundation.Collections.IObservableVector RandomizerShortcuts { get; }; + Windows.Foundation.Collections.IObservableVector MetadataShortcuts { get; }; String OriginalCount; String RenamedCount; diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml index 7126a63604..1c67d9a73b 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml @@ -330,6 +330,8 @@ + + + + + + + + + + + + + + + + + + + + @@ -560,31 +604,61 @@ FontFamily="{ThemeResource SymbolThemeFontFamily}" /> - + + + + + + + + + + + + + - - + + + + + + + + + diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp index 7cc3bf9543..01c7c517c2 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp @@ -1,4 +1,4 @@ -#include "pch.h" +#include "pch.h" #include "MainWindow.xaml.h" #if __has_include("MainWindow.g.cpp") #include "MainWindow.g.cpp" @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -225,6 +226,11 @@ namespace winrt::PowerRenameUI::implementation m_RandomizerShortcuts.Append(winrt::make(L"${rstringdigit=36}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Digit").ValueAsString())); m_RandomizerShortcuts.Append(winrt::make(L"${ruuidv4}", manager.MainResourceMap().GetValue(L"Resources/RandomizerCheatSheet_Uuid").ValueAsString())); + // Initialize metadata shortcuts - will be populated based on selected metadata type + m_metadataShortcuts = winrt::single_threaded_observable_vector(); + // Initialize with EXIF patterns (default) + UpdateMetadataShortcuts(PowerRenameLib::MetadataType::EXIF); + InitializeComponent(); m_etwTrace.UpdateState(true); @@ -356,7 +362,10 @@ namespace winrt::PowerRenameUI::implementation hstring MainWindow::OriginalCount() { UINT count = 0; - m_prManager->GetItemCount(&count); + if (m_prManager) + { + m_prManager->GetItemCount(&count); + } return hstring{ std::to_wstring(count) }; } @@ -394,13 +403,16 @@ namespace winrt::PowerRenameUI::implementation button_showAll().IsChecked(true); button_showRenamed().IsChecked(false); - DWORD filter = 0; - m_prManager->GetFilter(&filter); - if (filter != PowerRenameFilters::None) + if (m_prManager) { - m_prManager->SwitchFilter(0); - get_self(m_explorerItems)->SetIsFiltered(false); - InvalidateItemListViewState(); + DWORD filter = 0; + m_prManager->GetFilter(&filter); + if (filter != PowerRenameFilters::None) + { + m_prManager->SwitchFilter(0); + get_self(m_explorerItems)->SetIsFiltered(false); + InvalidateItemListViewState(); + } } } @@ -409,14 +421,17 @@ namespace winrt::PowerRenameUI::implementation button_showRenamed().IsChecked(true); button_showAll().IsChecked(false); - DWORD filter = 0; - m_prManager->GetFilter(&filter); - if (filter != PowerRenameFilters::ShouldRename) + if (m_prManager) { - m_prManager->SwitchFilter(0); - UpdateCounts(); - get_self(m_explorerItems)->SetIsFiltered(true); - InvalidateItemListViewState(); + DWORD filter = 0; + m_prManager->GetFilter(&filter); + if (filter != PowerRenameFilters::ShouldRename) + { + m_prManager->SwitchFilter(0); + UpdateCounts(); + get_self(m_explorerItems)->SetIsFiltered(true); + InvalidateItemListViewState(); + } } } @@ -434,6 +449,27 @@ namespace winrt::PowerRenameUI::implementation textBox_replace().Text(textBox_replace().Text() + s->Code()); } + void MainWindow::MetadataItemClick(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e) + { + auto s = e.ClickedItem().try_as(); + DateTimeFlyout().Hide(); + textBox_replace().Text(textBox_replace().Text() + s->Code()); + } + + void MainWindow::MetadataSourceComboBox_SelectionChanged(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const&) + { + int selectedIndex = comboBox_metadataSource().SelectedIndex(); + + // Get the selected metadata type based on ComboBox selection + PowerRenameLib::MetadataType metadataType = static_cast(selectedIndex); + + // Update the metadata shortcuts list + UpdateMetadataShortcuts(metadataType); + + // Update the metadata source flags + UpdateMetadataSourceFlags(selectedIndex); + } + void MainWindow::button_rename_Click(winrt::Microsoft::UI::Xaml::Controls::SplitButton const&, winrt::Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const&) { Rename(false); @@ -621,6 +657,12 @@ namespace winrt::PowerRenameUI::implementation { _TRACER_; + if (!m_prManager) + { + // Manager not initialized yet, ignore flag updates + return; + } + DWORD flags{}; m_prManager->GetFlags(&flags); @@ -818,6 +860,7 @@ namespace winrt::PowerRenameUI::implementation UpdateFlag(ModificationTime, UpdateFlagCommand::Reset); } }); + } void MainWindow::ToggleItem(int32_t id, bool checked) @@ -1049,6 +1092,15 @@ namespace winrt::PowerRenameUI::implementation { toggleButton_capitalize().IsChecked(true); } + + int metadataIndex = (flags & MetadataSourceXMP) ? 1 : 0; + if (comboBox_metadataSource().SelectedIndex() != metadataIndex) + { + comboBox_metadataSource().SelectedIndex(metadataIndex); + } + + auto metadataType = metadataIndex == 1 ? PowerRenameLib::MetadataType::XMP : PowerRenameLib::MetadataType::EXIF; + UpdateMetadataShortcuts(metadataType); } void MainWindow::UpdateCounts() @@ -1081,6 +1133,220 @@ namespace winrt::PowerRenameUI::implementation RenamedCount(hstring{ std::to_wstring(m_renamingCount) }); } + void MainWindow::UpdateMetadataShortcuts(PowerRenameLib::MetadataType metadataType) + { + // Clear existing list + m_metadataShortcuts.Clear(); + + // Get supported patterns for the selected metadata type + auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType); + + auto factory = winrt::get_activation_factory(); + ResourceManager manager = factory.CreateInstance(L"PowerToys.PowerRename.pri"); + + // Add each supported pattern to the list + for (const auto& pattern : supportedPatterns) + { + std::wstring resourceKey = L"Resources/MetadataCheatSheet_" + ConvertPatternToResourceKey(pattern); + winrt::hstring patternWithDollar = winrt::hstring(L"$" + pattern); + + try { + auto description = manager.MainResourceMap().GetValue(resourceKey).ValueAsString(); + m_metadataShortcuts.Append(winrt::make(patternWithDollar, description)); + } + catch (...) { + // If resource doesn't exist, use the pattern name as description + m_metadataShortcuts.Append(winrt::make(patternWithDollar, winrt::hstring(pattern))); + } + } + } + + std::wstring MainWindow::ConvertPatternToResourceKey(const std::wstring& pattern) + { + // Special cases for patterns that don't follow the standard naming convention + if (pattern == L"TITLE") + { + return L"DocTitle"; + } + else if (pattern == L"DATE_TAKEN_YYYY") + { + return L"DateTakenYear4"; + } + else if (pattern == L"DATE_TAKEN_YY") + { + return L"DateTakenYear2"; + } + else if (pattern == L"DATE_TAKEN_MM") + { + return L"DateTakenMonth"; + } + else if (pattern == L"DATE_TAKEN_DD") + { + return L"DateTakenDay"; + } + else if (pattern == L"DATE_TAKEN_HH") + { + return L"DateTakenHour"; + } + else if (pattern == L"DATE_TAKEN_mm") + { + return L"DateTakenMinute"; + } + else if (pattern == L"DATE_TAKEN_SS") + { + return L"DateTakenSecond"; + } + else if (pattern == L"CREATE_DATE_YYYY") + { + return L"CreateDateYear4"; + } + else if (pattern == L"CREATE_DATE_YY") + { + return L"CreateDateYear2"; + } + else if (pattern == L"CREATE_DATE_MM") + { + return L"CreateDateMonth"; + } + else if (pattern == L"CREATE_DATE_DD") + { + return L"CreateDateDay"; + } + else if (pattern == L"CREATE_DATE_HH") + { + return L"CreateDateHour"; + } + else if (pattern == L"CREATE_DATE_mm") + { + return L"CreateDateMinute"; + } + else if (pattern == L"CREATE_DATE_SS") + { + return L"CreateDateSecond"; + } + else if (pattern == L"MODIFY_DATE_YYYY") + { + return L"ModifyDateYear4"; + } + else if (pattern == L"MODIFY_DATE_YY") + { + return L"ModifyDateYear2"; + } + else if (pattern == L"MODIFY_DATE_MM") + { + return L"ModifyDateMonth"; + } + else if (pattern == L"MODIFY_DATE_DD") + { + return L"ModifyDateDay"; + } + else if (pattern == L"MODIFY_DATE_HH") + { + return L"ModifyDateHour"; + } + else if (pattern == L"MODIFY_DATE_mm") + { + return L"ModifyDateMinute"; + } + else if (pattern == L"MODIFY_DATE_SS") + { + return L"ModifyDateSecond"; + } + else if (pattern == L"METADATA_DATE_YYYY") + { + return L"MetadataDateYear4"; + } + else if (pattern == L"METADATA_DATE_YY") + { + return L"MetadataDateYear2"; + } + else if (pattern == L"METADATA_DATE_MM") + { + return L"MetadataDateMonth"; + } + else if (pattern == L"METADATA_DATE_DD") + { + return L"MetadataDateDay"; + } + else if (pattern == L"METADATA_DATE_HH") + { + return L"MetadataDateHour"; + } + else if (pattern == L"METADATA_DATE_mm") + { + return L"MetadataDateMinute"; + } + else if (pattern == L"METADATA_DATE_SS") + { + return L"MetadataDateSecond"; + } + else if (pattern == L"ISO") + { + return L"ISO"; + } + else if (pattern == L"TITLE") + { + return L"DocTitle"; + } + else if (pattern == L"DESCRIPTION") + { + return L"DocDescription"; + } + else if (pattern == L"CREATOR") + { + return L"DocCreator"; + } + else if (pattern == L"SUBJECT") + { + return L"DocSubject"; + } + else if (pattern == L"RIGHTS") + { + return L"Rights"; + } + + // Convert pattern name to resource key format + // e.g., "CAMERA_MAKE" -> "CameraMake" + std::wstring result; + bool capitalizeNext = true; + + for (wchar_t ch : pattern) + { + if (ch == L'_') + { + capitalizeNext = true; + } + else + { + if (capitalizeNext) + { + result += static_cast(std::toupper(ch)); + capitalizeNext = false; + } + else + { + result += static_cast(std::tolower(ch)); + } + } + } + + return result; + } + + void MainWindow::UpdateMetadataSourceFlags(int selectedIndex) + { + // Clear all metadata source flags first + UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Reset); + UpdateFlag(MetadataSourceXMP, UpdateFlagCommand::Reset); + + // Set the appropriate metadata source flag based on selection + switch(selectedIndex) { + case 0: UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Set); break; + case 1: UpdateFlag(MetadataSourceXMP, UpdateFlagCommand::Set); break; + default: UpdateFlag(MetadataSourceEXIF, UpdateFlagCommand::Set); break; // Default to EXIF + } + } + HRESULT MainWindow::OnRename(_In_ IPowerRenameItem* /*renameItem*/) { UpdateCounts(); @@ -1122,3 +1388,6 @@ namespace winrt::PowerRenameUI::implementation return S_OK; } } + + + diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h index 8c70194f1b..cff802f582 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "winrt/Windows.UI.Xaml.h" #include "winrt/Windows.UI.Xaml.Markup.h" @@ -20,6 +20,8 @@ #include #include #include +#include +#include namespace winrt::PowerRenameUI::implementation { @@ -88,6 +90,7 @@ namespace winrt::PowerRenameUI::implementation winrt::Windows::Foundation::Collections::IObservableVector DateTimeShortcuts() { return m_dateTimeShortcuts; } winrt::Windows::Foundation::Collections::IObservableVector CounterShortcuts() { return m_CounterShortcuts; } winrt::Windows::Foundation::Collections::IObservableVector RandomizerShortcuts() { return m_RandomizerShortcuts; } + winrt::Windows::Foundation::Collections::IObservableVector MetadataShortcuts() { return m_metadataShortcuts; } hstring OriginalCount(); void OriginalCount(hstring value); @@ -111,6 +114,7 @@ namespace winrt::PowerRenameUI::implementation winrt::Windows::Foundation::Collections::IObservableVector m_dateTimeShortcuts; winrt::Windows::Foundation::Collections::IObservableVector m_CounterShortcuts; winrt::Windows::Foundation::Collections::IObservableVector m_RandomizerShortcuts; + winrt::Windows::Foundation::Collections::IObservableVector m_metadataShortcuts; // Used by PowerRenameManagerEvents HRESULT OnRename(_In_ IPowerRenameItem* renameItem); @@ -144,6 +148,9 @@ namespace winrt::PowerRenameUI::implementation HRESULT OpenSettingsApp(); void SetCheckboxesFromFlags(DWORD flags); void UpdateCounts(); + void UpdateMetadataShortcuts(PowerRenameLib::MetadataType metadataType); + std::wstring ConvertPatternToResourceKey(const std::wstring& pattern); + void UpdateMetadataSourceFlags(int selectedIndex); Shared::Trace::ETWTrace m_etwTrace{}; @@ -167,6 +174,8 @@ namespace winrt::PowerRenameUI::implementation public: void RegExItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e); void DateTimeItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e); + void MetadataItemClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::ItemClickEventArgs const& e); + void MetadataSourceComboBox_SelectionChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::Controls::SelectionChangedEventArgs const& e); void button_rename_Click(winrt::Microsoft::UI::Xaml::Controls::SplitButton const& sender, winrt::Microsoft::UI::Xaml::Controls::SplitButtonClickEventArgs const& args); void MenuFlyoutItem_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e); void OpenDocs(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e); @@ -179,3 +188,4 @@ namespace winrt::PowerRenameUI::factory_implementation { }; } + diff --git a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw index 9af9e2365b..178106908d 100644 --- a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw +++ b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw @@ -414,6 +414,9 @@ Time used for replacement + + Metadata source for replacement + Creation Time @@ -423,4 +426,149 @@ Access Time + + + EXIF Metadata + + + XMP Metadata + + + + Replace with media metadata + + + Camera manufacturer name + + + Camera model name + + + Lens model name + + + ISO sensitivity value + + + F-number aperture value + + + Shutter speed value + + + Focal length in millimeters + + + Flash status (On/Off) + + + Image width in pixels + + + Image height in pixels + + + Image author/artist + + + Copyright information + + + GPS latitude coordinate + + + GPS longitude coordinate + + + GPS altitude in meters + + + Exposure compensation value + + + Image orientation + + + Color space information + + + Year photo was taken (4 digits) + + + Year photo was taken (2 digits) + + + Month photo was taken (01-12) + + + Day photo was taken (01-31) + + + Hour photo was taken (00-23) + + + Minute photo was taken (00-59) + + + Second photo was taken (00-59) + + + Year from XMP create date (4 digits) + + + Year from XMP create date (2 digits) + + + Month from XMP create date (01-12) + + + Day from XMP create date (01-31) + + + Hour from XMP create date (00-23) + + + Minute from XMP create date (00-59) + + + Second from XMP create date (00-59) + + + + + Software used to create/edit + + + + + Document title + + + Document description + + + Document creator/author + + + Keywords/tags + + + + + Copyright/rights information + + + + + Document unique identifier + + + Instance unique identifier + + + Original document identifier + + + Version identifier + \ No newline at end of file diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj b/src/modules/powerrename/dll/PowerRenameExt.vcxproj index 9c612a08ff..2364012861 100644 --- a/src/modules/powerrename/dll/PowerRenameExt.vcxproj +++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj @@ -24,7 +24,7 @@ ..\lib\;..\PowerRenameUILib\;..\;..\..\..\;..\..\..\common\telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) - Pathcch.lib;comctl32.lib;shcore.lib;%(AdditionalDependencies) + Pathcch.lib;comctl32.lib;shcore.lib;windowscodecs.lib;%(AdditionalDependencies) PowerRenameExt.def gdi32.dll;shell32.dll;ole32.dll;shlwapi.dll;oleaut32.dll;%(DelayLoadDLLs) diff --git a/src/modules/powerrename/lib/Helpers.cpp b/src/modules/powerrename/lib/Helpers.cpp index 2515a8a7ae..c3902c7b93 100644 --- a/src/modules/powerrename/lib/Helpers.cpp +++ b/src/modules/powerrename/lib/Helpers.cpp @@ -1,9 +1,13 @@ #include "pch.h" #include "Helpers.h" +#include "MetadataTypes.h" #include #include #include #include +#include +#include +#include namespace fs = std::filesystem; @@ -12,6 +16,50 @@ namespace const int MAX_INPUT_STRING_LEN = 1024; const wchar_t c_rootRegPath[] = L"Software\\Microsoft\\PowerRename"; + + // Helper function: Find the longest matching pattern starting at the given position + // Returns the matched pattern name, or empty string if no match found + std::wstring FindLongestPattern( + const std::wstring& input, + size_t startPos, + size_t maxPatternLength, + const std::unordered_set& validPatterns) + { + const size_t remaining = input.length() - startPos; + const size_t searchLength = std::min(maxPatternLength, remaining); + + // Try to match from longest to shortest to ensure greedy matching + // e.g., DATE_TAKEN_YYYY should be matched before DATE_TAKEN_YY + for (size_t len = searchLength; len > 0; --len) + { + std::wstring candidate = input.substr(startPos, len); + if (validPatterns.find(candidate) != validPatterns.end()) + { + return candidate; + } + } + + return L""; + } + + // Helper function: Get the replacement value for a pattern + // Returns the actual metadata value if available; if not, returns the pattern name with $ prefix + std::wstring GetPatternValue( + const std::wstring& patternName, + const PowerRenameLib::MetadataPatternMap& patterns) + { + auto it = patterns.find(patternName); + + // Return actual value if found and valid (non-empty) + if (it != patterns.end() && !it->second.empty()) + { + return it->second; + } + + // Return pattern name with $ prefix if value is unavailable + // This provides visual feedback that the field exists but has no data + return L"$" + patternName; + } } HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source) @@ -271,6 +319,72 @@ bool isFileTimeUsed(_In_ PCWSTR source) return used; } +bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath, bool isFolder) +{ + if (!source) return false; + + // Early exit: If file path is provided, check file type first (fastest checks) + // This avoids expensive pattern matching for files that don't support metadata + if (filePath != nullptr) + { + // Folders don't support metadata extraction + if (isFolder) + { + return false; + } + + // Check if file path is valid + if (wcslen(filePath) == 0) + { + return false; + } + + // Get file extension + std::wstring extension = fs::path(filePath).extension().wstring(); + + // Convert to lowercase for case-insensitive comparison + std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower); + + // According to the metadata support table, only these formats support metadata extraction: + // - JPEG (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding + // - TIFF (IFD, Exif, XMP, GPS, IPTC) - supports fast metadata encoding + // - PNG (text chunks) + static const std::unordered_set supportedExtensions = { + L".jpg", + L".jpeg", + L".png", + L".tif", + L".tiff" + }; + + // If file type doesn't support metadata, no need to check patterns + if (supportedExtensions.find(extension) == supportedExtensions.end()) + { + return false; + } + } + + // Now check if any metadata pattern exists in the source string + // This is the most expensive check, so we do it last + std::wstring str(source); + + // Get supported patterns for the specified metadata type + auto supportedPatterns = PowerRenameLib::MetadataPatternExtractor::GetSupportedPatterns(metadataType); + + // Check if any metadata pattern exists in the source string + for (const auto& pattern : supportedPatterns) + { + std::wstring searchPattern = L"$" + pattern; + if (str.find(searchPattern) != std::wstring::npos) + { + return true; + } + } + + // No metadata pattern found + return false; +} + HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime) { std::locale::global(std::locale("")); @@ -297,10 +411,10 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", (fileTime.wYear % 100)); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YY(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YYY, $YYYY, or metadata patterns StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10)); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $YY, $YYYY, or metadata patterns GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL); formattedDate[0] = towupper(formattedDate[0]); @@ -310,13 +424,13 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL); formattedDate[0] = towupper(formattedDate[0]); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM(?!M)"), replaceTerm); // Negative lookahead prevents matching $MMMM StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MMM, $MMMM, or metadata patterns StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMonth); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$M(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $MM, $MMM, $MMMM, or metadata patterns GetDateFormatEx(localeName, NULL, &fileTime, L"dddd", formattedDate, MAX_PATH, NULL); formattedDate[0] = towupper(formattedDate[0]); @@ -326,19 +440,19 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL); formattedDate[0] = towupper(formattedDate[0]); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDDD or metadata patterns StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wDay); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DD(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DDD, $DDDD, or metadata patterns StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $DD, $DDD, $DDDD, or metadata patterns like $DATE_TAKEN_YYYY StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HHH or metadata patterns StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H(?![A-Z])"), replaceTerm); // Negative lookahead prevents matching $HH or metadata patterns StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM"); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm); @@ -347,31 +461,31 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh(?!h)"), replaceTerm); // Negative lookahead prevents matching $hhh StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wHour); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$h(?!h)"), replaceTerm); // Negative lookahead prevents matching $hh StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMinute); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$mm(?!m)"), replaceTerm); // Negative lookahead prevents matching $mmm StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMinute); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$m(?!m)"), replaceTerm); // Negative lookahead prevents matching $mm StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wSecond); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ss(?!s)"), replaceTerm); // Negative lookahead prevents matching $sss StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wSecond); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$s(?!s)"), replaceTerm); // Negative lookahead prevents matching $ss StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff(?!f)"), replaceTerm); // Negative lookahead prevents matching $ffff StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff(?!f)"), replaceTerm); // Negative lookahead prevents matching $fff StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100); - res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f(?!f)"), replaceTerm); // Negative lookahead prevents matching $ff or $fff hr = StringCchCopy(result, cchMax, res.c_str()); } @@ -379,6 +493,91 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY return hr; } +HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns) +{ + if (!source || wcslen(source) == 0) + { + return E_INVALIDARG; + } + + std::wstring input(source); + std::wstring output; + output.reserve(input.length() * 2); // Reserve space to avoid frequent reallocations + + // Build pattern lookup table for fast validation + // Using all possible patterns to recognize valid pattern names even when metadata is unavailable + auto allPatterns = PowerRenameLib::MetadataPatternExtractor::GetAllPossiblePatterns(); + std::unordered_set validPatterns; + validPatterns.reserve(allPatterns.size()); + size_t maxPatternLength = 0; + for (const auto& pattern : allPatterns) + { + validPatterns.insert(pattern); + maxPatternLength = std::max(maxPatternLength, pattern.length()); + } + + size_t pos = 0; + while (pos < input.length()) + { + // Handle regular characters + if (input[pos] != L'$') + { + output += input[pos]; + pos++; + continue; + } + + // Count consecutive dollar signs + size_t dollarCount = 0; + while (pos < input.length() && input[pos] == L'$') + { + dollarCount++; + pos++; + } + + // Even number of dollars: all are escaped (e.g., $$ -> $, $$$$ -> $$) + if (dollarCount % 2 == 0) + { + output.append(dollarCount / 2, L'$'); + continue; + } + + // Odd number of dollars: pairs are escaped, last one might be a pattern prefix + // e.g., $ -> might be pattern, $$$ -> $ + might be pattern + size_t escapedDollars = dollarCount / 2; + + // If no more characters, output all dollar signs + if (pos >= input.length()) + { + output.append(dollarCount, L'$'); + continue; + } + + // Try to match a pattern (greedy matching for longest pattern) + std::wstring matchedPattern = FindLongestPattern(input, pos, maxPatternLength, validPatterns); + + if (matchedPattern.empty()) + { + // No pattern matched, output all dollar signs + output.append(dollarCount, L'$'); + } + else + { + // Pattern matched + output.append(escapedDollars, L'$'); // Output escaped dollars first + + // Replace pattern with its value or keep pattern name if value unavailable + std::wstring replacementValue = GetPatternValue(matchedPattern, patterns); + output += replacementValue; + + pos += matchedPattern.length(); + } + } + + return StringCchCopy(result, cchMax, output.c_str()); +} + + HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items) { *items = nullptr; @@ -707,4 +906,4 @@ std::wstring CreateGuidStringWithoutBrackets() } return L""; -} \ No newline at end of file +} diff --git a/src/modules/powerrename/lib/Helpers.h b/src/modules/powerrename/lib/Helpers.h index 05b8eab19d..83659637c9 100644 --- a/src/modules/powerrename/lib/Helpers.h +++ b/src/modules/powerrename/lib/Helpers.h @@ -1,13 +1,17 @@ #pragma once #include "PowerRenameInterfaces.h" +#include "MetadataTypes.h" +#include "MetadataPatternExtractor.h" #include HRESULT GetTrimmedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source); HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, DWORD flags, bool isFolder); HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime); +HRESULT GetMetadataFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, const PowerRenameLib::MetadataPatternMap& patterns); bool isFileTimeUsed(_In_ PCWSTR source); +bool isMetadataUsed(_In_ PCWSTR source, PowerRenameLib::MetadataType metadataType, _In_opt_ PCWSTR filePath = nullptr, bool isFolder = false); bool ShellItemArrayContainsRenamableItem(_In_ IShellItemArray* shellItemArray); bool DataObjectContainsRenamableItem(_In_ IUnknown* dataSource); HRESULT GetShellItemArrayFromDataObject(_In_ IUnknown* dataSource, _COM_Outptr_ IShellItemArray** items); diff --git a/src/modules/powerrename/lib/MetadataFormatHelper.cpp b/src/modules/powerrename/lib/MetadataFormatHelper.cpp new file mode 100644 index 0000000000..e6b88e913a --- /dev/null +++ b/src/modules/powerrename/lib/MetadataFormatHelper.cpp @@ -0,0 +1,237 @@ +// 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. + +#include "pch.h" +#include "MetadataFormatHelper.h" +#include +#include +#include + +using namespace PowerRenameLib; + +// Formatting functions + +std::wstring MetadataFormatHelper::FormatAperture(double aperture) +{ + return std::format(L"f/{:.1f}", aperture); +} + +std::wstring MetadataFormatHelper::FormatShutterSpeed(double speed) +{ + if (speed <= 0.0) + { + return L"0"; + } + + if (speed >= 1.0) + { + return std::format(L"{:.1f}s", speed); + } + + const double reciprocal = std::round(1.0 / speed); + if (reciprocal <= 1.0) + { + return std::format(L"{:.3f}s", speed); + } + + return std::format(L"1/{:.0f}s", reciprocal); +} + +std::wstring MetadataFormatHelper::FormatISO(int64_t iso) +{ + if (iso <= 0) + { + return L"ISO"; + } + + return std::format(L"ISO {}", iso); +} + +std::wstring MetadataFormatHelper::FormatFlash(int64_t flashValue) +{ + switch (flashValue & 0x1) + { + case 0: + return L"Flash Off"; + case 1: + return L"Flash On"; + default: + break; + } + + return std::format(L"Flash 0x{:X}", static_cast(flashValue)); +} + +std::wstring MetadataFormatHelper::FormatCoordinate(double coord, bool isLatitude) +{ + wchar_t direction = isLatitude ? (coord >= 0.0 ? L'N' : L'S') : (coord >= 0.0 ? L'E' : L'W'); + double absolute = std::abs(coord); + int degrees = static_cast(absolute); + double minutes = (absolute - static_cast(degrees)) * 60.0; + + return std::format(L"{:d}°{:.2f}'{}", degrees, minutes, direction); +} + +std::wstring MetadataFormatHelper::FormatSystemTime(const SYSTEMTIME& st) +{ + return std::format(L"{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}", + st.wYear, + st.wMonth, + st.wDay, + st.wHour, + st.wMinute, + st.wSecond); +} + +// Parsing functions + +double MetadataFormatHelper::ParseGPSRational(const PROPVARIANT& pv) +{ + if ((pv.vt & VT_VECTOR) && pv.caub.cElems >= 8) + { + return ParseSingleRational(pv.caub.pElems, 0); + } + return 0.0; +} + +double MetadataFormatHelper::ParseSingleRational(const uint8_t* bytes, size_t offset) +{ + // Parse a single rational number (8 bytes: numerator + denominator) + if (!bytes) + return 0.0; + + // Note: Callers are responsible for ensuring the buffer is large enough. + // This function assumes offset points to at least 8 bytes of valid data. + // All current callers perform cElems >= required_size checks before calling. + const uint8_t* rationalBytes = bytes + offset; + + // Parse as little-endian uint32_t values + uint32_t numerator = static_cast(rationalBytes[0]) | + (static_cast(rationalBytes[1]) << 8) | + (static_cast(rationalBytes[2]) << 16) | + (static_cast(rationalBytes[3]) << 24); + + uint32_t denominator = static_cast(rationalBytes[4]) | + (static_cast(rationalBytes[5]) << 8) | + (static_cast(rationalBytes[6]) << 16) | + (static_cast(rationalBytes[7]) << 24); + + if (denominator != 0) + { + return static_cast(numerator) / static_cast(denominator); + } + + return 0.0; +} + +double MetadataFormatHelper::ParseSingleSRational(const uint8_t* bytes, size_t offset) +{ + // Parse a single signed rational number (8 bytes: signed numerator + signed denominator) + if (!bytes) + return 0.0; + + // Note: Callers are responsible for ensuring the buffer is large enough. + // This function assumes offset points to at least 8 bytes of valid data. + // All current callers perform cElems >= required_size checks before calling. + const uint8_t* rationalBytes = bytes + offset; + + // Parse as little-endian int32_t values (signed) + // First construct as unsigned, then reinterpret as signed + uint32_t numerator_uint = static_cast(rationalBytes[0]) | + (static_cast(rationalBytes[1]) << 8) | + (static_cast(rationalBytes[2]) << 16) | + (static_cast(rationalBytes[3]) << 24); + + uint32_t denominator_uint = static_cast(rationalBytes[4]) | + (static_cast(rationalBytes[5]) << 8) | + (static_cast(rationalBytes[6]) << 16) | + (static_cast(rationalBytes[7]) << 24); + + // Reinterpret as signed + int32_t numerator = static_cast(numerator_uint); + int32_t denominator = static_cast(denominator_uint); + + if (denominator != 0) + { + return static_cast(numerator) / static_cast(denominator); + } + + return 0.0; +} + +std::pair MetadataFormatHelper::ParseGPSCoordinates( + const PROPVARIANT& latitude, + const PROPVARIANT& longitude, + const PROPVARIANT& latRef, + const PROPVARIANT& lonRef) +{ + double lat = 0.0, lon = 0.0; + + // Parse latitude - typically stored as 3 rationals (degrees, minutes, seconds) + if ((latitude.vt & VT_VECTOR) && latitude.caub.cElems >= 24) // 3 rationals * 8 bytes each + { + const uint8_t* bytes = latitude.caub.pElems; + + // degrees, minutes, seconds (each rational is 8 bytes) + double degrees = ParseSingleRational(bytes, 0); + double minutes = ParseSingleRational(bytes, 8); + double seconds = ParseSingleRational(bytes, 16); + + lat = degrees + minutes / 60.0 + seconds / 3600.0; + } + + // Parse longitude + if ((longitude.vt & VT_VECTOR) && longitude.caub.cElems >= 24) + { + const uint8_t* bytes = longitude.caub.pElems; + + double degrees = ParseSingleRational(bytes, 0); + double minutes = ParseSingleRational(bytes, 8); + double seconds = ParseSingleRational(bytes, 16); + + lon = degrees + minutes / 60.0 + seconds / 3600.0; + } + + // Apply direction references (N/S for latitude, E/W for longitude) + if (latRef.vt == VT_LPSTR && latRef.pszVal) + { + if (strcmp(latRef.pszVal, "S") == 0) + lat = -lat; + } + + if (lonRef.vt == VT_LPSTR && lonRef.pszVal) + { + if (strcmp(lonRef.pszVal, "W") == 0) + lon = -lon; + } + + return { lat, lon }; +} + +std::wstring MetadataFormatHelper::SanitizeForFileName(const std::wstring& str) +{ + // Windows illegal filename characters: < > : " / \ | ? * + // Also control characters (0-31) and some others + std::wstring sanitized = str; + + // Replace illegal characters with underscore + for (auto& ch : sanitized) + { + // Check for illegal characters + if (ch == L'<' || ch == L'>' || ch == L':' || ch == L'"' || + ch == L'/' || ch == L'\\' || ch == L'|' || ch == L'?' || ch == L'*' || + ch < 32) // Control characters + { + ch = L'_'; + } + } + + // Also remove trailing dots and spaces (Windows doesn't like them at end of filename) + while (!sanitized.empty() && (sanitized.back() == L'.' || sanitized.back() == L' ')) + { + sanitized.pop_back(); + } + + return sanitized; +} diff --git a/src/modules/powerrename/lib/MetadataFormatHelper.h b/src/modules/powerrename/lib/MetadataFormatHelper.h new file mode 100644 index 0000000000..86208225cf --- /dev/null +++ b/src/modules/powerrename/lib/MetadataFormatHelper.h @@ -0,0 +1,117 @@ +// 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. + +#pragma once +#include +#include +#include +#include + +namespace PowerRenameLib +{ + /// + /// Helper class for formatting and parsing metadata values + /// Provides static utility functions for converting metadata to human-readable strings + /// and parsing raw metadata values + /// + class MetadataFormatHelper + { + public: + // Formatting functions - Convert metadata values to display strings + + /// + /// Format aperture value (f-number) + /// + /// Aperture value (e.g., 2.8) + /// Formatted string (e.g., "f/2.8") + static std::wstring FormatAperture(double aperture); + + /// + /// Format shutter speed + /// + /// Shutter speed in seconds + /// Formatted string (e.g., "1/100s" or "2.5s") + static std::wstring FormatShutterSpeed(double speed); + + /// + /// Format ISO value + /// + /// ISO speed value + /// Formatted string (e.g., "ISO 400") + static std::wstring FormatISO(int64_t iso); + + /// + /// Format flash status + /// + /// Flash value from EXIF + /// Formatted string (e.g., "Flash On" or "Flash Off") + static std::wstring FormatFlash(int64_t flashValue); + + /// + /// Format GPS coordinate + /// + /// Coordinate value in decimal degrees + /// true for latitude, false for longitude + /// Formatted string (e.g., "40°26.76'N") + static std::wstring FormatCoordinate(double coord, bool isLatitude); + + /// + /// Format SYSTEMTIME to string + /// + /// SYSTEMTIME structure + /// Formatted string (e.g., "2024-03-15 14:30:45") + static std::wstring FormatSystemTime(const SYSTEMTIME& st); + + // Parsing functions - Convert raw metadata to usable values + + /// + /// Parse GPS rational value from PROPVARIANT + /// + /// PROPVARIANT containing GPS rational data + /// Parsed double value + static double ParseGPSRational(const PROPVARIANT& pv); + + /// + /// Parse single rational value from byte array + /// + /// Byte array containing rational data + /// Offset in the byte array + /// Parsed double value (numerator / denominator) + static double ParseSingleRational(const uint8_t* bytes, size_t offset); + + /// + /// Parse single signed rational value from byte array + /// + /// Byte array containing signed rational data + /// Offset in the byte array + /// Parsed double value (signed numerator / signed denominator) + static double ParseSingleSRational(const uint8_t* bytes, size_t offset); + + /// + /// Parse GPS coordinates from PROPVARIANT values + /// + /// PROPVARIANT containing latitude + /// PROPVARIANT containing longitude + /// PROPVARIANT containing latitude reference (N/S) + /// PROPVARIANT containing longitude reference (E/W) + /// Pair of (latitude, longitude) in decimal degrees + static std::pair ParseGPSCoordinates( + const PROPVARIANT& latitude, + const PROPVARIANT& longitude, + const PROPVARIANT& latRef, + const PROPVARIANT& lonRef); + + /// + /// Sanitize a string to make it safe for use in filenames + /// Replaces illegal filename characters (< > : " / \ | ? * and control chars) with underscore + /// Also removes trailing dots and spaces which Windows doesn't allow at end of filename + /// + /// IMPORTANT: This should ONLY be called in ExtractPatterns to avoid performance waste. + /// Do NOT call this function when reading raw metadata values. + /// + /// String to sanitize + /// Sanitized string safe for use in filename + static std::wstring SanitizeForFileName(const std::wstring& str); + }; +} diff --git a/src/modules/powerrename/lib/MetadataPatternExtractor.cpp b/src/modules/powerrename/lib/MetadataPatternExtractor.cpp new file mode 100644 index 0000000000..cfbc40837d --- /dev/null +++ b/src/modules/powerrename/lib/MetadataPatternExtractor.cpp @@ -0,0 +1,353 @@ +// 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. + +#include "pch.h" +#include "MetadataPatternExtractor.h" +#include "MetadataFormatHelper.h" +#include "WICMetadataExtractor.h" +#include +#include +#include +#include +#include +#include + +using namespace PowerRenameLib; + +MetadataPatternExtractor::MetadataPatternExtractor() + : extractor(std::make_unique()) +{ +} + +MetadataPatternExtractor::~MetadataPatternExtractor() = default; + +MetadataPatternMap MetadataPatternExtractor::ExtractPatterns( + const std::wstring& filePath, + MetadataType type) +{ + MetadataPatternMap patterns; + + switch (type) + { + case MetadataType::EXIF: + patterns = ExtractEXIFPatterns(filePath); + break; + case MetadataType::XMP: + patterns = ExtractXMPPatterns(filePath); + break; + default: + return MetadataPatternMap(); + } + + // Sanitize all pattern values for filename safety before returning + // This ensures all metadata values are safe to use in filenames (removes illegal chars like <>:"/\|?*) + // IMPORTANT: Only call SanitizeForFileName here to avoid performance waste + for (auto& [key, value] : patterns) + { + value = MetadataFormatHelper::SanitizeForFileName(value); + } + + return patterns; +} + +void MetadataPatternExtractor::ClearCache() +{ + if (extractor) + { + extractor->ClearCache(); + } +} + +MetadataPatternMap MetadataPatternExtractor::ExtractEXIFPatterns(const std::wstring& filePath) +{ + MetadataPatternMap patterns; + + EXIFMetadata exif; + if (!extractor->ExtractEXIFMetadata(filePath, exif)) + { + return patterns; + } + + if (exif.cameraMake.has_value()) + { + patterns[MetadataPatterns::CAMERA_MAKE] = exif.cameraMake.value(); + } + + if (exif.cameraModel.has_value()) + { + patterns[MetadataPatterns::CAMERA_MODEL] = exif.cameraModel.value(); + } + + if (exif.lensModel.has_value()) + { + patterns[MetadataPatterns::LENS] = exif.lensModel.value(); + } + + if (exif.iso.has_value()) + { + patterns[MetadataPatterns::ISO] = MetadataFormatHelper::FormatISO(exif.iso.value()); + } + + if (exif.aperture.has_value()) + { + patterns[MetadataPatterns::APERTURE] = MetadataFormatHelper::FormatAperture(exif.aperture.value()); + } + + if (exif.shutterSpeed.has_value()) + { + patterns[MetadataPatterns::SHUTTER] = MetadataFormatHelper::FormatShutterSpeed(exif.shutterSpeed.value()); + } + + if (exif.focalLength.has_value()) + { + patterns[MetadataPatterns::FOCAL] = std::to_wstring(static_cast(exif.focalLength.value())) + L"mm"; + } + + if (exif.flash.has_value()) + { + patterns[MetadataPatterns::FLASH] = MetadataFormatHelper::FormatFlash(exif.flash.value()); + } + + if (exif.width.has_value()) + { + patterns[MetadataPatterns::WIDTH] = std::to_wstring(exif.width.value()); + } + + if (exif.height.has_value()) + { + patterns[MetadataPatterns::HEIGHT] = std::to_wstring(exif.height.value()); + } + + if (exif.author.has_value()) + { + patterns[MetadataPatterns::AUTHOR] = exif.author.value(); + } + + if (exif.copyright.has_value()) + { + patterns[MetadataPatterns::COPYRIGHT] = exif.copyright.value(); + } + + if (exif.latitude.has_value()) + { + patterns[MetadataPatterns::LATITUDE] = MetadataFormatHelper::FormatCoordinate(exif.latitude.value(), true); + } + + if (exif.longitude.has_value()) + { + patterns[MetadataPatterns::LONGITUDE] = MetadataFormatHelper::FormatCoordinate(exif.longitude.value(), false); + } + + // Only extract DATE_TAKEN patterns (most commonly used) + if (exif.dateTaken.has_value()) + { + const SYSTEMTIME& date = exif.dateTaken.value(); + patterns[MetadataPatterns::DATE_TAKEN_YYYY] = std::format(L"{:04d}", date.wYear); + patterns[MetadataPatterns::DATE_TAKEN_YY] = std::format(L"{:02d}", date.wYear % 100); + patterns[MetadataPatterns::DATE_TAKEN_MM] = std::format(L"{:02d}", date.wMonth); + patterns[MetadataPatterns::DATE_TAKEN_DD] = std::format(L"{:02d}", date.wDay); + patterns[MetadataPatterns::DATE_TAKEN_HH] = std::format(L"{:02d}", date.wHour); + patterns[MetadataPatterns::DATE_TAKEN_mm] = std::format(L"{:02d}", date.wMinute); + patterns[MetadataPatterns::DATE_TAKEN_SS] = std::format(L"{:02d}", date.wSecond); + } + // Note: dateDigitized and dateModified are still extracted but not exposed as patterns + + if (exif.exposureBias.has_value()) + { + patterns[MetadataPatterns::EXPOSURE_BIAS] = std::format(L"{:.2f}", exif.exposureBias.value()); + } + + if (exif.orientation.has_value()) + { + patterns[MetadataPatterns::ORIENTATION] = std::to_wstring(exif.orientation.value()); + } + + if (exif.colorSpace.has_value()) + { + patterns[MetadataPatterns::COLOR_SPACE] = std::to_wstring(exif.colorSpace.value()); + } + + if (exif.altitude.has_value()) + { + patterns[MetadataPatterns::ALTITUDE] = std::format(L"{:.2f} m", exif.altitude.value()); + } + + return patterns; +} + +MetadataPatternMap MetadataPatternExtractor::ExtractXMPPatterns(const std::wstring& filePath) +{ + MetadataPatternMap patterns; + + XMPMetadata xmp; + if (!extractor->ExtractXMPMetadata(filePath, xmp)) + { + return patterns; + } + + if (xmp.creator.has_value()) + { + const auto& creator = xmp.creator.value(); + patterns[MetadataPatterns::AUTHOR] = creator; + patterns[MetadataPatterns::CREATOR] = creator; + } + + if (xmp.rights.has_value()) + { + const auto& rights = xmp.rights.value(); + patterns[MetadataPatterns::RIGHTS] = rights; + patterns[MetadataPatterns::COPYRIGHT] = rights; + } + + if (xmp.title.has_value()) + { + patterns[MetadataPatterns::TITLE] = xmp.title.value(); + } + + if (xmp.description.has_value()) + { + patterns[MetadataPatterns::DESCRIPTION] = xmp.description.value(); + } + + if (xmp.subject.has_value()) + { + std::wstring joined; + for (const auto& entry : xmp.subject.value()) + { + if (!joined.empty()) + { + joined.append(L"; "); + } + joined.append(entry); + } + if (!joined.empty()) + { + patterns[MetadataPatterns::SUBJECT] = joined; + } + } + + if (xmp.creatorTool.has_value()) + { + patterns[MetadataPatterns::CREATOR_TOOL] = xmp.creatorTool.value(); + } + + if (xmp.documentID.has_value()) + { + patterns[MetadataPatterns::DOCUMENT_ID] = xmp.documentID.value(); + } + + if (xmp.instanceID.has_value()) + { + patterns[MetadataPatterns::INSTANCE_ID] = xmp.instanceID.value(); + } + + if (xmp.originalDocumentID.has_value()) + { + patterns[MetadataPatterns::ORIGINAL_DOCUMENT_ID] = xmp.originalDocumentID.value(); + } + + if (xmp.versionID.has_value()) + { + patterns[MetadataPatterns::VERSION_ID] = xmp.versionID.value(); + } + + // Only extract CREATE_DATE patterns (primary creation time) + if (xmp.createDate.has_value()) + { + const SYSTEMTIME& date = xmp.createDate.value(); + patterns[MetadataPatterns::CREATE_DATE_YYYY] = std::format(L"{:04d}", date.wYear); + patterns[MetadataPatterns::CREATE_DATE_YY] = std::format(L"{:02d}", date.wYear % 100); + patterns[MetadataPatterns::CREATE_DATE_MM] = std::format(L"{:02d}", date.wMonth); + patterns[MetadataPatterns::CREATE_DATE_DD] = std::format(L"{:02d}", date.wDay); + patterns[MetadataPatterns::CREATE_DATE_HH] = std::format(L"{:02d}", date.wHour); + patterns[MetadataPatterns::CREATE_DATE_mm] = std::format(L"{:02d}", date.wMinute); + patterns[MetadataPatterns::CREATE_DATE_SS] = std::format(L"{:02d}", date.wSecond); + } + // Note: modifyDate and metadataDate are still extracted but not exposed as patterns + + return patterns; +} + +// AddDatePatterns function has been removed as dynamic patterns are no longer supported. +// Date patterns are now directly added inline for DATE_TAKEN and CREATE_DATE only. +// Formatting functions have been moved to MetadataFormatHelper for better testability. + +std::vector MetadataPatternExtractor::GetSupportedPatterns(MetadataType type) +{ + switch (type) + { + case MetadataType::EXIF: + return { + MetadataPatterns::CAMERA_MAKE, + MetadataPatterns::CAMERA_MODEL, + MetadataPatterns::LENS, + MetadataPatterns::ISO, + MetadataPatterns::APERTURE, + MetadataPatterns::SHUTTER, + MetadataPatterns::FOCAL, + MetadataPatterns::FLASH, + MetadataPatterns::WIDTH, + MetadataPatterns::HEIGHT, + MetadataPatterns::AUTHOR, + MetadataPatterns::COPYRIGHT, + MetadataPatterns::LATITUDE, + MetadataPatterns::LONGITUDE, + MetadataPatterns::DATE_TAKEN_YYYY, + MetadataPatterns::DATE_TAKEN_YY, + MetadataPatterns::DATE_TAKEN_MM, + MetadataPatterns::DATE_TAKEN_DD, + MetadataPatterns::DATE_TAKEN_HH, + MetadataPatterns::DATE_TAKEN_mm, + MetadataPatterns::DATE_TAKEN_SS, + MetadataPatterns::EXPOSURE_BIAS, + MetadataPatterns::ORIENTATION, + MetadataPatterns::COLOR_SPACE, + MetadataPatterns::ALTITUDE + }; + + case MetadataType::XMP: + return { + MetadataPatterns::AUTHOR, + MetadataPatterns::COPYRIGHT, + MetadataPatterns::RIGHTS, + MetadataPatterns::TITLE, + MetadataPatterns::DESCRIPTION, + MetadataPatterns::SUBJECT, + MetadataPatterns::CREATOR, + MetadataPatterns::CREATOR_TOOL, + MetadataPatterns::DOCUMENT_ID, + MetadataPatterns::INSTANCE_ID, + MetadataPatterns::ORIGINAL_DOCUMENT_ID, + MetadataPatterns::VERSION_ID, + MetadataPatterns::CREATE_DATE_YYYY, + MetadataPatterns::CREATE_DATE_YY, + MetadataPatterns::CREATE_DATE_MM, + MetadataPatterns::CREATE_DATE_DD, + MetadataPatterns::CREATE_DATE_HH, + MetadataPatterns::CREATE_DATE_mm, + MetadataPatterns::CREATE_DATE_SS + }; + + default: + return {}; + } +} + +std::vector MetadataPatternExtractor::GetAllPossiblePatterns() +{ + auto exifPatterns = GetSupportedPatterns(MetadataType::EXIF); + auto xmpPatterns = GetSupportedPatterns(MetadataType::XMP); + + std::vector allPatterns; + allPatterns.reserve(exifPatterns.size() + xmpPatterns.size()); + + allPatterns.insert(allPatterns.end(), exifPatterns.begin(), exifPatterns.end()); + allPatterns.insert(allPatterns.end(), xmpPatterns.begin(), xmpPatterns.end()); + + std::sort(allPatterns.begin(), allPatterns.end()); + allPatterns.erase(std::unique(allPatterns.begin(), allPatterns.end()), allPatterns.end()); + + return allPatterns; +} + diff --git a/src/modules/powerrename/lib/MetadataPatternExtractor.h b/src/modules/powerrename/lib/MetadataPatternExtractor.h new file mode 100644 index 0000000000..787e5c437d --- /dev/null +++ b/src/modules/powerrename/lib/MetadataPatternExtractor.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once +#include +#include +#include +#include +#include "MetadataTypes.h" + +namespace PowerRenameLib +{ + // Pattern-Value mapping for metadata replacement + using MetadataPatternMap = std::unordered_map; + + /// + /// Metadata pattern extractor that converts metadata into replaceable patterns + /// + class MetadataPatternExtractor + { + public: + MetadataPatternExtractor(); + ~MetadataPatternExtractor(); + + MetadataPatternMap ExtractPatterns(const std::wstring& filePath, MetadataType type); + + void ClearCache(); + + static std::vector GetSupportedPatterns(MetadataType type); + static std::vector GetAllPossiblePatterns(); + + private: + std::unique_ptr extractor; + + MetadataPatternMap ExtractEXIFPatterns(const std::wstring& filePath); + MetadataPatternMap ExtractXMPPatterns(const std::wstring& filePath); + }; +} diff --git a/src/modules/powerrename/lib/MetadataResultCache.cpp b/src/modules/powerrename/lib/MetadataResultCache.cpp new file mode 100644 index 0000000000..5f30b24abe --- /dev/null +++ b/src/modules/powerrename/lib/MetadataResultCache.cpp @@ -0,0 +1,87 @@ +// 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. + +#include "pch.h" +#include "MetadataResultCache.h" + +using namespace PowerRenameLib; + +namespace +{ + template + bool GetOrLoadInternal(const std::wstring& filePath, + Metadata& outMetadata, + Cache& cache, + Mutex& mutex, + const Loader& loader) + { + { + std::shared_lock sharedLock(mutex); + auto it = cache.find(filePath); + if (it != cache.end()) + { + // Return cached result (success or failure) + outMetadata = it->second.data; + return it->second.wasSuccessful; + } + } + + if (!loader) + { + // No loader provided + return false; + } + + Metadata loaded{}; + const bool result = loader(loaded); + + // Cache the result (success or failure) + { + std::unique_lock uniqueLock(mutex); + // Check if another thread cached it while we were loading + auto it = cache.find(filePath); + if (it == cache.end()) + { + // Not cached yet, insert our result + cache.emplace(filePath, CacheEntry{ result, loaded }); + } + else + { + // Another thread cached it, use their result + outMetadata = it->second.data; + return it->second.wasSuccessful; + } + } + + outMetadata = loaded; + return result; + } +} + +bool MetadataResultCache::GetOrLoadEXIF(const std::wstring& filePath, + EXIFMetadata& outMetadata, + const EXIFLoader& loader) +{ + return GetOrLoadInternal>(filePath, outMetadata, exifCache, exifMutex, loader); +} + +bool MetadataResultCache::GetOrLoadXMP(const std::wstring& filePath, + XMPMetadata& outMetadata, + const XMPLoader& loader) +{ + return GetOrLoadInternal>(filePath, outMetadata, xmpCache, xmpMutex, loader); +} + +void MetadataResultCache::ClearAll() +{ + { + std::unique_lock lock(exifMutex); + exifCache.clear(); + } + + { + std::unique_lock lock(xmpMutex); + xmpCache.clear(); + } +} diff --git a/src/modules/powerrename/lib/MetadataResultCache.h b/src/modules/powerrename/lib/MetadataResultCache.h new file mode 100644 index 0000000000..ad3b9782c4 --- /dev/null +++ b/src/modules/powerrename/lib/MetadataResultCache.h @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#pragma once +#include "MetadataTypes.h" +#include +#include +#include +#include + +namespace PowerRenameLib +{ + class MetadataResultCache + { + public: + using EXIFLoader = std::function; + using XMPLoader = std::function; + + bool GetOrLoadEXIF(const std::wstring& filePath, EXIFMetadata& outMetadata, const EXIFLoader& loader); + bool GetOrLoadXMP(const std::wstring& filePath, XMPMetadata& outMetadata, const XMPLoader& loader); + + void ClearAll(); + + private: + // Wrapper to cache both success and failure states + template + struct CacheEntry + { + bool wasSuccessful; + T data; + }; + + mutable std::shared_mutex exifMutex; + mutable std::shared_mutex xmpMutex; + std::unordered_map> exifCache; + std::unordered_map> xmpCache; + }; +} diff --git a/src/modules/powerrename/lib/MetadataTypes.h b/src/modules/powerrename/lib/MetadataTypes.h new file mode 100644 index 0000000000..aa6a721e4c --- /dev/null +++ b/src/modules/powerrename/lib/MetadataTypes.h @@ -0,0 +1,156 @@ +// 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. + +#pragma once +#include +#include +#include +#include + +namespace PowerRenameLib +{ + /// + /// Supported metadata format types + /// + enum class MetadataType + { + EXIF, // EXIF metadata (camera settings, date taken, etc.) + XMP // XMP metadata (Dublin Core, Photoshop, etc.) + }; + + /// + /// Complete EXIF metadata structure + /// Contains all commonly used EXIF fields with optional values + /// + struct EXIFMetadata + { + // Date and time information + std::optional dateTaken; // DateTimeOriginal + std::optional dateDigitized; // DateTimeDigitized + std::optional dateModified; // DateTime + + // Camera information + std::optional cameraMake; // Make + std::optional cameraModel; // Model + std::optional lensModel; // LensModel + + // Shooting parameters + std::optional iso; // ISO speed + std::optional aperture; // F-number + std::optional shutterSpeed; // Exposure time + std::optional focalLength; // Focal length in mm + std::optional exposureBias; // Exposure bias value + std::optional flash; // Flash status + + // Image properties + std::optional width; // Image width in pixels + std::optional height; // Image height in pixels + std::optional orientation; // Image orientation + std::optional colorSpace; // Color space + + // Author and copyright + std::optional author; // Artist + std::optional copyright; // Copyright notice + + // GPS information + std::optional latitude; // GPS latitude in decimal degrees + std::optional longitude; // GPS longitude in decimal degrees + std::optional altitude; // GPS altitude in meters + }; + + /// + /// XMP (Extensible Metadata Platform) metadata structure + /// Contains XMP Basic, Dublin Core, Rights and Media Management schema fields + /// + struct XMPMetadata + { + // XMP Basic schema - https://ns.adobe.com/xap/1.0/ + std::optional createDate; // xmp:CreateDate + std::optional modifyDate; // xmp:ModifyDate + std::optional metadataDate; // xmp:MetadataDate + std::optional creatorTool; // xmp:CreatorTool + + // Dublin Core schema - http://purl.org/dc/elements/1.1/ + std::optional title; // dc:title + std::optional description; // dc:description + std::optional creator; // dc:creator (author) + std::optional> subject; // dc:subject (keywords) + + // XMP Rights Management schema - http://ns.adobe.com/xap/1.0/rights/ + std::optional rights; // xmpRights:WebStatement (copyright) + + // XMP Media Management schema - http://ns.adobe.com/xap/1.0/mm/ + std::optional documentID; // xmpMM:DocumentID + std::optional instanceID; // xmpMM:InstanceID + std::optional originalDocumentID; // xmpMM:OriginalDocumentID + std::optional versionID; // xmpMM:VersionID + }; + + + + + /// + /// Constants for metadata pattern names + /// + namespace MetadataPatterns + { + // EXIF patterns + constexpr wchar_t CAMERA_MAKE[] = L"CAMERA_MAKE"; + constexpr wchar_t CAMERA_MODEL[] = L"CAMERA_MODEL"; + constexpr wchar_t LENS[] = L"LENS"; + constexpr wchar_t ISO[] = L"ISO"; + constexpr wchar_t APERTURE[] = L"APERTURE"; + constexpr wchar_t SHUTTER[] = L"SHUTTER"; + constexpr wchar_t FOCAL[] = L"FOCAL"; + constexpr wchar_t FLASH[] = L"FLASH"; + constexpr wchar_t WIDTH[] = L"WIDTH"; + constexpr wchar_t HEIGHT[] = L"HEIGHT"; + constexpr wchar_t AUTHOR[] = L"AUTHOR"; + constexpr wchar_t COPYRIGHT[] = L"COPYRIGHT"; + constexpr wchar_t LATITUDE[] = L"LATITUDE"; + constexpr wchar_t LONGITUDE[] = L"LONGITUDE"; + + // Date components from EXIF DateTimeOriginal (when photo was taken) + constexpr wchar_t DATE_TAKEN_YYYY[] = L"DATE_TAKEN_YYYY"; + constexpr wchar_t DATE_TAKEN_YY[] = L"DATE_TAKEN_YY"; + constexpr wchar_t DATE_TAKEN_MM[] = L"DATE_TAKEN_MM"; + constexpr wchar_t DATE_TAKEN_DD[] = L"DATE_TAKEN_DD"; + constexpr wchar_t DATE_TAKEN_HH[] = L"DATE_TAKEN_HH"; + constexpr wchar_t DATE_TAKEN_mm[] = L"DATE_TAKEN_mm"; + constexpr wchar_t DATE_TAKEN_SS[] = L"DATE_TAKEN_SS"; + + // Additional EXIF patterns + constexpr wchar_t EXPOSURE_BIAS[] = L"EXPOSURE_BIAS"; + constexpr wchar_t ORIENTATION[] = L"ORIENTATION"; + constexpr wchar_t COLOR_SPACE[] = L"COLOR_SPACE"; + constexpr wchar_t ALTITUDE[] = L"ALTITUDE"; + + // XMP patterns + constexpr wchar_t CREATOR_TOOL[] = L"CREATOR_TOOL"; + + // Date components from XMP CreateDate + constexpr wchar_t CREATE_DATE_YYYY[] = L"CREATE_DATE_YYYY"; + constexpr wchar_t CREATE_DATE_YY[] = L"CREATE_DATE_YY"; + constexpr wchar_t CREATE_DATE_MM[] = L"CREATE_DATE_MM"; + constexpr wchar_t CREATE_DATE_DD[] = L"CREATE_DATE_DD"; + constexpr wchar_t CREATE_DATE_HH[] = L"CREATE_DATE_HH"; + constexpr wchar_t CREATE_DATE_mm[] = L"CREATE_DATE_mm"; + constexpr wchar_t CREATE_DATE_SS[] = L"CREATE_DATE_SS"; + + // Dublin Core patterns + constexpr wchar_t TITLE[] = L"TITLE"; + constexpr wchar_t DESCRIPTION[] = L"DESCRIPTION"; + constexpr wchar_t CREATOR[] = L"CREATOR"; + constexpr wchar_t SUBJECT[] = L"SUBJECT"; // Keywords + + // XMP Rights pattern + constexpr wchar_t RIGHTS[] = L"RIGHTS"; // Copyright + + // XMP Media Management patterns + constexpr wchar_t DOCUMENT_ID[] = L"DOCUMENT_ID"; + constexpr wchar_t INSTANCE_ID[] = L"INSTANCE_ID"; + constexpr wchar_t ORIGINAL_DOCUMENT_ID[] = L"ORIGINAL_DOCUMENT_ID"; + constexpr wchar_t VERSION_ID[] = L"VERSION_ID"; + } +} \ No newline at end of file diff --git a/src/modules/powerrename/lib/PowerRenameInterfaces.h b/src/modules/powerrename/lib/PowerRenameInterfaces.h index ea761d156a..7e3402433b 100644 --- a/src/modules/powerrename/lib/PowerRenameInterfaces.h +++ b/src/modules/powerrename/lib/PowerRenameInterfaces.h @@ -1,7 +1,10 @@ #pragma once #include "pch.h" +#include "MetadataTypes.h" +#include "MetadataPatternExtractor.h" #include #include +#include enum PowerRenameFlags { @@ -22,6 +25,9 @@ enum PowerRenameFlags CreationTime = 0x4000, ModificationTime = 0x8000, AccessTime = 0x10000, + // Metadata source flags + MetadataSourceEXIF = 0x20000, // Default + MetadataSourceXMP = 0x40000, }; enum PowerRenameFilters @@ -47,6 +53,7 @@ public: IFACEMETHOD(OnReplaceTermChanged)(_In_ PCWSTR replaceTerm) = 0; IFACEMETHOD(OnFlagsChanged)(_In_ DWORD flags) = 0; IFACEMETHOD(OnFileTimeChanged)(_In_ SYSTEMTIME fileTime) = 0; + IFACEMETHOD(OnMetadataChanged)() = 0; }; interface __declspec(uuid("E3ED45B5-9CE0-47E2-A595-67EB950B9B72")) IPowerRenameRegEx : public IUnknown @@ -62,6 +69,9 @@ public: IFACEMETHOD(PutFlags)(_In_ DWORD flags) = 0; IFACEMETHOD(PutFileTime)(_In_ SYSTEMTIME fileTime) = 0; IFACEMETHOD(ResetFileTime)() = 0; + IFACEMETHOD(PutMetadataPatterns)(_In_ const PowerRenameLib::MetadataPatternMap& patterns) = 0; + IFACEMETHOD(ResetMetadata)() = 0; + IFACEMETHOD(GetMetadataType)(_Out_ PowerRenameLib::MetadataType* metadataType) = 0; IFACEMETHOD(Replace)(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex) = 0; }; diff --git a/src/modules/powerrename/lib/PowerRenameItem.cpp b/src/modules/powerrename/lib/PowerRenameItem.cpp index b33fccfb89..61e07a93fc 100644 --- a/src/modules/powerrename/lib/PowerRenameItem.cpp +++ b/src/modules/powerrename/lib/PowerRenameItem.cpp @@ -74,7 +74,7 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME* else { // Default to modification time if no specific flag is set - parsedTimeType = PowerRenameFlags::CreationTime; + parsedTimeType = PowerRenameFlags::CreationTime; } if (m_isTimeParsed && parsedTimeType == m_parsedTimeType) @@ -86,6 +86,13 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME* HANDLE hFile = CreateFileW(m_path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); if (hFile != INVALID_HANDLE_VALUE) { + // Use RAII-style scope guard to ensure handle is always closed + struct FileHandleCloser + { + HANDLE handle; + ~FileHandleCloser() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); } + } scopedHandle{ hFile }; + FILETIME FileTime; bool success = false; @@ -122,8 +129,6 @@ IFACEMETHODIMP CPowerRenameItem::GetTime(_In_ DWORD flags, _Outptr_ SYSTEMTIME* } } } - - CloseHandle(hFile); } *time = m_time; return hr; diff --git a/src/modules/powerrename/lib/PowerRenameLib.vcxproj b/src/modules/powerrename/lib/PowerRenameLib.vcxproj index 103eab8e8e..bd5740dee7 100644 --- a/src/modules/powerrename/lib/PowerRenameLib.vcxproj +++ b/src/modules/powerrename/lib/PowerRenameLib.vcxproj @@ -16,19 +16,24 @@ - - - ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\ + $(ProjectDir)..\..\..\..\deps + + + Level3 WIN32;_LIB;%(PreprocessorDefinitions) $(ProjectDir)..\;$(ProjectDir)..\ui;$(ProjectDir)..\dll;$(ProjectDir)..\lib;$(ProjectDir)..\..\..\;$(ProjectDir)..\..\..\common\Telemetry;%(AdditionalIncludeDirectories);$(GeneratedFilesDir) + /FS %(AdditionalOptions) + + windowscodecs.lib;propsys.lib;ole32.lib;%(AdditionalDependencies) + @@ -47,6 +52,12 @@ + + + + + + @@ -64,6 +75,10 @@ Create + + + + diff --git a/src/modules/powerrename/lib/PowerRenameManager.cpp b/src/modules/powerrename/lib/PowerRenameManager.cpp index b6641374ba..160d064e09 100644 --- a/src/modules/powerrename/lib/PowerRenameManager.cpp +++ b/src/modules/powerrename/lib/PowerRenameManager.cpp @@ -462,6 +462,12 @@ IFACEMETHODIMP CPowerRenameManager::OnFileTimeChanged(_In_ SYSTEMTIME /*fileTime return S_OK; } +IFACEMETHODIMP CPowerRenameManager::OnMetadataChanged() +{ + _PerformRegExRename(); + return S_OK; +} + HRESULT CPowerRenameManager::s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm) { *ppsrm = nullptr; diff --git a/src/modules/powerrename/lib/PowerRenameManager.h b/src/modules/powerrename/lib/PowerRenameManager.h index f339f5c9d4..a9fa44d144 100644 --- a/src/modules/powerrename/lib/PowerRenameManager.h +++ b/src/modules/powerrename/lib/PowerRenameManager.h @@ -50,6 +50,7 @@ public: IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm); IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags); IFACEMETHODIMP OnFileTimeChanged(_In_ SYSTEMTIME fileTime); + IFACEMETHODIMP OnMetadataChanged(); static HRESULT s_CreateInstance(_Outptr_ IPowerRenameManager** ppsrm); diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.cpp b/src/modules/powerrename/lib/PowerRenameRegEx.cpp index 34d3cc5c0c..567df48606 100644 --- a/src/modules/powerrename/lib/PowerRenameRegEx.cpp +++ b/src/modules/powerrename/lib/PowerRenameRegEx.cpp @@ -328,6 +328,22 @@ IFACEMETHODIMP CPowerRenameRegEx::ResetFileTime() return S_OK; } +IFACEMETHODIMP CPowerRenameRegEx::PutMetadataPatterns(_In_ const PowerRenameLib::MetadataPatternMap& patterns) +{ + m_metadataPatterns = patterns; + m_useMetadata = true; + _OnMetadataChanged(); + return S_OK; +} + +IFACEMETHODIMP CPowerRenameRegEx::ResetMetadata() +{ + m_metadataPatterns.clear(); + m_useMetadata = false; + _OnMetadataChanged(); + return S_OK; +} + HRESULT CPowerRenameRegEx::s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx) { *renameRegEx = nullptr; @@ -387,10 +403,39 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u // TODO: creating the regex could be costly. May want to cache this. wchar_t newReplaceTerm[MAX_PATH] = { 0 }; bool fileTimeErrorOccurred = false; + bool metadataErrorOccurred = false; + bool appliedTemplateTransform = false; + + std::wstring replaceTemplate; + if (m_replaceTerm) + { + replaceTemplate = m_replaceTerm; + } + if (m_useFileTime) { - if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), m_replaceTerm, m_fileTime))) + if (FAILED(GetDatedFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_fileTime))) + { fileTimeErrorOccurred = true; + } + else + { + replaceTemplate.assign(newReplaceTerm); + appliedTemplateTransform = true; + } + } + + if (m_useMetadata) + { + if (FAILED(GetMetadataFileName(newReplaceTerm, ARRAYSIZE(newReplaceTerm), replaceTemplate.c_str(), m_metadataPatterns))) + { + metadataErrorOccurred = true; + } + else + { + replaceTemplate.assign(newReplaceTerm); + appliedTemplateTransform = true; + } } std::wstring sourceToUse; @@ -399,9 +444,9 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, u std::wstring searchTerm(m_searchTerm); std::wstring replaceTerm; - if (m_useFileTime && !fileTimeErrorOccurred) + if (appliedTemplateTransform) { - replaceTerm = newReplaceTerm; + replaceTerm = replaceTemplate; } else if (m_replaceTerm) { @@ -606,3 +651,43 @@ void CPowerRenameRegEx::_OnFileTimeChanged() } } } + +void CPowerRenameRegEx::_OnMetadataChanged() +{ + CSRWSharedAutoLock lock(&m_lockEvents); + + for (auto it : m_renameRegExEvents) + { + if (it.pEvents) + { + it.pEvents->OnMetadataChanged(); + } + } +} + +PowerRenameLib::MetadataType CPowerRenameRegEx::_GetMetadataTypeFromFlags() const +{ + if (m_flags & MetadataSourceXMP) + return PowerRenameLib::MetadataType::XMP; + + // Default to EXIF + return PowerRenameLib::MetadataType::EXIF; +} + +// Interface method implementation +IFACEMETHODIMP CPowerRenameRegEx::GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType) +{ + if (metadataType == nullptr) + return E_POINTER; + + *metadataType = _GetMetadataTypeFromFlags(); + return S_OK; +} + +// Convenience method for internal use +PowerRenameLib::MetadataType CPowerRenameRegEx::GetMetadataType() const +{ + return _GetMetadataTypeFromFlags(); +} + + diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.h b/src/modules/powerrename/lib/PowerRenameRegEx.h index 55c6c14c17..9e43107efa 100644 --- a/src/modules/powerrename/lib/PowerRenameRegEx.h +++ b/src/modules/powerrename/lib/PowerRenameRegEx.h @@ -5,6 +5,8 @@ #include "Enumerating.h" #include "Randomizer.h" +#include "MetadataTypes.h" +#include "MetadataPatternExtractor.h" #include "PowerRenameInterfaces.h" @@ -29,7 +31,13 @@ public: IFACEMETHODIMP PutFlags(_In_ DWORD flags); IFACEMETHODIMP PutFileTime(_In_ SYSTEMTIME fileTime); IFACEMETHODIMP ResetFileTime(); + IFACEMETHODIMP PutMetadataPatterns(_In_ const PowerRenameLib::MetadataPatternMap& patterns); + IFACEMETHODIMP ResetMetadata(); + IFACEMETHODIMP GetMetadataType(_Out_ PowerRenameLib::MetadataType* metadataType); IFACEMETHODIMP Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex); + + // Get current metadata type based on flags + PowerRenameLib::MetadataType GetMetadataType() const; static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx); @@ -41,7 +49,9 @@ protected: void _OnReplaceTermChanged(); void _OnFlagsChanged(); void _OnFileTimeChanged(); + void _OnMetadataChanged(); HRESULT _OnEnumerateOrRandomizeItemsChanged(); + PowerRenameLib::MetadataType _GetMetadataTypeFromFlags() const; size_t _Find(std::wstring data, std::wstring toSearch, bool caseInsensitive, size_t pos); @@ -54,6 +64,9 @@ protected: SYSTEMTIME m_fileTime = { 0 }; bool m_useFileTime = false; + PowerRenameLib::MetadataPatternMap m_metadataPatterns; + bool m_useMetadata = false; + CSRWLock m_lock; CSRWLock m_lockEvents; diff --git a/src/modules/powerrename/lib/PropVariantValue.h b/src/modules/powerrename/lib/PropVariantValue.h new file mode 100644 index 0000000000..23e5973d25 --- /dev/null +++ b/src/modules/powerrename/lib/PropVariantValue.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +namespace PowerRenameLib +{ + /// + /// RAII wrapper around PROPVARIANT to ensure proper initialization and cleanup. + /// Move-only semantics keep ownership simple while still allowing use in optionals. + /// + struct PropVariantValue + { + PropVariantValue() noexcept + { + PropVariantInit(&value); + } + + ~PropVariantValue() + { + PropVariantClear(&value); + } + + PropVariantValue(const PropVariantValue&) = delete; + PropVariantValue& operator=(const PropVariantValue&) = delete; + + PropVariantValue(PropVariantValue&& other) noexcept + { + value = other.value; + PropVariantInit(&other.value); // Properly clear the moved-from object + } + + PropVariantValue& operator=(PropVariantValue&& other) noexcept + { + if (this != &other) + { + PropVariantClear(&value); + value = other.value; + PropVariantInit(&other.value); // Properly clear the moved-from object + } + return *this; + } + + PROPVARIANT* GetAddressOf() noexcept + { + return &value; + } + + PROPVARIANT& Get() noexcept + { + return value; + } + + const PROPVARIANT& Get() const noexcept + { + return value; + } + + private: + PROPVARIANT value; + }; +} diff --git a/src/modules/powerrename/lib/Renaming.cpp b/src/modules/powerrename/lib/Renaming.cpp index aa21666783..028621eef4 100644 --- a/src/modules/powerrename/lib/Renaming.cpp +++ b/src/modules/powerrename/lib/Renaming.cpp @@ -1,9 +1,13 @@ #include "pch.h" #include +#include +#include +#include #include "Renaming.h" #include - +#include "MetadataPatternExtractor.h" +#include "PowerRenameRegEx.h" namespace fs = std::filesystem; bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnumIndex, CComPtr& spItem) @@ -14,6 +18,7 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum PWSTR replaceTerm = nullptr; bool useFileTime = false; + bool useMetadata = false; winrt::check_hresult(spRenameRegEx->GetReplaceTerm(&replaceTerm)); @@ -21,7 +26,6 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum { useFileTime = true; } - CoTaskMemFree(replaceTerm); int id = -1; winrt::check_hresult(spItem->GetId(&id)); @@ -30,6 +34,29 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum bool isSubFolderContent = false; winrt::check_hresult(spItem->GetIsFolder(&isFolder)); winrt::check_hresult(spItem->GetIsSubFolderContent(&isSubFolderContent)); + + // Get metadata type to check if metadata patterns are used + PowerRenameLib::MetadataType metadataType; + HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType); + if (FAILED(hr)) + { + // Fallback to default metadata type if call fails + metadataType = PowerRenameLib::MetadataType::EXIF; + } + + // Check if metadata is used AND if this file type supports metadata + // Get file path early for metadata type checking and reuse later + PWSTR filePath = nullptr; + winrt::check_hresult(spItem->GetPath(&filePath)); + std::wstring filePathStr(filePath); // Copy once for reuse + CoTaskMemFree(filePath); // Free immediately after copying + + if (isMetadataUsed(replaceTerm, metadataType, filePathStr.c_str(), isFolder)) + { + useMetadata = true; + } + + CoTaskMemFree(replaceTerm); if ((isFolder && (flags & PowerRenameFlags::ExcludeFolders)) || (!isFolder && (flags & PowerRenameFlags::ExcludeFiles)) || (isSubFolderContent && (flags & PowerRenameFlags::ExcludeSubfolders)) || @@ -82,6 +109,53 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum winrt::check_hresult(spRenameRegEx->PutFileTime(fileTime)); } + if (useMetadata) + { + // Extract metadata patterns from the file + // Note: filePathStr was already obtained and saved earlier for reuse + + // Get metadata type using the interface method + PowerRenameLib::MetadataType metadataType; + HRESULT hr = spRenameRegEx->GetMetadataType(&metadataType); + if (FAILED(hr)) + { + // Fallback to default metadata type if call fails + metadataType = PowerRenameLib::MetadataType::EXIF; + } + // Extract all patterns for the selected metadata type + // At this point we know the file is a supported image format (jpg/jpeg/png/tif/tiff) + static std::mutex s_metadataMutex; // Mutex to protect static variables + static std::once_flag s_metadataExtractorInitFlag; + static std::shared_ptr s_metadataExtractor; + static std::optional s_activeMetadataType; + + // Initialize the extractor only once + std::call_once(s_metadataExtractorInitFlag, []() { + s_metadataExtractor = std::make_shared(); + }); + + // Protect access to shared state + { + std::lock_guard lock(s_metadataMutex); + + // Clear cache if metadata type has changed + if (s_activeMetadataType.has_value() && s_activeMetadataType.value() != metadataType) + { + s_metadataExtractor->ClearCache(); + } + + // Update the active metadata type + s_activeMetadataType = metadataType; + } + + // Extract patterns (this can be done outside the lock if ExtractPatterns is thread-safe) + PowerRenameLib::MetadataPatternMap patterns = s_metadataExtractor->ExtractPatterns(filePathStr, metadataType); + + // Always call PutMetadataPatterns to ensure all patterns get replaced + // Even if empty, this keeps metadata placeholders consistent when no values are extracted + winrt::check_hresult(spRenameRegEx->PutMetadataPatterns(patterns)); + } + PWSTR newName = nullptr; // Failure here means we didn't match anything or had nothing to match @@ -93,6 +167,10 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum winrt::check_hresult(spRenameRegEx->ResetFileTime()); } + if (useMetadata) + { + winrt::check_hresult(spRenameRegEx->ResetMetadata()); + } wchar_t resultName[MAX_PATH] = { 0 }; PWSTR newNameToUse = nullptr; @@ -206,4 +284,4 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum CoTaskMemFree(originalName); return wouldRename; -} \ No newline at end of file +} diff --git a/src/modules/powerrename/lib/WICMetadataExtractor.cpp b/src/modules/powerrename/lib/WICMetadataExtractor.cpp new file mode 100644 index 0000000000..bd2f9c08dc --- /dev/null +++ b/src/modules/powerrename/lib/WICMetadataExtractor.cpp @@ -0,0 +1,1021 @@ +// 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. + +#include "pch.h" +#include "WICMetadataExtractor.h" +#include "MetadataFormatHelper.h" +#include +#include +#include +#include +#include +#include + +using namespace PowerRenameLib; + +namespace +{ + // Documentation: https://learn.microsoft.com/en-us/windows/win32/wic/-wic-native-image-format-metadata-queries + + // WIC metadata property paths + const std::wstring EXIF_DATE_TAKEN = L"/app1/ifd/exif/{ushort=36867}"; // DateTimeOriginal + const std::wstring EXIF_DATE_DIGITIZED = L"/app1/ifd/exif/{ushort=36868}"; // DateTimeDigitized + const std::wstring EXIF_DATE_MODIFIED = L"/app1/ifd/{ushort=306}"; // DateTime + const std::wstring EXIF_CAMERA_MAKE = L"/app1/ifd/{ushort=271}"; // Make + const std::wstring EXIF_CAMERA_MODEL = L"/app1/ifd/{ushort=272}"; // Model + const std::wstring EXIF_LENS_MODEL = L"/app1/ifd/exif/{ushort=42036}"; // LensModel + const std::wstring EXIF_ISO = L"/app1/ifd/exif/{ushort=34855}"; // ISOSpeedRatings + const std::wstring EXIF_APERTURE = L"/app1/ifd/exif/{ushort=33437}"; // FNumber + const std::wstring EXIF_SHUTTER_SPEED = L"/app1/ifd/exif/{ushort=33434}"; // ExposureTime + const std::wstring EXIF_FOCAL_LENGTH = L"/app1/ifd/exif/{ushort=37386}"; // FocalLength + const std::wstring EXIF_EXPOSURE_BIAS = L"/app1/ifd/exif/{ushort=37380}"; // ExposureBiasValue + const std::wstring EXIF_FLASH = L"/app1/ifd/exif/{ushort=37385}"; // Flash + const std::wstring EXIF_ORIENTATION = L"/app1/ifd/{ushort=274}"; // Orientation + const std::wstring EXIF_COLOR_SPACE = L"/app1/ifd/exif/{ushort=40961}"; // ColorSpace + const std::wstring EXIF_WIDTH = L"/app1/ifd/exif/{ushort=40962}"; // PixelXDimension - actual image width + const std::wstring EXIF_HEIGHT = L"/app1/ifd/exif/{ushort=40963}"; // PixelYDimension - actual image height + const std::wstring EXIF_ARTIST = L"/app1/ifd/{ushort=315}"; // Artist + const std::wstring EXIF_COPYRIGHT = L"/app1/ifd/{ushort=33432}"; // Copyright + + // GPS paths + const std::wstring GPS_LATITUDE = L"/app1/ifd/gps/{ushort=2}"; // GPSLatitude + const std::wstring GPS_LATITUDE_REF = L"/app1/ifd/gps/{ushort=1}"; // GPSLatitudeRef + const std::wstring GPS_LONGITUDE = L"/app1/ifd/gps/{ushort=4}"; // GPSLongitude + const std::wstring GPS_LONGITUDE_REF = L"/app1/ifd/gps/{ushort=3}"; // GPSLongitudeRef + const std::wstring GPS_ALTITUDE = L"/app1/ifd/gps/{ushort=6}"; // GPSAltitude + const std::wstring GPS_ALTITUDE_REF = L"/app1/ifd/gps/{ushort=5}"; // GPSAltitudeRef + + + // Documentation: https://developer.adobe.com/xmp/docs/XMPNamespaces/xmp/ + // Based on actual WIC path format discovered through enumeration + // XMP Basic schema - xmp: namespace + const std::wstring XMP_CREATE_DATE = L"/xmp/xmp:CreateDate"; // XMP Create Date + const std::wstring XMP_MODIFY_DATE = L"/xmp/xmp:ModifyDate"; // XMP Modify Date + const std::wstring XMP_METADATA_DATE = L"/xmp/xmp:MetadataDate"; // XMP Metadata Date + const std::wstring XMP_CREATOR_TOOL = L"/xmp/xmp:CreatorTool"; // XMP Creator Tool + + // Dublin Core schema - dc: namespace + // Note: For language alternatives like title/description, we need to append /x-default + const std::wstring XMP_DC_TITLE = L"/xmp/dc:title/x-default"; // Title (default language) + const std::wstring XMP_DC_DESCRIPTION = L"/xmp/dc:description/x-default"; // Description (default language) + const std::wstring XMP_DC_CREATOR = L"/xmp/dc:creator"; // Creator/Author + const std::wstring XMP_DC_SUBJECT = L"/xmp/dc:subject"; // Subject/Keywords (array) + + // XMP Rights Management schema - xmpRights: namespace + const std::wstring XMP_RIGHTS = L"/xmp/xmpRights:WebStatement"; // Copyright/Rights + + // XMP Media Management schema - xmpMM: namespace + const std::wstring XMP_MM_DOCUMENT_ID = L"/xmp/xmpMM:DocumentID"; // Document ID + const std::wstring XMP_MM_INSTANCE_ID = L"/xmp/xmpMM:InstanceID"; // Instance ID + const std::wstring XMP_MM_ORIGINAL_DOCUMENT_ID = L"/xmp/xmpMM:OriginalDocumentID"; // Original Document ID + const std::wstring XMP_MM_VERSION_ID = L"/xmp/xmpMM:VersionID"; // Version ID + + + std::wstring TrimWhitespace(const std::wstring& value) + { + const auto first = value.find_first_not_of(L" \t\r\n"); + if (first == std::wstring::npos) + { + return {}; + } + + const auto last = value.find_last_not_of(L" \t\r\n"); + return value.substr(first, last - first + 1); + } + + bool TryParseFixedWidthInt(const std::wstring& source, size_t start, size_t length, int& value) + { + if (start + length > source.size()) + { + return false; + } + + int result = 0; + for (size_t i = 0; i < length; ++i) + { + const wchar_t ch = source[start + i]; + if (ch < L'0' || ch > L'9') + { + return false; + } + + result = result * 10 + static_cast(ch - L'0'); + } + + value = result; + return true; + } + + bool ValidateAndBuildSystemTime(int year, int month, int day, int hour, int minute, int second, int milliseconds, SYSTEMTIME& outTime) + { + if (year < 1601 || year > 9999 || + month < 1 || month > 12 || + day < 1 || day > 31 || + hour < 0 || hour > 23 || + minute < 0 || minute > 59 || + second < 0 || second > 59 || + milliseconds < 0 || milliseconds > 999) + { + return false; + } + + SYSTEMTIME candidate{}; + candidate.wYear = static_cast(year); + candidate.wMonth = static_cast(month); + candidate.wDay = static_cast(day); + candidate.wHour = static_cast(hour); + candidate.wMinute = static_cast(minute); + candidate.wSecond = static_cast(second); + candidate.wMilliseconds = static_cast(milliseconds); + + FILETIME fileTime{}; + if (!SystemTimeToFileTime(&candidate, &fileTime)) + { + return false; + } + + outTime = candidate; + return true; + } + + std::optional ParseExifDateTime(const std::wstring& date) + { + if (date.size() < 19) + { + return std::nullopt; + } + + if (date[4] != L':' || date[7] != L':' || + (date[10] != L' ' && date[10] != L'T') || + date[13] != L':' || date[16] != L':') + { + return std::nullopt; + } + + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int second = 0; + + if (!TryParseFixedWidthInt(date, 0, 4, year) || + !TryParseFixedWidthInt(date, 5, 2, month) || + !TryParseFixedWidthInt(date, 8, 2, day) || + !TryParseFixedWidthInt(date, 11, 2, hour) || + !TryParseFixedWidthInt(date, 14, 2, minute) || + !TryParseFixedWidthInt(date, 17, 2, second)) + { + return std::nullopt; + } + + int milliseconds = 0; + size_t pos = 19; + if (pos < date.size() && (date[pos] == L'.' || date[pos] == L',')) + { + ++pos; + int digits = 0; + while (pos < date.size() && std::iswdigit(date[pos]) && digits < 3) + { + milliseconds = milliseconds * 10 + static_cast(date[pos] - L'0'); + ++pos; + ++digits; + } + + while (digits > 0 && digits < 3) + { + milliseconds *= 10; + ++digits; + } + } + + SYSTEMTIME result{}; + if (!ValidateAndBuildSystemTime(year, month, day, hour, minute, second, milliseconds, result)) + { + return std::nullopt; + } + + return result; + } + + std::optional ParseIso8601DateTime(const std::wstring& date) + { + if (date.size() < 19) + { + return std::nullopt; + } + + size_t separator = date.find(L'T'); + if (separator == std::wstring::npos) + { + separator = date.find(L' '); + } + + if (separator == std::wstring::npos) + { + return std::nullopt; + } + + int year = 0; + int month = 0; + int day = 0; + if (!TryParseFixedWidthInt(date, 0, 4, year) || + date[4] != L'-' || + !TryParseFixedWidthInt(date, 5, 2, month) || + date[7] != L'-' || + !TryParseFixedWidthInt(date, 8, 2, day)) + { + return std::nullopt; + } + + size_t timePos = separator + 1; + if (timePos + 7 >= date.size()) + { + return std::nullopt; + } + + int hour = 0; + int minute = 0; + int second = 0; + if (!TryParseFixedWidthInt(date, timePos, 2, hour) || + date[timePos + 2] != L':' || + !TryParseFixedWidthInt(date, timePos + 3, 2, minute) || + date[timePos + 5] != L':' || + !TryParseFixedWidthInt(date, timePos + 6, 2, second)) + { + return std::nullopt; + } + + size_t pos = timePos + 8; + int milliseconds = 0; + if (pos < date.size() && (date[pos] == L'.' || date[pos] == L',')) + { + ++pos; + int digits = 0; + while (pos < date.size() && std::iswdigit(date[pos]) && digits < 3) + { + milliseconds = milliseconds * 10 + static_cast(date[pos] - L'0'); + ++pos; + ++digits; + } + + while (pos < date.size() && std::iswdigit(date[pos])) + { + ++pos; + } + + while (digits > 0 && digits < 3) + { + milliseconds *= 10; + ++digits; + } + } + + bool hasOffset = false; + int offsetMinutes = 0; + if (pos < date.size()) + { + const wchar_t tzIndicator = date[pos]; + if (tzIndicator == L'Z' || tzIndicator == L'z') + { + hasOffset = true; + offsetMinutes = 0; + ++pos; + } + else if (tzIndicator == L'+' || tzIndicator == L'-') + { + hasOffset = true; + const int sign = (tzIndicator == L'-') ? -1 : 1; + ++pos; + + int offsetHours = 0; + int offsetMins = 0; + if (!TryParseFixedWidthInt(date, pos, 2, offsetHours)) + { + return std::nullopt; + } + pos += 2; + + if (pos < date.size() && date[pos] == L':') + { + ++pos; + } + + if (pos + 1 < date.size() && std::iswdigit(date[pos]) && std::iswdigit(date[pos + 1])) + { + if (!TryParseFixedWidthInt(date, pos, 2, offsetMins)) + { + return std::nullopt; + } + pos += 2; + } + + if (offsetHours < 0 || offsetHours > 23 || offsetMins < 0 || offsetMins > 59) + { + return std::nullopt; + } + + offsetMinutes = sign * (offsetHours * 60 + offsetMins); + } + + while (pos < date.size() && std::iswspace(date[pos])) + { + ++pos; + } + + if (pos != date.size()) + { + return std::nullopt; + } + } + + SYSTEMTIME baseTime{}; + if (!ValidateAndBuildSystemTime(year, month, day, hour, minute, second, milliseconds, baseTime)) + { + return std::nullopt; + } + + if (!hasOffset) + { + return baseTime; + } + + FILETIME utcFileTime{}; + if (!SystemTimeToFileTime(&baseTime, &utcFileTime)) + { + return std::nullopt; + } + + ULARGE_INTEGER timeValue{}; + timeValue.LowPart = utcFileTime.dwLowDateTime; + timeValue.HighPart = utcFileTime.dwHighDateTime; + + constexpr long long TicksPerMinute = 60LL * 10000000LL; + timeValue.QuadPart -= static_cast(offsetMinutes) * TicksPerMinute; + + FILETIME adjustedUtc{}; + adjustedUtc.dwLowDateTime = timeValue.LowPart; + adjustedUtc.dwHighDateTime = timeValue.HighPart; + + FILETIME localFileTime{}; + if (!FileTimeToLocalFileTime(&adjustedUtc, &localFileTime)) + { + return std::nullopt; + } + + SYSTEMTIME localTime{}; + if (!FileTimeToSystemTime(&localFileTime, &localTime)) + { + return std::nullopt; + } + + return localTime; + } +// Global WIC factory management with thread-safe access + CComPtr g_wicFactory; + std::once_flag g_wicInitFlag; + std::mutex g_wicFactoryMutex; // Protect access to g_wicFactory +} + +WICMetadataExtractor::WICMetadataExtractor() +{ + InitializeWIC(); +} + +WICMetadataExtractor::~WICMetadataExtractor() +{ + // WIC cleanup handled statically +} + +void WICMetadataExtractor::InitializeWIC() +{ + std::call_once(g_wicInitFlag, []() { + // Don't initialize COM in library code - assume caller has done it + // Just create the WIC factory + HRESULT hr = CoCreateInstance( + CLSID_WICImagingFactory, + nullptr, + CLSCTX_INPROC_SERVER, + IID_IWICImagingFactory, + reinterpret_cast(&g_wicFactory) + ); + + if (FAILED(hr)) + { + g_wicFactory = nullptr; + } + }); +} + +CComPtr WICMetadataExtractor::GetWICFactory() +{ + std::lock_guard lock(g_wicFactoryMutex); + return g_wicFactory; +} + +bool WICMetadataExtractor::ExtractEXIFMetadata( + const std::wstring& filePath, + EXIFMetadata& outMetadata) +{ + return cache.GetOrLoadEXIF(filePath, outMetadata, [this, &filePath](EXIFMetadata& metadata) { + return LoadEXIFMetadata(filePath, metadata); + }); +} + +bool WICMetadataExtractor::LoadEXIFMetadata( + const std::wstring& filePath, + EXIFMetadata& outMetadata) +{ + CComPtr reader; + + if (!PathFileExistsW(filePath.c_str())) + { +#ifdef _DEBUG + std::wstring msg = L"[PowerRename] EXIF metadata extraction failed: File not found - " + filePath + L"\n"; + OutputDebugStringW(msg.c_str()); +#endif + return false; + } + + auto decoder = CreateDecoder(filePath); + if (!decoder) + { +#ifdef _DEBUG + std::wstring msg = L"[PowerRename] EXIF metadata extraction: Unsupported format or unable to create decoder - " + filePath + L"\n"; + OutputDebugStringW(msg.c_str()); +#endif + return false; + } + + CComPtr frame; + if (FAILED(decoder->GetFrame(0, &frame))) + { +#ifdef _DEBUG + std::wstring msg = L"[PowerRename] EXIF metadata extraction failed: WIC decoder error - " + filePath + L"\n"; + OutputDebugStringW(msg.c_str()); +#endif + return false; + } + + reader = GetMetadataReader(decoder); + if (!reader) + { + // No metadata is not necessarily an error - just means the file has no EXIF data + return false; + } + + ExtractAllEXIFFields(reader, outMetadata); + ExtractGPSData(reader, outMetadata); + + return true; +} + +void WICMetadataExtractor::ClearCache() +{ + cache.ClearAll(); +} + +CComPtr WICMetadataExtractor::CreateDecoder(const std::wstring& filePath) +{ + auto factory = GetWICFactory(); + if (!factory) + { + return nullptr; + } + + CComPtr decoder; + HRESULT hr = factory->CreateDecoderFromFilename( + filePath.c_str(), + nullptr, + GENERIC_READ, + WICDecodeMetadataCacheOnLoad, + &decoder + ); + + if (FAILED(hr)) + { + return nullptr; + } + + return decoder; +} + +CComPtr WICMetadataExtractor::GetMetadataReader(IWICBitmapDecoder* decoder) +{ + if (!decoder) + { + return nullptr; + } + + CComPtr frame; + if (FAILED(decoder->GetFrame(0, &frame))) + { + return nullptr; + } + + CComPtr reader; + frame->GetMetadataQueryReader(&reader); + + return reader; +} + +void WICMetadataExtractor::ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata) +{ + if (!reader) + return; + + // Extract date/time fields + metadata.dateTaken = ReadDateTime(reader, EXIF_DATE_TAKEN); + metadata.dateDigitized = ReadDateTime(reader, EXIF_DATE_DIGITIZED); + metadata.dateModified = ReadDateTime(reader, EXIF_DATE_MODIFIED); + + // Extract camera information + metadata.cameraMake = ReadString(reader, EXIF_CAMERA_MAKE); + metadata.cameraModel = ReadString(reader, EXIF_CAMERA_MODEL); + metadata.lensModel = ReadString(reader, EXIF_LENS_MODEL); + + // Extract shooting parameters + metadata.iso = ReadInteger(reader, EXIF_ISO); + metadata.aperture = ReadDouble(reader, EXIF_APERTURE); + metadata.shutterSpeed = ReadDouble(reader, EXIF_SHUTTER_SPEED); + metadata.focalLength = ReadDouble(reader, EXIF_FOCAL_LENGTH); + metadata.exposureBias = ReadDouble(reader, EXIF_EXPOSURE_BIAS); + metadata.flash = ReadInteger(reader, EXIF_FLASH); + + // Extract image properties + metadata.width = ReadInteger(reader, EXIF_WIDTH); + metadata.height = ReadInteger(reader, EXIF_HEIGHT); + metadata.orientation = ReadInteger(reader, EXIF_ORIENTATION); + metadata.colorSpace = ReadInteger(reader, EXIF_COLOR_SPACE); + + // Extract author information + metadata.author = ReadString(reader, EXIF_ARTIST); + metadata.copyright = ReadString(reader, EXIF_COPYRIGHT); +} + +void WICMetadataExtractor::ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata) +{ + if (!reader) + { + return; + } + + auto lat = ReadMetadata(reader, GPS_LATITUDE); + auto lon = ReadMetadata(reader, GPS_LONGITUDE); + auto latRef = ReadMetadata(reader, GPS_LATITUDE_REF); + auto lonRef = ReadMetadata(reader, GPS_LONGITUDE_REF); + + if (lat && lon) + { + PropVariantValue emptyLatRef; + PropVariantValue emptyLonRef; + + const PROPVARIANT& latRefVar = latRef ? latRef->Get() : emptyLatRef.Get(); + const PROPVARIANT& lonRefVar = lonRef ? lonRef->Get() : emptyLonRef.Get(); + + auto coords = MetadataFormatHelper::ParseGPSCoordinates( + lat->Get(), + lon->Get(), + latRefVar, + lonRefVar); + + metadata.latitude = coords.first; + metadata.longitude = coords.second; + } + + auto alt = ReadMetadata(reader, GPS_ALTITUDE); + if (alt) + { + metadata.altitude = MetadataFormatHelper::ParseGPSRational(alt->Get()); + } +} + + +std::optional WICMetadataExtractor::ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path) +{ + auto propVar = ReadMetadata(reader, path); + if (!propVar) + { + return std::nullopt; + } + + std::wstring rawValue; + const PROPVARIANT& variant = propVar->Get(); + + switch (variant.vt) + { + case VT_LPWSTR: + if (variant.pwszVal) + { + rawValue = variant.pwszVal; + } + break; + case VT_BSTR: + if (variant.bstrVal) + { + rawValue = variant.bstrVal; + } + break; + case VT_LPSTR: + if (variant.pszVal) + { + const int size = MultiByteToWideChar(CP_UTF8, 0, variant.pszVal, -1, nullptr, 0); + if (size > 1) + { + rawValue.resize(static_cast(size) - 1); + MultiByteToWideChar(CP_UTF8, 0, variant.pszVal, -1, &rawValue[0], size); + } + } + break; + default: + break; + } + + if (rawValue.empty()) + { + return std::nullopt; + } + + const std::wstring normalized = TrimWhitespace(rawValue); + if (normalized.empty()) + { + return std::nullopt; + } + + if (auto exifDate = ParseExifDateTime(normalized)) + { + return exifDate; + } + + if (auto isoDate = ParseIso8601DateTime(normalized)) + { + return isoDate; + } + + return std::nullopt; +} + +std::optional WICMetadataExtractor::ReadString(IWICMetadataQueryReader* reader, const std::wstring& path) +{ + auto propVar = ReadMetadata(reader, path); + if (!propVar.has_value()) + return std::nullopt; + + std::wstring result; + switch (propVar->Get().vt) + { + case VT_LPWSTR: + if (propVar->Get().pwszVal) + result = propVar->Get().pwszVal; + break; + case VT_BSTR: + if (propVar->Get().bstrVal) + result = propVar->Get().bstrVal; + break; + case VT_LPSTR: + if (propVar->Get().pszVal) + { + int size = MultiByteToWideChar(CP_UTF8, 0, propVar->Get().pszVal, -1, nullptr, 0); + if (size > 1) + { + result.resize(static_cast(size) - 1); + MultiByteToWideChar(CP_UTF8, 0, propVar->Get().pszVal, -1, &result[0], size); + } + } + break; + } + + + // Trim whitespace from both ends + if (!result.empty()) + { + size_t start = result.find_first_not_of(L" \t\r\n"); + size_t end = result.find_last_not_of(L" \t\r\n"); + if (start != std::wstring::npos && end != std::wstring::npos) + { + result = result.substr(start, end - start + 1); + } + else if (start == std::wstring::npos) + { + result.clear(); + } + } + + return result.empty() ? std::nullopt : std::make_optional(result); +} + +std::optional WICMetadataExtractor::ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path) +{ + auto propVar = ReadMetadata(reader, path); + if (!propVar.has_value()) + return std::nullopt; + + int64_t result = 0; + switch (propVar->Get().vt) + { + case VT_I1: result = propVar->Get().cVal; break; + case VT_I2: result = propVar->Get().iVal; break; + case VT_I4: result = propVar->Get().lVal; break; + case VT_I8: result = propVar->Get().hVal.QuadPart; break; + case VT_UI1: result = propVar->Get().bVal; break; + case VT_UI2: result = propVar->Get().uiVal; break; + case VT_UI4: result = propVar->Get().ulVal; break; + case VT_UI8: result = static_cast(propVar->Get().uhVal.QuadPart); break; + default: + return std::nullopt; + } + + return result; +} + +std::optional WICMetadataExtractor::ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path) +{ + auto propVar = ReadMetadata(reader, path); + if (!propVar.has_value()) + return std::nullopt; + + double result = 0.0; + switch (propVar->Get().vt) + { + case VT_R4: + result = static_cast(propVar->Get().fltVal); + break; + case VT_R8: + result = propVar->Get().dblVal; + break; + case VT_UI1 | VT_VECTOR: + case VT_UI4 | VT_VECTOR: + // Handle rational number (common for EXIF values) + // Rational data is stored as 8 bytes: 4-byte numerator + 4-byte denominator + if (propVar->Get().caub.cElems >= 8) + { + // ExposureBias (EXIF tag 37380) uses SRATIONAL type (signed rational) + // which can represent negative values like -0.33 EV for exposure compensation. + // Most other EXIF fields use RATIONAL type (unsigned) for values like aperture, shutter speed. + if (path == EXIF_EXPOSURE_BIAS) + { + // Parse as signed rational: int32_t / int32_t + result = MetadataFormatHelper::ParseSingleSRational(propVar->Get().caub.pElems, 0); + break; + } + else + { + // Parse as unsigned rational: uint32_t / uint32_t + // First check if denominator is valid (non-zero) to avoid division by zero + const uint8_t* bytes = propVar->Get().caub.pElems; + uint32_t denominator = static_cast(bytes[4]) | + (static_cast(bytes[5]) << 8) | + (static_cast(bytes[6]) << 16) | + (static_cast(bytes[7]) << 24); + + if (denominator != 0) + { + result = MetadataFormatHelper::ParseSingleRational(propVar->Get().caub.pElems, 0); + break; + } + } + } + return std::nullopt; + default: + // Try integer conversion + switch (propVar->Get().vt) + { + case VT_I1: result = static_cast(propVar->Get().cVal); break; + case VT_I2: result = static_cast(propVar->Get().iVal); break; + case VT_I4: result = static_cast(propVar->Get().lVal); break; + case VT_I8: + { + // ExposureBias (EXIF tag 37380) may be stored as VT_I8 in some WIC implementations + // It represents a signed rational (SRATIONAL) packed into a 64-bit integer + if (path == EXIF_EXPOSURE_BIAS) + { + // Parse signed rational from int64: low 32 bits = numerator, high 32 bits = denominator + // Some implementations may reverse the order, so we try both + int32_t numerator = static_cast(propVar->Get().hVal.QuadPart & 0xFFFFFFFF); + int32_t denominator = static_cast(propVar->Get().hVal.QuadPart >> 32); + if (denominator != 0) + { + result = static_cast(numerator) / static_cast(denominator); + } + else + { + // Try reversed order: high 32 bits = numerator, low 32 bits = denominator + numerator = static_cast(propVar->Get().hVal.QuadPart >> 32); + denominator = static_cast(propVar->Get().hVal.QuadPart & 0xFFFFFFFF); + if (denominator != 0) + { + result = static_cast(numerator) / static_cast(denominator); + } + else + { + result = 0.0; // Default to 0 if both attempts fail + } + } + } + else + { + // For other fields, treat VT_I8 as a simple 64-bit integer + result = static_cast(propVar->Get().hVal.QuadPart); + } + } + break; + case VT_UI1: result = static_cast(propVar->Get().bVal); break; + case VT_UI2: result = static_cast(propVar->Get().uiVal); break; + case VT_UI4: result = static_cast(propVar->Get().ulVal); break; + case VT_UI8: + { + // ExposureBias (EXIF tag 37380) may be stored as VT_UI8 in some WIC implementations + // Even though it's unsigned, we need to reinterpret it as signed for SRATIONAL + if (path == EXIF_EXPOSURE_BIAS) + { + // Parse signed rational from uint64 (reinterpret as signed) + // Low 32 bits = numerator, high 32 bits = denominator + int32_t numerator = static_cast(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF); + int32_t denominator = static_cast(propVar->Get().uhVal.QuadPart >> 32); + if (denominator != 0) + { + result = static_cast(numerator) / static_cast(denominator); + } + else + { + // Try reversed order: high 32 bits = numerator, low 32 bits = denominator + numerator = static_cast(propVar->Get().uhVal.QuadPart >> 32); + denominator = static_cast(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF); + if (denominator != 0) + { + result = static_cast(numerator) / static_cast(denominator); + } + else + { + result = 0.0; // Default to 0 if both attempts fail + } + } + } + else + { + // For other EXIF rational fields (unsigned), try both byte orders to handle different encodings + // First try: low 32 bits = numerator, high 32 bits = denominator + uint32_t numerator = static_cast(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF); + uint32_t denominator = static_cast(propVar->Get().uhVal.QuadPart >> 32); + + if (denominator != 0) + { + result = static_cast(numerator) / static_cast(denominator); + } + else + { + // Second try: high 32 bits = numerator, low 32 bits = denominator + numerator = static_cast(propVar->Get().uhVal.QuadPart >> 32); + denominator = static_cast(propVar->Get().uhVal.QuadPart & 0xFFFFFFFF); + if (denominator != 0) + { + result = static_cast(numerator) / static_cast(denominator); + } + else + { + // Fall back to treating as regular integer if denominator is 0 + result = static_cast(propVar->Get().uhVal.QuadPart); + } + } + } + } + break; + default: + return std::nullopt; + } + } + + return result; +} + +std::optional WICMetadataExtractor::ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path) +{ + if (!reader) + return std::nullopt; + + PropVariantValue value; + + HRESULT hr = reader->GetMetadataByName(path.c_str(), value.GetAddressOf()); + if (SUCCEEDED(hr)) + { + return std::optional(std::move(value)); + } + + return std::nullopt; +} + +// GPS parsing functions have been moved to MetadataFormatHelper for better testability + +bool WICMetadataExtractor::ExtractXMPMetadata( + const std::wstring& filePath, + XMPMetadata& outMetadata) +{ + return cache.GetOrLoadXMP(filePath, outMetadata, [this, &filePath](XMPMetadata& metadata) { + return LoadXMPMetadata(filePath, metadata); + }); +} + +bool WICMetadataExtractor::LoadXMPMetadata( + const std::wstring& filePath, + XMPMetadata& outMetadata) +{ + if (!PathFileExistsW(filePath.c_str())) + { +#ifdef _DEBUG + std::wstring msg = L"[PowerRename] XMP metadata extraction failed: File not found - " + filePath + L"\n"; + OutputDebugStringW(msg.c_str()); +#endif + return false; + } + + auto decoder = CreateDecoder(filePath); + if (!decoder) + { +#ifdef _DEBUG + std::wstring msg = L"[PowerRename] XMP metadata extraction: Unsupported format or unable to create decoder - " + filePath + L"\n"; + OutputDebugStringW(msg.c_str()); +#endif + return false; + } + + CComPtr frame; + if (FAILED(decoder->GetFrame(0, &frame))) + { +#ifdef _DEBUG + std::wstring msg = L"[PowerRename] XMP metadata extraction failed: WIC decoder error - " + filePath + L"\n"; + OutputDebugStringW(msg.c_str()); +#endif + return false; + } + + CComPtr rootReader; + if (FAILED(frame->GetMetadataQueryReader(&rootReader))) + { + // No metadata is not necessarily an error - just means the file has no XMP data + return false; + } + + ExtractAllXMPFields(rootReader, outMetadata); + + return true; +} + +// Batch extraction method implementations +void WICMetadataExtractor::ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata) +{ + if (!reader) + return; + + // XMP Basic schema - xmp: namespace + metadata.creatorTool = ReadString(reader, XMP_CREATOR_TOOL); + metadata.createDate = ReadDateTime(reader, XMP_CREATE_DATE); + metadata.modifyDate = ReadDateTime(reader, XMP_MODIFY_DATE); + metadata.metadataDate = ReadDateTime(reader, XMP_METADATA_DATE); + + // Dublin Core schema - dc: namespace + metadata.title = ReadString(reader, XMP_DC_TITLE); + metadata.description = ReadString(reader, XMP_DC_DESCRIPTION); + metadata.creator = ReadString(reader, XMP_DC_CREATOR); + + // For dc:subject, we need to handle the array structure + // Try to read individual elements + // XMP allows for large arrays, but we limit to a reasonable number to avoid performance issues + constexpr int MAX_XMP_SUBJECTS = 50; + std::vector subjects; + for (int i = 0; i < MAX_XMP_SUBJECTS; ++i) + { + std::wstring subjectPath = L"/xmp/dc:subject/{ulong=" + std::to_wstring(i) + L"}"; + auto subject = ReadString(reader, subjectPath); + if (subject.has_value()) + { + subjects.push_back(subject.value()); + } + else + { + break; // No more subjects + } + } + if (!subjects.empty()) + { + metadata.subject = subjects; + } + + // XMP Rights Management schema + metadata.rights = ReadString(reader, XMP_RIGHTS); + + // XMP Media Management schema - xmpMM: namespace + metadata.documentID = ReadString(reader, XMP_MM_DOCUMENT_ID); + metadata.instanceID = ReadString(reader, XMP_MM_INSTANCE_ID); + metadata.originalDocumentID = ReadString(reader, XMP_MM_ORIGINAL_DOCUMENT_ID); + metadata.versionID = ReadString(reader, XMP_MM_VERSION_ID); +} + + + + + + + + + diff --git a/src/modules/powerrename/lib/WICMetadataExtractor.h b/src/modules/powerrename/lib/WICMetadataExtractor.h new file mode 100644 index 0000000000..868d18aa7c --- /dev/null +++ b/src/modules/powerrename/lib/WICMetadataExtractor.h @@ -0,0 +1,64 @@ +// 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. + +#pragma once +#include "MetadataTypes.h" +#include "MetadataResultCache.h" +#include "PropVariantValue.h" +#include +#include + +namespace PowerRenameLib +{ + /// + /// Windows Imaging Component (WIC) implementation for metadata extraction + /// Provides efficient batch extraction of all metadata types with built-in caching + /// + class WICMetadataExtractor + { + public: + WICMetadataExtractor(); + ~WICMetadataExtractor(); + + // Public metadata extraction methods + bool ExtractEXIFMetadata( + const std::wstring& filePath, + EXIFMetadata& outMetadata); + + bool ExtractXMPMetadata( + const std::wstring& filePath, + XMPMetadata& outMetadata); + + void ClearCache(); + + private: + // WIC factory management + static CComPtr GetWICFactory(); + static void InitializeWIC(); + + // WIC operations + CComPtr CreateDecoder(const std::wstring& filePath); + CComPtr GetMetadataReader(IWICBitmapDecoder* decoder); + + bool LoadEXIFMetadata(const std::wstring& filePath, EXIFMetadata& outMetadata); + bool LoadXMPMetadata(const std::wstring& filePath, XMPMetadata& outMetadata); + + // Batch extraction methods + void ExtractAllEXIFFields(IWICMetadataQueryReader* reader, EXIFMetadata& metadata); + void ExtractGPSData(IWICMetadataQueryReader* reader, EXIFMetadata& metadata); + void ExtractAllXMPFields(IWICMetadataQueryReader* reader, XMPMetadata& metadata); + + // Field reading helpers + std::optional ReadDateTime(IWICMetadataQueryReader* reader, const std::wstring& path); + std::optional ReadString(IWICMetadataQueryReader* reader, const std::wstring& path); + std::optional ReadInteger(IWICMetadataQueryReader* reader, const std::wstring& path); + std::optional ReadDouble(IWICMetadataQueryReader* reader, const std::wstring& path); + + // Helper methods + std::optional ReadMetadata(IWICMetadataQueryReader* reader, const std::wstring& path); + + private: + MetadataResultCache cache; + }; +} diff --git a/src/modules/powerrename/lib/pch.h b/src/modules/powerrename/lib/pch.h index c5a4711a03..2ee372ae61 100644 --- a/src/modules/powerrename/lib/pch.h +++ b/src/modules/powerrename/lib/pch.h @@ -28,5 +28,17 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include + +// Windows Imaging Component (WIC) headers +#include +#include +#include +#include diff --git a/src/modules/powerrename/unittests/HelpersTests.cpp b/src/modules/powerrename/unittests/HelpersTests.cpp new file mode 100644 index 0000000000..9a6d1b2028 --- /dev/null +++ b/src/modules/powerrename/unittests/HelpersTests.cpp @@ -0,0 +1,766 @@ +#include "pch.h" +#include "Helpers.h" +#include "MetadataPatternExtractor.h" +#include "MetadataTypes.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +namespace HelpersTests +{ + TEST_CLASS(GetMetadataFileNameTests) + { + public: + TEST_METHOD(BasicPatternReplacement) + { + // Test basic pattern replacement with available metadata + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + patterns[L"ISO"] = L"ISO 400"; + patterns[L"DATE_TAKEN_YYYY"] = L"2024"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_Canon_ISO 400", result); + } + + TEST_METHOD(PatternWithoutValueShowsPatternName) + { + // Test that patterns without values show the pattern name with $ prefix + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + // ISO is not in the map + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_Canon_$ISO", result); + } + + TEST_METHOD(EmptyPatternShowsPatternName) + { + // Test that patterns with empty value show the pattern name with $ prefix + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + patterns[L"ISO"] = L""; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$ISO", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_Canon_$ISO", result); + } + + TEST_METHOD(EscapedDollarSigns) + { + // Test that $$ is converted to single $ + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"ISO"] = L"ISO 400"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$_$ISO", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_$_ISO 400", result); + } + + TEST_METHOD(MultipleEscapedDollarSigns) + { + // Test that $$$$ is converted to $$ + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"ISO"] = L"ISO 400"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$$price", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_$$price", result); + } + + TEST_METHOD(OddDollarSignsWithPattern) + { + // Test that $$$ becomes $ followed by pattern + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"ISO"] = L"ISO 400"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$$$ISO", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_$ISO 400", result); + } + + TEST_METHOD(LongestPatternMatchPriority) + { + // Test that longer patterns are matched first (DATE_TAKEN_YYYY vs DATE_TAKEN_YY) + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"DATE_TAKEN_YYYY"] = L"2024"; + patterns[L"DATE_TAKEN_YY"] = L"24"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$DATE_TAKEN_YYYY", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_2024", result); + } + + TEST_METHOD(MultiplePatterns) + { + // Test multiple patterns in one string + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + patterns[L"CAMERA_MODEL"] = L"EOS R5"; + patterns[L"ISO"] = L"ISO 800"; + patterns[L"DATE_TAKEN_YYYY"] = L"2024"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, + L"$DATE_TAKEN_YYYY-$CAMERA_MAKE-$CAMERA_MODEL-$ISO", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"2024-Canon-EOS R5-ISO 800", result); + } + + TEST_METHOD(UnrecognizedPatternIgnored) + { + // Test that unrecognized patterns are not replaced + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE_$INVALID_PATTERN", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_Canon_$INVALID_PATTERN", result); + } + + TEST_METHOD(NoPatterns) + { + // Test string with no patterns + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_name_without_patterns", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_name_without_patterns", result); + } + + TEST_METHOD(EmptyInput) + { + // Test with empty input string + PowerRenameLib::MetadataPatternMap patterns; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"", patterns); + + Assert::IsTrue(FAILED(hr)); + } + + TEST_METHOD(NullInput) + { + // Test with null input + PowerRenameLib::MetadataPatternMap patterns; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, nullptr, patterns); + + Assert::IsTrue(FAILED(hr)); + } + + TEST_METHOD(DollarAtEnd) + { + // Test dollar sign at the end of string + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"ISO"] = L"ISO 400"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO$", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_ISO 400$", result); + } + + TEST_METHOD(ThreeDollarsAtEnd) + { + // Test three dollar signs at the end + PowerRenameLib::MetadataPatternMap patterns; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo$$$", result); + } + + TEST_METHOD(ComplexMixedScenario) + { + // Test complex scenario with mixed patterns, escapes, and regular text + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + patterns[L"ISO"] = L"ISO 400"; + patterns[L"APERTURE"] = L"f/2.8"; + patterns[L"LENS"] = L""; // Empty value + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, + L"$$price_$CAMERA_MAKE_$$$ISO_$APERTURE_$LENS_$$end", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"$price_Canon_$ISO 400_f/2.8_$LENS_$end", result); + } + + TEST_METHOD(AllEXIFPatterns) + { + // Test with various EXIF patterns + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"WIDTH"] = L"4000"; + patterns[L"HEIGHT"] = L"3000"; + patterns[L"FOCAL"] = L"50mm"; + patterns[L"SHUTTER"] = L"1/100s"; + patterns[L"FLASH"] = L"Flash Off"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, + L"photo_$WIDTH x $HEIGHT_$FOCAL_$SHUTTER_$FLASH", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_4000 x 3000_50mm_1/100s_Flash Off", result); + } + + TEST_METHOD(AllXMPPatterns) + { + // Test with various XMP patterns + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"TITLE"] = L"Sunset"; + patterns[L"CREATOR"] = L"John Doe"; + patterns[L"DESCRIPTION"] = L"Beautiful sunset"; + patterns[L"CREATE_DATE_YYYY"] = L"2024"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, + L"$CREATE_DATE_YYYY-$TITLE-by-$CREATOR", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"2024-Sunset-by-John Doe", result); + } + + TEST_METHOD(DateComponentPatterns) + { + // Test date component patterns + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"DATE_TAKEN_YYYY"] = L"2024"; + patterns[L"DATE_TAKEN_MM"] = L"03"; + patterns[L"DATE_TAKEN_DD"] = L"15"; + patterns[L"DATE_TAKEN_HH"] = L"14"; + patterns[L"DATE_TAKEN_mm"] = L"30"; + patterns[L"DATE_TAKEN_SS"] = L"45"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, + L"photo_$DATE_TAKEN_YYYY-$DATE_TAKEN_MM-$DATE_TAKEN_DD_$DATE_TAKEN_HH-$DATE_TAKEN_mm-$DATE_TAKEN_SS", + patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_2024-03-15_14-30-45", result); + } + + TEST_METHOD(SpecialCharactersInValues) + { + // Test that special characters in metadata values are preserved + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"TITLE"] = L"Photo (with) [brackets] & symbols!"; + patterns[L"DESCRIPTION"] = L"Test: value; with, punctuation."; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, + L"$TITLE - $DESCRIPTION", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"Photo (with) [brackets] & symbols! - Test: value; with, punctuation.", result); + } + + TEST_METHOD(ConsecutivePatternsWithoutSeparator) + { + // Test consecutive patterns without separator + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + patterns[L"CAMERA_MODEL"] = L"R5"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE$CAMERA_MODEL", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"CanonR5", result); + } + + TEST_METHOD(PatternAtStart) + { + // Test pattern at the beginning of string + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE_photo", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"Canon_photo", result); + } + + TEST_METHOD(PatternAtEnd) + { + // Test pattern at the end of string + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$CAMERA_MAKE", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo_Canon", result); + } + + TEST_METHOD(OnlyPattern) + { + // Test string with only a pattern + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MAKE", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"Canon", result); + } + }; + + TEST_CLASS(PatternMatchingTests) + { + public: + TEST_METHOD(VerifyLongestPatternMatching) + { + // This test verifies the greedy matching behavior + // When we have overlapping pattern names, the longest should be matched first + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"DATE_TAKEN_Y"] = L"4"; + patterns[L"DATE_TAKEN_YY"] = L"24"; + patterns[L"DATE_TAKEN_YYYY"] = L"2024"; + + wchar_t result[MAX_PATH] = { 0 }; + + // Should match YYYY (longest) + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YYYY", patterns); + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"2024", result); + + // Should match YY (available pattern) + hr = GetMetadataFileName(result, MAX_PATH, L"$DATE_TAKEN_YY", patterns); + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"24", result); + } + + TEST_METHOD(PartialPatternNames) + { + // Test that partial pattern names don't match longer patterns + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MODEL"] = L"EOS R5"; + + wchar_t result[MAX_PATH] = { 0 }; + // CAMERA is not a valid pattern, should not match + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$CAMERA_MODEL", patterns); + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"EOS R5", result); + } + + TEST_METHOD(CaseSensitivePatterns) + { + // Test that pattern names are case-sensitive + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + + wchar_t result[MAX_PATH] = { 0 }; + // lowercase should not match + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$camera_make", patterns); + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"$camera_make", result); // Not replaced + } + + TEST_METHOD(EmptyPatternMap) + { + // Test with empty pattern map + PowerRenameLib::MetadataPatternMap patterns; // Empty + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo_$ISO_$CAMERA_MAKE", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + // Patterns should show with $ prefix since they're valid but have no values + Assert::AreEqual(L"photo_$ISO_$CAMERA_MAKE", result); + } + }; + + TEST_CLASS(EdgeCaseTests) + { + public: + TEST_METHOD(VeryLongString) + { + // Test with a very long input string + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"CAMERA_MAKE"] = L"Canon"; + + std::wstring longInput = L"prefix_"; + for (int i = 0; i < 100; i++) + { + longInput += L"$CAMERA_MAKE_"; + } + + wchar_t result[4096] = { 0 }; + HRESULT hr = GetMetadataFileName(result, 4096, longInput.c_str(), patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + // Verify it starts correctly + Assert::IsTrue(wcsstr(result, L"prefix_Canon_") == result); + } + + TEST_METHOD(ManyConsecutiveDollars) + { + // Test with many consecutive dollar signs + PowerRenameLib::MetadataPatternMap patterns; + + wchar_t result[MAX_PATH] = { 0 }; + // 8 dollars should become 4 dollars + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"photo$$$$$$$$name", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"photo$$$$name", result); + } + + TEST_METHOD(OnlyDollars) + { + // Test string with only dollar signs + PowerRenameLib::MetadataPatternMap patterns; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$$$$", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"$$", result); + } + + TEST_METHOD(UnicodeCharacters) + { + // Test with unicode characters in pattern values + PowerRenameLib::MetadataPatternMap patterns; + patterns[L"TITLE"] = L"照片_фото_φωτογÏαφία"; + patterns[L"CREATOR"] = L"张三_Иван_Γιάννης"; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"$TITLE-$CREATOR", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"照片_фото_φωτογÏαφία-张三_Иван_Γιάννης", result); + } + + TEST_METHOD(SingleDollar) + { + // Test with single dollar not followed by pattern + PowerRenameLib::MetadataPatternMap patterns; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"price$100", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"price$100", result); + } + + TEST_METHOD(DollarFollowedByNumber) + { + // Test dollar followed by numbers (not a pattern) + PowerRenameLib::MetadataPatternMap patterns; + + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetMetadataFileName(result, MAX_PATH, L"cost_$123.45", patterns); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"cost_$123.45", result); + } + }; + + TEST_CLASS(GetDatedFileNameTests) + { + public: + // Helper to get a fixed test time for consistent testing + SYSTEMTIME GetTestTime() + { + SYSTEMTIME testTime = { 0 }; + testTime.wYear = 2024; + testTime.wMonth = 3; // March + testTime.wDay = 15; // 15th + testTime.wHour = 14; // 2 PM (24-hour format) + testTime.wMinute = 30; + testTime.wSecond = 45; + testTime.wMilliseconds = 123; + testTime.wDayOfWeek = 5; // Friday (0=Sunday, 5=Friday) + return testTime; + } + + // Category 1: Tests for invalid patterns with extra characters (verify negative lookahead prevents wrong matching) + + TEST_METHOD(InvalidPattern_YYY_NotMatched) + { + // Test $YYY (3 Y's) is not a valid pattern and should remain unchanged + // Negative lookahead in $YY(?!Y) prevents matching $YYY + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYY", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_$YYY", result); // $YYY is invalid, should remain unchanged + } + + TEST_METHOD(InvalidPattern_DDD_NotPartiallyMatched) + { + // Test that $DDD (short weekday) is not confused with $DD (2-digit day) + // This verifies negative lookahead works correctly + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_Fri", result); // Should be "Fri", not "15D" + } + + TEST_METHOD(InvalidPattern_MMM_NotPartiallyMatched) + { + // Test that $MMM (short month name) is not confused with $MM (2-digit month) + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "03M" + } + + TEST_METHOD(InvalidPattern_HHH_NotMatched) + { + // Test $HHH (3 H's) is not valid and negative lookahead prevents $HH from matching + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HHH", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_$HHH", result); // Should remain unchanged + } + + TEST_METHOD(SeparatedPatterns_SingleY) + { + // Test multiple $Y with separators works correctly + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y-$Y-$Y", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_4-4-4", result); // Each $Y outputs "4" (from 2024) + } + + TEST_METHOD(SeparatedPatterns_SingleD) + { + // Test multiple $D with separators works correctly + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$D.$D.$D", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_15.15.15", result); // Each $D outputs "15" + } + + // Category 2: Tests for mixed length patterns (verify longer patterns don't get matched incorrectly) + + TEST_METHOD(MixedLengthYear_QuadFollowedBySingle) + { + // Test $YYYY$Y - should be 2024 + 4 + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY$Y", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_20244", result); + } + + TEST_METHOD(MixedLengthDay_TripleFollowedBySingle) + { + // Test $DDD$D - should be "Fri" + "15" + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDD$D", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_Fri15", result); + } + + TEST_METHOD(MixedLengthDay_QuadFollowedByDouble) + { + // Test $DDDD$DD - should be "Friday" + "15" + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD$DD", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_Friday15", result); + } + + TEST_METHOD(MixedLengthMonth_TripleFollowedBySingle) + { + // Test $MMM$M - should be "Mar" + "3" + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM$M", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_Mar3", result); + } + + // Category 3: Tests for boundary conditions (patterns at start, end, with special chars) + + TEST_METHOD(PatternAtStart) + { + // Test pattern at the very start of filename + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY$M$D", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"2024315", result); + } + + TEST_METHOD(PatternAtEnd) + { + // Test pattern at the very end of filename + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$Y", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_4", result); + } + + TEST_METHOD(PatternWithSpecialChars) + { + // Test patterns surrounded by special characters + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file-$Y.$Y-$M", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file-4.4-3", result); + } + + TEST_METHOD(EmptyFileName) + { + // Test with empty input string - should return E_INVALIDARG + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"", testTime); + + Assert::IsTrue(FAILED(hr)); // Empty string should fail + Assert::AreEqual(E_INVALIDARG, hr); + } + + // Category 4: Tests to explicitly verify negative lookahead is working + + TEST_METHOD(NegativeLookahead_YearNotMatchedInYYYY) + { + // Verify $Y doesn't match when part of $YYYY + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$YYYY", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_2024", result); // Should be "2024", not "202Y" + } + + TEST_METHOD(NegativeLookahead_MonthNotMatchedInMMM) + { + // Verify $M doesn't match when part of $MMM + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$MMM", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_Mar", result); // Should be "Mar", not "3ar" + } + + TEST_METHOD(NegativeLookahead_DayNotMatchedInDDDD) + { + // Verify $D doesn't match when part of $DDDD + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$DDDD", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_Friday", result); // Should be "Friday", not "15riday" + } + + TEST_METHOD(NegativeLookahead_HourNotMatchedInHH) + { + // Verify $H doesn't match when part of $HH + // Note: $HH is 12-hour format, so 14:00 (2 PM) displays as "02" + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$HH", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_02", result); // 14:00 in 12-hour format is "02 PM" + } + + TEST_METHOD(NegativeLookahead_MillisecondNotMatchedInFFF) + { + // Verify $f doesn't match when part of $fff + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"file_$fff", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"file_123", result); // Should be "123", not "1ff" + } + + // Category 5: Complex mixed scenarios + + TEST_METHOD(ComplexMixedPattern_AllFormats) + { + // Test a complex realistic filename with mixed pattern lengths + // Note: Using $hh for 24-hour format instead of $HH (which is 12-hour) + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"Photo_$YYYY-$MM-$DD_$hh-$mm-$ss_$fff", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"Photo_2024-03-15_14-30-45_123", result); + } + + TEST_METHOD(ComplexMixedPattern_WithSeparators) + { + // Test multiple patterns of different lengths with separators + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$YYYY_$Y-$Y_$MM_$M", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"2024_4-4_03_3", result); + } + + TEST_METHOD(ComplexMixedPattern_DayFormats) + { + // Test all day format variations in one string + SYSTEMTIME testTime = GetTestTime(); + wchar_t result[MAX_PATH] = { 0 }; + HRESULT hr = GetDatedFileName(result, MAX_PATH, L"$D-$DD-$DDD-$DDDD", testTime); + + Assert::IsTrue(SUCCEEDED(hr)); + Assert::AreEqual(L"15-15-Fri-Friday", result); + } + }; +} diff --git a/src/modules/powerrename/unittests/MetadataFormatHelperTests.cpp b/src/modules/powerrename/unittests/MetadataFormatHelperTests.cpp new file mode 100644 index 0000000000..6fd5badca8 --- /dev/null +++ b/src/modules/powerrename/unittests/MetadataFormatHelperTests.cpp @@ -0,0 +1,487 @@ +#include "pch.h" +#include "MetadataFormatHelper.h" +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace PowerRenameLib; + +namespace MetadataFormatHelperTests +{ + TEST_CLASS(FormatApertureTests) + { + public: + TEST_METHOD(FormatAperture_ValidValue) + { + // Test formatting a typical aperture value + std::wstring result = MetadataFormatHelper::FormatAperture(2.8); + Assert::AreEqual(L"f/2.8", result.c_str()); + } + + TEST_METHOD(FormatAperture_SmallValue) + { + // Test small aperture (large f-number) + std::wstring result = MetadataFormatHelper::FormatAperture(1.4); + Assert::AreEqual(L"f/1.4", result.c_str()); + } + + TEST_METHOD(FormatAperture_LargeValue) + { + // Test large aperture (small f-number) + std::wstring result = MetadataFormatHelper::FormatAperture(22.0); + Assert::AreEqual(L"f/22.0", result.c_str()); + } + + TEST_METHOD(FormatAperture_RoundedValue) + { + // Test rounding to one decimal place + std::wstring result = MetadataFormatHelper::FormatAperture(5.66666); + Assert::AreEqual(L"f/5.7", result.c_str()); + } + + TEST_METHOD(FormatAperture_Zero) + { + // Test zero value + std::wstring result = MetadataFormatHelper::FormatAperture(0.0); + Assert::AreEqual(L"f/0.0", result.c_str()); + } + }; + + TEST_CLASS(FormatShutterSpeedTests) + { + public: + TEST_METHOD(FormatShutterSpeed_FastSpeed) + { + // Test fast shutter speed (fraction of second) + std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.002); + Assert::AreEqual(L"1/500s", result.c_str()); + } + + TEST_METHOD(FormatShutterSpeed_VeryFastSpeed) + { + // Test very fast shutter speed + std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0001); + Assert::AreEqual(L"1/10000s", result.c_str()); + } + + TEST_METHOD(FormatShutterSpeed_SlowSpeed) + { + // Test slow shutter speed (more than 1 second) + std::wstring result = MetadataFormatHelper::FormatShutterSpeed(2.5); + Assert::AreEqual(L"2.5s", result.c_str()); + } + + TEST_METHOD(FormatShutterSpeed_OneSecond) + { + // Test exactly 1 second + std::wstring result = MetadataFormatHelper::FormatShutterSpeed(1.0); + Assert::AreEqual(L"1.0s", result.c_str()); + } + + TEST_METHOD(FormatShutterSpeed_VerySlowSpeed) + { + // Test very slow shutter speed (< 1 second but close) + std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.5); + Assert::AreEqual(L"1/2s", result.c_str()); + } + + TEST_METHOD(FormatShutterSpeed_Zero) + { + // Test zero value + std::wstring result = MetadataFormatHelper::FormatShutterSpeed(0.0); + Assert::AreEqual(L"0", result.c_str()); + } + + TEST_METHOD(FormatShutterSpeed_Negative) + { + // Test negative value (invalid but should handle gracefully) + std::wstring result = MetadataFormatHelper::FormatShutterSpeed(-1.0); + Assert::AreEqual(L"0", result.c_str()); + } + }; + + TEST_CLASS(FormatISOTests) + { + public: + TEST_METHOD(FormatISO_TypicalValue) + { + // Test typical ISO value + std::wstring result = MetadataFormatHelper::FormatISO(400); + Assert::AreEqual(L"ISO 400", result.c_str()); + } + + TEST_METHOD(FormatISO_LowValue) + { + // Test low ISO value + std::wstring result = MetadataFormatHelper::FormatISO(100); + Assert::AreEqual(L"ISO 100", result.c_str()); + } + + TEST_METHOD(FormatISO_HighValue) + { + // Test high ISO value + std::wstring result = MetadataFormatHelper::FormatISO(12800); + Assert::AreEqual(L"ISO 12800", result.c_str()); + } + + TEST_METHOD(FormatISO_Zero) + { + // Test zero value + std::wstring result = MetadataFormatHelper::FormatISO(0); + Assert::AreEqual(L"ISO", result.c_str()); + } + + TEST_METHOD(FormatISO_Negative) + { + // Test negative value (invalid but should handle gracefully) + std::wstring result = MetadataFormatHelper::FormatISO(-100); + Assert::AreEqual(L"ISO", result.c_str()); + } + }; + + TEST_CLASS(FormatFlashTests) + { + public: + TEST_METHOD(FormatFlash_Off) + { + // Test flash off (bit 0 = 0) + std::wstring result = MetadataFormatHelper::FormatFlash(0x0); + Assert::AreEqual(L"Flash Off", result.c_str()); + } + + TEST_METHOD(FormatFlash_On) + { + // Test flash on (bit 0 = 1) + std::wstring result = MetadataFormatHelper::FormatFlash(0x1); + Assert::AreEqual(L"Flash On", result.c_str()); + } + + TEST_METHOD(FormatFlash_OnWithAdditionalFlags) + { + // Test flash on with additional flags + std::wstring result = MetadataFormatHelper::FormatFlash(0x5); // 0b0101 = fired, return detected + Assert::AreEqual(L"Flash On", result.c_str()); + } + + TEST_METHOD(FormatFlash_OffWithAdditionalFlags) + { + // Test flash off with additional flags + std::wstring result = MetadataFormatHelper::FormatFlash(0x10); // Bit 0 is 0 + Assert::AreEqual(L"Flash Off", result.c_str()); + } + }; + + TEST_CLASS(FormatCoordinateTests) + { + public: + TEST_METHOD(FormatCoordinate_NorthLatitude) + { + // Test north latitude + std::wstring result = MetadataFormatHelper::FormatCoordinate(40.7128, true); + Assert::AreEqual(L"40°42.77'N", result.c_str()); + } + + TEST_METHOD(FormatCoordinate_SouthLatitude) + { + // Test south latitude + std::wstring result = MetadataFormatHelper::FormatCoordinate(-33.8688, true); + Assert::AreEqual(L"33°52.13'S", result.c_str()); + } + + TEST_METHOD(FormatCoordinate_EastLongitude) + { + // Test east longitude + std::wstring result = MetadataFormatHelper::FormatCoordinate(151.2093, false); + Assert::AreEqual(L"151°12.56'E", result.c_str()); + } + + TEST_METHOD(FormatCoordinate_WestLongitude) + { + // Test west longitude + std::wstring result = MetadataFormatHelper::FormatCoordinate(-74.0060, false); + Assert::AreEqual(L"74°0.36'W", result.c_str()); + } + + TEST_METHOD(FormatCoordinate_ZeroLatitude) + { + // Test equator (0 degrees latitude) + std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, true); + Assert::AreEqual(L"0°0.00'N", result.c_str()); + } + + TEST_METHOD(FormatCoordinate_ZeroLongitude) + { + // Test prime meridian (0 degrees longitude) + std::wstring result = MetadataFormatHelper::FormatCoordinate(0.0, false); + Assert::AreEqual(L"0°0.00'E", result.c_str()); + } + }; + + TEST_CLASS(FormatSystemTimeTests) + { + public: + TEST_METHOD(FormatSystemTime_ValidDateTime) + { + // Test formatting a valid date and time + SYSTEMTIME st = { 0 }; + st.wYear = 2024; + st.wMonth = 3; + st.wDay = 15; + st.wHour = 14; + st.wMinute = 30; + st.wSecond = 45; + + std::wstring result = MetadataFormatHelper::FormatSystemTime(st); + Assert::AreEqual(L"2024-03-15 14:30:45", result.c_str()); + } + + TEST_METHOD(FormatSystemTime_Midnight) + { + // Test midnight time + SYSTEMTIME st = { 0 }; + st.wYear = 2024; + st.wMonth = 1; + st.wDay = 1; + st.wHour = 0; + st.wMinute = 0; + st.wSecond = 0; + + std::wstring result = MetadataFormatHelper::FormatSystemTime(st); + Assert::AreEqual(L"2024-01-01 00:00:00", result.c_str()); + } + + TEST_METHOD(FormatSystemTime_EndOfDay) + { + // Test end of day time + SYSTEMTIME st = { 0 }; + st.wYear = 2024; + st.wMonth = 12; + st.wDay = 31; + st.wHour = 23; + st.wMinute = 59; + st.wSecond = 59; + + std::wstring result = MetadataFormatHelper::FormatSystemTime(st); + Assert::AreEqual(L"2024-12-31 23:59:59", result.c_str()); + } + }; + + TEST_CLASS(ParseSingleRationalTests) + { + public: + TEST_METHOD(ParseSingleRational_ValidValue) + { + // Test parsing a valid rational: 5/2 = 2.5 + uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 }; + double result = MetadataFormatHelper::ParseSingleRational(bytes, 0); + Assert::AreEqual(2.5, result, 0.001); + } + + TEST_METHOD(ParseSingleRational_IntegerResult) + { + // Test parsing rational that results in integer: 10/5 = 2.0 + uint8_t bytes[] = { 10, 0, 0, 0, 5, 0, 0, 0 }; + double result = MetadataFormatHelper::ParseSingleRational(bytes, 0); + Assert::AreEqual(2.0, result, 0.001); + } + + TEST_METHOD(ParseSingleRational_LargeNumerator) + { + // Test parsing with large numerator: 1000/100 = 10.0 + uint8_t bytes[] = { 0xE8, 0x03, 0, 0, 100, 0, 0, 0 }; // 1000 in little-endian + double result = MetadataFormatHelper::ParseSingleRational(bytes, 0); + Assert::AreEqual(10.0, result, 0.001); + } + + TEST_METHOD(ParseSingleRational_ZeroDenominator) + { + // Test parsing with zero denominator (should return 0.0) + uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 }; + double result = MetadataFormatHelper::ParseSingleRational(bytes, 0); + Assert::AreEqual(0.0, result, 0.001); + } + + TEST_METHOD(ParseSingleRational_ZeroNumerator) + { + // Test parsing with zero numerator: 0/5 = 0.0 + uint8_t bytes[] = { 0, 0, 0, 0, 5, 0, 0, 0 }; + double result = MetadataFormatHelper::ParseSingleRational(bytes, 0); + Assert::AreEqual(0.0, result, 0.001); + } + + TEST_METHOD(ParseSingleRational_WithOffset) + { + // Test parsing with offset + uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 10, 0, 0, 0, 5, 0, 0, 0 }; // Offset = 4 + double result = MetadataFormatHelper::ParseSingleRational(bytes, 4); + Assert::AreEqual(2.0, result, 0.001); + } + + TEST_METHOD(ParseSingleRational_NullPointer) + { + // Test with null pointer (should return 0.0) + double result = MetadataFormatHelper::ParseSingleRational(nullptr, 0); + Assert::AreEqual(0.0, result, 0.001); + } + }; + + TEST_CLASS(ParseSingleSRationalTests) + { + public: + TEST_METHOD(ParseSingleSRational_PositiveValue) + { + // Test parsing positive signed rational: 5/2 = 2.5 + uint8_t bytes[] = { 5, 0, 0, 0, 2, 0, 0, 0 }; + double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0); + Assert::AreEqual(2.5, result, 0.001); + } + + TEST_METHOD(ParseSingleSRational_NegativeNumerator) + { + // Test parsing negative numerator: -5/2 = -2.5 + uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 2, 0, 0, 0 }; // -5 in two's complement + double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0); + Assert::AreEqual(-2.5, result, 0.001); + } + + TEST_METHOD(ParseSingleSRational_NegativeDenominator) + { + // Test parsing negative denominator: 5/-2 = -2.5 + uint8_t bytes[] = { 5, 0, 0, 0, 0xFE, 0xFF, 0xFF, 0xFF }; // -2 in two's complement + double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0); + Assert::AreEqual(-2.5, result, 0.001); + } + + TEST_METHOD(ParseSingleSRational_BothNegative) + { + // Test parsing both negative: -5/-2 = 2.5 + uint8_t bytes[] = { 0xFB, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF }; + double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0); + Assert::AreEqual(2.5, result, 0.001); + } + + TEST_METHOD(ParseSingleSRational_ExposureBias) + { + // Test typical exposure bias value: -1/3 ≈ -0.333 + uint8_t bytes[] = { 0xFF, 0xFF, 0xFF, 0xFF, 3, 0, 0, 0 }; // -1/3 + double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0); + Assert::AreEqual(-0.333, result, 0.001); + } + + TEST_METHOD(ParseSingleSRational_ZeroDenominator) + { + // Test with zero denominator (should return 0.0) + uint8_t bytes[] = { 5, 0, 0, 0, 0, 0, 0, 0 }; + double result = MetadataFormatHelper::ParseSingleSRational(bytes, 0); + Assert::AreEqual(0.0, result, 0.001); + } + + TEST_METHOD(ParseSingleSRational_NullPointer) + { + // Test with null pointer (should return 0.0) + double result = MetadataFormatHelper::ParseSingleSRational(nullptr, 0); + Assert::AreEqual(0.0, result, 0.001); + } + }; + + TEST_CLASS(SanitizeForFileNameTests) + { + public: + TEST_METHOD(SanitizeForFileName_ValidString) + { + // Test string without illegal characters + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Canon EOS 5D"); + Assert::AreEqual(L"Canon EOS 5D", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithColon) + { + // Test string with colon (illegal character) + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo:001"); + Assert::AreEqual(L"Photo_001", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithSlashes) + { + // Test string with forward and backward slashes + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photos/2024\\January"); + Assert::AreEqual(L"Photos_2024_January", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithMultipleIllegalChars) + { + // Test string with multiple illegal characters + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L":File|Name*?.txt"); + Assert::AreEqual(L"_Test__File_Name__.txt", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithQuotes) + { + // Test string with quotes + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Photo \"Best Shot\""); + Assert::AreEqual(L"Photo _Best Shot_", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithTrailingDot) + { + // Test string with trailing dot (should be removed) + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename."); + Assert::AreEqual(L"filename", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithTrailingSpace) + { + // Test string with trailing space (should be removed) + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename "); + Assert::AreEqual(L"filename", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithMultipleTrailingDotsAndSpaces) + { + // Test string with multiple trailing dots and spaces + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"filename. . "); + Assert::AreEqual(L"filename", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_WithControlCharacters) + { + // Test string with control characters + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"File\x01Name\x1F"); + Assert::AreEqual(L"File_Name_", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_EmptyString) + { + // Test empty string + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L""); + Assert::AreEqual(L"", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_OnlyIllegalCharacters) + { + // Test string with only illegal characters + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"<>:\"/\\|?*"); + Assert::AreEqual(L"_________", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_OnlyTrailingCharacters) + { + // Test string with only dots and spaces (should return empty) + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L". . "); + Assert::AreEqual(L"", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_UnicodeCharacters) + { + // Test string with valid Unicode characters + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"照片_2024å¹´"); + Assert::AreEqual(L"照片_2024å¹´", result.c_str()); + } + + TEST_METHOD(SanitizeForFileName_MixedContent) + { + // Test realistic metadata string with multiple issues + std::wstring result = MetadataFormatHelper::SanitizeForFileName(L"Copyright © 2024: John/Jane Doe. "); + Assert::AreEqual(L"Copyright © 2024_ John_Jane Doe", result.c_str()); + } + }; +} diff --git a/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp index a882802499..97c3f2fa2d 100644 --- a/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp +++ b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.cpp @@ -62,6 +62,12 @@ IFACEMETHODIMP CMockPowerRenameRegExEvents::OnFileTimeChanged(_In_ SYSTEMTIME fi return S_OK; } +IFACEMETHODIMP CMockPowerRenameRegExEvents::OnMetadataChanged() +{ + return S_OK; +} + + HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree) { *ppsrree = nullptr; @@ -74,3 +80,4 @@ HRESULT CMockPowerRenameRegExEvents::s_CreateInstance(_Outptr_ IPowerRenameRegEx } return hr; } + diff --git a/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h index f65108b123..b68f3775e8 100644 --- a/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h +++ b/src/modules/powerrename/unittests/MockPowerRenameRegExEvents.h @@ -19,6 +19,7 @@ public: IFACEMETHODIMP OnReplaceTermChanged(_In_ PCWSTR replaceTerm); IFACEMETHODIMP OnFlagsChanged(_In_ DWORD flags); IFACEMETHODIMP OnFileTimeChanged(_In_ SYSTEMTIME fileTime); + IFACEMETHODIMP OnMetadataChanged(); static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegExEvents** ppsrree); @@ -39,3 +40,4 @@ public: SYSTEMTIME m_fileTime = { 0 }; long m_refCount; }; + diff --git a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj index a6bd505342..3a3c5663aa 100644 --- a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj +++ b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj @@ -34,7 +34,7 @@ $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) - $(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies) + $(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;windowscodecs.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies) @@ -49,11 +49,14 @@ + + + Create @@ -73,8 +76,30 @@ + + + true + + + true + + + true + + + true + + + true + + + + + + + diff --git a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters index db5bc09af4..42b6d1d17b 100644 --- a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters +++ b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters @@ -1,6 +1,7 @@  + @@ -30,6 +31,9 @@ {d34a343a-52ef-4296-83c9-a94fa62062ff} + + {8c9f3e2d-1a4b-4e5f-9c7d-2b8a6f3e1d4c} + @@ -38,5 +42,20 @@ + + testdata + + + testdata + + + testdata + + + testdata + + + testdata + \ No newline at end of file diff --git a/src/modules/powerrename/unittests/WICMetadataExtractorTests.cpp b/src/modules/powerrename/unittests/WICMetadataExtractorTests.cpp new file mode 100644 index 0000000000..c6d1d9b16c --- /dev/null +++ b/src/modules/powerrename/unittests/WICMetadataExtractorTests.cpp @@ -0,0 +1,244 @@ +#include "pch.h" +#include "WICMetadataExtractor.h" +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace PowerRenameLib; + +namespace WICMetadataExtractorTests +{ + // Helper function to get the test data directory path + std::wstring GetTestDataPath() + { + // Get the directory where the test DLL is located + // When running with vstest, we need to get the DLL module handle + HMODULE hModule = nullptr; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&GetTestDataPath), + &hModule); + + wchar_t modulePath[MAX_PATH]; + GetModuleFileNameW(hModule, modulePath, MAX_PATH); + std::filesystem::path dllPath(modulePath); + + // Navigate to the test data directory + // The test data is in the output directory alongside the DLL + std::filesystem::path testDataPath = dllPath.parent_path() / L"testdata"; + + return testDataPath.wstring(); + } + + TEST_CLASS(ExtractEXIFMetadataTests) + { + public: + TEST_METHOD(ExtractEXIF_InvalidFile_ReturnsFalse) + { + // Test that EXIF extraction fails for nonexistent file + WICMetadataExtractor extractor; + EXIFMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg"; + bool result = extractor.ExtractEXIFMetadata(testFile, metadata); + + Assert::IsFalse(result, L"EXIF extraction should fail for nonexistent file"); + } + + TEST_METHOD(ExtractEXIF_ExifTest_AllFields) + { + // Test exif_test.jpg which contains comprehensive EXIF data + WICMetadataExtractor extractor; + EXIFMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg"; + bool result = extractor.ExtractEXIFMetadata(testFile, metadata); + + Assert::IsTrue(result, L"EXIF extraction should succeed"); + + // Verify all the fields that are in exif_test.jpg + Assert::IsTrue(metadata.cameraMake.has_value(), L"Camera make should be present"); + Assert::AreEqual(L"samsung", metadata.cameraMake.value().c_str(), L"Camera make should be samsung"); + + Assert::IsTrue(metadata.cameraModel.has_value(), L"Camera model should be present"); + Assert::AreEqual(L"SM-G930P", metadata.cameraModel.value().c_str(), L"Camera model should be SM-G930P"); + + Assert::IsTrue(metadata.lensModel.has_value(), L"Lens model should be present"); + Assert::AreEqual(L"Samsung Galaxy S7 Rear Camera", metadata.lensModel.value().c_str(), L"Lens model should match"); + + Assert::IsTrue(metadata.iso.has_value(), L"ISO should be present"); + Assert::AreEqual(40, static_cast(metadata.iso.value()), L"ISO should be 40"); + + Assert::IsTrue(metadata.aperture.has_value(), L"Aperture should be present"); + Assert::AreEqual(1.7, metadata.aperture.value(), 0.01, L"Aperture should be f/1.7"); + + Assert::IsTrue(metadata.shutterSpeed.has_value(), L"Shutter speed should be present"); + Assert::AreEqual(0.000625, metadata.shutterSpeed.value(), 0.000001, L"Shutter speed should be 0.000625s"); + + Assert::IsTrue(metadata.focalLength.has_value(), L"Focal length should be present"); + Assert::AreEqual(4.2, metadata.focalLength.value(), 0.1, L"Focal length should be 4.2mm"); + + Assert::IsTrue(metadata.flash.has_value(), L"Flash should be present"); + Assert::AreEqual(0u, static_cast(metadata.flash.value()), L"Flash should be 0x0"); + + Assert::IsTrue(metadata.exposureBias.has_value(), L"Exposure bias should be present"); + Assert::AreEqual(0.0, metadata.exposureBias.value(), 0.01, L"Exposure bias should be 0 EV"); + + Assert::IsTrue(metadata.author.has_value(), L"Author should be present"); + Assert::AreEqual(L"Carl Seibert (Exif)", metadata.author.value().c_str(), L"Author should match"); + + Assert::IsTrue(metadata.copyright.has_value(), L"Copyright should be present"); + Assert::IsTrue(metadata.copyright.value().find(L"Carl Seibert") != std::wstring::npos, L"Copyright should contain Carl Seibert"); + } + + TEST_METHOD(ExtractEXIF_ExifTest2_WidthHeight) + { + // Test exif_test_2.jpg which only contains width and height + WICMetadataExtractor extractor; + EXIFMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\exif_test_2.jpg"; + bool result = extractor.ExtractEXIFMetadata(testFile, metadata); + + Assert::IsTrue(result, L"EXIF extraction should succeed"); + + // exif_test_2.jpg only has width and height + Assert::IsTrue(metadata.width.has_value(), L"Width should be present"); + Assert::AreEqual(1080u, static_cast(metadata.width.value()), L"Width should be 1080px"); + + Assert::IsTrue(metadata.height.has_value(), L"Height should be present"); + Assert::AreEqual(810u, static_cast(metadata.height.value()), L"Height should be 810px"); + + // Other fields should not be present + Assert::IsFalse(metadata.cameraMake.has_value(), L"Camera make should not be present in exif_test_2.jpg"); + Assert::IsFalse(metadata.cameraModel.has_value(), L"Camera model should not be present in exif_test_2.jpg"); + Assert::IsFalse(metadata.iso.has_value(), L"ISO should not be present in exif_test_2.jpg"); + } + + TEST_METHOD(ExtractEXIF_ClearCache) + { + // Test cache clearing works + WICMetadataExtractor extractor; + EXIFMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\exif_test.jpg"; + + bool result1 = extractor.ExtractEXIFMetadata(testFile, metadata); + Assert::IsTrue(result1); + + extractor.ClearCache(); + + EXIFMetadata metadata2; + bool result2 = extractor.ExtractEXIFMetadata(testFile, metadata2); + Assert::IsTrue(result2); + + // Both calls should succeed + Assert::AreEqual(metadata.cameraMake.value().c_str(), metadata2.cameraMake.value().c_str()); + } + }; + + TEST_CLASS(ExtractXMPMetadataTests) + { + public: + TEST_METHOD(ExtractXMP_InvalidFile_ReturnsFalse) + { + // Test that XMP extraction fails for nonexistent file + WICMetadataExtractor extractor; + XMPMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\nonexistent.jpg"; + bool result = extractor.ExtractXMPMetadata(testFile, metadata); + + Assert::IsFalse(result, L"XMP extraction should fail for nonexistent file"); + } + + TEST_METHOD(ExtractXMP_XmpTest_AllFields) + { + // Test xmp_test.jpg which contains comprehensive XMP data + WICMetadataExtractor extractor; + XMPMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg"; + bool result = extractor.ExtractXMPMetadata(testFile, metadata); + + Assert::IsTrue(result, L"XMP extraction should succeed"); + + // Verify all the fields that are in xmp_test.jpg + Assert::IsTrue(metadata.title.has_value(), L"Title should be present"); + Assert::AreEqual(L"object name here", metadata.title.value().c_str(), L"Title should match"); + + Assert::IsTrue(metadata.description.has_value(), L"Description should be present"); + Assert::IsTrue(metadata.description.value().find(L"This is a metadata test file") != std::wstring::npos, + L"Description should contain expected text"); + + Assert::IsTrue(metadata.rights.has_value(), L"Rights should be present"); + Assert::AreEqual(L"metadatamatters.blog", metadata.rights.value().c_str(), L"Rights should match"); + + Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present"); + Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop Lightroom") != std::wstring::npos, + L"Creator tool should contain Lightroom"); + + Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present"); + Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos, + L"Document ID should start with xmp.did:"); + + Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present"); + Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos, + L"Instance ID should start with xmp.iid:"); + + Assert::IsTrue(metadata.subject.has_value(), L"Subject keywords should be present"); + Assert::IsTrue(metadata.subject.value().size() > 0, L"Should have at least one keyword"); + } + + TEST_METHOD(ExtractXMP_XmpTest2_BasicFields) + { + // Test xmp_test_2.jpg which only contains basic XMP fields + WICMetadataExtractor extractor; + XMPMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\xmp_test_2.jpg"; + bool result = extractor.ExtractXMPMetadata(testFile, metadata); + + Assert::IsTrue(result, L"XMP extraction should succeed"); + + // xmp_test_2.jpg only has CreatorTool, DocumentID, and InstanceID + Assert::IsTrue(metadata.creatorTool.has_value(), L"Creator tool should be present"); + Assert::IsTrue(metadata.creatorTool.value().find(L"Adobe Photoshop CS6") != std::wstring::npos, + L"Creator tool should be Photoshop CS6"); + + Assert::IsTrue(metadata.documentID.has_value(), L"Document ID should be present"); + Assert::IsTrue(metadata.documentID.value().find(L"xmp.did:") != std::wstring::npos, + L"Document ID should start with xmp.did:"); + + Assert::IsTrue(metadata.instanceID.has_value(), L"Instance ID should be present"); + Assert::IsTrue(metadata.instanceID.value().find(L"xmp.iid:") != std::wstring::npos, + L"Instance ID should start with xmp.iid:"); + + // Other fields should not be present + Assert::IsFalse(metadata.title.has_value(), L"Title should not be present in xmp_test_2.jpg"); + Assert::IsFalse(metadata.description.has_value(), L"Description should not be present in xmp_test_2.jpg"); + Assert::IsFalse(metadata.rights.has_value(), L"Rights should not be present in xmp_test_2.jpg"); + Assert::IsFalse(metadata.creator.has_value(), L"Creator should not be present in xmp_test_2.jpg"); + } + + TEST_METHOD(ExtractXMP_ClearCache) + { + // Test cache clearing works + WICMetadataExtractor extractor; + XMPMetadata metadata; + + std::wstring testFile = GetTestDataPath() + L"\\xmp_test.jpg"; + + bool result1 = extractor.ExtractXMPMetadata(testFile, metadata); + Assert::IsTrue(result1); + + extractor.ClearCache(); + + XMPMetadata metadata2; + bool result2 = extractor.ExtractXMPMetadata(testFile, metadata2); + Assert::IsTrue(result2); + + // Both calls should succeed + Assert::AreEqual(metadata.title.value().c_str(), metadata2.title.value().c_str()); + } + }; +} diff --git a/src/modules/powerrename/unittests/testdata/ATTRIBUTION.md b/src/modules/powerrename/unittests/testdata/ATTRIBUTION.md new file mode 100644 index 0000000000..88844e57b8 --- /dev/null +++ b/src/modules/powerrename/unittests/testdata/ATTRIBUTION.md @@ -0,0 +1,45 @@ +# Test Data Attribution + +This directory contains test image files used for PowerRename metadata extraction unit tests. These images are sourced from Wikimedia Commons and are used under the Creative Commons licenses specified below. + +## Test Files and Licenses + +### Files from Carlseibert + +**License:** [Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/) + +- `exif_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons +- `xmp_test.jpg` - Uploaded by [Carlseibert](https://commons.wikimedia.org/wiki/File%3AMetadata_test_file_-_includes_data_in_IIM%2C_XMP%2C_and_Exif.jpg) on Wikimedia Commons + +### Files from Edward Steven + +**License:** [Creative Commons Attribution-ShareAlike 2.0 Generic (CC BY-SA 2.0)](https://creativecommons.org/licenses/by-sa/2.0/) + +- `exif_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons +- `xmp_test_2.jpg` - Uploaded by [Edward Steven](https://commons.wikimedia.org/wiki/File%3AAreca_hutchinsoniana.jpg) on Wikimedia Commons + +## Acknowledgments + +We gratefully acknowledge the contributions of Carlseibert and Edward Steven for making these images available under Creative Commons licenses. Their work enables us to test metadata extraction functionality with real-world EXIF and XMP data. + +## Usage + +These test images are used in PowerRename's unit tests to verify correct extraction of: +- EXIF metadata (camera make, model, ISO, aperture, shutter speed, etc.) +- XMP metadata (creator, title, description, copyright, etc.) +- GPS coordinates +- Date/time information + +## License Compliance + +These test images are distributed as part of the PowerToys source code repository under their original Creative Commons licenses: +- Files from Carlseibert: CC BY-SA 4.0 +- Files from Edward Steven: CC BY-SA 2.0 + +**Modifications:** These images have not been modified from their original versions downloaded from Wikimedia Commons. They are used in their original form for metadata extraction testing purposes. + +**Distribution:** These test images are included in the PowerToys source repository and comply with the terms of their respective Creative Commons licenses through proper attribution in this file. While included in the source code, these images are not distributed in end-user installation packages or releases. + +**Derivatives:** Any modifications or derivative works of these images must comply with the respective CC BY-SA license terms, including proper attribution and applying the same license to the modified versions. + +For more information about Creative Commons licenses, visit: https://creativecommons.org/licenses/ diff --git a/src/modules/powerrename/unittests/testdata/exif_test.jpg b/src/modules/powerrename/unittests/testdata/exif_test.jpg new file mode 100644 index 0000000000..5b40a5a688 Binary files /dev/null and b/src/modules/powerrename/unittests/testdata/exif_test.jpg differ diff --git a/src/modules/powerrename/unittests/testdata/exif_test_2.jpg b/src/modules/powerrename/unittests/testdata/exif_test_2.jpg new file mode 100644 index 0000000000..ec2a3ad703 Binary files /dev/null and b/src/modules/powerrename/unittests/testdata/exif_test_2.jpg differ diff --git a/src/modules/powerrename/unittests/testdata/xmp_test.jpg b/src/modules/powerrename/unittests/testdata/xmp_test.jpg new file mode 100644 index 0000000000..5b40a5a688 Binary files /dev/null and b/src/modules/powerrename/unittests/testdata/xmp_test.jpg differ diff --git a/src/modules/powerrename/unittests/testdata/xmp_test_2.jpg b/src/modules/powerrename/unittests/testdata/xmp_test_2.jpg new file mode 100644 index 0000000000..ec2a3ad703 Binary files /dev/null and b/src/modules/powerrename/unittests/testdata/xmp_test_2.jpg differ diff --git a/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/MainWindow.xaml b/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/MainWindow.xaml index ce24f2c778..c9cb36d746 100644 --- a/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/MainWindow.xaml +++ b/src/modules/registrypreview/RegistryPreview/RegistryPreviewXAML/MainWindow.xaml @@ -17,7 +17,7 @@ - + (obj.GetNamedNumber(L"dashboard_sort_order", static_cast(static_cast(fallback)))); + return raw_value == static_cast(DashboardSortOrder::ByStatus) ? DashboardSortOrder::ByStatus : DashboardSortOrder::Alphabetical; + } + + if (json::has(obj, L"dashboard_sort_order", json::JsonValueType::String)) + { + const auto raw = obj.GetNamedString(L"dashboard_sort_order"); + if (raw == L"ByStatus") + { + return DashboardSortOrder::ByStatus; + } + + if (raw == L"Alphabetical") + { + return DashboardSortOrder::Alphabetical; + } + } + + return fallback; + } } // TODO: would be nice to get rid of these globals, since they're basically cached json settings @@ -46,6 +71,7 @@ static bool download_updates_automatically = true; static bool show_whats_new_after_updates = true; static bool enable_experimentation = true; static bool enable_warnings_elevated_apps = true; +static DashboardSortOrder dashboard_sort_order = DashboardSortOrder::Alphabetical; static json::JsonObject ignored_conflict_properties = create_default_ignored_conflict_properties(); json::JsonObject GeneralSettings::to_json() @@ -75,6 +101,7 @@ json::JsonObject GeneralSettings::to_json() result.SetNamedValue(L"download_updates_automatically", json::value(downloadUpdatesAutomatically)); result.SetNamedValue(L"show_whats_new_after_updates", json::value(showWhatsNewAfterUpdates)); result.SetNamedValue(L"enable_experimentation", json::value(enableExperimentation)); + result.SetNamedValue(L"dashboard_sort_order", json::value(static_cast(dashboardSortOrder))); result.SetNamedValue(L"is_admin", json::value(isAdmin)); result.SetNamedValue(L"enable_warnings_elevated_apps", json::value(enableWarningsElevatedApps)); result.SetNamedValue(L"theme", json::value(theme)); @@ -99,6 +126,7 @@ json::JsonObject load_general_settings() show_whats_new_after_updates = loaded.GetNamedBoolean(L"show_whats_new_after_updates", true); enable_experimentation = loaded.GetNamedBoolean(L"enable_experimentation", true); enable_warnings_elevated_apps = loaded.GetNamedBoolean(L"enable_warnings_elevated_apps", true); + dashboard_sort_order = parse_dashboard_sort_order(loaded, dashboard_sort_order); if (json::has(loaded, L"ignored_conflict_properties", json::JsonValueType::Object)) { @@ -128,6 +156,7 @@ GeneralSettings get_general_settings() .downloadUpdatesAutomatically = download_updates_automatically && is_user_admin, .showWhatsNewAfterUpdates = show_whats_new_after_updates, .enableExperimentation = enable_experimentation, + .dashboardSortOrder = dashboard_sort_order, .theme = settings_theme, .systemTheme = WindowsColors::is_dark_mode() ? L"dark" : L"light", .powerToysVersion = get_product_version(), @@ -159,6 +188,7 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save) show_whats_new_after_updates = general_configs.GetNamedBoolean(L"show_whats_new_after_updates", true); enable_experimentation = general_configs.GetNamedBoolean(L"enable_experimentation", true); + dashboard_sort_order = parse_dashboard_sort_order(general_configs, dashboard_sort_order); // apply_general_settings is called by the runner's WinMain, so we can just force the run at startup gpo rule here. auto gpo_run_as_startup = powertoys_gpo::getConfiguredRunAtStartupValue(); diff --git a/src/runner/general_settings.h b/src/runner/general_settings.h index 38fbd5789a..b4f7638846 100644 --- a/src/runner/general_settings.h +++ b/src/runner/general_settings.h @@ -2,6 +2,12 @@ #include +enum class DashboardSortOrder +{ + Alphabetical = 0, + ByStatus = 1, +}; + struct GeneralSettings { bool isStartupEnabled; @@ -16,6 +22,7 @@ struct GeneralSettings bool downloadUpdatesAutomatically; bool showWhatsNewAfterUpdates; bool enableExperimentation; + DashboardSortOrder dashboardSortOrder; std::wstring theme; std::wstring systemTheme; std::wstring powerToysVersion; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 4b29149f78..c20293f9ed 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -161,6 +161,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"PowerToys.MouseJump.dll", L"PowerToys.AlwaysOnTopModuleInterface.dll", L"PowerToys.MousePointerCrosshairs.dll", + L"PowerToys.CursorWrap.dll", L"PowerToys.PowerAccentModuleInterface.dll", L"PowerToys.PowerOCRModuleInterface.dll", L"PowerToys.AdvancedPasteModuleInterface.dll", diff --git a/src/runner/runner.base.rc b/src/runner/runner.base.rc index 367735ade4..55b4e13fdd 100644 Binary files a/src/runner/runner.base.rc and b/src/runner/runner.base.rc differ diff --git a/src/runner/runner.vcxproj b/src/runner/runner.vcxproj index afff599d8e..1eae5a3573 100644 --- a/src/runner/runner.vcxproj +++ b/src/runner/runner.vcxproj @@ -28,6 +28,7 @@ v143 None true + true diff --git a/src/settings-ui/Settings.UI.Library/AIServiceType.cs b/src/settings-ui/Settings.UI.Library/AIServiceType.cs new file mode 100644 index 0000000000..27eccff1cf --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AIServiceType.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Supported AI service types for PowerToys AI experiences. + /// + public enum AIServiceType + { + Unknown = 0, + OpenAI, + AzureOpenAI, + Onnx, + ML, + FoundryLocal, + Mistral, + Google, + AzureAIInference, + Ollama, + } +} diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs new file mode 100644 index 0000000000..5b19212eba --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeExtensions.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public static class AIServiceTypeExtensions + { + /// + /// Convert a persisted string value into an . + /// Supports historical casing and aliases. + /// + public static AIServiceType ToAIServiceType(this string serviceType) + { + if (string.IsNullOrWhiteSpace(serviceType)) + { + return AIServiceType.OpenAI; + } + + var normalized = serviceType.Trim().ToLowerInvariant(); + return normalized switch + { + "openai" => AIServiceType.OpenAI, + "azureopenai" or "azure" => AIServiceType.AzureOpenAI, + "onnx" => AIServiceType.Onnx, + "foundrylocal" or "foundry" or "fl" => AIServiceType.FoundryLocal, + "ml" or "windowsml" or "winml" => AIServiceType.ML, + "mistral" => AIServiceType.Mistral, + "google" or "googleai" or "googlegemini" => AIServiceType.Google, + "azureaiinference" or "azureinference" => AIServiceType.AzureAIInference, + "ollama" => AIServiceType.Ollama, + _ => AIServiceType.Unknown, + }; + } + + /// + /// Convert an to the canonical string used for persistence. + /// + public static string ToConfigurationString(this AIServiceType serviceType) + { + return serviceType switch + { + AIServiceType.OpenAI => "OpenAI", + AIServiceType.AzureOpenAI => "AzureOpenAI", + AIServiceType.Onnx => "Onnx", + AIServiceType.FoundryLocal => "FoundryLocal", + AIServiceType.ML => "ML", + AIServiceType.Mistral => "Mistral", + AIServiceType.Google => "Google", + AIServiceType.AzureAIInference => "AzureAIInference", + AIServiceType.Ollama => "Ollama", + AIServiceType.Unknown => string.Empty, + _ => throw new ArgumentOutOfRangeException(nameof(serviceType), serviceType, "Unsupported AI service type."), + }; + } + + /// + /// Convert an into the normalized key used internally. + /// + public static string ToNormalizedKey(this AIServiceType serviceType) + { + return serviceType switch + { + AIServiceType.OpenAI => "openai", + AIServiceType.AzureOpenAI => "azureopenai", + AIServiceType.Onnx => "onnx", + AIServiceType.FoundryLocal => "foundrylocal", + AIServiceType.ML => "ml", + AIServiceType.Mistral => "mistral", + AIServiceType.Google => "google", + AIServiceType.AzureAIInference => "azureaiinference", + AIServiceType.Ollama => "ollama", + _ => string.Empty, + }; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs new file mode 100644 index 0000000000..df01b1816a --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeMetadata.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Metadata information for an AI service type. + /// + public class AIServiceTypeMetadata + { + public AIServiceType ServiceType { get; init; } + + public string DisplayName { get; init; } + + public string IconPath { get; init; } + + public bool IsOnlineService { get; init; } + + public bool IsAvailableInUI { get; init; } = true; + + public bool IsLocalModel { get; init; } + + public string LegalDescription { get; init; } + + public string TermsLabel { get; init; } + + public Uri TermsUri { get; init; } + + public string PrivacyLabel { get; init; } + + public Uri PrivacyUri { get; init; } + + public bool HasLegalInfo => !string.IsNullOrWhiteSpace(LegalDescription); + + public bool HasTermsLink => TermsUri is not null && !string.IsNullOrEmpty(TermsLabel); + + public bool HasPrivacyLink => PrivacyUri is not null && !string.IsNullOrEmpty(PrivacyLabel); + } +} diff --git a/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs new file mode 100644 index 0000000000..653b85553e --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AIServiceTypeRegistry.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.PowerToys.Settings.UI.Library; + +/// +/// Centralized registry for AI service type metadata. +/// +public static class AIServiceTypeRegistry +{ + private static readonly Dictionary MetadataMap = new() + { + [AIServiceType.AzureAIInference] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.AzureAIInference, + DisplayName = "Azure AI Inference", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/Azure.svg", + IsOnlineService = true, + LegalDescription = "AdvancedPaste_AzureAIInference_LegalDescription", + TermsLabel = "AdvancedPaste_AzureAIInference_TermsLabel", + TermsUri = new Uri("https://azure.microsoft.com/support/legal/"), + PrivacyLabel = "AdvancedPaste_AzureAIInference_PrivacyLabel", + PrivacyUri = new Uri("https://privacy.microsoft.com/privacystatement"), + }, + [AIServiceType.AzureOpenAI] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.AzureOpenAI, + DisplayName = "Azure OpenAI", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/AzureAI.svg", + IsOnlineService = true, + LegalDescription = "AdvancedPaste_AzureOpenAI_LegalDescription", + TermsLabel = "AdvancedPaste_AzureOpenAI_TermsLabel", + TermsUri = new Uri("https://azure.microsoft.com/support/legal/"), + PrivacyLabel = "AdvancedPaste_AzureOpenAI_PrivacyLabel", + PrivacyUri = new Uri("https://privacy.microsoft.com/privacystatement"), + }, + [AIServiceType.FoundryLocal] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.FoundryLocal, + DisplayName = "Foundry Local", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg", + IsOnlineService = false, + IsLocalModel = true, + LegalDescription = "AdvancedPaste_FoundryLocal_LegalDescription", // Resource key for localized description + }, + [AIServiceType.Google] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.Google, + DisplayName = "Google", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/Gemini.svg", + IsOnlineService = true, + LegalDescription = "AdvancedPaste_Google_LegalDescription", + TermsLabel = "AdvancedPaste_Google_TermsLabel", + TermsUri = new Uri("https://ai.google.dev/gemini-api/terms"), + PrivacyLabel = "AdvancedPaste_Google_PrivacyLabel", + PrivacyUri = new Uri("https://support.google.com/gemini/answer/13594961"), + }, + [AIServiceType.Mistral] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.Mistral, + DisplayName = "Mistral", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/Mistral.svg", + IsOnlineService = true, + LegalDescription = "AdvancedPaste_Mistral_LegalDescription", + TermsLabel = "AdvancedPaste_Mistral_TermsLabel", + TermsUri = new Uri("https://mistral.ai/terms-of-service/"), + PrivacyLabel = "AdvancedPaste_Mistral_PrivacyLabel", + PrivacyUri = new Uri("https://mistral.ai/privacy-policy/"), + }, + [AIServiceType.ML] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.ML, + DisplayName = "Windows ML", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/WindowsML.svg", + LegalDescription = "AdvancedPaste_LocalModel_LegalDescription", + IsAvailableInUI = false, + IsOnlineService = false, + IsLocalModel = true, + }, + [AIServiceType.Ollama] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.Ollama, + DisplayName = "Ollama", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/Ollama.svg", + + // Ollama provide online service, but we treat it as local model at first version since it can is known for local model. + IsOnlineService = false, + IsLocalModel = true, + LegalDescription = "AdvancedPaste_LocalModel_LegalDescription", + TermsLabel = "AdvancedPaste_Ollama_TermsLabel", + TermsUri = new Uri("https://ollama.org/terms"), + PrivacyLabel = "AdvancedPaste_Ollama_PrivacyLabel", + PrivacyUri = new Uri("https://ollama.org/privacy"), + }, + [AIServiceType.Onnx] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.Onnx, + DisplayName = "ONNX", + LegalDescription = "AdvancedPaste_LocalModel_LegalDescription", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/Onnx.svg", + IsOnlineService = false, + IsAvailableInUI = false, + }, + [AIServiceType.OpenAI] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.OpenAI, + DisplayName = "OpenAI", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg", + IsOnlineService = true, + LegalDescription = "AdvancedPaste_OpenAI_LegalDescription", + TermsLabel = "AdvancedPaste_OpenAI_TermsLabel", + TermsUri = new Uri("https://openai.com/terms"), + PrivacyLabel = "AdvancedPaste_OpenAI_PrivacyLabel", + PrivacyUri = new Uri("https://openai.com/privacy"), + }, + [AIServiceType.Unknown] = new AIServiceTypeMetadata + { + ServiceType = AIServiceType.Unknown, + DisplayName = "Unknown", + IconPath = "ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg", + IsOnlineService = false, + IsAvailableInUI = false, + }, + }; + + /// + /// Get metadata for a specific service type. + /// + public static AIServiceTypeMetadata GetMetadata(AIServiceType serviceType) + { + return MetadataMap.TryGetValue(serviceType, out var metadata) + ? metadata + : MetadataMap[AIServiceType.Unknown]; + } + + /// + /// Get metadata for a service type from its string representation. + /// + public static AIServiceTypeMetadata GetMetadata(string serviceType) + { + var type = serviceType.ToAIServiceType(); + return GetMetadata(type); + } + + /// + /// Get icon path for a service type. + /// + public static string GetIconPath(AIServiceType serviceType) + { + return GetMetadata(serviceType).IconPath; + } + + /// + /// Get icon path for a service type from its string representation. + /// + public static string GetIconPath(string serviceType) + { + return GetMetadata(serviceType).IconPath; + } + + /// + /// Get all service types available in the UI. + /// + public static IEnumerable GetAvailableServiceTypes() + { + return MetadataMap.Values.Where(m => m.IsAvailableInUI); + } + + /// + /// Get all online service types available in the UI. + /// + public static IEnumerable GetOnlineServiceTypes() + { + return GetAvailableServiceTypes().Where(m => m.IsOnlineService); + } + + /// + /// Get all local service types available in the UI. + /// + public static IEnumerable GetLocalServiceTypes() + { + return GetAvailableServiceTypes().Where(m => m.IsLocalModel); + } +} diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs index 43baf89351..c981295906 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomAction.cs @@ -14,6 +14,7 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction { private int _id; private string _name = string.Empty; + private string _description = string.Empty; private string _prompt = string.Empty; private HotkeySettings _shortcut = new(); private bool _isShown; @@ -43,6 +44,13 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction } } + [JsonPropertyName("description")] + public string Description + { + get => _description; + set => Set(ref _description, value ?? string.Empty); + } + [JsonPropertyName("prompt")] public string Prompt { @@ -128,6 +136,7 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction { Id = other.Id; Name = other.Name; + Description = other.Description; Prompt = other.Prompt; Shortcut = other.GetShortcutClone(); IsShown = other.IsShown; diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomActions.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomActions.cs index 5ab4331393..87d74bae5f 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomActions.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteCustomActions.cs @@ -10,7 +10,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library; public sealed class AdvancedPasteCustomActions { - private static readonly JsonSerializerOptions _serializerOptions = new() + private static readonly JsonSerializerOptions _serializerOptions = new(SettingsSerializationContext.Default.Options) { WriteIndented = true, }; diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs new file mode 100644 index 0000000000..b61f83dd3d --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteMigrationHelper.cs @@ -0,0 +1,105 @@ +// 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.ObjectModel; +using System.Linq; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Helper methods for migrating legacy Advanced Paste settings to the updated schema. + /// + public static class AdvancedPasteMigrationHelper + { + /// + /// Ensures an OpenAI provider exists in the configuration, creating one if necessary. + /// + /// The configuration instance. + /// The ensured provider and a flag indicating whether changes were made. + public static (PasteAIProviderDefinition Provider, bool Updated) EnsureOpenAIProvider(PasteAIConfiguration configuration) + { + if (configuration is null) + { + return (null, false); + } + + configuration.Providers ??= new ObservableCollection(); + + const string serviceTypeKey = "OpenAI"; + var existingProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.ServiceType, serviceTypeKey, StringComparison.OrdinalIgnoreCase)); + bool updated = false; + + if (existingProvider is null) + { + existingProvider = CreateProvider(serviceTypeKey); + configuration.Providers.Add(existingProvider); + updated = true; + } + + updated |= EnsureActiveProviderIsValid(configuration, existingProvider); + + return (existingProvider, updated); + } + + /// + /// Creates a provider with default values for the requested service type. + /// + private static PasteAIProviderDefinition CreateProvider(string serviceTypeKey) + { + var serviceType = serviceTypeKey.ToAIServiceType(); + var metadata = AIServiceTypeRegistry.GetMetadata(serviceType); + var provider = new PasteAIProviderDefinition + { + ServiceType = serviceTypeKey, + ModelName = PasteAIProviderDefaults.GetDefaultModelName(serviceType), + EndpointUrl = string.Empty, + ApiVersion = string.Empty, + DeploymentName = string.Empty, + ModelPath = string.Empty, + SystemPrompt = string.Empty, + ModerationEnabled = serviceType == AIServiceType.OpenAI, + IsLocalModel = metadata.IsLocalModel, + }; + + return provider; + } + + private static bool EnsureActiveProviderIsValid(PasteAIConfiguration configuration, PasteAIProviderDefinition preferredProvider = null) + { + if (configuration?.Providers is null || configuration.Providers.Count == 0) + { + if (!string.IsNullOrWhiteSpace(configuration?.ActiveProviderId)) + { + configuration.ActiveProviderId = string.Empty; + return true; + } + + return false; + } + + bool updated = false; + + var activeProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.Id, configuration.ActiveProviderId, StringComparison.OrdinalIgnoreCase)); + if (activeProvider is null) + { + activeProvider = preferredProvider ?? configuration.Providers.First(); + configuration.ActiveProviderId = activeProvider.Id; + updated = true; + } + + foreach (var provider in configuration.Providers) + { + bool shouldBeActive = string.Equals(provider.Id, configuration.ActiveProviderId, StringComparison.OrdinalIgnoreCase); + if (provider.IsActive != shouldBeActive) + { + provider.IsActive = shouldBeActive; + updated = true; + } + } + + return updated; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs index d40bd686d3..9e2fa7ee12 100644 --- a/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs +++ b/src/settings-ui/Settings.UI.Library/AdvancedPasteProperties.cs @@ -2,6 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; @@ -23,13 +24,51 @@ namespace Microsoft.PowerToys.Settings.UI.Library PasteAsJsonShortcut = new(); CustomActions = new(); AdditionalActions = new(); - IsAdvancedAIEnabled = false; + IsAIEnabled = false; ShowCustomPreview = true; CloseAfterLosingFocus = false; + EnableClipboardPreview = true; + PasteAIConfiguration = new(); } [JsonConverter(typeof(BoolPropertyJsonConverter))] - public bool IsAdvancedAIEnabled { get; set; } + public bool IsAIEnabled { get; set; } + + private bool? _legacyAdvancedAIEnabled; + + [JsonPropertyName("IsAdvancedAIEnabled")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public BoolProperty LegacyAdvancedAIEnabledProperty + { + get => null; + set + { + if (value is not null) + { + LegacyAdvancedAIEnabled = value.Value; + } + } + } + + [JsonIgnore] + public bool? LegacyAdvancedAIEnabled + { + get => _legacyAdvancedAIEnabled; + private set => _legacyAdvancedAIEnabled = value; + } + + public bool TryConsumeLegacyAdvancedAIEnabled(out bool value) + { + if (_legacyAdvancedAIEnabled is bool flag) + { + value = flag; + _legacyAdvancedAIEnabled = null; + return true; + } + + value = default; + return false; + } [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool ShowCustomPreview { get; set; } @@ -37,6 +76,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool CloseAfterLosingFocus { get; set; } + [JsonConverter(typeof(BoolPropertyJsonConverter))] + public bool EnableClipboardPreview { get; set; } + [JsonPropertyName("advanced-paste-ui-hotkey")] public HotkeySettings AdvancedPasteUIShortcut { get; set; } @@ -57,7 +99,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library [CmdConfigureIgnoreAttribute] public AdvancedPasteAdditionalActions AdditionalActions { get; init; } + [JsonPropertyName("paste-ai-configuration")] + [CmdConfigureIgnoreAttribute] + public PasteAIConfiguration PasteAIConfiguration { get; set; } + public override string ToString() - => JsonSerializer.Serialize(this); + => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.AdvancedPasteProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/BasePTModuleSettings.cs b/src/settings-ui/Settings.UI.Library/BasePTModuleSettings.cs index ef99b37216..82869fd9aa 100644 --- a/src/settings-ui/Settings.UI.Library/BasePTModuleSettings.cs +++ b/src/settings-ui/Settings.UI.Library/BasePTModuleSettings.cs @@ -2,13 +2,32 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Text.Json; using System.Text.Json.Serialization; namespace Microsoft.PowerToys.Settings.UI.Library { + /// + /// Base class for all PowerToys module settings. + /// + /// + /// IMPORTANT for Native AOT compatibility: + /// When creating a new class that inherits from , + /// you MUST register it in by adding a + /// [JsonSerializable(typeof(YourNewSettingsClass))] attribute. + /// Failure to register the type will cause to throw + /// at runtime. + /// See for registration instructions. + /// public abstract class BasePTModuleSettings { + // Cached JsonSerializerOptions for Native AOT compatibility + private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions + { + TypeInfoResolver = SettingsSerializationContext.Default, + }; + // Gets or sets name of the powertoy module. [JsonPropertyName("name")] public string Name { get; set; } @@ -17,11 +36,33 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("version")] public string Version { get; set; } - // converts the current to a json string. + /// + /// Converts the current settings object to a JSON string. + /// + /// JSON string representation of this settings object. + /// + /// Thrown when the runtime type is not registered in . + /// All derived types must be registered with [JsonSerializable(typeof(YourType))] attribute. + /// + /// + /// This method uses Native AOT-compatible JSON serialization. The runtime type must be + /// registered in for serialization to work. + /// public virtual string ToJsonString() { // By default JsonSerializer will only serialize the properties in the base class. This can be avoided by passing the object type (more details at https://stackoverflow.com/a/62498888) - return JsonSerializer.Serialize(this, GetType()); + var runtimeType = GetType(); + + // For Native AOT compatibility, get JsonTypeInfo from the TypeInfoResolver + var typeInfo = _jsonSerializerOptions.TypeInfoResolver?.GetTypeInfo(runtimeType, _jsonSerializerOptions); + + if (typeInfo == null) + { + throw new InvalidOperationException($"Type {runtimeType.FullName} is not registered in SettingsSerializationContext. Please add it to the [JsonSerializable] attributes."); + } + + // Use AOT-friendly serialization + return JsonSerializer.Serialize(this, typeInfo); } public override int GetHashCode() diff --git a/src/settings-ui/Settings.UI.Library/BoolProperty.cs b/src/settings-ui/Settings.UI.Library/BoolProperty.cs index 6842770a79..96b0807dd1 100644 --- a/src/settings-ui/Settings.UI.Library/BoolProperty.cs +++ b/src/settings-ui/Settings.UI.Library/BoolProperty.cs @@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.BoolProperty); } public bool TryToCmdRepresentable(out string result) diff --git a/src/settings-ui/Settings.UI.Library/BoolPropertyJsonConverter.cs b/src/settings-ui/Settings.UI.Library/BoolPropertyJsonConverter.cs index 3cbc0b72b5..140bbffec1 100644 --- a/src/settings-ui/Settings.UI.Library/BoolPropertyJsonConverter.cs +++ b/src/settings-ui/Settings.UI.Library/BoolPropertyJsonConverter.cs @@ -12,14 +12,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library { public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - var boolProperty = JsonSerializer.Deserialize(ref reader, options); + var boolProperty = JsonSerializer.Deserialize(ref reader, SettingsSerializationContext.Default.BoolProperty); return boolProperty.Value; } public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) { var boolProperty = new BoolProperty(value); - JsonSerializer.Serialize(writer, boolProperty, options); + JsonSerializer.Serialize(writer, boolProperty, SettingsSerializationContext.Default.BoolProperty); } } } diff --git a/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs b/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs index 9102609b6b..c738335827 100644 --- a/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs +++ b/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs @@ -43,7 +43,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement)) { - Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText()); + Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText(), SettingsSerializationContext.Default.HotkeySettings); } } catch (Exception) diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs index c4d870cfbd..45548791b6 100644 --- a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs @@ -87,6 +87,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library public bool ShowColorName { get; set; } public override string ToString() - => JsonSerializer.Serialize(this); + => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerPropertiesVersion1.cs b/src/settings-ui/Settings.UI.Library/ColorPickerPropertiesVersion1.cs index 24f593213b..bc9f0d9f6f 100644 --- a/src/settings-ui/Settings.UI.Library/ColorPickerPropertiesVersion1.cs +++ b/src/settings-ui/Settings.UI.Library/ColorPickerPropertiesVersion1.cs @@ -54,6 +54,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library public bool ShowColorName { get; set; } public override string ToString() - => JsonSerializer.Serialize(this); + => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ColorPickerPropertiesVersion1); } } diff --git a/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs b/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs new file mode 100644 index 0000000000..cf66b4ba09 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/CursorWrapProperties.cs @@ -0,0 +1,32 @@ +// 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.Text.Json.Serialization; + +using Settings.UI.Library.Attributes; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class CursorWrapProperties + { + [CmdConfigureIgnore] + public HotkeySettings DefaultActivationShortcut => new HotkeySettings(true, false, true, false, 0x55); // Win + Alt + U + + [JsonPropertyName("activation_shortcut")] + public HotkeySettings ActivationShortcut { get; set; } + + [JsonPropertyName("auto_activate")] + public BoolProperty AutoActivate { get; set; } + + [JsonPropertyName("disable_wrap_during_drag")] + public BoolProperty DisableWrapDuringDrag { get; set; } + + public CursorWrapProperties() + { + ActivationShortcut = DefaultActivationShortcut; + AutoActivate = new BoolProperty(false); + DisableWrapDuringDrag = new BoolProperty(true); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs b/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs new file mode 100644 index 0000000000..8c9059123c --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/CursorWrapSettings.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class CursorWrapSettings : BasePTModuleSettings, ISettingsConfig, IHotkeyConfig + { + public const string ModuleName = "CursorWrap"; + + [JsonPropertyName("properties")] + public CursorWrapProperties Properties { get; set; } + + public CursorWrapSettings() + { + Name = ModuleName; + Properties = new CursorWrapProperties(); + Version = "1.0"; + } + + public string GetModuleName() + { + return Name; + } + + public ModuleType GetModuleType() => ModuleType.CursorWrap; + + public HotkeyAccessor[] GetAllHotkeyAccessors() + { + var hotkeyAccessors = new List + { + new HotkeyAccessor( + () => Properties.ActivationShortcut, + value => Properties.ActivationShortcut = value ?? Properties.DefaultActivationShortcut, + "MouseUtils_CursorWrap_ActivationShortcut"), + }; + + return hotkeyAccessors.ToArray(); + } + + // This can be utilized in the future if the settings.json file is to be modified/deleted. + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/DoubleProperty.cs b/src/settings-ui/Settings.UI.Library/DoubleProperty.cs index 7854f70529..701268b3be 100644 --- a/src/settings-ui/Settings.UI.Library/DoubleProperty.cs +++ b/src/settings-ui/Settings.UI.Library/DoubleProperty.cs @@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library // Returns a JSON version of the class settings configuration class. public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.DoubleProperty); } } } diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index 977c03b839..d7100d9ae4 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -513,6 +513,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool cursorWrap; // defaulting to off + + [JsonPropertyName("CursorWrap")] + public bool CursorWrap + { + get => cursorWrap; + set + { + if (cursorWrap != value) + { + LogTelemetryEvent(value); + cursorWrap = value; + } + } + } + private bool lightSwitch; [JsonPropertyName("LightSwitch")] diff --git a/src/settings-ui/Settings.UI.Library/Enumerations/HostsDeleteBackupMode.cs b/src/settings-ui/Settings.UI.Library/Enumerations/HostsDeleteBackupMode.cs new file mode 100644 index 0000000000..782bcccf48 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/Enumerations/HostsDeleteBackupMode.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Settings.UI.Library.Enumerations +{ + public enum HostsDeleteBackupMode + { + Never = 0, + Count = 1, + Age = 2, + } +} diff --git a/src/settings-ui/Settings.UI.Library/FileLocksmithLocalProperties.cs b/src/settings-ui/Settings.UI.Library/FileLocksmithLocalProperties.cs index 747baf6dff..57e3d842c4 100644 --- a/src/settings-ui/Settings.UI.Library/FileLocksmithLocalProperties.cs +++ b/src/settings-ui/Settings.UI.Library/FileLocksmithLocalProperties.cs @@ -21,7 +21,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public string ToJsonString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithLocalProperties); } // This function is required to implement the ISettingsConfig interface and obtain the settings configurations. diff --git a/src/settings-ui/Settings.UI.Library/FileLocksmithProperties.cs b/src/settings-ui/Settings.UI.Library/FileLocksmithProperties.cs index 3ec6e6d492..a6e37e9ba3 100644 --- a/src/settings-ui/Settings.UI.Library/FileLocksmithProperties.cs +++ b/src/settings-ui/Settings.UI.Library/FileLocksmithProperties.cs @@ -17,6 +17,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("bool_show_extended_menu")] public BoolProperty ExtendedContextMenuOnly { get; set; } - public override string ToString() => JsonSerializer.Serialize(this); + public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.FileLocksmithProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs index 24ff4584fe..8bd9390df7 100644 --- a/src/settings-ui/Settings.UI.Library/GeneralSettings.cs +++ b/src/settings-ui/Settings.UI.Library/GeneralSettings.cs @@ -13,6 +13,12 @@ using Settings.UI.Library.Attributes; namespace Microsoft.PowerToys.Settings.UI.Library { + public enum DashboardSortOrder + { + Alphabetical, + ByStatus, + } + public class GeneralSettings : ISettingsConfig { // Gets or sets a value indicating whether run powertoys on start-up. @@ -76,6 +82,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("enable_experimentation")] public bool EnableExperimentation { get; set; } + [JsonPropertyName("dashboard_sort_order")] + public DashboardSortOrder DashboardSortOrder { get; set; } + [JsonPropertyName("ignored_conflict_properties")] public ShortcutConflictProperties IgnoredConflictProperties { get; set; } @@ -89,6 +98,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library ShowNewUpdatesToastNotification = true; AutoDownloadUpdates = false; EnableExperimentation = true; + DashboardSortOrder = DashboardSortOrder.Alphabetical; Theme = "system"; SystemTheme = "light"; try @@ -109,7 +119,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library // converts the current to a json string. public string ToJsonString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettings); } private static string DefaultPowertoysVersion() diff --git a/src/settings-ui/Settings.UI.Library/GeneralSettingsCustomAction.cs b/src/settings-ui/Settings.UI.Library/GeneralSettingsCustomAction.cs index f180ca3186..d9cc2727e8 100644 --- a/src/settings-ui/Settings.UI.Library/GeneralSettingsCustomAction.cs +++ b/src/settings-ui/Settings.UI.Library/GeneralSettingsCustomAction.cs @@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.GeneralSettingsCustomAction); } } } diff --git a/src/settings-ui/Settings.UI.Library/Helpers/SunTimes.cs b/src/settings-ui/Settings.UI.Library/Helpers/SunTimes.cs index 2f4f31fc57..6c24d1c557 100644 --- a/src/settings-ui/Settings.UI.Library/Helpers/SunTimes.cs +++ b/src/settings-ui/Settings.UI.Library/Helpers/SunTimes.cs @@ -12,13 +12,18 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Helpers { public struct SunTimes { - public int SunriseHour; - public int SunriseMinute; - public int SunsetHour; - public int SunsetMinute; - public string Text; + public int SunriseHour { get; set; } - public bool HasSunrise; - public bool HasSunset; + public int SunriseMinute { get; set; } + + public int SunsetHour { get; set; } + + public int SunsetMinute { get; set; } + + public string Text { get; set; } + + public bool HasSunrise { get; set; } + + public bool HasSunset { get; set; } } } diff --git a/src/settings-ui/Settings.UI.Library/HostsProperties.cs b/src/settings-ui/Settings.UI.Library/HostsProperties.cs index 6ec9924049..b4feedce45 100644 --- a/src/settings-ui/Settings.UI.Library/HostsProperties.cs +++ b/src/settings-ui/Settings.UI.Library/HostsProperties.cs @@ -1,10 +1,10 @@ -// Copyright (c) Microsoft Corporation +// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.IO; using System.Text.Json.Serialization; - -using Settings.UI.Library.Attributes; using Settings.UI.Library.Enumerations; namespace Microsoft.PowerToys.Settings.UI.Library @@ -27,6 +27,17 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonConverter(typeof(BoolPropertyJsonConverter))] public bool NoLeadingSpaces { get; set; } + [JsonConverter(typeof(BoolPropertyJsonConverter))] + public bool BackupHosts { get; set; } + + public string BackupPath { get; set; } + + public HostsDeleteBackupMode DeleteBackupsMode { get; set; } + + public int DeleteBackupsDays { get; set; } + + public int DeleteBackupsCount { get; set; } + public HostsProperties() { ShowStartupWarning = true; @@ -35,6 +46,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library AdditionalLinesPosition = HostsAdditionalLinesPosition.Top; Encoding = HostsEncoding.Utf8; NoLeadingSpaces = false; + BackupHosts = true; + BackupPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"System32\drivers\etc"); + DeleteBackupsMode = HostsDeleteBackupMode.Age; + DeleteBackupsDays = 15; + DeleteBackupsCount = 5; } } } diff --git a/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs b/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs index acef5a2f4d..90080d1c5a 100644 --- a/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ImageResizerProperties.cs @@ -86,7 +86,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public string ToJsonString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ImageResizerProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/ImageResizerSettings.cs b/src/settings-ui/Settings.UI.Library/ImageResizerSettings.cs index 97f785d6c2..f3dffca9a0 100644 --- a/src/settings-ui/Settings.UI.Library/ImageResizerSettings.cs +++ b/src/settings-ui/Settings.UI.Library/ImageResizerSettings.cs @@ -37,8 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public override string ToJsonString() { - var options = _serializerOptions; - return JsonSerializer.Serialize(this, options); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.ImageResizerSettings); } public string GetModuleName() diff --git a/src/settings-ui/Settings.UI.Library/IntProperty.cs b/src/settings-ui/Settings.UI.Library/IntProperty.cs index ac6a87ad4a..63fae3e22b 100644 --- a/src/settings-ui/Settings.UI.Library/IntProperty.cs +++ b/src/settings-ui/Settings.UI.Library/IntProperty.cs @@ -42,7 +42,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library // Returns a JSON version of the class settings configuration class. public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.IntProperty); } public static implicit operator IntProperty(int v) diff --git a/src/settings-ui/Settings.UI.Library/KeyboardManagerProfile.cs b/src/settings-ui/Settings.UI.Library/KeyboardManagerProfile.cs index 983f9a1f6a..091c70eb39 100644 --- a/src/settings-ui/Settings.UI.Library/KeyboardManagerProfile.cs +++ b/src/settings-ui/Settings.UI.Library/KeyboardManagerProfile.cs @@ -34,7 +34,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public string ToJsonString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.KeyboardManagerProfile); } public string GetModuleName() diff --git a/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs b/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs index 5edc9f9175..48fc544ed4 100644 --- a/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MeasureToolProperties.cs @@ -46,6 +46,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library public IntProperty DefaultMeasureStyle { get; set; } - public override string ToString() => JsonSerializer.Serialize(this); + public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.MeasureToolProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs index e234ee6b2f..2970cdb654 100644 --- a/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs +++ b/src/settings-ui/Settings.UI.Library/MouseWithoutBordersProperties.cs @@ -15,8 +15,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library public struct ConnectionRequest #pragma warning restore SA1649 // File name should match first type name { - public string PCName; - public string SecurityKey; + public string PCName { get; set; } + + public string SecurityKey { get; set; } } public struct NewKeyGenerationRequest diff --git a/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs b/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs index ccdbeb9601..c2ad8cc328 100644 --- a/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs +++ b/src/settings-ui/Settings.UI.Library/NewPlusProperties.cs @@ -33,6 +33,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library [JsonPropertyName("ReplaceVariables")] public BoolProperty ReplaceVariables { get; set; } - public override string ToString() => JsonSerializer.Serialize(this); + public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.NewPlusProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/OutGoingGeneralSettings.cs b/src/settings-ui/Settings.UI.Library/OutGoingGeneralSettings.cs index d040016660..dad5f02b25 100644 --- a/src/settings-ui/Settings.UI.Library/OutGoingGeneralSettings.cs +++ b/src/settings-ui/Settings.UI.Library/OutGoingGeneralSettings.cs @@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingGeneralSettings); } } } diff --git a/src/settings-ui/Settings.UI.Library/OutGoingLanguageSettings.cs b/src/settings-ui/Settings.UI.Library/OutGoingLanguageSettings.cs index 61a16eb778..7f2d55faf6 100644 --- a/src/settings-ui/Settings.UI.Library/OutGoingLanguageSettings.cs +++ b/src/settings-ui/Settings.UI.Library/OutGoingLanguageSettings.cs @@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.OutGoingLanguageSettings); } } } diff --git a/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs b/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs new file mode 100644 index 0000000000..2b743c4670 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PasteAIConfiguration.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Configuration for Paste AI features (custom action transformations like custom prompt processing) + /// + public class PasteAIConfiguration : INotifyPropertyChanged + { + private string _activeProviderId = string.Empty; + private ObservableCollection _providers = new(); + + public event PropertyChangedEventHandler PropertyChanged; + + [JsonPropertyName("active-provider-id")] + public string ActiveProviderId + { + get => _activeProviderId; + set => SetProperty(ref _activeProviderId, value ?? string.Empty); + } + + [JsonPropertyName("providers")] + public ObservableCollection Providers + { + get => _providers; + set => SetProperty(ref _providers, value ?? new ObservableCollection()); + } + + [JsonIgnore] + public PasteAIProviderDefinition ActiveProvider + { + get + { + if (_providers is null || _providers.Count == 0) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(_activeProviderId)) + { + var match = _providers.FirstOrDefault(provider => string.Equals(provider.Id, _activeProviderId, StringComparison.OrdinalIgnoreCase)); + if (match is not null) + { + return match; + } + } + + return _providers[0]; + } + } + + [JsonIgnore] + public AIServiceType ActiveServiceTypeKind => ActiveProvider?.ServiceTypeKind ?? AIServiceType.OpenAI; + + public override string ToString() + => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PasteAIConfiguration); + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + field = value; + OnPropertyChanged(propertyName); + return true; + } + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs new file mode 100644 index 0000000000..1ccfa753fa --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefaults.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Provides default values for Paste AI provider definitions. + /// + public static class PasteAIProviderDefaults + { + /// + /// Gets the default model name for a given AI service type. + /// + public static string GetDefaultModelName(AIServiceType serviceType) + { + return serviceType switch + { + AIServiceType.OpenAI => "gpt-4o", + AIServiceType.AzureOpenAI => "gpt-4o", + AIServiceType.Mistral => "mistral-large-latest", + AIServiceType.Google => "gemini-1.5-pro", + AIServiceType.AzureAIInference => "gpt-4o-mini", + AIServiceType.Ollama => "llama3", + _ => string.Empty, + }; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs new file mode 100644 index 0000000000..0fbb3328e7 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/PasteAIProviderDefinition.cs @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// Represents a single Paste AI provider configuration entry. + /// + public class PasteAIProviderDefinition : INotifyPropertyChanged + { + private string _id = Guid.NewGuid().ToString("N"); + private string _serviceType = "OpenAI"; + private string _modelName = string.Empty; + private string _endpointUrl = string.Empty; + private string _apiVersion = string.Empty; + private string _deploymentName = string.Empty; + private string _modelPath = string.Empty; + private string _systemPrompt = string.Empty; + private bool _moderationEnabled = true; + private bool _isActive; + private bool _enableAdvancedAI; + private bool _isLocalModel; + + public event PropertyChangedEventHandler PropertyChanged; + + [JsonPropertyName("id")] + public string Id + { + get => _id; + set => SetProperty(ref _id, value); + } + + [JsonPropertyName("service-type")] + public string ServiceType + { + get => _serviceType; + set + { + if (SetProperty(ref _serviceType, string.IsNullOrWhiteSpace(value) ? "OpenAI" : value)) + { + OnPropertyChanged(nameof(DisplayName)); + } + } + } + + [JsonIgnore] + public AIServiceType ServiceTypeKind + { + get => ServiceType.ToAIServiceType(); + set => ServiceType = value.ToConfigurationString(); + } + + [JsonPropertyName("model-name")] + public string ModelName + { + get => _modelName; + set + { + if (SetProperty(ref _modelName, value ?? string.Empty)) + { + OnPropertyChanged(nameof(DisplayName)); + } + } + } + + [JsonPropertyName("endpoint-url")] + public string EndpointUrl + { + get => _endpointUrl; + set => SetProperty(ref _endpointUrl, value ?? string.Empty); + } + + [JsonPropertyName("api-version")] + public string ApiVersion + { + get => _apiVersion; + set => SetProperty(ref _apiVersion, value ?? string.Empty); + } + + [JsonPropertyName("deployment-name")] + public string DeploymentName + { + get => _deploymentName; + set => SetProperty(ref _deploymentName, value ?? string.Empty); + } + + [JsonPropertyName("model-path")] + public string ModelPath + { + get => _modelPath; + set => SetProperty(ref _modelPath, value ?? string.Empty); + } + + [JsonPropertyName("system-prompt")] + public string SystemPrompt + { + get => _systemPrompt; + set => SetProperty(ref _systemPrompt, value?.Trim() ?? string.Empty); + } + + [JsonPropertyName("moderation-enabled")] + public bool ModerationEnabled + { + get => _moderationEnabled; + set => SetProperty(ref _moderationEnabled, value); + } + + [JsonPropertyName("enable-advanced-ai")] + public bool EnableAdvancedAI + { + get => _enableAdvancedAI; + set => SetProperty(ref _enableAdvancedAI, value); + } + + [JsonPropertyName("is-local-model")] + public bool IsLocalModel + { + get => _isLocalModel; + set => SetProperty(ref _isLocalModel, value); + } + + [JsonIgnore] + public bool IsActive + { + get => _isActive; + set => SetProperty(ref _isActive, value); + } + + [JsonIgnore] + public string DisplayName => string.IsNullOrWhiteSpace(ModelName) ? ServiceType : ModelName; + + public PasteAIProviderDefinition Clone() + { + return new PasteAIProviderDefinition + { + Id = Id, + ServiceType = ServiceType, + ModelName = ModelName, + EndpointUrl = EndpointUrl, + ApiVersion = ApiVersion, + DeploymentName = DeploymentName, + ModelPath = ModelPath, + SystemPrompt = SystemPrompt, + ModerationEnabled = ModerationEnabled, + EnableAdvancedAI = EnableAdvancedAI, + IsLocalModel = IsLocalModel, + IsActive = IsActive, + }; + } + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + field = value; + OnPropertyChanged(propertyName); + return true; + } + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs b/src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs index fa2fb3bf6c..b139076fe8 100644 --- a/src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs +++ b/src/settings-ui/Settings.UI.Library/PeekPreviewSettings.cs @@ -34,7 +34,7 @@ namespace Settings.UI.Library public string ToJsonString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, Microsoft.PowerToys.Settings.UI.Library.SettingsSerializationContext.Default.PeekPreviewSettings); } public string GetModuleName() diff --git a/src/settings-ui/Settings.UI.Library/PeekProperties.cs b/src/settings-ui/Settings.UI.Library/PeekProperties.cs index e6eea746d6..e8b8692888 100644 --- a/src/settings-ui/Settings.UI.Library/PeekProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PeekProperties.cs @@ -32,6 +32,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library public BoolProperty EnableSpaceToActivate { get; set; } - public override string ToString() => JsonSerializer.Serialize(this); + public override string ToString() => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PeekProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs b/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs index 5cba3cfb3e..1570038f22 100644 --- a/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerOcrProperties.cs @@ -24,6 +24,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library public string PreferredLanguage { get; set; } public override string ToString() - => JsonSerializer.Serialize(this); + => JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PowerOcrProperties); } } diff --git a/src/settings-ui/Settings.UI.Library/PowerPreviewProperties.cs b/src/settings-ui/Settings.UI.Library/PowerPreviewProperties.cs index 72113e67fc..b99b19aae7 100644 --- a/src/settings-ui/Settings.UI.Library/PowerPreviewProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerPreviewProperties.cs @@ -340,7 +340,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PowerPreviewProperties); } private static void LogTelemetryEvent(bool value, [CallerMemberName] string propertyName = null) diff --git a/src/settings-ui/Settings.UI.Library/PowerRenameLocalProperties.cs b/src/settings-ui/Settings.UI.Library/PowerRenameLocalProperties.cs index 726faf2bc0..cec4b472c7 100644 --- a/src/settings-ui/Settings.UI.Library/PowerRenameLocalProperties.cs +++ b/src/settings-ui/Settings.UI.Library/PowerRenameLocalProperties.cs @@ -54,7 +54,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library public string ToJsonString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.PowerRenameLocalProperties); } // This function is required to implement the ISettingsConfig interface and obtain the settings configurations. diff --git a/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs new file mode 100644 index 0000000000..8ffcfb2e7c --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/SettingsSerializationContext.cs @@ -0,0 +1,172 @@ +// 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.Text.Json.Serialization; +using SettingsUILibrary = Settings.UI.Library; +using SettingsUILibraryHelpers = Settings.UI.Library.Helpers; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + /// + /// JSON serialization context for Native AOT compatibility. + /// This context provides source-generated serialization for all PowerToys settings types. + /// + /// + /// âš ï¸ CRITICAL REQUIREMENT FOR ALL NEW SETTINGS CLASSES âš ï¸ + /// + /// When adding a new PowerToys module or any class that inherits from , + /// you MUST add a [JsonSerializable(typeof(YourNewSettingsClass))] attribute + /// to this class. This is a MANDATORY step for Native AOT compatibility. + /// + /// Steps to add a new settings class: + /// + /// Create your new settings class (e.g., MyNewModuleSettings) that inherits from + /// Add [JsonSerializable(typeof(MyNewModuleSettings))] attribute to this class + /// If you have a corresponding Properties class, also add [JsonSerializable(typeof(MyNewModuleProperties))] + /// Rebuild the project - source generator will create serialization code at compile time + /// + /// âš ï¸ Failure to register types will cause runtime errors: + /// + /// If you forget to add the [JsonSerializable] attribute, calling ToJsonString() or + /// deserialization methods will throw at runtime with a clear + /// error message indicating which type is missing registration. + /// + /// + [JsonSourceGenerationOptions( + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true)] + + // Main Settings Classes + [JsonSerializable(typeof(GeneralSettings))] + [JsonSerializable(typeof(AdvancedPasteSettings))] + [JsonSerializable(typeof(AlwaysOnTopSettings))] + [JsonSerializable(typeof(AwakeSettings))] + [JsonSerializable(typeof(CmdNotFoundSettings))] + [JsonSerializable(typeof(ColorPickerSettings))] + [JsonSerializable(typeof(ColorPickerSettingsVersion1))] + [JsonSerializable(typeof(CropAndLockSettings))] + [JsonSerializable(typeof(CursorWrapSettings))] + [JsonSerializable(typeof(EnvironmentVariablesSettings))] + [JsonSerializable(typeof(FancyZonesSettings))] + [JsonSerializable(typeof(FileLocksmithSettings))] + [JsonSerializable(typeof(FindMyMouseSettings))] + [JsonSerializable(typeof(HostsSettings))] + [JsonSerializable(typeof(ImageResizerSettings))] + [JsonSerializable(typeof(KeyboardManagerSettings))] + [JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))] + [JsonSerializable(typeof(MeasureToolSettings))] + [JsonSerializable(typeof(MouseHighlighterSettings))] + [JsonSerializable(typeof(MouseJumpSettings))] + [JsonSerializable(typeof(MousePointerCrosshairsSettings))] + [JsonSerializable(typeof(MouseWithoutBordersSettings))] + [JsonSerializable(typeof(NewPlusSettings))] + [JsonSerializable(typeof(PeekSettings))] + [JsonSerializable(typeof(PowerAccentSettings))] + [JsonSerializable(typeof(PowerLauncherSettings))] + [JsonSerializable(typeof(PowerOcrSettings))] + [JsonSerializable(typeof(PowerPreviewSettings))] + [JsonSerializable(typeof(PowerRenameSettings))] + [JsonSerializable(typeof(RegistryPreviewSettings))] + [JsonSerializable(typeof(ShortcutGuideSettings))] + [JsonSerializable(typeof(WorkspacesSettings))] + [JsonSerializable(typeof(ZoomItSettings))] + + // Properties Classes + [JsonSerializable(typeof(AdvancedPasteProperties))] + [JsonSerializable(typeof(AlwaysOnTopProperties))] + [JsonSerializable(typeof(AwakeProperties))] + [JsonSerializable(typeof(CmdPalProperties))] + [JsonSerializable(typeof(ColorPickerProperties))] + [JsonSerializable(typeof(ColorPickerPropertiesVersion1))] + [JsonSerializable(typeof(CropAndLockProperties))] + [JsonSerializable(typeof(CursorWrapProperties))] + [JsonSerializable(typeof(EnvironmentVariablesProperties))] + [JsonSerializable(typeof(FileLocksmithProperties))] + [JsonSerializable(typeof(FileLocksmithLocalProperties))] + [JsonSerializable(typeof(FindMyMouseProperties))] + [JsonSerializable(typeof(FZConfigProperties))] + [JsonSerializable(typeof(HostsProperties))] + [JsonSerializable(typeof(ImageResizerProperties))] + [JsonSerializable(typeof(KeyboardManagerProperties))] + [JsonSerializable(typeof(KeyboardManagerProfile))] + [JsonSerializable(typeof(LightSwitchProperties))] + [JsonSerializable(typeof(MeasureToolProperties))] + [JsonSerializable(typeof(MouseHighlighterProperties))] + [JsonSerializable(typeof(MouseJumpProperties))] + [JsonSerializable(typeof(MousePointerCrosshairsProperties))] + [JsonSerializable(typeof(MouseWithoutBordersProperties))] + [JsonSerializable(typeof(NewPlusProperties))] + [JsonSerializable(typeof(PeekProperties))] + [JsonSerializable(typeof(SettingsUILibrary.PeekPreviewSettings))] + [JsonSerializable(typeof(PowerAccentProperties))] + [JsonSerializable(typeof(PowerLauncherProperties))] + [JsonSerializable(typeof(PowerOcrProperties))] + [JsonSerializable(typeof(PowerPreviewProperties))] + [JsonSerializable(typeof(PowerRenameProperties))] + [JsonSerializable(typeof(PowerRenameLocalProperties))] + [JsonSerializable(typeof(RegistryPreviewProperties))] + [JsonSerializable(typeof(ShortcutConflictProperties))] + [JsonSerializable(typeof(ShortcutGuideProperties))] + [JsonSerializable(typeof(WorkspacesProperties))] + [JsonSerializable(typeof(ZoomItProperties))] + + // Base Property Types (used throughout settings) + [JsonSerializable(typeof(BoolProperty))] + [JsonSerializable(typeof(StringProperty))] + [JsonSerializable(typeof(IntProperty))] + [JsonSerializable(typeof(DoubleProperty))] + + // Helper and Utility Types + [JsonSerializable(typeof(HotkeySettings))] + [JsonSerializable(typeof(ColorFormatModel))] + [JsonSerializable(typeof(ImageSize))] + [JsonSerializable(typeof(KeysDataModel))] + [JsonSerializable(typeof(EnabledModules))] + [JsonSerializable(typeof(GeneralSettingsCustomAction))] + [JsonSerializable(typeof(OutGoingGeneralSettings))] + [JsonSerializable(typeof(OutGoingLanguageSettings))] + [JsonSerializable(typeof(AdvancedPasteCustomActions))] + [JsonSerializable(typeof(AdvancedPasteAdditionalActions))] + [JsonSerializable(typeof(AdvancedPasteCustomAction))] + [JsonSerializable(typeof(AdvancedPasteAdditionalAction))] + [JsonSerializable(typeof(AdvancedPastePasteAsFileAction))] + [JsonSerializable(typeof(AdvancedPasteTranscodeAction))] + [JsonSerializable(typeof(PasteAIConfiguration))] + [JsonSerializable(typeof(PasteAIProviderDefinition))] + [JsonSerializable(typeof(ImageResizerSizes))] + [JsonSerializable(typeof(ImageResizerCustomSizeProperty))] + [JsonSerializable(typeof(KeyboardKeysProperty))] + [JsonSerializable(typeof(SettingsUILibraryHelpers.SearchLocation))] + + // IPC Send Message Wrapper Classes (Snd*) + [JsonSerializable(typeof(SndAwakeSettings))] + [JsonSerializable(typeof(SndCursorWrapSettings))] + [JsonSerializable(typeof(SndFindMyMouseSettings))] + [JsonSerializable(typeof(SndLightSwitchSettings))] + [JsonSerializable(typeof(SndMouseHighlighterSettings))] + [JsonSerializable(typeof(SndMouseJumpSettings))] + [JsonSerializable(typeof(SndMousePointerCrosshairsSettings))] + [JsonSerializable(typeof(SndPowerAccentSettings))] + [JsonSerializable(typeof(SndPowerPreviewSettings))] + [JsonSerializable(typeof(SndPowerRenameSettings))] + [JsonSerializable(typeof(SndShortcutGuideSettings))] + + // IPC Message Generic Wrapper Types (SndModuleSettings) + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + [JsonSerializable(typeof(SndModuleSettings))] + + public partial class SettingsSerializationContext : JsonSerializerContext + { + } +} diff --git a/src/settings-ui/Settings.UI.Library/SettingsUtils.cs b/src/settings-ui/Settings.UI.Library/SettingsUtils.cs index ea466f7077..6109df0646 100644 --- a/src/settings-ui/Settings.UI.Library/SettingsUtils.cs +++ b/src/settings-ui/Settings.UI.Library/SettingsUtils.cs @@ -2,6 +2,8 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#nullable enable + using System; using System.IO; using System.IO.Abstractions; @@ -18,27 +20,28 @@ namespace Microsoft.PowerToys.Settings.UI.Library private const string DefaultModuleName = ""; private readonly IFile _file; private readonly ISettingsPath _settingsPath; - - private static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions - { - MaxDepth = 0, - IncludeFields = true, - }; + private readonly JsonSerializerOptions _serializerOptions; public SettingsUtils() : this(new FileSystem()) { } - public SettingsUtils(IFileSystem fileSystem) - : this(fileSystem?.File, new SettingPath(fileSystem?.Directory, fileSystem?.Path)) + public SettingsUtils(IFileSystem? fileSystem, JsonSerializerOptions? serializerOptions = null) + : this(fileSystem?.File!, new SettingPath(fileSystem?.Directory, fileSystem?.Path), serializerOptions) { } - public SettingsUtils(IFile file, ISettingsPath settingPath) + public SettingsUtils(IFile file, ISettingsPath settingPath, JsonSerializerOptions? serializerOptions = null) { _file = file ?? throw new ArgumentNullException(nameof(file)); _settingsPath = settingPath; + _serializerOptions = serializerOptions ?? new JsonSerializerOptions + { + MaxDepth = 0, + IncludeFields = true, + TypeInfoResolver = SettingsSerializationContext.Default, + }; } public bool SettingsExists(string powertoy = DefaultModuleName, string fileName = DefaultFileName) @@ -108,7 +111,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library /// This function creates a file in the powertoy folder if it does not exist and returns an object with default properties. /// /// Deserialized json settings object. - public T GetSettingsOrDefault(string powertoy = DefaultModuleName, string fileName = DefaultFileName, Func settingsUpgrader = null) + public T GetSettingsOrDefault(string powertoy = DefaultModuleName, string fileName = DefaultFileName, Func? settingsUpgrader = null) where T : ISettingsConfig, new() where T2 : ISettingsConfig, new() { @@ -128,7 +131,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library try { T2 oldSettings = GetSettings(powertoy, fileName); - T newSettings = (T)settingsUpgrader(oldSettings); + T newSettings = (T)settingsUpgrader!(oldSettings); Logger.LogInfo($"Settings file {fileName} for {powertoy} was read successfully in the old format."); // If the file needs to be modified, to save the new configurations accordingly. @@ -156,7 +159,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library return newSettingsItem; } - // Given the powerToy folder name and filename to be accessed, this function deserializes and returns the file. + /// + /// Deserializes settings from a JSON file. + /// + /// The settings type to deserialize. Must be registered in . + /// The PowerToy module folder name. + /// The settings file name. + /// Deserialized settings object of type T. + /// + /// Thrown when type T is not registered in . + /// All settings types must be registered with [JsonSerializable(typeof(T))] attribute + /// for Native AOT compatibility. + /// + /// + /// This method uses Native AOT-compatible JSON deserialization. Type T must be registered + /// in before calling this method. + /// private T GetFile(string powertoyFolderName = DefaultModuleName, string fileName = DefaultFileName) { // Adding Trim('\0') to overcome possible NTFS file corruption. @@ -165,8 +183,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library // The file itself did write the content correctly but something is off with the actual end of the file, hence the 0x00 bug var jsonSettingsString = _file.ReadAllText(_settingsPath.GetSettingsPath(powertoyFolderName, fileName)).Trim('\0'); - var options = _serializerOptions; - return JsonSerializer.Deserialize(jsonSettingsString, options); + // For Native AOT compatibility, get JsonTypeInfo from the TypeInfoResolver + var typeInfo = _serializerOptions.TypeInfoResolver?.GetTypeInfo(typeof(T), _serializerOptions); + + if (typeInfo == null) + { + throw new InvalidOperationException($"Type {typeof(T).FullName} is not registered in SettingsSerializationContext. Please add it to the [JsonSerializable] attributes."); + } + + // Use AOT-friendly deserialization + return (T)JsonSerializer.Deserialize(jsonSettingsString, typeInfo)!; } // Save settings to a json file. diff --git a/src/settings-ui/Settings.UI.Library/SndCursorWrapSettings.cs b/src/settings-ui/Settings.UI.Library/SndCursorWrapSettings.cs new file mode 100644 index 0000000000..3d6d781d03 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/SndCursorWrapSettings.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class SndCursorWrapSettings + { + [JsonPropertyName("CursorWrap")] + public CursorWrapSettings CursorWrap { get; set; } + + public SndCursorWrapSettings() + { + } + + public SndCursorWrapSettings(CursorWrapSettings settings) + { + CursorWrap = settings; + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/StringProperty.cs b/src/settings-ui/Settings.UI.Library/StringProperty.cs index 8b0d86b177..5d521189dc 100644 --- a/src/settings-ui/Settings.UI.Library/StringProperty.cs +++ b/src/settings-ui/Settings.UI.Library/StringProperty.cs @@ -27,7 +27,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library // Returns a JSON version of the class settings configuration class. public override string ToString() { - return JsonSerializer.Serialize(this); + return JsonSerializer.Serialize(this, SettingsSerializationContext.Default.StringProperty); } public static StringProperty ToStringProperty(string v) diff --git a/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs b/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs index 9325907a72..1bca1b573a 100644 --- a/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs +++ b/src/settings-ui/Settings.UI.Library/ZoomItProperties.cs @@ -87,6 +87,8 @@ namespace Microsoft.PowerToys.Settings.UI.Library public IntProperty RecordScaling { get; set; } + public StringProperty RecordFormat { get; set; } + public BoolProperty CaptureAudio { get; set; } public StringProperty MicrophoneDeviceId { get; set; } diff --git a/src/settings-ui/Settings.UI.UnitTests/ModelsTests/BasePTModuleSettingsSerializationTests.cs b/src/settings-ui/Settings.UI.UnitTests/ModelsTests/BasePTModuleSettingsSerializationTests.cs new file mode 100644 index 0000000000..29fe1d0508 --- /dev/null +++ b/src/settings-ui/Settings.UI.UnitTests/ModelsTests/BasePTModuleSettingsSerializationTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommonLibTest +{ + [TestClass] + public class BasePTModuleSettingsSerializationTests + { + /// + /// Test to verify that all classes derived from BasePTModuleSettings are registered + /// in the SettingsSerializationContext for Native AOT compatibility. + /// + [TestMethod] + public void AllBasePTModuleSettingsClasses_ShouldBeRegisteredInSerializationContext() + { + // Arrange + var assembly = typeof(BasePTModuleSettings).Assembly; + var settingsClasses = assembly.GetTypes() + .Where(t => typeof(BasePTModuleSettings).IsAssignableFrom(t) && !t.IsAbstract && t != typeof(BasePTModuleSettings)) + .OrderBy(t => t.Name) + .ToList(); + + Assert.IsTrue(settingsClasses.Count > 0, "No BasePTModuleSettings derived classes found. This test may be broken."); + + var jsonSerializerOptions = new JsonSerializerOptions + { + TypeInfoResolver = SettingsSerializationContext.Default, + }; + + var unregisteredTypes = new System.Collections.Generic.List(); + + // Act & Assert + foreach (var settingsType in settingsClasses) + { + var typeInfo = jsonSerializerOptions.TypeInfoResolver?.GetTypeInfo(settingsType, jsonSerializerOptions); + + if (typeInfo == null) + { + unregisteredTypes.Add(settingsType.FullName ?? settingsType.Name); + } + } + + // Assert + if (unregisteredTypes.Count > 0) + { + var errorMessage = $"The following {unregisteredTypes.Count} settings class(es) are NOT registered in SettingsSerializationContext:\n" + + $"{string.Join("\n", unregisteredTypes.Select(t => $" - {t}"))}\n\n" + + $"Please add [JsonSerializable(typeof(ClassName))] attribute to SettingsSerializationContext.cs for each missing type."; + Assert.Fail(errorMessage); + } + + // Print success message with count + Console.WriteLine($"✓ All {settingsClasses.Count} BasePTModuleSettings derived classes are properly registered in SettingsSerializationContext."); + } + + /// + /// Test to verify that calling ToJsonString() on an unregistered type throws InvalidOperationException + /// with a helpful error message. + /// + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToJsonString_UnregisteredType_ShouldThrowInvalidOperationException() + { + // Arrange + var unregisteredSettings = new UnregisteredTestSettings + { + Name = "UnregisteredModule", + Version = "1.0.0", + }; + + // Act - This should throw InvalidOperationException + var jsonString = unregisteredSettings.ToJsonString(); + + // Assert - Exception should be thrown, so this line should never be reached + Assert.Fail("Expected InvalidOperationException was not thrown."); + } + + /// + /// Test to verify that the error message for unregistered types is helpful and contains + /// necessary information for developers. + /// + [TestMethod] + public void ToJsonString_UnregisteredType_ShouldHaveHelpfulErrorMessage() + { + // Arrange + var unregisteredSettings = new UnregisteredTestSettings + { + Name = "UnregisteredModule", + Version = "1.0.0", + }; + + // Act & Assert + try + { + var jsonString = unregisteredSettings.ToJsonString(); + Assert.Fail("Expected InvalidOperationException was not thrown."); + } + catch (InvalidOperationException ex) + { + // Verify the error message contains helpful information + Assert.IsTrue(ex.Message.Contains("UnregisteredTestSettings"), "Error message should contain the type name."); + Assert.IsTrue(ex.Message.Contains("SettingsSerializationContext"), "Error message should mention SettingsSerializationContext."); + Assert.IsTrue(ex.Message.Contains("JsonSerializable"), "Error message should mention JsonSerializable attribute."); + + Console.WriteLine($"✓ Error message is helpful: {ex.Message}"); + } + } + + /// + /// Test class that is intentionally NOT registered in SettingsSerializationContext + /// to verify error handling for unregistered types. + /// + private sealed class UnregisteredTestSettings : BasePTModuleSettings + { + // Intentionally empty - this class should NOT be registered in SettingsSerializationContext + } + } +} diff --git a/src/settings-ui/Settings.UI.UnitTests/ModelsTests/BasePTSettingsTest.cs b/src/settings-ui/Settings.UI.UnitTests/ModelsTests/BasePTSettingsTest.cs index 8d73d2a6ee..c4e92fb322 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ModelsTests/BasePTSettingsTest.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ModelsTests/BasePTSettingsTest.cs @@ -2,8 +2,10 @@ // 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.Text.Json; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.UnitTests; namespace Microsoft.PowerToys.Settings.UnitTest { @@ -24,5 +26,11 @@ namespace Microsoft.PowerToys.Settings.UnitTest { return false; } + + // Override ToJsonString to use test-specific serialization context + public override string ToJsonString() + { + return JsonSerializer.Serialize(this, TestSettingsSerializationContext.Default.BasePTSettingsTest); + } } } diff --git a/src/settings-ui/Settings.UI.UnitTests/ModelsTests/SettingsUtilsTests.cs b/src/settings-ui/Settings.UI.UnitTests/ModelsTests/SettingsUtilsTests.cs index 9b75925cb8..37dfcee4d8 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ModelsTests/SettingsUtilsTests.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ModelsTests/SettingsUtilsTests.cs @@ -9,6 +9,7 @@ using System.Text.Json; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Microsoft.PowerToys.Settings.UI.UnitTests; using Microsoft.PowerToys.Settings.UnitTest; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,7 +23,13 @@ namespace CommonLibTest { // Arrange var mockFileSystem = new MockFileSystem(); - var settingsUtils = new SettingsUtils(mockFileSystem); + var testSerializerOptions = new JsonSerializerOptions + { + MaxDepth = 0, + IncludeFields = true, + TypeInfoResolver = TestSettingsSerializationContext.Default, + }; + var settingsUtils = new SettingsUtils(mockFileSystem, testSerializerOptions); string file_name = "\\test"; string file_contents_correct_json_content = "{\"name\":\"powertoy module name\",\"version\":\"powertoy version\"}"; @@ -42,7 +49,13 @@ namespace CommonLibTest { // Arrange var mockFileSystem = new MockFileSystem(); - var settingsUtils = new SettingsUtils(mockFileSystem); + var testSerializerOptions = new JsonSerializerOptions + { + MaxDepth = 0, + IncludeFields = true, + TypeInfoResolver = TestSettingsSerializationContext.Default, + }; + var settingsUtils = new SettingsUtils(mockFileSystem, testSerializerOptions); string file_name = "test\\Test Folder"; string file_contents_correct_json_content = "{\"name\":\"powertoy module name\",\"version\":\"powertoy version\"}"; diff --git a/src/settings-ui/Settings.UI.UnitTests/TestSettingsSerializationContext.cs b/src/settings-ui/Settings.UI.UnitTests/TestSettingsSerializationContext.cs new file mode 100644 index 0000000000..bf38245105 --- /dev/null +++ b/src/settings-ui/Settings.UI.UnitTests/TestSettingsSerializationContext.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UnitTest; + +namespace Microsoft.PowerToys.Settings.UI.UnitTests +{ + /// + /// JSON serialization context for unit tests. + /// This context provides source-generated serialization for test-specific types. + /// + [JsonSourceGenerationOptions( + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true)] + [JsonSerializable(typeof(BasePTSettingsTest))] + public partial class TestSettingsSerializationContext : JsonSerializerContext + { + } +} diff --git a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs index ef230bde0a..de6421785b 100644 --- a/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs +++ b/src/settings-ui/Settings.UI.UnitTests/ViewModelTests/FancyZones.cs @@ -6,6 +6,7 @@ using System; using System.Text.Json; using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.UnitTests.BackwardsCompatibility; using Microsoft.PowerToys.Settings.UI.UnitTests.Mocks; using Microsoft.PowerToys.Settings.UI.ViewModels; @@ -100,6 +101,22 @@ namespace ViewModelTests mockFancyZonesSettingsUtils = ISettingsUtilsMocks.GetStubSettingsUtils(); } + [TestCleanup] + public void CleanUp() + { + // Reset singleton instances to prevent state pollution between tests + ResetSettingsRepository(); + ResetSettingsRepository(); + } + + private void ResetSettingsRepository() + where T : class, ISettingsConfig, new() + { + var repositoryType = typeof(SettingsRepository); + var field = repositoryType.GetField("settingsRepository", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + field?.SetValue(null, null); + } + [TestMethod] public void IsEnabledShouldDisableModuleWhenSuccessful() { diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/CursorWrap.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/CursorWrap.png new file mode 100644 index 0000000000..c32f1e309a Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/CursorWrap.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png index 6cdb55cb66..581c317518 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/FindMyMouse.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Azure.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Azure.svg new file mode 100644 index 0000000000..7497187ad7 --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Azure.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/AzureAI.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/AzureAI.svg new file mode 100644 index 0000000000..e6fd7121b2 --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/AzureAI.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/FoundryLocal.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/FoundryLocal.svg new file mode 100644 index 0000000000..53747d557d --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/FoundryLocal.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Gemini.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Gemini.svg new file mode 100644 index 0000000000..56a5fe461b --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Gemini.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Mistral.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Mistral.svg new file mode 100644 index 0000000000..ce2471552e --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Mistral.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Ollama.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Ollama.svg new file mode 100644 index 0000000000..e44dda654d --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Ollama.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Onnx.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Onnx.svg new file mode 100644 index 0000000000..301a40fd55 --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/Onnx.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/OpenAI.dark.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/OpenAI.dark.svg new file mode 100644 index 0000000000..87aacb3a4f --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/OpenAI.dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/OpenAI.light.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/OpenAI.light.svg new file mode 100644 index 0000000000..f72a3c64d1 --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/OpenAI.light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/WindowsML.svg b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/WindowsML.svg new file mode 100644 index 0000000000..fafc16b59f --- /dev/null +++ b/src/settings-ui/Settings.UI/Assets/Settings/Icons/Models/WindowsML.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png index 7e9a2da1a8..69ed506e99 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseHighlighter.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png index 15568110cc..14d5e71d53 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseJump.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png index 83d1fbc553..99a8f64ed4 100644 Binary files a/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/MouseWithoutBorders.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdPal_Background.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdPal_Background.png new file mode 100644 index 0000000000..929a637d34 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdPal_Background.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdPal_Hero.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdPal_Hero.png new file mode 100644 index 0000000000..a8889dcc05 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CmdPal_Hero.png differ diff --git a/src/settings-ui/Settings.UI/Converters/EnumToBooleanConverter.cs b/src/settings-ui/Settings.UI/Converters/EnumToBooleanConverter.cs new file mode 100644 index 0000000000..27689435cc --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public partial class EnumToBooleanConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value == null || parameter == null) + { + return false; + } + + // Get the enum value as string + var enumString = value.ToString(); + var parameterString = parameter.ToString(); + + return enumString.Equals(parameterString, StringComparison.OrdinalIgnoreCase); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs b/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs new file mode 100644 index 0000000000..7d632906c2 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ServiceTypeToIconConverter.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Media.Imaging; + +namespace Microsoft.PowerToys.Settings.UI.Converters; + +public partial class ServiceTypeToIconConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is not string serviceType || string.IsNullOrWhiteSpace(serviceType)) + { + return new ImageIcon { Source = new SvgImageSource(new Uri(AIServiceTypeRegistry.GetIconPath(AIServiceType.OpenAI))) }; + } + + var iconPath = AIServiceTypeRegistry.GetIconPath(serviceType); + return new ImageIcon { Source = new SvgImageSource(new Uri(iconPath)) }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/settings-ui/Settings.UI/Converters/StringToDouble.cs b/src/settings-ui/Settings.UI/Converters/StringToDouble.cs new file mode 100644 index 0000000000..fae0618467 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/StringToDouble.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public partial class StringToDoubleConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is string s && double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out double result)) + { + return result; + } + + return 0.0; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is double d) + { + return d.ToString(CultureInfo.InvariantCulture); + } + + return "0"; + } + } +} diff --git a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs index b33ac264ed..89b22d8164 100644 --- a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs +++ b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs @@ -22,7 +22,8 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.FindMyMouse: case ModuleType.MouseHighlighter: case ModuleType.MouseJump: - case ModuleType.MousePointerCrosshairs: return $"MouseUtils_{moduleType}/Header"; + case ModuleType.MousePointerCrosshairs: + case ModuleType.CursorWrap: return $"MouseUtils_{moduleType}/Header"; default: return $"{moduleType}/ModuleTitle"; } } @@ -52,6 +53,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.CmdPal: return generalSettingsConfig.Enabled.CmdPal; case ModuleType.ColorPicker: return generalSettingsConfig.Enabled.ColorPicker; case ModuleType.CropAndLock: return generalSettingsConfig.Enabled.CropAndLock; + case ModuleType.CursorWrap: return generalSettingsConfig.Enabled.CursorWrap; case ModuleType.LightSwitch: return generalSettingsConfig.Enabled.LightSwitch; case ModuleType.EnvironmentVariables: return generalSettingsConfig.Enabled.EnvironmentVariables; case ModuleType.FancyZones: return generalSettingsConfig.Enabled.FancyZones; @@ -89,6 +91,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.CmdPal: generalSettingsConfig.Enabled.CmdPal = isEnabled; break; case ModuleType.ColorPicker: generalSettingsConfig.Enabled.ColorPicker = isEnabled; break; case ModuleType.CropAndLock: generalSettingsConfig.Enabled.CropAndLock = isEnabled; break; + case ModuleType.CursorWrap: generalSettingsConfig.Enabled.CursorWrap = isEnabled; break; case ModuleType.LightSwitch: generalSettingsConfig.Enabled.LightSwitch = isEnabled; break; case ModuleType.EnvironmentVariables: generalSettingsConfig.Enabled.EnvironmentVariables = isEnabled; break; case ModuleType.FancyZones: generalSettingsConfig.Enabled.FancyZones = isEnabled; break; @@ -125,6 +128,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.CmdPal: return GPOWrapper.GetConfiguredCmdPalEnabledValue(); case ModuleType.ColorPicker: return GPOWrapper.GetConfiguredColorPickerEnabledValue(); case ModuleType.CropAndLock: return GPOWrapper.GetConfiguredCropAndLockEnabledValue(); + case ModuleType.CursorWrap: return GPOWrapper.GetConfiguredCursorWrapEnabledValue(); case ModuleType.EnvironmentVariables: return GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue(); case ModuleType.FancyZones: return GPOWrapper.GetConfiguredFancyZonesEnabledValue(); case ModuleType.FileLocksmith: return GPOWrapper.GetConfiguredFileLocksmithEnabledValue(); @@ -161,6 +165,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers ModuleType.CmdPal => typeof(CmdPalPage), ModuleType.ColorPicker => typeof(ColorPickerPage), ModuleType.CropAndLock => typeof(CropAndLockPage), + ModuleType.CursorWrap => typeof(MouseUtilsPage), ModuleType.LightSwitch => typeof(LightSwitchPage), ModuleType.EnvironmentVariables => typeof(EnvironmentVariablesPage), ModuleType.FancyZones => typeof(FancyZonesPage), diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index dd70af7533..a6751adb98 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -20,8 +20,16 @@ PowerToys.Settings.pri + + + + + + + + @@ -53,11 +61,19 @@ + + + + PreserveNewest + + + + @@ -105,6 +121,7 @@ + @@ -197,4 +214,4 @@ - \ No newline at end of file + diff --git a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs index 838149a04e..dafd6d828b 100644 --- a/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs +++ b/src/settings-ui/Settings.UI/SerializationContext/SourceGenerationContextContext.cs @@ -10,7 +10,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; -using Settings.UI.Library; +using SettingsUILibrary = Settings.UI.Library; namespace Microsoft.PowerToys.Settings.UI.SerializationContext; @@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext; [JsonSerializable(typeof(FileLocksmithSettings))] [JsonSerializable(typeof(FindMyMouseSettings))] [JsonSerializable(typeof(IList))] -[JsonSerializable(typeof(LightSwitchSettings))] +[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))] [JsonSerializable(typeof(MeasureToolSettings))] [JsonSerializable(typeof(MouseHighlighterSettings))] [JsonSerializable(typeof(MouseJumpSettings))] diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml index 34abbe993d..9563bfceb3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/Card.xaml @@ -33,6 +33,7 @@ diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml index b071e7f6fe..44470ebbc1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/Dashboard/ShortcutConflictControl.xaml @@ -9,7 +9,10 @@ mc:Ignorable="d"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml.cs new file mode 100644 index 0000000000..400074f9d3 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ModelPicker/FoundryLocalModelPicker.xaml.cs @@ -0,0 +1,447 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using LanguageModelProvider; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Controls; + +public sealed partial class FoundryLocalModelPicker : UserControl +{ + private INotifyCollectionChanged _cachedModelsSubscription; + private INotifyCollectionChanged _downloadableModelsSubscription; + private bool _suppressSelection; + + public FoundryLocalModelPicker() + { + InitializeComponent(); + Loaded += (_, _) => UpdateVisualStates(); + } + + public delegate void ModelSelectionChangedEventHandler(object sender, ModelDetails model); + + public delegate void DownloadRequestedEventHandler(object sender, object payload); + + public delegate void LoadRequestedEventHandler(object sender); + + public event ModelSelectionChangedEventHandler SelectionChanged; + + public event LoadRequestedEventHandler LoadRequested; + + public IEnumerable CachedModels + { + get => (IEnumerable)GetValue(CachedModelsProperty); + set => SetValue(CachedModelsProperty, value); + } + + public static readonly DependencyProperty CachedModelsProperty = + DependencyProperty.Register(nameof(CachedModels), typeof(IEnumerable), typeof(FoundryLocalModelPicker), new PropertyMetadata(null, OnCachedModelsChanged)); + + public IEnumerable DownloadableModels + { + get => (IEnumerable)GetValue(DownloadableModelsProperty); + set => SetValue(DownloadableModelsProperty, value); + } + + public static readonly DependencyProperty DownloadableModelsProperty = + DependencyProperty.Register(nameof(DownloadableModels), typeof(IEnumerable), typeof(FoundryLocalModelPicker), new PropertyMetadata(null, OnDownloadableModelsChanged)); + + public ModelDetails SelectedModel + { + get => (ModelDetails)GetValue(SelectedModelProperty); + set => SetValue(SelectedModelProperty, value); + } + + public static readonly DependencyProperty SelectedModelProperty = + DependencyProperty.Register(nameof(SelectedModel), typeof(ModelDetails), typeof(FoundryLocalModelPicker), new PropertyMetadata(null, OnSelectedModelChanged)); + + public bool IsLoading + { + get => (bool)GetValue(IsLoadingProperty); + set => SetValue(IsLoadingProperty, value); + } + + public static readonly DependencyProperty IsLoadingProperty = + DependencyProperty.Register(nameof(IsLoading), typeof(bool), typeof(FoundryLocalModelPicker), new PropertyMetadata(false, OnStatePropertyChanged)); + + public bool IsAvailable + { + get => (bool)GetValue(IsAvailableProperty); + set => SetValue(IsAvailableProperty, value); + } + + public static readonly DependencyProperty IsAvailableProperty = + DependencyProperty.Register(nameof(IsAvailable), typeof(bool), typeof(FoundryLocalModelPicker), new PropertyMetadata(false, OnStatePropertyChanged)); + + public string StatusText + { + get => (string)GetValue(StatusTextProperty); + set => SetValue(StatusTextProperty, value); + } + + public static readonly DependencyProperty StatusTextProperty = + DependencyProperty.Register(nameof(StatusText), typeof(string), typeof(FoundryLocalModelPicker), new PropertyMetadata(string.Empty, OnStatePropertyChanged)); + + public bool HasCachedModels => CachedModels?.Any() ?? false; + + public bool HasDownloadableModels => DownloadableModels?.Cast().Any() ?? false; + + public void RequestLoad() + { + if (IsLoading) + { + // Allow refresh requests to continue even if already loading by cancelling via host. + } + else + { + IsLoading = true; + } + + IsAvailable = false; + StatusText = "Loading Foundry Local status..."; + LoadRequested?.Invoke(this); + } + + private static void OnCachedModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (FoundryLocalModelPicker)d; + control.SubscribeToCachedModels(e.OldValue as IEnumerable, e.NewValue as IEnumerable); + control.UpdateVisualStates(); + } + + private static void OnDownloadableModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (FoundryLocalModelPicker)d; + control.SubscribeToDownloadableModels(e.OldValue as IEnumerable, e.NewValue as IEnumerable); + control.UpdateVisualStates(); + } + + private static void OnSelectedModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (FoundryLocalModelPicker)d; + if (control._suppressSelection) + { + return; + } + + try + { + control._suppressSelection = true; + if (control.CachedModelsComboBox is not null) + { + control.CachedModelsComboBox.SelectedItem = e.NewValue; + } + } + finally + { + control._suppressSelection = false; + } + + control.UpdateSelectedModelDetails(); + } + + private static void OnStatePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (FoundryLocalModelPicker)d; + control.UpdateVisualStates(); + } + + private void SubscribeToCachedModels(IEnumerable oldValue, IEnumerable newValue) + { + if (_cachedModelsSubscription is not null) + { + _cachedModelsSubscription.CollectionChanged -= CachedModels_CollectionChanged; + _cachedModelsSubscription = null; + } + + if (newValue is INotifyCollectionChanged observable) + { + observable.CollectionChanged += CachedModels_CollectionChanged; + _cachedModelsSubscription = observable; + } + } + + private void SubscribeToDownloadableModels(IEnumerable oldValue, IEnumerable newValue) + { + if (_downloadableModelsSubscription is not null) + { + _downloadableModelsSubscription.CollectionChanged -= DownloadableModels_CollectionChanged; + _downloadableModelsSubscription = null; + } + + if (newValue is INotifyCollectionChanged observable) + { + observable.CollectionChanged += DownloadableModels_CollectionChanged; + _downloadableModelsSubscription = observable; + } + } + + private void CachedModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateVisualStates(); + } + + private void DownloadableModels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateVisualStates(); + } + + private void CachedModelsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_suppressSelection) + { + return; + } + + try + { + _suppressSelection = true; + var selected = CachedModelsComboBox.SelectedItem as ModelDetails; + SetValue(SelectedModelProperty, selected); + SelectionChanged?.Invoke(this, selected); + } + finally + { + _suppressSelection = false; + } + + UpdateSelectedModelDetails(); + } + + private void UpdateSelectedModelDetails() + { + if (SelectedModelDetailsPanel is null || SelectedModelDescriptionText is null || SelectedModelTagsPanel is null) + { + return; + } + + if (!HasCachedModels || SelectedModel is not ModelDetails model) + { + SelectedModelDetailsPanel.Visibility = Visibility.Collapsed; + SelectedModelDescriptionText.Text = string.Empty; + SelectedModelTagsPanel.Children.Clear(); + SelectedModelTagsPanel.Visibility = Visibility.Collapsed; + return; + } + + SelectedModelDetailsPanel.Visibility = Visibility.Visible; + SelectedModelDescriptionText.Text = string.IsNullOrWhiteSpace(model.Description) + ? "No description provided." + : model.Description; + + SelectedModelTagsPanel.Children.Clear(); + + AddTag(GetModelSizeText(model.Size)); + AddTag(GetLicenseShortText(model.License), model.License); + + foreach (var deviceTag in GetDeviceTags(model.HardwareAccelerators)) + { + AddTag(deviceTag); + } + + SelectedModelTagsPanel.Visibility = SelectedModelTagsPanel.Children.Count > 0 ? Visibility.Visible : Visibility.Collapsed; + + void AddTag(string text, string tooltip = null) + { + if (string.IsNullOrWhiteSpace(text) || SelectedModelTagsPanel is null) + { + return; + } + + Border tag = new(); + if (Resources.TryGetValue("TagBorderStyle", out var borderStyleObj) && borderStyleObj is Style borderStyle) + { + tag.Style = borderStyle; + } + + TextBlock label = new() + { + Text = text, + }; + + if (Resources.TryGetValue("TagTextStyle", out var textStyleObj) && textStyleObj is Style textStyle) + { + label.Style = textStyle; + } + + tag.Child = label; + + if (!string.IsNullOrWhiteSpace(tooltip)) + { + ToolTipService.SetToolTip(tag, new TextBlock + { + Text = tooltip, + TextWrapping = TextWrapping.Wrap, + }); + } + + SelectedModelTagsPanel.Children.Add(tag); + } + } + + private void LaunchFoundryModelListButton_Click(object sender, RoutedEventArgs e) + { + try + { + ProcessStartInfo processInfo = new() + { + FileName = "powershell.exe", + Arguments = "-NoExit -Command \"foundry model list\"", + UseShellExecute = true, + }; + + Process.Start(processInfo); + StatusText = "Opening PowerShell and running 'foundry model list'..."; + } + catch (Exception ex) + { + StatusText = $"Unable to start PowerShell. {ex.Message}"; + Debug.WriteLine($"[FoundryLocalModelPicker] Failed to run 'foundry model list': {ex}"); + } + } + + private void RefreshModelsButton_Click(object sender, RoutedEventArgs e) + { + RequestLoad(); + } + + private void UpdateVisualStates() + { + LoadingIndicator.IsActive = IsLoading; + + if (IsLoading) + { + VisualStateManager.GoToState(this, "ShowLoading", true); + } + else if (!IsAvailable) + { + VisualStateManager.GoToState(this, "ShowNotAvailable", true); + } + else + { + VisualStateManager.GoToState(this, "ShowModels", true); + } + + if (LoadingStatusTextBlock is not null) + { + LoadingStatusTextBlock.Text = string.IsNullOrWhiteSpace(StatusText) + ? "Loading Foundry Local status..." + : StatusText; + } + + NoModelsPanel.Visibility = HasCachedModels ? Visibility.Collapsed : Visibility.Visible; + if (CachedModelsComboBox is not null) + { + CachedModelsComboBox.Visibility = HasCachedModels ? Visibility.Visible : Visibility.Collapsed; + CachedModelsComboBox.IsEnabled = HasCachedModels; + } + + UpdateSelectedModelDetails(); + + Bindings.Update(); + } + + public static string GetModelSizeText(long size) + { + if (size <= 0) + { + return string.Empty; + } + + const long kiloByte = 1024; + const long megaByte = kiloByte * 1024; + const long gigaByte = megaByte * 1024; + + if (size >= gigaByte) + { + return $"{size / (double)gigaByte:0.##} GB"; + } + + if (size >= megaByte) + { + return $"{size / (double)megaByte:0.##} MB"; + } + + if (size >= kiloByte) + { + return $"{size / (double)kiloByte:0.##} KB"; + } + + return $"{size} B"; + } + + public static Visibility GetModelSizeVisibility(long size) + { + return size > 0 ? Visibility.Visible : Visibility.Collapsed; + } + + public static IEnumerable GetDeviceTags(IReadOnlyCollection accelerators) + { + if (accelerators is null || accelerators.Count == 0) + { + return Array.Empty(); + } + + HashSet tags = new(StringComparer.OrdinalIgnoreCase); + + foreach (var accelerator in accelerators) + { + switch (accelerator) + { + case HardwareAccelerator.CPU: + tags.Add("CPU"); + break; + case HardwareAccelerator.GPU: + case HardwareAccelerator.DML: + tags.Add("GPU"); + break; + case HardwareAccelerator.NPU: + case HardwareAccelerator.QNN: + tags.Add("NPU"); + break; + } + } + + return tags.Count > 0 ? tags.ToArray() : Array.Empty(); + } + + public static Visibility GetDeviceVisibility(IReadOnlyCollection accelerators) + { + return GetDeviceTags(accelerators).Any() ? Visibility.Visible : Visibility.Collapsed; + } + + public static string GetLicenseShortText(string license) + { + if (string.IsNullOrWhiteSpace(license)) + { + return string.Empty; + } + + var trimmed = license.Trim(); + int separatorIndex = trimmed.IndexOfAny(['(', '[', ':']); + if (separatorIndex > 0) + { + trimmed = trimmed[..separatorIndex].Trim(); + } + + if (trimmed.Length > 24) + { + trimmed = $"{trimmed[..24].TrimEnd()}…"; + } + + return trimmed; + } + + public static Visibility GetLicenseVisibility(string license) + { + return string.IsNullOrWhiteSpace(license) ? Visibility.Collapsed : Visibility.Visible; + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml index 07b6a00c21..8dea006b08 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml @@ -111,7 +111,8 @@ x:Uid="UpdateAvailableInfoBar" IsClosable="False" IsOpen="{x:Bind ViewModel.IsUpdateAvailable, Mode=OneWay}" - Severity="Success" /> + Severity="Success" + Tapped="UpdateInfoBar_Tapped" /> diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs index aad7dcf215..51219309e0 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs @@ -10,6 +10,7 @@ using Microsoft.PowerToys.Settings.UI.Controls; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; using Microsoft.PowerToys.Settings.UI.ViewModels; +using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -183,5 +184,14 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout // Closing manually the flyout since no window will steal the focus App.GetFlyoutWindow()?.Hide(); } + + private void UpdateInfoBar_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e) + { + // Hide the flyout before opening settings window + App.GetFlyoutWindow()?.Hide(); + + // Open Settings window directly to General page where update controls are located + App.OpenSettingsWindow(typeof(GeneralPage)); + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml index 628df84c01..ae4bbeb438 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPastePage.xaml @@ -3,28 +3,43 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Microsoft.PowerToys.Settings.UI.Controls" + xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="using:Microsoft.PowerToys.Settings.UI.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:models="using:Microsoft.PowerToys.Settings.UI.Library" xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" xmlns:ui="using:CommunityToolkit.WinUI" + xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels" x:Name="RootPage" AutomationProperties.LandmarkType="Main" mc:Ignorable="d"> + + + ms-appx:///Assets/Settings/Modules/APDialog.dark.png + ms-appx:///Assets/Settings/Icons/Models/OpenAI.dark.svg ms-appx:///Assets/Settings/Modules/APDialog.light.png + ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg ms-appx:///Assets/Settings/Modules/APDialog.light.png + ms-appx:///Assets/Settings/Icons/Models/OpenAI.light.svg + + + - + - + IsEnabled="{x:Bind ViewModel.IsOnlineAIModelsDisallowedByGPO, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}" + IsExpanded="{x:Bind ViewModel.IsAIEnabled, Mode=OneWay}" + ItemsSource="{x:Bind ViewModel.PasteAIConfiguration.Providers, Mode=OneWay}"> + - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + DataContext="{x:Bind ViewModel.AdditionalActions.ImageToText, Mode=OneWay}" + HeaderIcon="{ui:FontIcon Glyph=}"> + + + + + + + + + + + + - - - - - - - - - - - - - - - - | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + 900 + 700 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs index a06e5838a4..0d7273f924 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs @@ -66,5 +66,15 @@ namespace Microsoft.PowerToys.Settings.UI.Views App.GetOobeWindow().Activate(); } + + private void SortAlphabetical_Click(object sender, RoutedEventArgs e) + { + ViewModel.DashboardSortOrder = DashboardSortOrder.Alphabetical; + } + + private void SortByStatus_Click(object sender, RoutedEventArgs e) + { + ViewModel.DashboardSortOrder = DashboardSortOrder.ByStatus; + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml index fcdcc0f60d..cf67586a3e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml @@ -240,6 +240,7 @@ @@ -262,7 +263,10 @@ - + @@ -273,12 +277,16 @@ Name="GeneralPageRunAtStartUp" x:Uid="GeneralPage_RunAtStartUp" IsEnabled="{x:Bind ViewModel.IsRunAtStartupGPOManaged, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}"> - + - + @@ -398,12 +406,16 @@ - + - + - - + + - + + + + + @@ -70,6 +85,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs index 72de0843d1..19375d90f7 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/HostsPage.xaml.cs @@ -18,6 +18,10 @@ namespace Microsoft.PowerToys.Settings.UI.Views InitializeComponent(); var settingsUtils = new SettingsUtils(); ViewModel = new HostsViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage, App.IsElevated); + BackupsCountInputSettingsCard.Header = ResourceLoaderInstance.ResourceLoader.GetString("Hosts_Backup_CountInput_Header"); + BackupsCountInputSettingsCard.Description = ResourceLoaderInstance.ResourceLoader.GetString("Hosts_Backup_CountInput_Description"); + BackupsCountInputAgeSettingsCard.Header = ResourceLoaderInstance.ResourceLoader.GetString("Hosts_Backup_CountInput_Header"); + BackupsCountInputAgeSettingsCard.Description = ResourceLoaderInstance.ResourceLoader.GetString("Hosts_Backup_CountInput_Age_Description"); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml index 7f55e31cd8..ec61a0fcd5 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml @@ -1,24 +1,23 @@  - - + - + + - - - + - - - + - - - - - - - - - - + + Text="{x:Bind ViewModel.LocationPanelLightTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}" + TextAlignment="Left" /> @@ -321,12 +325,12 @@ + Text="{x:Bind ViewModel.LocationPanelDarkTime, Converter={StaticResource TimeSpanToFriendlyTimeConverter}, Mode=OneWay}" + TextAlignment="Left" /> @@ -358,4 +362,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs index 4a8e8905d7..974447a20e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/LightSwitchPage.xaml.cs @@ -12,88 +12,94 @@ using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library.Helpers; -using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using PowerToys.GPOWrapper; using Settings.UI.Library; -using Settings.UI.Library.Helpers; using Windows.Devices.Geolocation; -using Windows.Services.Maps; namespace Microsoft.PowerToys.Settings.UI.Views { - public sealed partial class LightSwitchPage : Page + public sealed partial class LightSwitchPage : NavigablePage, IRefreshablePage { - private readonly string _appName = "LightSwitch"; - private readonly SettingsUtils _settingsUtils; - private readonly Func _sendConfigMsg = ShellPage.SendDefaultIPCMessage; + private readonly string appName = "LightSwitch"; + private readonly SettingsUtils settingsUtils; + private readonly Func sendConfigMsg = ShellPage.SendDefaultIPCMessage; - private readonly ISettingsRepository _generalSettingsRepository; - private readonly ISettingsRepository _moduleSettingsRepository; + private readonly SettingsRepository generalSettingsRepository; + private readonly SettingsRepository moduleSettingsRepository; - private readonly IFileSystem _fileSystem; - private readonly IFileSystemWatcher _fileSystemWatcher; - private readonly DispatcherQueue _dispatcherQueue; + private readonly IFileSystem fileSystem; + private readonly IFileSystemWatcher fileSystemWatcher; + private readonly DispatcherQueue dispatcherQueue; + private bool suppressViewModelUpdates; private LightSwitchViewModel ViewModel { get; set; } public LightSwitchPage() { - _settingsUtils = new SettingsUtils(); - _sendConfigMsg = ShellPage.SendDefaultIPCMessage; + this.settingsUtils = new SettingsUtils(); + this.sendConfigMsg = ShellPage.SendDefaultIPCMessage; - _generalSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); - _moduleSettingsRepository = SettingsRepository.GetInstance(_settingsUtils); + this.generalSettingsRepository = SettingsRepository.GetInstance(this.settingsUtils); + this.moduleSettingsRepository = SettingsRepository.GetInstance(this.settingsUtils); // Get settings from JSON (or defaults if JSON missing) - var darkSettings = _moduleSettingsRepository.SettingsConfig; + var darkSettings = this.moduleSettingsRepository.SettingsConfig; // Pass them into the ViewModel - ViewModel = new LightSwitchViewModel(darkSettings, ShellPage.SendDefaultIPCMessage); - ViewModel.PropertyChanged += ViewModel_PropertyChanged; + this.ViewModel = new LightSwitchViewModel(darkSettings, this.sendConfigMsg); + this.ViewModel.PropertyChanged += ViewModel_PropertyChanged; - LoadSettings(_generalSettingsRepository, _moduleSettingsRepository); + this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository); - DataContext = ViewModel; + DataContext = this.ViewModel; - var settingsPath = _settingsUtils.GetSettingsFilePath(_appName); + var settingsPath = this.settingsUtils.GetSettingsFilePath(this.appName); - _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - _fileSystem = new FileSystem(); + this.dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + this.fileSystem = new FileSystem(); - _fileSystemWatcher = _fileSystem.FileSystemWatcher.New(); - _fileSystemWatcher.Path = _fileSystem.Path.GetDirectoryName(settingsPath); - _fileSystemWatcher.Filter = _fileSystem.Path.GetFileName(settingsPath); - _fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime; - _fileSystemWatcher.Changed += Settings_Changed; - _fileSystemWatcher.EnableRaisingEvents = true; + this.fileSystemWatcher = this.fileSystem.FileSystemWatcher.New(); + this.fileSystemWatcher.Path = this.fileSystem.Path.GetDirectoryName(settingsPath); + this.fileSystemWatcher.Filter = this.fileSystem.Path.GetFileName(settingsPath); + this.fileSystemWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime; + this.fileSystemWatcher.Changed += Settings_Changed; + this.fileSystemWatcher.EnableRaisingEvents = true; this.InitializeComponent(); - this.Loaded += LightSwitchPage_Loaded; - this.Loaded += (s, e) => ViewModel.OnPageLoaded(); + Loaded += LightSwitchPage_Loaded; + Loaded += (s, e) => this.ViewModel.OnPageLoaded(); + } + + public void RefreshEnabledState() + { + this.ViewModel.RefreshEnabledState(); } private void LightSwitchPage_Loaded(object sender, RoutedEventArgs e) { - if (ViewModel.SearchLocations.Count == 0) + if (this.ViewModel.SearchLocations.Count == 0) { foreach (var city in SearchLocationLoader.GetAll()) { - ViewModel.SearchLocations.Add(city); + this.ViewModel.SearchLocations.Add(city); } } - ViewModel.InitializeScheduleMode(); + this.ViewModel.InitializeScheduleMode(); } - private async Task GetGeoLocation() + private async void GetGeoLocation_Click(object sender, RoutedEventArgs e) { - SyncButton.IsEnabled = false; - SyncLoader.IsActive = true; - SyncLoader.Visibility = Visibility.Visible; + this.LatitudeBox.IsEnabled = false; + this.LongitudeBox.IsEnabled = false; + this.SyncButton.IsEnabled = false; + this.SyncLoader.IsActive = true; + this.SyncLoader.Visibility = Visibility.Visible; + this.LocationResultPanel.Visibility = Visibility.Collapsed; try { @@ -112,75 +118,110 @@ namespace Microsoft.PowerToys.Settings.UI.Views double latitude = Math.Round(pos.Coordinate.Point.Position.Latitude); double longitude = Math.Round(pos.Coordinate.Point.Position.Longitude); - SunTimes result = SunCalc.CalculateSunriseSunset( - latitude, - longitude, - DateTime.Now.Year, - DateTime.Now.Month, - DateTime.Now.Day); + ViewModel.LocationPanelLatitude = latitude; + ViewModel.LocationPanelLongitude = longitude; - ViewModel.LightTime = (result.SunriseHour * 60) + result.SunriseMinute; - ViewModel.DarkTime = (result.SunsetHour * 60) + result.SunsetMinute; - ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture); - ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture); + var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); + + this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute; + this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute; // Since we use this mode, we can remove the selected city data. - ViewModel.SelectedCity = null; - - // CityAutoSuggestBox.Text = string.Empty; - ViewModel.SyncButtonInformation = $"{ViewModel.Latitude}°, {ViewModel.Longitude}°"; + this.ViewModel.SelectedCity = null; // ViewModel.CityTimesText = $"Sunrise: {result.SunriseHour}:{result.SunriseMinute:D2}\n" + $"Sunset: {result.SunsetHour}:{result.SunsetMinute:D2}"; - SyncButton.IsEnabled = true; - SyncLoader.IsActive = false; - SyncLoader.Visibility = Visibility.Collapsed; - LocationDialog.IsPrimaryButtonEnabled = true; - LocationResultPanel.Visibility = Visibility.Visible; + this.SyncButton.IsEnabled = true; + this.SyncLoader.IsActive = false; + this.SyncLoader.Visibility = Visibility.Collapsed; + this.LocationDialog.IsPrimaryButtonEnabled = true; + this.LatitudeBox.IsEnabled = true; + this.LongitudeBox.IsEnabled = true; + this.LocationResultPanel.Visibility = Visibility.Visible; } catch (Exception ex) { - SyncButton.IsEnabled = true; - SyncLoader.IsActive = false; - System.Diagnostics.Debug.WriteLine("Location error: " + ex.Message); + this.SyncButton.IsEnabled = true; + this.SyncLoader.IsActive = false; + this.SyncLoader.Visibility = Visibility.Collapsed; + this.LocationResultPanel.Visibility = Visibility.Collapsed; + this.LatitudeBox.IsEnabled = true; + this.LongitudeBox.IsEnabled = true; + Logger.LogInfo($"Location error: " + ex.Message); + } + } + + private void LatLonBox_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args) + { + double latitude = this.LatitudeBox.Value; + double longitude = this.LongitudeBox.Value; + + if (double.IsNaN(latitude) || double.IsNaN(longitude) || (latitude == 0 && longitude == 0)) + { + return; + } + + var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); + + this.ViewModel.LocationPanelLightTimeMinutes = (result.SunriseHour * 60) + result.SunriseMinute; + this.ViewModel.LocationPanelDarkTimeMinutes = (result.SunsetHour * 60) + result.SunsetMinute; + + this.LocationResultPanel.Visibility = Visibility.Visible; + if (this.LocationDialog != null) + { + this.LocationDialog.IsPrimaryButtonEnabled = true; } } private void LocationDialog_PrimaryButtonClick(object sender, ContentDialogButtonClickEventArgs args) { - if (ViewModel.ScheduleMode == "SunriseToSunsetUser") + if (double.IsNaN(this.LatitudeBox.Value) || double.IsNaN(this.LongitudeBox.Value)) { - ViewModel.SyncButtonInformation = ViewModel.SelectedCity.City; - } - else if (ViewModel.ScheduleMode == "SunriseToSunsetGeo") - { - ViewModel.SyncButtonInformation = $"{ViewModel.Latitude}°, {ViewModel.Longitude}°"; + return; } - SunriseModeChartState(); + double latitude = this.LatitudeBox.Value; + double longitude = this.LongitudeBox.Value; + + // need to save the values + this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture); + this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture); + this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}°, {this.ViewModel.Longitude}°"; + + var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); + + this.ViewModel.LightTime = (result.SunriseHour * 60) + result.SunriseMinute; + this.ViewModel.DarkTime = (result.SunsetHour * 60) + result.SunsetMinute; + + this.SunriseModeChartState(); } private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { + if (this.suppressViewModelUpdates) + { + return; + } + if (e.PropertyName == "IsEnabled") { - if (ViewModel.IsEnabled != _generalSettingsRepository.SettingsConfig.Enabled.LightSwitch) + if (this.ViewModel.IsEnabled != this.generalSettingsRepository.SettingsConfig.Enabled.LightSwitch) { - _generalSettingsRepository.SettingsConfig.Enabled.LightSwitch = ViewModel.IsEnabled; + this.generalSettingsRepository.SettingsConfig.Enabled.LightSwitch = this.ViewModel.IsEnabled; - var generalSettingsMessage = new OutGoingGeneralSettings(_generalSettingsRepository.SettingsConfig).ToString(); + var generalSettingsMessage = new OutGoingGeneralSettings(this.generalSettingsRepository.SettingsConfig).ToString(); Logger.LogInfo($"Saved general settings from Light Switch page."); - _sendConfigMsg?.Invoke(generalSettingsMessage); + this.sendConfigMsg?.Invoke(generalSettingsMessage); } } else { - if (ViewModel.ModuleSettings != null) + if (this.ViewModel.ModuleSettings != null) { - SndLightSwitchSettings currentSettings = new(_moduleSettingsRepository.SettingsConfig); + SndLightSwitchSettings currentSettings = new(this.moduleSettingsRepository.SettingsConfig); SndModuleSettings csIpcMessage = new(currentSettings); - SndLightSwitchSettings outSettings = new(ViewModel.ModuleSettings); + SndLightSwitchSettings outSettings = new(this.ViewModel.ModuleSettings); SndModuleSettings outIpcMessage = new(outSettings); string csMessage = csIpcMessage.ToJsonString(); @@ -190,13 +231,13 @@ namespace Microsoft.PowerToys.Settings.UI.Views { Logger.LogInfo($"Saved Light Switch settings from Light Switch page."); - _sendConfigMsg?.Invoke(outMessage); + this.sendConfigMsg?.Invoke(outMessage); } } } } - private void LoadSettings(ISettingsRepository generalSettingsRepository, ISettingsRepository moduleSettingsRepository) + private void LoadSettings(SettingsRepository generalSettingsRepository, SettingsRepository moduleSettingsRepository) { if (generalSettingsRepository != null) { @@ -221,8 +262,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views { if (generalSettings != null) { - ViewModel.IsEnabled = generalSettings.Enabled.LightSwitch; - ViewModel.ModuleSettings = (LightSwitchSettings)lightSwitchSettings.Clone(); + this.ViewModel.IsEnabled = generalSettings.Enabled.LightSwitch; + this.ViewModel.ModuleSettings = (LightSwitchSettings)lightSwitchSettings.Clone(); UpdateEnabledState(generalSettings.Enabled.LightSwitch); } @@ -239,10 +280,14 @@ namespace Microsoft.PowerToys.Settings.UI.Views private void Settings_Changed(object sender, FileSystemEventArgs e) { - _dispatcherQueue.TryEnqueue(() => + this.dispatcherQueue.TryEnqueue(() => { - _moduleSettingsRepository.ReloadSettings(); - LoadSettings(_generalSettingsRepository, _moduleSettingsRepository); + this.suppressViewModelUpdates = true; + + this.moduleSettingsRepository.ReloadSettings(); + this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository); + + this.suppressViewModelUpdates = false; }); } @@ -253,20 +298,20 @@ namespace Microsoft.PowerToys.Settings.UI.Views if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled) { // Get the enabled state from GPO. - ViewModel.IsEnabledGpoConfigured = true; - ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; + this.ViewModel.IsEnabledGpoConfigured = true; + this.ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled; } else { - ViewModel.IsEnabled = recommendedState; + this.ViewModel.IsEnabled = recommendedState; } } private async void SyncLocationButton_Click(object sender, RoutedEventArgs e) { - LocationDialog.IsPrimaryButtonEnabled = false; - LocationResultPanel.Visibility = Visibility.Collapsed; - await LocationDialog.ShowAsync(); + this.LocationDialog.IsPrimaryButtonEnabled = false; + this.LocationResultPanel.Visibility = Visibility.Collapsed; + await this.LocationDialog.ShowAsync(); } private void CityAutoSuggestBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) @@ -276,7 +321,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views string query = sender.Text.ToLower(CultureInfo.CurrentCulture); // Filter your cities (assuming ViewModel.Cities is a List) - var filtered = ViewModel.SearchLocations + var filtered = this.ViewModel.SearchLocations .Where(c => (c.City?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false) || (c.Country?.Contains(query, StringComparison.CurrentCultureIgnoreCase) ?? false)) @@ -286,7 +331,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views } } - private void CityAutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) + /* private void CityAutoSuggestBox_SuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) { if (args.SelectedItem is SearchLocation location) { @@ -296,43 +341,38 @@ namespace Microsoft.PowerToys.Settings.UI.Views LocationDialog.IsPrimaryButtonEnabled = true; LocationResultPanel.Visibility = Visibility.Visible; } - } + } */ private void ModeSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) { - switch (ViewModel.ScheduleMode) + switch (this.ViewModel.ScheduleMode) { case "FixedHours": VisualStateManager.GoToState(this, "ManualState", true); - TimelineCard.Visibility = Visibility.Visible; + this.TimelineCard.Visibility = Visibility.Visible; break; case "SunsetToSunrise": VisualStateManager.GoToState(this, "SunsetToSunriseState", true); - SunriseModeChartState(); + this.SunriseModeChartState(); break; default: VisualStateManager.GoToState(this, "OffState", true); - TimelineCard.Visibility = Visibility.Collapsed; + this.TimelineCard.Visibility = Visibility.Collapsed; break; } } - private async void LocationDialog_Opened(ContentDialog sender, ContentDialogOpenedEventArgs args) - { - await GetGeoLocation(); - } - private void SunriseModeChartState() { - if (ViewModel.Latitude != "0.0" && ViewModel.Longitude != "0.0") + if (this.ViewModel.Latitude != "0.0" && this.ViewModel.Longitude != "0.0") { - TimelineCard.Visibility = Visibility.Visible; - LocationWarningBar.Visibility = Visibility.Collapsed; + this.TimelineCard.Visibility = Visibility.Visible; + this.LocationWarningBar.Visibility = Visibility.Collapsed; } else { - TimelineCard.Visibility = Visibility.Collapsed; - LocationWarningBar.Visibility = Visibility.Visible; + this.TimelineCard.Visibility = Visibility.Collapsed; + this.LocationWarningBar.Visibility = Visibility.Visible; } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml index 498adf4803..60d0f5370d 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/MouseUtilsPage.xaml @@ -273,6 +273,34 @@ + + + + + + + + + + + + + + + + + + + - .GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), + SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml index 6a597a43bc..d837f35c3f 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/NewPlusPage.xaml @@ -23,7 +23,10 @@ Name="NewPlusEnableToggle" x:Uid="NewPlus_Enable_Toggle" HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/NewPlus.png}"> - + @@ -60,12 +63,18 @@ Name="NewPlusHideFileExtensionToggle" x:Uid="NewPlus_Hide_File_Extension_Toggle" IsEnabled="{x:Bind ViewModel.IsHideFileExtSettingsCardEnabled, Mode=OneWay}"> - + - + @@ -79,7 +88,10 @@ x:Uid="NewPlus_Behaviour_Replace_Variables_Toggle" IsEnabled="{x:Bind ViewModel.IsReplaceVariablesSettingsCardEnabled, Mode=OneWay}"> - +
Event Name Description
Microsoft.PowerToys.NewPlus_ChangedTemplateLocationTriggered when the template folder location is changed.
Microsoft.PowerToys.NewPlus_EventCopyTemplate Triggered when an item from New+ is created (copied to the current directory).Microsoft.PowerToys.NewPlus_EventCopyTemplateResult Logs the success of item creation (copying).
Microsoft.PowerToys.NewPlus_EventOpenTemplatesTriggered when the templates folder is opened.
Microsoft.PowerToys.NewPlus_EventShowTemplateItems Triggered when the New+ context menu flyout is displayed.Description
Microsoft.PowerToys.ShortcutGuide_EnableGuideTriggered when Shortcut Guide is enabled.
Microsoft.PowerToys.ShortcutGuide_HideGuideOccurs when Shortcut Guide is hidden from view.Microsoft.PowerToys.ShortcutGuide_GuideSessionLogs a Shortcut Guide session including duration and how it was closed.
Microsoft.PowerToys.ShortcutGuide_Settings