diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt index 5c5b55762d..2ef0425846 100644 --- a/.github/actions/spell-check/allow/code.txt +++ b/.github/actions/spell-check/allow/code.txt @@ -282,3 +282,12 @@ xef xes PACKAGEVERSIONNUMBER APPXMANIFESTVERSION + +# MRU lists +CACHEWRITE +MRUCMPPROC +MRUINFO +REGSTR + +# Misc Win32 APIs and PInvokes +INVOKEIDLIST \ No newline at end of file diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 46ef798d13..004d9f4269 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -25,11 +25,14 @@ ADMINS adml admx advancedpaste +advancedpasteui +advancedpasteuishortcut advfirewall AFeature affordances AFX AGGREGATABLE +AHK AHybrid akv ALarger @@ -38,7 +41,9 @@ ALLAPPS ALLCHILDREN ALLINPUT Allman +Allmodule ALLOWUNDO +allpc ALLVIEW ALPHATYPE AModifier @@ -114,6 +119,7 @@ bigbar bigobj binlog binres +binskim BITMAPFILEHEADER bitmapimage BITMAPINFO @@ -218,6 +224,7 @@ coclass CODENAME codereview Codespaces +Coen COINIT colid colorconv @@ -245,6 +252,7 @@ CONTEXTMENUHANDLER contractversion CONTROLPARENT copiedcolorrepresentation +coppied copyable COPYPEN COREWINDOW @@ -252,6 +260,7 @@ Corpor cotaskmem COULDNOT countof +covrun cpcontrols cph cplusplus @@ -443,6 +452,7 @@ ERRORIMAGE ERRORTITLE ESettings esrp +etd ETDT etl etw @@ -527,8 +537,8 @@ frm FROMTOUCH fsanitize fsmgmt -fxf fuzzingtesting +fxf FZE gacutil Gaeilge @@ -623,6 +633,7 @@ HKCU hkey HKLM HKM +hkmng HKPD HKU HMD @@ -631,6 +642,7 @@ hmodule hmonitor homies homljgmgpmcbpjbnjpfijnhipfkiclkd +HOOKPROC HORZRES HORZSIZE Hostbackdropbrush @@ -639,7 +651,11 @@ Hostx hotfixes hotkeycontrol HOTKEYF +hotkeylockmachine +hotkeyreconnect hotkeys +hotkeyswitch +hotkeytoggleeasymouse hotlight hotspot HPAINTBUFFER @@ -652,6 +668,7 @@ HROW hsb HSCROLL hsi +HSpeed HTCLIENT hthumbnail HTOUCHINPUT @@ -697,9 +714,12 @@ IMAGERESIZERCONTEXTMENU IMAGERESIZEREXT imageresizerinput imageresizersettings +imagetotext +imagetotextshortcut imagingdevices ime imgflip +inapp inbox INCONTACT Indo @@ -733,6 +753,7 @@ INSTALLSTARTMENUSHORTCUT INSTALLSTATE Inste Interlop +intput INTRESOURCE INVALIDARG invalidoperatioexception @@ -752,6 +773,7 @@ istep ith ITHUMBNAIL IUI +IUWP IWIC jfif jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi @@ -781,6 +803,7 @@ keyvault KILLFOCUS killrunner kmph +kvp Kybd lastcodeanalysissucceeded LASTEXITCODE @@ -815,9 +838,11 @@ LMEM LMENU LOADFROMFILE LOBYTE +localappdata localpackage LOCALSYSTEM LOCATIONCHANGE +LOCKMACHINE LOCKTYPE LOGFONT LOGFONTW @@ -903,6 +928,7 @@ MDL mdtext mdtxt mdwn +measuretool meme memicmp MENUITEMINFO @@ -952,6 +978,7 @@ MOUSEHWHEEL MOUSEINPUT mousejump mousepointer +mousepointercrosshairs mouseutils MOVESIZEEND MOVESIZESTART @@ -963,6 +990,7 @@ msc mscorlib msctls msdata +msdia MSDL MSGFLT MSHCTX @@ -1118,6 +1146,7 @@ oldtheme oleaut OLECHAR onebranch +OOBEUI openas opencode OPENFILENAME @@ -1151,6 +1180,18 @@ PARENTRELATIVEFORADDRESSBAR PARENTRELATIVEPARSING parray PARTIALCONFIRMATIONDIALOGTITLE +pasteashtmlfile +pasteashtmlfileshortcut +pasteasjson +pasteasjsonshortcut +pasteasmarkdown +pasteasmarkdownshortcut +pasteasplaintext +pasteasplaintextshortcut +pasteaspngfile +pasteaspngfileshortcut +pasteastxtfile +pasteastxtfileshortcut PATCOPY PATHMUSTEXIST PATINVERT @@ -1218,6 +1259,7 @@ Pomodoro Popups POPUPWINDOW POSITIONITEM +powerocr POWERRENAMECONTEXTMENU powerrenameinput POWERRENAMETEST @@ -1358,6 +1400,7 @@ Removelnk renamable RENAMEONCOLLISION reparented +reparenthotkey reparenting reportfileaccesses requery @@ -1383,8 +1426,8 @@ RIDEV RIGHTSCROLLBAR riid RKey -RNumber Rns +RNumber rop ROUNDSMALL ROWSETEXT @@ -1395,6 +1438,7 @@ Rsp rstringalnum rstringalpha rstringdigit +rtb RTB RTLREADING rtm @@ -1433,6 +1477,7 @@ secpol securestring SEEMASKINVOKEIDLIST SELCHANGE +selfhost SENDCHANGE sendvirtualinput serverside @@ -1529,6 +1574,7 @@ SLGP sln SMALLICON smartphone +smileys SMTO SNAPPROCESS snk @@ -1604,6 +1650,7 @@ STYLECHANGED STYLECHANGING subkeys sublang +Subdomain SUBMODULEUPDATE subresource Superbar @@ -1615,6 +1662,8 @@ svgz SVSI SWFO SWP +SWPNOSIZE +SWPNOZORDER SWRESTORE symbolrequestprod SYMCACHE @@ -1672,6 +1721,7 @@ THH THICKFRAME THISCOMPONENT throughs +thumbnailhotkey TILEDWINDOW TILLSON timedate @@ -1686,6 +1736,7 @@ tlb tlbimp tlc TNP +TOGGLEEASYMOUSE Toolhelp toolkitconverters toolwindow @@ -1699,6 +1750,7 @@ tracelogging tracerpt trackbar trafficmanager +transcodetomp transicc TRAYMOUSEMESSAGE triaging @@ -1754,8 +1806,8 @@ Uptool urld Usb USEDEFAULT -USEINSTALLERFORTEST USEFILEATTRIBUTES +USEINSTALLERFORTEST USESHOWWINDOW USESTDHANDLES USRDLL @@ -1813,6 +1865,7 @@ VSINSTALLDIR VSM vso vsonline +VSpeed vstemplate vstest VSTHRD @@ -1869,6 +1922,7 @@ winexe winforms winget wingetcreate +wingetpkgs Winhook WINL winlogon @@ -1948,10 +2002,13 @@ XNamespace Xoshiro XPels XPixel +XPos XResource xsi +XSpeed XStr xstyler +XTimer XUP XVIRTUALSCREEN xxxxxx @@ -1961,7 +2018,10 @@ YIncrement yinle yinyue YPels +YPos YResolution +YSpeed +YTimer YStr YVIRTUALSCREEN ZEROINIT diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 2db246d63b..04f9cfaaeb 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ ## PR Checklist -- [ ] **Closes:** #xxx +- [ ] 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/msstore-submissions.yml b/.github/workflows/msstore-submissions.yml index 8878780987..a44dafb199 100644 --- a/.github/workflows/msstore-submissions.yml +++ b/.github/workflows/msstore-submissions.yml @@ -17,7 +17,7 @@ jobs: steps: - name: BODGY - Set up Gnome Keyring for future Cert Auth run: |- - sudo apt-get install -y gnome-keyring + sudo apt-get update && sudo apt-get install -y gnome-keyring export $(dbus-launch --sh-syntax) export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --unlock) export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh) diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml index 18163e899a..d6c2177720 100644 --- a/.pipelines/v2/release.yml +++ b/.pipelines/v2/release.yml @@ -64,6 +64,10 @@ extends: tsa: enabled: true configFile: '$(Build.SourcesDirectory)\.pipelines\tsa.json' + binskim: + enabled: true + # Exclude every dll/exe in tests/*, as well as all msdia*, covrun* and vcruntime* + analyzeTargetGlob: +:file|$(Build.ArtifactStagingDirectory)/**/*.dll;+:file|$(Build.ArtifactStagingDirectory)/**/*.exe;-:file:regex|tests.*\.(dll|exe)$;-:file:regex|(covrun.*)\.dll$;-:file:regex|(msdia.*)\.dll$;-:file:regex|(vcruntime.*)\.dll$ stages: - stage: Build diff --git a/.pipelines/v2/templates/job-build-ui-tests.yml b/.pipelines/v2/templates/job-build-ui-tests.yml index ca99c00932..b9fad16d44 100644 --- a/.pipelines/v2/templates/job-build-ui-tests.yml +++ b/.pipelines/v2/templates/job-build-ui-tests.yml @@ -123,7 +123,7 @@ jobs: displayName: Stage UI Test Build Outputs inputs: sourceFolder: '$(Build.SourcesDirectory)' - contents: '$(BuildPlatform)/$(BuildConfiguration)/**/*' + contents: '**/$(BuildPlatform)/$(BuildConfiguration)/tests/**/*' targetFolder: '$(JobOutputDirectory)\$(BuildPlatform)\$(BuildConfiguration)' - publish: $(JobOutputDirectory) diff --git a/.pipelines/v2/templates/job-test-project.yml b/.pipelines/v2/templates/job-test-project.yml index 6b52351222..2c5fdc78ff 100644 --- a/.pipelines/v2/templates/job-test-project.yml +++ b/.pipelines/v2/templates/job-test-project.yml @@ -11,12 +11,14 @@ parameters: - name: useLatestWebView2 type: boolean default: false - - name: useLatestOfficialBuild - type: boolean - default: true - - name: useCurrentBranchBuild - type: boolean - default: false + - name: buildSource + type: string + default: "latestMainOfficialBuild" + displayName: "Build Source" + - name: specificBuildId + type: string + default: "xxxx" + displayName: "Build ID (for specific builds)" - name: uiTestModules type: object default: [] @@ -113,16 +115,17 @@ jobs: & '$(build.sourcesdirectory)\.pipelines\InstallWinAppDriver.ps1' displayName: Download and install WinAppDriver - - ${{ if eq(parameters.useLatestOfficialBuild, true) }}: + - ${{ if ne(parameters.buildSource, 'buildNow') }}: - task: DownloadPipelineArtifact@2 inputs: buildType: 'specific' project: 'Dart' definition: '76541' - buildVersionToDownload: 'latestFromBranch' - ${{ if eq(parameters.useCurrentBranchBuild, true) }}: - branchName: '$(Build.SourceBranch)' + ${{ if eq(parameters.buildSource, 'specificBuildId') }}: + buildVersionToDownload: 'specific' + buildId: '${{ parameters.specificBuildId }}' ${{ else }}: + buildVersionToDownload: 'latestFromBranch' branchName: 'refs/heads/main' artifactName: 'build-$(BuildPlatform)-Release' targetPath: '$(Build.ArtifactStagingDirectory)' @@ -133,7 +136,7 @@ jobs: patterns: | **/PowerToysSetup*.exe - - ${{ if eq(parameters.useLatestOfficialBuild, true) }}: + - ${{ if ne(parameters.buildSource, 'buildNow') }}: - ${{ if eq(parameters.installMode, 'peruser') }}: - pwsh: |- & "$(build.sourcesdirectory)\.pipelines\installPowerToys.ps1" -InstallMode "PerUser" @@ -169,7 +172,7 @@ jobs: !**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll env: platform: '$(TestPlatform)' - useInstallerForTest: ${{ parameters.useLatestOfficialBuild }} + useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }} - ${{ if ne(length(parameters.uiTestModules), 0) }}: - ${{ each module in parameters.uiTestModules }}: @@ -191,4 +194,4 @@ jobs: !**\UITests-FancyZones\**\UITests-FancyZonesEditor.dll env: platform: '$(TestPlatform)' - useInstallerForTest: ${{ parameters.useLatestOfficialBuild }} + useInstallerForTest: ${{ ne(parameters.buildSource, 'buildNow') }} diff --git a/.pipelines/v2/templates/pipeline-ui-tests-automation.yml b/.pipelines/v2/templates/pipeline-ui-tests-automation.yml index f5e90e45d9..0682cc5e32 100644 --- a/.pipelines/v2/templates/pipeline-ui-tests-automation.yml +++ b/.pipelines/v2/templates/pipeline-ui-tests-automation.yml @@ -19,155 +19,40 @@ parameters: - name: useLatestWebView2 type: boolean default: false - - name: useLatestOfficialBuild - type: boolean - default: true - - name: testBothInstallModes - type: boolean - default: true - - name: useCurrentBranchBuild - type: boolean - default: false + - name: buildSource + type: string + default: "latestMainOfficialBuild" + displayName: "Build Source" + values: + - latestMainOfficialBuild + - buildNow + - specificBuildId + - name: specificBuildId + type: string + default: 'xxxx' + displayName: "Build ID (only used when Build Source = specificBuildId)" - name: uiTestModules type: object default: [] stages: - ${{ each platform in parameters.buildPlatforms }}: - - ${{ if eq(parameters.useLatestOfficialBuild, false) }}: - - stage: Build_${{ platform }} - displayName: Build ${{ platform }} - dependsOn: [] - jobs: - - template: job-build-project.yml - parameters: - pool: - ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: - name: SHINE-INT-L - ${{ else }}: - name: SHINE-OSS-L - ${{ if eq(parameters.useVSPreview, true) }}: - demands: ImageOverride -equals SHINE-VS17-Preview - buildPlatforms: - - ${{ platform }} - buildConfigurations: [Release] - enablePackageCaching: true - enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }} - runTests: false - buildTests: true - useVSPreview: ${{ parameters.useVSPreview }} - timeoutInMinutes: 90 + # Full build path: build PowerToys + UI tests + run tests + - ${{ if eq(parameters.buildSource, 'buildNow') }}: + - template: pipeline-ui-tests-full-build.yml + parameters: + platform: ${{ platform }} + enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }} + useVSPreview: ${{ parameters.useVSPreview }} + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + uiTestModules: ${{ parameters.uiTestModules }} - - ${{ if eq(parameters.useLatestOfficialBuild, true) }}: - - stage: BuildUITests_${{ platform }} - displayName: Build UI Tests Only - dependsOn: [] - jobs: - - template: job-build-ui-tests.yml - parameters: - pool: - ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: - name: SHINE-INT-L - ${{ else }}: - name: SHINE-OSS-L - ${{ if eq(parameters.useVSPreview, true) }}: - demands: ImageOverride -equals SHINE-VS17-Preview - buildPlatforms: - - ${{ platform }} - uiTestModules: ${{ parameters.uiTestModules }} - - - ${{ if eq(platform, 'x64') }}: - - stage: Test_x64Win10 - displayName: Test x64Win10 - ${{ if eq(parameters.useLatestOfficialBuild, true) }}: - dependsOn: - - BuildUITests_${{ platform }} - ${{ else }}: - dependsOn: - - Build_${{ platform }} - jobs: - - template: job-test-project.yml - parameters: - platform: x64Win10 - configuration: Release - useLatestWebView2: ${{ parameters.useLatestWebView2 }} - useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }} - useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }} - uiTestModules: ${{ parameters.uiTestModules }} - - # Additional per-user installation test (when both modes are enabled) - - ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}: - - template: job-test-project.yml - parameters: - platform: x64Win10 - configuration: Release - useLatestWebView2: ${{ parameters.useLatestWebView2 }} - useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }} - useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }} - uiTestModules: ${{ parameters.uiTestModules }} - installMode: 'peruser' - jobSuffix: '_PerUser' - - - ${{ if eq(platform, 'x64') }}: - - stage: Test_x64Win11 - displayName: Test x64Win11 - ${{ if eq(parameters.useLatestOfficialBuild, true) }}: - dependsOn: - - BuildUITests_${{ platform }} - ${{ else }}: - dependsOn: - - Build_${{ platform }} - jobs: - - template: job-test-project.yml - parameters: - platform: x64Win11 - configuration: Release - useLatestWebView2: ${{ parameters.useLatestWebView2 }} - useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }} - useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }} - uiTestModules: ${{ parameters.uiTestModules }} - - # Additional per-user installation test (when both modes are enabled) - - ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}: - - template: job-test-project.yml - parameters: - platform: x64Win11 - configuration: Release - useLatestWebView2: ${{ parameters.useLatestWebView2 }} - useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }} - useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }} - uiTestModules: ${{ parameters.uiTestModules }} - installMode: 'peruser' - jobSuffix: '_PerUser' - - - ${{ if ne(platform, 'x64') }}: - - stage: Test_${{ platform }} - displayName: Test ${{ platform }} - ${{ if eq(parameters.useLatestOfficialBuild, true) }}: - dependsOn: - - BuildUITests_${{ platform }} - ${{ else }}: - dependsOn: - - Build_${{ platform }} - jobs: - - template: job-test-project.yml - parameters: - platform: ${{ platform }} - configuration: Release - useLatestWebView2: ${{ parameters.useLatestWebView2 }} - useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }} - useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }} - uiTestModules: ${{ parameters.uiTestModules }} - - # Additional per-user installation test (when both modes are enabled) - - ${{ if and(eq(parameters.useLatestOfficialBuild, true), eq(parameters.testBothInstallModes, true)) }}: - - template: job-test-project.yml - parameters: - platform: ${{ platform }} - configuration: Release - useLatestWebView2: ${{ parameters.useLatestWebView2 }} - useLatestOfficialBuild: ${{ parameters.useLatestOfficialBuild }} - useCurrentBranchBuild: ${{ parameters.useCurrentBranchBuild }} - uiTestModules: ${{ parameters.uiTestModules }} - installMode: 'peruser' - jobSuffix: '_PerUser' \ No newline at end of file + # Official build path: build UI tests only + download official build + run tests + - ${{ if ne(parameters.buildSource, 'buildNow') }}: + - template: pipeline-ui-tests-official-build.yml + parameters: + platform: ${{ platform }} + buildSource: ${{ parameters.buildSource }} + specificBuildId: ${{ parameters.specificBuildId }} + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + uiTestModules: ${{ parameters.uiTestModules }} diff --git a/.pipelines/v2/templates/pipeline-ui-tests-full-build.yml b/.pipelines/v2/templates/pipeline-ui-tests-full-build.yml new file mode 100644 index 0000000000..a2373feb80 --- /dev/null +++ b/.pipelines/v2/templates/pipeline-ui-tests-full-build.yml @@ -0,0 +1,80 @@ +# Template for full build path: Build PowerToys + Build UI Tests + Run Tests +parameters: + - name: platform + type: string + - name: enableMsBuildCaching + type: boolean + default: false + - name: useVSPreview + type: boolean + default: false + - name: useLatestWebView2 + type: boolean + default: false + - name: uiTestModules + type: object + default: [] + +stages: + # Stage 1: Build full PowerToys project + - stage: Build_${{ parameters.platform }} + displayName: Build PowerToys ${{ parameters.platform }} + dependsOn: [] + jobs: + - template: job-build-project.yml + parameters: + pool: + ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: + name: SHINE-INT-L + ${{ else }}: + name: SHINE-OSS-L + ${{ if eq(parameters.useVSPreview, true) }}: + demands: ImageOverride -equals SHINE-VS17-Preview + buildPlatforms: + - ${{ parameters.platform }} + buildConfigurations: [Release] + enablePackageCaching: true + enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }} + runTests: false + buildTests: true + useVSPreview: ${{ parameters.useVSPreview }} + timeoutInMinutes: 90 + + # Stage 2: Run UI Tests + - ${{ if eq(parameters.platform, 'x64') }}: + - stage: Test_x64Win10_FullBuild + displayName: Test x64Win10 (Full Build) + dependsOn: Build_${{ parameters.platform }} + jobs: + - template: job-test-project.yml + parameters: + platform: x64Win10 + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: 'buildNow' + uiTestModules: ${{ parameters.uiTestModules }} + + - stage: Test_x64Win11_FullBuild + displayName: Test x64Win11 (Full Build) + dependsOn: Build_${{ parameters.platform }} + jobs: + - template: job-test-project.yml + parameters: + platform: x64Win11 + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: 'buildNow' + uiTestModules: ${{ parameters.uiTestModules }} + + - ${{ if ne(parameters.platform, 'x64') }}: + - stage: Test_${{ parameters.platform }}_FullBuild + displayName: Test ${{ parameters.platform }} (Full Build) + dependsOn: Build_${{ parameters.platform }} + jobs: + - template: job-test-project.yml + parameters: + platform: ${{ parameters.platform }} + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: 'buildNow' + uiTestModules: ${{ parameters.uiTestModules }} diff --git a/.pipelines/v2/templates/pipeline-ui-tests-official-build.yml b/.pipelines/v2/templates/pipeline-ui-tests-official-build.yml new file mode 100644 index 0000000000..1da11324fe --- /dev/null +++ b/.pipelines/v2/templates/pipeline-ui-tests-official-build.yml @@ -0,0 +1,110 @@ +# Template for official build path: Download Official Build + Build UI Tests Only + Run Tests +parameters: + - name: platform + type: string + - name: buildSource + type: string + - name: specificBuildId + type: string + default: 'xxxx' + - name: useLatestWebView2 + type: boolean + default: false + - name: uiTestModules + type: object + default: [] + +stages: + # Stage 1: Build UI Tests Only + - stage: BuildUITests_${{ parameters.platform }} + displayName: Build UI Tests Only ${{ parameters.platform }} + dependsOn: [] + jobs: + - template: job-build-ui-tests.yml + parameters: + pool: + ${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}: + name: SHINE-INT-L + ${{ else }}: + name: SHINE-OSS-L + buildPlatforms: + - ${{ parameters.platform }} + uiTestModules: ${{ parameters.uiTestModules }} + + # Stage 2: Run UI Tests with Official Build + - ${{ if eq(parameters.platform, 'x64') }}: + - stage: Test_x64Win10_OfficialBuild + displayName: Test x64Win10 (Official Build) + dependsOn: BuildUITests_${{ parameters.platform }} + jobs: + - template: job-test-project.yml + parameters: + platform: x64Win10 + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: ${{ parameters.buildSource }} + specificBuildId: ${{ parameters.specificBuildId }} + uiTestModules: ${{ parameters.uiTestModules }} + + # Additional per-user installation test + - template: job-test-project.yml + parameters: + platform: x64Win10 + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: ${{ parameters.buildSource }} + specificBuildId: ${{ parameters.specificBuildId }} + uiTestModules: ${{ parameters.uiTestModules }} + installMode: 'peruser' + jobSuffix: '_PerUser' + + - stage: Test_x64Win11_OfficialBuild + displayName: Test x64Win11 (Official Build) + dependsOn: BuildUITests_${{ parameters.platform }} + jobs: + - template: job-test-project.yml + parameters: + platform: x64Win11 + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: ${{ parameters.buildSource }} + specificBuildId: ${{ parameters.specificBuildId }} + uiTestModules: ${{ parameters.uiTestModules }} + + # Additional per-user installation test + - template: job-test-project.yml + parameters: + platform: x64Win11 + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: ${{ parameters.buildSource }} + specificBuildId: ${{ parameters.specificBuildId }} + uiTestModules: ${{ parameters.uiTestModules }} + installMode: 'peruser' + jobSuffix: '_PerUser' + + - ${{ if ne(parameters.platform, 'x64') }}: + - stage: Test_${{ parameters.platform }}_OfficialBuild + displayName: Test ${{ parameters.platform }} (Official Build) + dependsOn: BuildUITests_${{ parameters.platform }} + jobs: + - template: job-test-project.yml + parameters: + platform: ${{ parameters.platform }} + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: ${{ parameters.buildSource }} + specificBuildId: ${{ parameters.specificBuildId }} + uiTestModules: ${{ parameters.uiTestModules }} + + # Additional per-user installation test + - template: job-test-project.yml + parameters: + platform: ${{ parameters.platform }} + configuration: Release + useLatestWebView2: ${{ parameters.useLatestWebView2 }} + buildSource: ${{ parameters.buildSource }} + specificBuildId: ${{ parameters.specificBuildId }} + uiTestModules: ${{ parameters.uiTestModules }} + installMode: 'peruser' + jobSuffix: '_PerUser' diff --git a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 index e3120836c8..af9ab8ff6f 100644 --- a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 +++ b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 @@ -57,12 +57,19 @@ $totalList = $projFiles | ForEach-Object -Parallel { $p = -split $p $p = $p[1, 2] - $tempString = $p[0] + " " + $p[1] + $tempString = $p[0] - if(![string]::IsNullOrWhiteSpace($tempString)) + if([string]::IsNullOrWhiteSpace($tempString)) { - echo "- $tempString"; + Continue } + + if($tempString.StartsWith("Microsoft.") -Or $tempString.StartsWith("System.")) + { + Continue + } + + echo "- $tempString" } $csproj = $null; } diff --git a/Directory.Packages.props b/Directory.Packages.props index d6e18efd77..a02bfb1039 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -9,6 +9,7 @@ + @@ -33,22 +34,22 @@ - + - + - - - - - + + + + + - + - + @@ -77,28 +79,28 @@ - + - - - + + + - + - - + + - + - - - - + + + + diff --git a/NOTICE.md b/NOTICE.md index b2232e4984..058f0863b1 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1491,92 +1491,49 @@ SOFTWARE. ## NuGet Packages used by PowerToys -- AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta -- AdaptiveCards.Rendering.WinUI3 2.1.0-beta -- AdaptiveCards.Templating 2.0.5 -- Appium.WebDriver 4.4.5 -- Azure.AI.OpenAI 1.0.0-beta.17 -- CommunityToolkit.Common 8.4.0 -- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock 0.1.250703-build.2173 -- CommunityToolkit.Mvvm 8.4.0 -- CommunityToolkit.WinUI.Animations 8.2.250402 -- CommunityToolkit.WinUI.Collections 8.2.250402 -- CommunityToolkit.WinUI.Controls.Primitives 8.2.250402 -- CommunityToolkit.WinUI.Controls.Segmented 8.2.250402 -- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250402 -- CommunityToolkit.WinUI.Controls.Sizers 8.2.250402 -- CommunityToolkit.WinUI.Converters 8.2.250402 -- CommunityToolkit.WinUI.Extensions 8.2.250402 -- CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2 -- CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2 -- ControlzEx 6.0.0 -- HelixToolkit 2.24.0 -- HelixToolkit.Core.Wpf 2.24.0 -- hyjiacan.pinyin4net 4.1.1 -- Interop.Microsoft.Office.Interop.OneNote 1.1.0.2 -- LazyCache 2.4.0 -- Mages 3.0.0 -- Markdig.Signed 0.34.0 -- MessagePack 3.1.3 -- Microsoft.Bcl.AsyncInterfaces 9.0.7 -- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0 -- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0 -- Microsoft.Data.Sqlite 9.0.7 -- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16 -- Microsoft.DotNet.ILCompiler (A) -- Microsoft.Extensions.DependencyInjection 9.0.7 -- Microsoft.Extensions.Hosting 9.0.7 -- Microsoft.Extensions.Hosting.WindowsServices 9.0.7 -- Microsoft.Extensions.Logging 9.0.7 -- Microsoft.Extensions.Logging.Abstractions 9.0.7 -- Microsoft.NET.ILLink.Tasks (A) -- Microsoft.SemanticKernel 1.15.0 -- Microsoft.Toolkit.Uwp.Notifications 7.1.2 -- Microsoft.Web.WebView2 1.0.2903.40 -- Microsoft.Win32.SystemEvents 9.0.7 -- Microsoft.Windows.Compatibility 9.0.7 -- Microsoft.Windows.CsWin32 0.3.183 -- Microsoft.Windows.CsWinRT 2.2.0 -- Microsoft.Windows.SDK.BuildTools 10.0.26100.4188 -- Microsoft.WindowsAppSDK 1.7.250513003 -- Microsoft.WindowsPackageManager.ComInterop 1.10.340 -- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9 -- Microsoft.Xaml.Behaviors.Wpf 1.1.39 -- ModernWpfUI 0.9.4 -- Moq 4.18.4 -- MSTest 3.8.3 -- NLog.Extensions.Logging 5.3.8 -- NLog.Schema 5.2.8 -- OpenAI 2.0.0 -- ReverseMarkdown 4.1.0 -- ScipBe.Common.Office.OneNote 3.0.1 -- SharpCompress 0.37.2 -- SkiaSharp.Views.WinUI 2.88.9 -- StreamJsonRpc 2.21.69 -- StyleCop.Analyzers 1.2.0-beta.556 -- System.CodeDom 9.0.7 -- System.CommandLine 2.0.0-beta4.22272.1 -- System.ComponentModel.Composition 9.0.7 -- System.Configuration.ConfigurationManager 9.0.7 -- System.Data.OleDb 9.0.7 -- System.Data.SqlClient 4.9.0 -- System.Diagnostics.EventLog 9.0.7 -- System.Diagnostics.PerformanceCounter 9.0.7 -- System.Drawing.Common 9.0.7 -- System.IO.Abstractions 22.0.13 -- System.IO.Abstractions.TestingHelpers 22.0.13 -- System.Management 9.0.7 -- System.Net.Http 4.3.4 -- System.Private.Uri 4.3.2 -- System.Reactive 6.0.1 -- System.Runtime.Caching 9.0.7 -- System.ServiceProcess.ServiceController 9.0.7 -- System.Text.Encoding.CodePages 9.0.7 -- System.Text.Json 9.0.7 -- System.Text.RegularExpressions 4.3.1 -- UnicodeInformation 2.6.0 -- UnitsNet 5.56.0 -- UTF.Unknown 2.5.1 -- WinUIEx 2.2.0 -- WPF-UI 3.0.5 -- WyHash 1.0.5 +- AdaptiveCards.ObjectModel.WinUI3 +- AdaptiveCards.Rendering.WinUI3 +- AdaptiveCards.Templating +- Appium.WebDriver +- Azure.AI.OpenAI +- CoenM.ImageSharp.ImageHash +- CommunityToolkit.Common +- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock +- CommunityToolkit.Mvvm +- CommunityToolkit.WinUI.Animations +- CommunityToolkit.WinUI.Collections +- CommunityToolkit.WinUI.Controls.Primitives +- CommunityToolkit.WinUI.Controls.Segmented +- CommunityToolkit.WinUI.Controls.SettingsControls +- CommunityToolkit.WinUI.Controls.Sizers +- CommunityToolkit.WinUI.Converters +- CommunityToolkit.WinUI.Extensions +- CommunityToolkit.WinUI.UI.Controls.DataGrid +- CommunityToolkit.WinUI.UI.Controls.Markdown +- ControlzEx +- HelixToolkit +- HelixToolkit.Core.Wpf +- hyjiacan.pinyin4net +- Interop.Microsoft.Office.Interop.OneNote +- LazyCache +- Mages +- Markdig.Signed +- MessagePack +- ModernWpfUI +- Moq +- MSTest +- NLog.Extensions.Logging +- NLog.Schema +- OpenAI +- ReverseMarkdown +- ScipBe.Common.Office.OneNote +- SharpCompress +- SkiaSharp.Views.WinUI +- StreamJsonRpc +- StyleCop.Analyzers +- UnicodeInformation +- UnitsNet +- UTF.Unknown +- WinUIEx +- WPF-UI +- WyHash \ No newline at end of file diff --git a/PowerToys.sln b/PowerToys.sln index 6010ed421d..6033ca1481 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -39,14 +39,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fancyzones", "fancyzones", 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}") = "UnitTests-FancyZones", "src\modules\fancyzones\FancyZonesTests\UnitTests\UnitTests.vcxproj", "{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}" +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}") = "UnitTests-CommonLib", "src\common\UnitTests-CommonLib\UnitTests-CommonLib.vcxproj", "{1A066C63-64B3-45F8-92FE-664E1CCE8077}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Common.Lib.UnitTests", "src\common\UnitTests-CommonLib\UnitTests-CommonLib.vcxproj", "{1A066C63-64B3-45F8-92FE-664E1CCE8077}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditor", "src\modules\fancyzones\editor\FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}" EndProject @@ -60,11 +60,8 @@ 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}" - ProjectSection(ProjectDependencies) = postProject - {51920F1F-C28C-4ADF-8660-4238766796C2} = {51920F1F-C28C-4ADF-8660-4238766796C2} - EndProjectSection EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameUnitTests", "src\modules\powerrename\unittests\PowerRenameLibUnitTests.vcxproj", "{2151F984-E006-4A9F-92EF-C6DDE3DC8413}" +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} @@ -80,7 +77,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageResizerUI", "src\modul 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}") = "ImageResizerUITest", "src\modules\imageresizer\tests\ImageResizerUITest.csproj", "{E0CC7526-D85E-43AC-844F-D5DF0D2F5AB8}" +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 @@ -152,13 +149,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PreviewHandlerCommon", "src 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}") = "UnitTests-MarkdownPreviewHandler", "src\modules\previewpane\UnitTests-MarkdownPreviewHandler\UnitTests-MarkdownPreviewHandler.csproj", "{A2B51B8B-8F90-424E-BC97-F9AB7D76CA1A}" +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}") = "UnitTests-SvgPreviewHandler", "src\modules\previewpane\UnitTests-SvgPreviewHandler\UnitTests-SvgPreviewHandler.csproj", "{060D75DA-2D1C-48E6-A4A1-6F0718B64661}" +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}") = "UnitTests-PreviewHandlerCommon", "src\modules\previewpane\UnitTests-PreviewHandlerCommon\UnitTests-PreviewHandlerCommon.csproj", "{748417CA-F17E-487F-9411-CAFB6D3F4877}" +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 @@ -195,7 +192,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ManagedCommon", "src\common 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}") = "UnitTests-SvgThumbnailProvider", "src\modules\previewpane\UnitTests-SvgThumbnailProvider\UnitTests-SvgThumbnailProvider.csproj", "{1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E}" +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 @@ -216,10 +213,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Settings.UI.UnitTests", "sr 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Plugin.Folder.UnitTests", "src\modules\launcher\Plugins\Microsoft.Plugin.Folder.UnitTests\Microsoft.Plugin.Folder.UnitTests.csproj", "{4FA206A5-F69F-4193-BF8F-F6EEB496734C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest-ColorPickerUI", "src\modules\colorPicker\UnitTest-ColorPickerUI\UnitTest-ColorPickerUI.csproj", "{090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}" -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}" @@ -235,7 +228,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "logger", "src\common\logger 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}") = "Microsoft.Interop.Tests", "src\common\interop\interop-tests\Microsoft.Interop.Tests.csproj", "{58736667-1027-4AD7-BFDF-7A3A6474103A}" +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 @@ -307,7 +300,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.UI", "src\common\Com 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}") = "UnitTests-PdfPreviewHandler", "src\modules\previewpane\UnitTests-PdfPreviewHandler\UnitTests-PdfPreviewHandler.csproj", "{ECC20689-002A-4354-95A6-B58DF089C6FF}" +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 @@ -317,13 +310,13 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEngine", "sr 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}") = "KeyboardManagerEngineTest", "src\modules\keyboardmanager\KeyboardManagerEngineTest\KeyboardManagerEngineTest.vcxproj", "{7F4B3A60-BC27-45A7-8000-68B0B6EA7466}" +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}") = "KeyboardManagerEditorTest", "src\modules\keyboardmanager\KeyboardManagerEditorTest\KeyboardManagerEditorTest.vcxproj", "{62173D9A-6724-4C00-A1C8-FB646480A9EC}" +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 @@ -351,7 +344,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plu 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}") = "UnitTests-PdfThumbnailProvider", "src\modules\previewpane\UnitTests-PdfThumbnailProvider\UnitTests-PdfThumbnailProvider.csproj", "{F40C3397-1834-4530-B2D9-8F8B8456BCDF}" +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 @@ -365,11 +358,11 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MouseHighlighter", "src\mod 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}") = "UnitTests-GcodeThumbnailProvider", "src\modules\previewpane\UnitTests-GcodeThumbnailProvider\UnitTests-GcodeThumbnailProvider.csproj", "{133281D8-1BCE-4D07-B31E-796612A9609E}" +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}") = "UnitTests-GcodePreviewHandler", "src\modules\previewpane\UnitTests-GcodePreviewHandler\UnitTests-GcodePreviewHandler.csproj", "{FCF3E52D-B80A-4FC3-98FD-6391354F0EE3}" +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 @@ -383,7 +376,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MousePointerCrosshairs", "s 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}") = "UnitTests-StlThumbnailProvider", "src\modules\previewpane\UnitTests-StlThumbnailProvider\UnitTests-StlThumbnailProvider.csproj", "{F1F6B6B6-9F18-4A17-8B5C-97DF552C53DC}" +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 @@ -419,6 +412,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerOCR", "src\modules\Pow 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}" @@ -440,7 +435,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HostsUILib", "src\modules\H EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hosts", "Hosts", "{F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hosts.Tests", "src\modules\Hosts\Hosts.Tests\Hosts.Tests.csproj", "{E2D03E0F-7A75-4813-9F4B-D8763D43FD3A}" +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 @@ -466,6 +461,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Peek.Common", "src\modules\ 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}" @@ -530,8 +527,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLockModuleInterface" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cmdNotFound", "cmdNotFound", "{4C0D0746-BE5B-49EE-BD5D-A7811628AE8B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests-FancyZonesEditor", "src\modules\fancyzones\UnitTests-FancyZonesEditor\UnitTests-FancyZonesEditor.csproj", "{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}" -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}" @@ -546,9 +541,9 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "QoiPreviewHandlerCpp", "src 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}") = "UnitTests-QoiPreviewHandler", "src\modules\previewpane\UnitTests-QoiPreviewHandler\UnitTests-QoiPreviewHandler.csproj", "{3940AD4D-F748-4BE4-9083-85769CD553EF}" +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}") = "UnitTests-QoiThumbnailProvider", "src\modules\previewpane\UnitTests-QoiThumbnailProvider\UnitTests-QoiThumbnailProvider.csproj", "{F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38}" +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 @@ -564,10 +559,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreview", "src\modu 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}") = "UITests-FancyZones", "src\modules\fancyzones\UITests-FancyZones\UITests-FancyZones.csproj", "{FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITests-FancyZonesEditor", "src\modules\fancyzones\UITests-FancyZonesEditor\UITests-FancyZonesEditor.csproj", "{3A9A791E-94A9-49F8-8401-C11CE288D5FB}" -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}" @@ -604,7 +595,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowProperties", "WindowP 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}") = "WorkspacesLibUnitTests", "src\modules\Workspaces\WorkspacesLib.UnitTests\WorkspacesLibUnitTests.vcxproj", "{A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C}" +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 @@ -680,10 +671,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSea 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.UnitTests", "src\modules\AdvancedPaste\AdvancedPaste.UnitTests\AdvancedPaste.UnitTests.csproj", "{D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdvancedPaste.FuzzTests", "src\modules\AdvancedPaste\AdvancedPaste.FuzzTests\AdvancedPaste.FuzzTests.csproj", "{7F5B9557-5878-4438-A721-3E28296BA193}" -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}" @@ -700,19 +687,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KeyboardManagerEditorUI", " 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}") = "Hosts.FuzzTests", "src\modules\Hosts\Hosts.FuzzTests\Hosts.FuzzTests.csproj", "{EBED240C-8702-452D-B764-6DB9DA9179AF}" +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}") = "Hosts.UITests", "src\modules\Hosts\Hosts.UITests\Hosts.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}" +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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZones.FuzzTests", "src\modules\fancyzones\FancyZones.FuzzTests\FancyZones.FuzzTests.csproj", "{0217E86E-3476-9946-DE8E-9D200CEBD47A}" -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.FuzzingTest", "src\modules\powerrename\PowerRename.FuzzingTest\PowerRename.FuzzingTest.vcxproj", "{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}" +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 @@ -722,24 +707,91 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "BgcodeThumbnailProviderCpp" 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}") = "UnitTests-BgcodePreviewHandler", "src\modules\previewpane\UnitTests-BgcodePreviewHandler\UnitTests-BgcodePreviewHandler.csproj", "{99CA1509-FB73-456E-AFAF-AB89C017BD72}" +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}") = "UnitTests-BgcodeThumbnailProvider", "src\modules\previewpane\UnitTests-BgcodeThumbnailProvider\UnitTests-BgcodeThumbnailProvider.csproj", "{61CBF221-9452-4934-B685-146285E080D7}" +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}") = "MouseUtils.UITests", "src\modules\MouseUtils\MouseUtils.UITests\MouseUtils.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkspacesEditorUITest", "src\modules\Workspaces\WorkspacesEditorUITest\WorkspacesEditorUITest.csproj", "{43E779F3-D83C-48B1-BA8D-1912DBD76FC9}" +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}") = "PowerRenameUITest", "src\modules\powerrename\PowerRenameUITest\PowerRenameUITest.csproj", "{9D3F3793-EFE3-4525-8782-238015DABA62}" +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\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("{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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -1140,22 +1192,6 @@ Global {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 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|ARM64.Build.0 = Debug|ARM64 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|x64.ActiveCfg = Debug|x64 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Debug|x64.Build.0 = Debug|x64 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|ARM64.ActiveCfg = Release|ARM64 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|ARM64.Build.0 = Release|ARM64 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.ActiveCfg = Release|x64 - {4FA206A5-F69F-4193-BF8F-F6EEB496734C}.Release|x64.Build.0 = Release|x64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|ARM64.Build.0 = Debug|ARM64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|x64.ActiveCfg = Debug|x64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Debug|x64.Build.0 = Debug|x64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|ARM64.ActiveCfg = Release|ARM64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|ARM64.Build.0 = Release|ARM64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.Release|x64.ActiveCfg = Release|x64 - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1}.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 @@ -1824,6 +1860,14 @@ Global {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 @@ -2040,14 +2084,6 @@ Global {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 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Debug|ARM64.Build.0 = Debug|ARM64 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Debug|x64.ActiveCfg = Debug|x64 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Debug|x64.Build.0 = Debug|x64 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|ARM64.ActiveCfg = Release|ARM64 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|ARM64.Build.0 = Release|ARM64 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|x64.ActiveCfg = Release|x64 - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.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 @@ -2172,22 +2208,6 @@ Global {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 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Debug|ARM64.Build.0 = Debug|ARM64 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Debug|x64.ActiveCfg = Debug|x64 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Debug|x64.Build.0 = Debug|x64 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Release|ARM64.ActiveCfg = Release|ARM64 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Release|ARM64.Build.0 = Release|ARM64 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Release|x64.ActiveCfg = Release|x64 - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52}.Release|x64.Build.0 = Release|x64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.Debug|ARM64.Build.0 = Debug|ARM64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.Debug|x64.ActiveCfg = Debug|x64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.Debug|x64.Build.0 = Debug|x64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.Release|ARM64.ActiveCfg = Release|ARM64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.Release|ARM64.Build.0 = Release|ARM64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.Release|x64.ActiveCfg = Release|x64 - {3A9A791E-94A9-49F8-8401-C11CE288D5FB}.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 @@ -2512,22 +2532,6 @@ Global {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 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|ARM64.Build.0 = Debug|ARM64 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|x64.ActiveCfg = Debug|x64 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Debug|x64.Build.0 = Debug|x64 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|ARM64.ActiveCfg = Release|ARM64 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|ARM64.Build.0 = Release|ARM64 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x64.ActiveCfg = Release|x64 - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE}.Release|x64.Build.0 = Release|x64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Debug|ARM64.Build.0 = Debug|ARM64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Debug|x64.ActiveCfg = Debug|x64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Debug|x64.Build.0 = Debug|x64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Release|ARM64.ActiveCfg = Release|ARM64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Release|ARM64.Build.0 = Release|ARM64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Release|x64.ActiveCfg = Release|x64 - {7F5B9557-5878-4438-A721-3E28296BA193}.Release|x64.Build.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 @@ -2620,14 +2624,6 @@ Global {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 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|ARM64.Build.0 = Debug|ARM64 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.ActiveCfg = Debug|x64 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Debug|x64.Build.0 = Debug|x64 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.ActiveCfg = Release|ARM64 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|ARM64.Build.0 = Release|ARM64 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.Release|x64.ActiveCfg = Release|x64 - {0217E86E-3476-9946-DE8E-9D200CEBD47A}.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 @@ -2722,14 +2718,6 @@ Global {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 - {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 {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 @@ -2738,6 +2726,150 @@ Global {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 + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2746,26 +2878,26 @@ Global {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} = {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} = {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} = {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} = {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} @@ -2778,31 +2910,29 @@ Global {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} = {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} = {2F305555-C296-497E-AC20-5FA1B237996A} - {748417CA-F17E-487F-9411-CAFB6D3F4877} = {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} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {1EF1EEF0-10F0-4F2E-8550-39B6D8044D3E} = {2F305555-C296-497E-AC20-5FA1B237996A} + {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} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {0F85E674-34AE-443D-954C-8321EB8B93B1} = {C3081D9A-1586-441A-B5F4-ED815B3719C1} - {632BBE62-5421-49EA-835A-7FFA4F499BD6} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {4FA206A5-F69F-4193-BF8F-F6EEB496734C} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {090CD7B7-3B0C-4D1D-BC98-83EB5D799BC1} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0} + {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} = {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} @@ -2821,20 +2951,20 @@ Global {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} = {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} = {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} = {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} = {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} = {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} @@ -2842,27 +2972,27 @@ Global {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} = {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} = {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} = {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} = {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} = {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} = {4AFC9975-2456-4C70-94A4-84073C1CED93} - {8FE5A5EE-1B59-401C-9FB3-B04ECD3E29C1} = {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} @@ -2876,6 +3006,7 @@ Global {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} @@ -2884,7 +3015,7 @@ Global {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} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} + {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} @@ -2897,6 +3028,7 @@ Global {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} @@ -2913,7 +3045,7 @@ Global {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} = {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} @@ -2924,12 +3056,11 @@ Global {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} = {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} - {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {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} @@ -2937,8 +3068,8 @@ Global {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} = {2F305555-C296-497E-AC20-5FA1B237996A} - {F8FFFC12-A31A-4AFA-B3DF-14DCF42B5E38} = {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} @@ -2946,8 +3077,6 @@ Global {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} - {FE38FC07-1C05-4B57-ADA3-2FE2F53C6A52} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} - {3A9A791E-94A9-49F8-8401-C11CE288D5FB} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {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} @@ -2957,7 +3086,7 @@ Global {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} = {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} @@ -2965,7 +3094,7 @@ Global {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} = {B6C42F16-73EB-477E-8B0D-4E6CF6C20AAC} + {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} @@ -2995,8 +3124,6 @@ Global {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} - {D5E5F5EA-1B6C-4A73-88BE-304F36C9E4EE} = {9873BA05-4C41-4819-9283-CF45D795431B} - {7F5B9557-5878-4438-A721-3E28296BA193} = {9873BA05-4C41-4819-9283-CF45D795431B} {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} @@ -3005,26 +3132,57 @@ Global {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} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA} - {5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5} + {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} - {0217E86E-3476-9946-DE8E-9D200CEBD47A} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79} - {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} + {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} = {2F305555-C296-497E-AC20-5FA1B237996A} - {61CBF221-9452-4934-B685-146285E080D7} = {2F305555-C296-497E-AC20-5FA1B237996A} - {4E0AE3A4-2EE0-44D7-A2D0-8769977254A1} = {322566EF-20DC-43A6-B9F8-616AF942579A} - {43E779F3-D83C-48B1-BA8D-1912DBD76FC9} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} + {99CA1509-FB73-456E-AFAF-AB89C017BD72} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} + {61CBF221-9452-4934-B685-146285E080D7} = {6B01F1CF-F4DB-48B5-BFE7-0BF576C1D704} + {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} - {9D3F3793-EFE3-4525-8782-238015DABA62} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} + {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} + {E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} + {E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/README.md b/README.md index 493878bbde..27c98d07ff 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Command Palette](https://aka.ms/PowerToysOverview_CmdPal) | | [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | -| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | +| [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse Utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [New+](https://aka.ms/PowerToysOverview_NewPlus) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | | [Peek](https://aka.ms/PowerToysOverview_Peek) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | | [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | @@ -35,19 +35,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user. -[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22 -[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.92%22 -[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-x64.exe -[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysUserSetup-0.92.1-arm64.exe -[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-x64.exe -[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.92.1/PowerToysSetup-0.92.1-arm64.exe +[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.94%22 +[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.93%22 +[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-x64.exe +[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysUserSetup-0.93.0-arm64.exe +[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-x64.exe +[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.93.0/PowerToysSetup-0.93.0-arm64.exe | Description | Filename | |----------------|----------| -| Per user - x64 | [PowerToysUserSetup-0.92.1-x64.exe][ptUserX64] | -| Per user - ARM64 | [PowerToysUserSetup-0.92.1-arm64.exe][ptUserArm64] | -| Machine wide - x64 | [PowerToysSetup-0.92.1-x64.exe][ptMachineX64] | -| Machine wide - ARM64 | [PowerToysSetup-0.92.1-arm64.exe][ptMachineArm64] | +| Per user - x64 | [PowerToysUserSetup-0.93.0-x64.exe][ptUserX64] | +| Per user - ARM64 | [PowerToysUserSetup-0.93.0-arm64.exe][ptUserArm64] | +| Machine wide - x64 | [PowerToysSetup-0.93.0-x64.exe][ptMachineX64] | +| Machine wide - ARM64 | [PowerToysSetup-0.93.0-arm64.exe][ptMachineArm64] | This is our preferred method. @@ -93,139 +93,119 @@ For guidance on developing for PowerToys, please read the [developer docs](./doc Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on. -### 0.92 - June 2025 Update +### 0.93 - Aug 2025 Update In this release, we focused on new features, stability, optimization improvements, and automation. **✨Highlights** - - PowerToys settings now has a toggle for the system tray icon, giving users control over its visibility based on personal preference. Thanks [@BLM16](https://github.com/BLM16)! - - Command Palette now has Ahead-of-Time ([AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot)) compatibility for all first-party extensions, improved extensibility, and core UX fixes, resulting in better performance and stability across commands. - - Color Picker now has customizable mouse button actions, enabling more personalized workflows by assigning functions to left, right, and middle clicks. Thanks [@PesBandi](https://github.com/PesBandi)! - - Bug Report Tool now has a faster and clearer reporting process, with progress indicators, improved compression, auto-cleanup of old trace logs, and inclusion of MSIX installer logs for more efficient diagnostics. - - File Explorer add-ons now have improved rendering stability, resolving issues with PDF previews, blank thumbnails, and text file crashes during file browsing. - -### Color Picker - - - Added mouse button actions so you can choose what left, right, or middle click does. Thanks [@PesBandi](https://github.com/PesBandi)! - -### Crop & Lock - - - Aligned window styling with current Windows theme for a cleaner look. Thanks [@sadirano](https://github.com/sadirano)! + - PowerToys settings debuts a modern, card-based dashboard with clearer descriptions and faster navigation for a streamlined user experience. + - Command Palette had over 99 issues resolved, including bringing back Clipboard History, adding context menu shortcuts, pinning favorite apps, and supporting history in Run. + - Command Palette reduced its startup memory usage by ~15%, load time by ~40%, built-in extensions loading time by ~70%, and installation size by ~55%—all due to using the full Ahead-of-Time (AOT) compilation mode in Windows App SDK. + - Peek now supports instant previews and embedded thumbnails for Binary G-code (.bgcode) 3D printing files, making it easy to inspect models at a glance. Thanks [@pedrolamas](https://github.com/pedrolamas)! + - Mouse Utilities introduces a new spotlight highlighting mode that dims the screen and draws attention to your cursor, perfect for presentations. + - Test coverage improvements for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename — ensuring better reliability and quality, with over 600 new unit tests (mostly for Command Palette) and doubled UI automation coverage. ### Command Palette - - Enhanced performance by resolving a regression in page loading. - - Applied consistent hotkey handling across all Command Palette commands for a smoother user experience. - - Improved graceful closing of Command Palette. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Fixed consistency issue for extensions' alias with "Direct" setting and enabled localization for "Direct" and "Indirect" for better user understanding. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! - - Improved visual clarity by styling critical context items correctly. - - Automatically focused the field when only one is present on the content page. - - Improved stability and efficiency when loading file icons in SDK ThumbnailHelper.cs by removing unnecessary operations. Thanks [@OldUser101](https://github.com/OldUser101)! - - Enhanced details view with commands implementation. (See [Extension sample](./src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPageWithDetails.cs)) + - Ensured screen readers are notified when the selected item in the list changes for better accessibility. + - Fixed command title changes not being properly notified to screen readers. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Made icon controls excluded from keyboard navigation by default for better accessibility. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Improved UI design with better text sizing and alignment. + - Fixed keyboard shortcuts to work better in text boxes and context menus. + - Added right-click context menus with critical command styling and separators. + - Improved various context menu issues, improving item selection, handling of long titles, search bar text scaling, initial item behavior, and primary button functionality. + - Fixed context menu crashes with better type handling. + - Fixed "Reload" command to work with both uppercase and lowercase letters. + - Added mouse back button support for easier navigation. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed Alt+Left Arrow navigation not working when search box contains text. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Updated back button tooltip to show keyboard shortcut information. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed Command Palette window not appearing properly when activated. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed Command Palette window staying hidden from taskbar after File Explorer restarts. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed window focus not returning to previous app properly. + - Fixed Command Palette window to always appear on top when shown and move to bottom when hidden. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed window hiding to properly work on UI thread. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Fixed crashes and improved stability with better synchronization of Command list updates. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Improved extension disposal with better error handling to prevent crashes. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Improved stability by fixing a UI threading issue when loading more results, preventing possible crashes and ensuring the loading state resets if loading fails. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Enhanced icon loading stability with better exception handling. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Added thread safety to recent commands to prevent crashes. Thanks [@MaoShengelia](https://github.com/MaoShengelia)! + - Fixed acrylic (frosted glass) system backdrop display issues by ensuring proper UI thread handling. Thanks [@jiripolasek](https://github.com/jiripolasek)! ### Command Palette extensions - - Added "Copy Path" command to *App* search results for convenience. Thanks [@PesBandi](https://github.com/PesBandi)! - - Improved *Calculator* input experience by ignoring leading equal signs. Thanks [@PesBandi](https://github.com/PesBandi)! - - Corrected input handling in the *Calculator* extension to avoid showing errors for input with only leading whitespace. - - Improved *New Extension* wizard by validating names to prevent namespace errors. - - Ensured consistent context items display for the *Run* extension between fallback and top-level results. - - Fixed missing *Time & Date* commands in fallback results. Thanks [@htcfreek](https://github.com/htcfreek)! - - Fixed outdated results in the *Time & Date* extension. Thanks [@htcfreek](https://github.com/htcfreek)! - - Fixed an issue where *Web Search* always opened Microsoft Edge instead of the user's default browser on Windows 11 24H2 and later. Thanks [@RuggMatt](https://github.com/RuggMatt)! - - Improved ordering of *Windows Settings* extension search results from alphabetical to relevance-based for quicker access. - - Added "Restart Windows Explorer" command to the *Windows System Commands* provider for gracefully terminate and relaunch explorer.exe. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Added settings to each provider to control which fallback commands are enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! for fixing a regression in this feature. + - Added sample code showing how Command Palette extensions can track when their pages are loaded or unloaded. [Check it out here](./src/modules/cmdpal/ext/SamplePagesExtension/OnLoadPage.cs). + - Fixed *Calculator* to accept regular spaces in numbers that use space separators. Thanks [@PesBandi](https://github.com/PesBandi)! + - Added a new setting to *Calculator* to make "Copy" the primary button (replacing “Save”) and enable "Close on Enter", streamlining the workflow. Thanks [@PesBandi](https://github.com/PesBandi)! + - Improved *Apps* indexing error handling and removed obsolete code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)! + - Prevented apps from showing in search when the *Apps* extension is disabled. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Added ability to pin/unpin *Apps* using Ctrl+P shortcut. + - Added keyboard shortcuts to the *Apps* context menu items for faster access. + - Added all file context menu options to the *Apps* items context menu, making all file actions available there for better functionality. + - Streamlined All *Apps* extension settings by removing redundant descriptions, making the UI clearer. + - Added command history to the *Run* page for easier access to previous commands. + - Fixed directory path handling in *Run* fallback for better file navigation. + - Fixed URL fallback item hiding properly in *Web Search* extension when search query becomes invalid. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Added proper empty state message for *Web Search* extension when no results found. Thanks [@jiripolasek](https://github.com/jiripolasek)! + - Added fallback command to *Windows Settings* extension for better search results. + - Re-enabled *Clipboard History* feature with proper window handling. + - Improved *Add Bookmark* extension to automatically detect file, folder, or URL types without manual input. + - Updated terminology from "Kill process" to "End task" in *Window Walker* for consistency with Windows. + - Fixed minor grammar error in SamplePagesExtension code comments. Thanks [@purofle](https://github.com/purofle)! -### Command Palette Ahead-of-Time (AOT) readiness +### Mouse Utilities - - We’ve made foundational changes to prepare the Command Palette for future Ahead-of-Time (AOT) publishing. This includes replacing the calculator library with ExprTk, improving COM object handling, refining Win32 interop, and correcting trimming behavior—all to ensure compatibility, performance, and reliability under AOT constraints. All first-party extensions are now AOT-compatible. These improvements lay the groundwork for publishing Command Palette as an AOT application in the next release. - - Special thanks to [@Sergio0694](https://github.com/Sergio0694) for guidance on making COM APIs AOT-compatible, [@jtschuster](https://github.com/jtschuster) for fixing COM object handling, [@ArashPartow](https://github.com/ArashPartow) from ExprTk for integration suggestions, and [@tian-lt](https://github.com/tian-lt) from the Windows Calculator team for valuable suggestion throughout the migration journey and review. - - As part of the upcoming release, we’re also enabling AOT compatibility for key dependencies, including markdown rendering, Adaptive Cards, internal logging and telemetry library, and the core Command Palette UX. - -### FancyZones - - - Fixed DPI-scaling issues to ensure FancyZones Editor displays crisply on high-resolution monitors. Thanks [@HO-COOH](https://github.com/HO-COOH)! This inspired us a broader review across other PowerToys modules, leading to DPI display optimizations in Awake, Color Picker, PowerAccent, and more. - -### File Explorer add-ons - - - Fixed potential failures in PDF previewer and thumbnail generation, improving reliability when browsing PDF files. Thanks [@mohiuddin-khan-shiam](https://github.com/mohiuddin-khan-shiam)! - - Prevented Monaco Preview Handler crash when opening UTF-8-BOM text files. - -### Hosts File Editor - - - Added an in-app *“Learn more”* link to warning dialogs for quick guidance. Thanks [@PesBandi](https://github.com/PesBandi)! - -### Mouse Without Borders - - - Fixed firewall rule so MWB now accepts connections from IPs outside your local subnet. - - Cleaned legacy logs to reduce disk usage and noise. + - Added a new spotlight highlighting mode that creates a large transparent circle around your cursor with a backdrop effect, providing an alternative to the traditional circle highlight. Perfect for presentations where you want to focus attention on a specific area while dimming the rest of the screen. ### Peek - - Updated QOI reader so 3-channel QOI images preview correctly in Peek and File Explorer. Thanks [@mbartlett21](https://github.com/mbartlett21)! - - Added codec detection with a clear warning when a video can’t be previewed, along with a link to the Microsoft Store to download the required codec. + - Added preview and thumbnail support for Binary G-code (.bgcode) files used in 3D printing. You can now see embedded thumbnails and preview these compressed 3D printing files directly in Peek and File Explorer. Thanks [@pedrolamas](https://github.com/pedrolamas)! -### PowerRename +### Quick Accent - - Added support for $YY-$MM-$DD in ModificationTime and AccessTime to enable flexible date-based renaming. - -### PowerToys Run - - - Suppressed error UI for known WPF-related crashes to reduce user confusion, while retaining diagnostic logging for analysis. This targets COMException 0xD0000701 and 0x80263001 caused by temporary DWM unavailability. - -### Registry Preview - - - Added "Extended data preview" via magnifier icon and context menu in the Data Grid, enabled easier inspection of complex registry types like REG_BINARY, REG_EXPAND_SZ, and REG_MULTI_SZ, etc. Thanks [@htcfreek](https://github.com/htcfreek)! - - Improved file-saving experience in Registry Preview by aligning with Notepad-like behavior, enhancing user prompts, error handling, and preventing crashes during unsaved or interrupted actions. Thanks [@htcfreek](https://github.com/htcfreek)! + - Added Vietnamese language support to Quick Accent, mappings for Vietnamese vowels (a, e, i, o, u, y) and the letter d. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)! ### Settings - - Added an option to hide or show the PowerToys system tray icon. Thanks [@BLM16](https://github.com/BLM16)! - - Improved settings to show progress while a bug report package is being generated. - -### Workspaces - - - Stored Workspaces icons in user AppData to ensure profile portability and prevent loss during temporary folder cleanup. - - Enabled capture and launch of PWAs on non-default Edge or Chrome profiles, ensuring consistent behavior during creation and execution. + - Completely redesigned the Settings dashboard with a modern card-based layout featuring organized sections for quick actions and shortcuts overview, replacing the old module list. + - Rewrote setting descriptions to be more concise and follow Windows writing style guidelines, making them easier to understand. + - Improved formatting and readability of release notes in the "What's New" section with better typography and spacing. + - Added missing deep link support for various settings pages (Peek, Quick Accent, PowerToys Run, etc.) so you can jump directly to specific settings. + - Resolved an issue where the settings page header would drift away from its position when resizing the settings window. + - Resolved a settings crash related to incompatible property names in ZoomIt configuration. ### Documentation - - Added SpeedTest and Dictionary Definition to the third-party plugins documentation for PowerToys Run. Thanks [@ruslanlap](https://github.com/ruslanlap)! - - Corrected sample links and typo in Command Palette documentation. Thanks [@daverayment](https://github.com/daverayment) and [@roycewilliams](https://github.com/roycewilliams)! + - Added detailed step-by-step instructions for first-time developers building the Command Palette module, including prerequisites and Visual Studio setup guidance. Thanks [@chatasweetie](https://github.com/chatasweetie)! + - **Fixed Broken SDK Link**: Corrected a broken markdown link in the Command Palette SDK README that was pointing to an incorrect directory path. Thanks [@ChrisGuzak](https://github.com/ChrisGuzak)! + - Added documentation for the "Open With Cursor" plugin that enables opening Visual Studio and VS Code recent files using Cursor AI. Thanks [@VictorNoxx](https://github.com/VictorNoxx)! + - Added documentation for two new community plugins - Hotkeys plugin for creating custom keyboard shortcuts, and RandomGen plugin for generating random data like passwords, colors, and placeholder text. Thanks [@ruslanlap](https://github.com/ruslanlap)! ### Development - - Updated .NET libraries to 9.0.6 for performance and security. Thanks [@snickler](https://github.com/snickler)! - - Updated WinAppSDK to 1.7.2 for better stability and Windows support. - - Introduced a one-step local build script that generates a signed installer, enhancing developer productivity. - - Generated portable PDBs so cross-platform debuggers can read symbol files, improving debugging experience in VSCode and other tools. - - Simplified WinGet configuration files by using the [Microsoft.Windows.Settings](https://www.powershellgallery.com/packages/Microsoft.Windows.Settings) module to enable Developer Mode. Thanks [@mdanish-kh](https://github.com/mdanish-kh)! - - Adjusted build scripts for the latest Az.Accounts module to keep CI green. - - Streamlined release pipeline by removing hard-coded telemetry version numbers, and unified Command Palette versioning with Windows Terminal's versioning method for consistent updates. - - Enhanced the build validation step to show detailed differences between NOTICE.md and actual package dependencies and versions. - - Improved spell-checking accuracy across the repo. Thanks [@rovercoder](https://github.com/rovercoder)! - - Upgraded CI to TouchdownBuild v5 for faster pipelines. - - Added context comments to *Resources.resw* to help translators. - - Expanded fuzz testing coverage to include FancyZones. - - Integrated all unit tests into the CI pipeline, increasing from ~3,000 to ~5,000 tests. - - Enabled daily UI test automation on the main branch, now covering over 370 UI tests for end-to-end validation. - - Newly added unit tests for WorkspacesLib to improve reliability and maintainability. + - Updated .NET libraries to 9.0.8 for performance and security. Thanks [@snickler](https://github.com/snickler)! + - Updated the spell check system to version 0.0.25 with better GitHub integration and SARIF reporting, plus fixed numerous spelling errors throughout the codebase including property names and documentation. Thanks [@jsoref](https://github.com/jsoref)! + - Cleaned up spelling check configuration to eliminate false positives and excessive noise that was appearing in every pull request, making the development process smoother. + - Replaced NuGet feed with Azure Artifacts for better package management. + - Implemented configurable UI test pipeline that can use pre-built official releases instead of building everything from scratch, reducing test execution time from 2+ hours. + - Replaced brittle pixel-by-pixel image comparison with perceptual hash (pHash) technology that's more robust to minor rendering differences - no more test failures due to anti-aliasing or compression artifacts. + - Reduced CI/fuzzing/UI test timeouts from 4 hours to 90 minutes, dramatically improving developer feedback loops and preventing long waits when builds get stuck. + - Standardized test project naming across the entire codebase and improved pipeline result identification by adding platform/install mode context to test run titles. Thanks [@khmyznikov](https://github.com/khmyznikov)! + - Added comprehensive UI test suites for multiple PowerToys modules including Command Palette, Advanced Paste, Peek, Text Extractor, and PowerRename - ensuring better reliability and quality. + - Enhanced UI test automation with command-line argument support, better session management, and improved element location methods using pattern matching to avoid failures from minor differences in exact matches. -### General +### What is being planned over the next few releases -- Updated bug report compression library (cziplib 0.3.3) for faster and more reliable package creation. Thanks [@Chubercik](https://github.com/Chubercik)! -- Included App Installer (“AppX Deployment Server”) event logs in bug reports for more thorough diagnostics. - -### What is being planned for version 0.93 - -For [v0.93][github-next-release-work], we'll work on the items below: +For [v0.94][github-next-release-work], we'll work on the items below: - Continued Command Palette polish - - New UI automation tests - - Working on installer upgrades + - Working on Shortcut Guide v2 (Thanks [@noraa-junker](https://github.com/noraa-junker)!) + - Working on upgrading the installer to WiX 5 - Working on shortcut conflict detection + - Working on setting search - Upgrading Keyboard Manager's editor UI + - New UI automation tests - Stability, bug fixes ## PowerToys Community diff --git a/doc/devdocs/UITests.md b/doc/devdocs/UITests.md index 5e2e39c75b..63bddb0591 100644 --- a/doc/devdocs/UITests.md +++ b/doc/devdocs/UITests.md @@ -22,23 +22,23 @@ The PowerToys UI test pipeline provides flexible options for building and testin ### Pipeline Options -- **useLatestOfficialBuild**: When checked, downloads the latest official PowerToys build and installs it for testing. This skips the full solution build and only builds UI test projects. +- **buildSource**: Select the build type for testing: + - `latestMainOfficialBuild`: Downloads and uses the latest official PowerToys build from main branch + - `buildNow`: Builds PowerToys from current source code and uses it for testing + - `specificBuildId`: Downloads a specific PowerToys build using the build ID specified in `specificBuildId` parameter -- **useCurrentBranchBuild**: When checked along with `useLatestOfficialBuild`, downloads the official build from the current branch instead of main. + **Default value**: `latestMainOfficialBuild` - **Default value**: `false` (downloads from main branch) +- **specificBuildId**: When `buildSource` is set to `specificBuildId`, specify the exact PowerToys build ID to download and test against. + + **Default value**: `"xxxx"` (placeholder, enter actual build ID when using specificBuildId option) **When to use this**: - - **Default scenario**: The pipeline tests against the latest signed PowerToys build from the `main` branch, regardless of which branch your test code changes are from - - **Custom branch testing**: Only specify `true` when: - - Your branch has produced its own signed PowerToys build via the official build pipeline - - You want to test against that specific branch's PowerToys build instead of main - - You are testing PowerToys functionality changes that are only available in your branch's build + - Testing against a specific known build for reproducibility + - Regression testing against a particular build version + - Validating fixes in a specific build before release - **Important notes**: - - The test pipeline itself runs from your specified branch, but by default tests against the main branch's PowerToys build - - Not all branches have signed builds available - only use this if you're certain your branch has a signed build - - If enabled but no build exists for your branch, the pipeline may fail or fall back to main + **Usage**: Enter the build ID number (e.g., `12345`) to download that specific build. Only used when `buildSource` is set to `specificBuildId`. - **uiTestModules**: Specify which UI test modules to build and run. This parameter controls both the `.csproj` projects to build and the `.dll` test assemblies to execute. Examples: - `['UITests-FancyZones']` - Only FancyZones UI tests @@ -50,25 +50,25 @@ The PowerToys UI test pipeline provides flexible options for building and testin ### Build Modes -1. **Official Build + Selective Testing** (`useLatestOfficialBuild = true`) - - Downloads and installs official PowerToys build - - Builds only specified UI test projects - - Runs specified UI tests against installed PowerToys - - Controlled by `uiTestModules` parameter +1. **Official Build Testing** (`buildSource = latestMainOfficialBuild` or `specificBuildId`) + - Downloads and installs official PowerToys build (latest from main or specific build ID) + - Builds only UI test projects (all or specific based on `uiTestModules`) + - Runs UI tests against installed PowerToys + - Tests both machine-level and per-user installation modes automatically -2. **Full Build + Testing** (`useLatestOfficialBuild = false`) - - Builds entire PowerToys solution +2. **Current Source Build Testing** (`buildSource = buildNow`) + - Builds entire PowerToys solution from current source code - Builds UI test projects (all or specific based on `uiTestModules`) - - Runs UI tests (all or specific based on `uiTestModules`) - - Uses freshly built PowerToys for testing + - Runs UI tests against freshly built PowerToys + - Uses artifacts from current pipeline build -> **Note**: Both modes support the `uiTestModules` parameter to control which specific UI test modules to build and run. +> **Note**: All modes support the `uiTestModules` parameter to control which specific UI test modules to build and run. Both machine-level and per-user installation modes are tested automatically when using official builds. ### Pipeline Access - Pipeline: https://microsoft.visualstudio.com/Dart/_build?definitionId=161438&_a=summary ## How to add the first UI tests for your modules - +- Follow the naming convention: ![{ModuleFolder}/Tests/{ModuleName}-{TestType(Fuzz/UI/Unit)}Tests](images/uitests/naming.png) - Create a new project and add the following references to the project file. Change the OutputPath to your own module's path. ``` diff --git a/doc/devdocs/core/installer.md b/doc/devdocs/core/installer.md index 781f31d682..b4619e26cd 100644 --- a/doc/devdocs/core/installer.md +++ b/doc/devdocs/core/installer.md @@ -87,6 +87,13 @@ ### Building PowerToys Locally +#### One stop script for building installer +1. Open developer powershell for vs 2022 +2. Run tools\build\build-installer.ps1 +> For the first-time setup, please run the installer as an administrator. This ensures that the Wix tool can move wix.target to the desired location and trust the certificate used to sign the MSIX packages. + +The following manual steps will not install the MSIX apps (such as Command Palette) on your local installer. + #### Prerequisites for building the MSI installer 1. Install the [WiX Toolset Visual Studio 2022 Extension](https://marketplace.visualstudio.com/items?itemName=WixToolset.WixToolsetVisualStudio2022Extension). diff --git a/doc/devdocs/core/settings/settings-implementation.md b/doc/devdocs/core/settings/settings-implementation.md index d97aff2dac..defe59a3fa 100644 --- a/doc/devdocs/core/settings/settings-implementation.md +++ b/doc/devdocs/core/settings/settings-implementation.md @@ -71,6 +71,41 @@ When the user changes settings in the UI: 3. The runner calls the `set_config` function on the appropriate module 4. The module parses the JSON and applies the new settings +# Shortcut Conflict Detection + +Steps to enable conflict detection for a hotkey: + +### 1. Implement module interface for hotkeys +Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional GetHotkeyEx()`. + +- If not yet implemented, you need to add it so that it returns all hotkeys used by the module. +- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup. +- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp` + +### 2. Implement IHotkeyConfig in the module settings (UI side) +Make sure the module’s settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`. + +- This method should return all hotkeys used in the module. +- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`). +- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs` +- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings. +It provides both `getter` and `setter` methods to read and update the corresponding hotkey. +Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey. +This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw` + +### 3. Update the module’s ViewModel +The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary GetAllHotkeySettings()`. + +- This method should return all hotkeys, maintaining the same order as in steps 1 and 2. +- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs` + +### 4. Ensure the module’s Views call `OnPageLoaded()` +Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method: +```cs +Loaded += (s, e) => ViewModel.OnPageLoaded(); +``` +- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs` + ## Debugging Settings To debug settings issues: diff --git a/doc/devdocs/development/test-winget-install-locally.md b/doc/devdocs/development/test-winget-install-locally.md new file mode 100644 index 0000000000..a59d32c52d --- /dev/null +++ b/doc/devdocs/development/test-winget-install-locally.md @@ -0,0 +1,33 @@ +## If for any reason, you'd like to test winget install scenario, you can follow this doc: + +### Powertoys winget manifest definition: +[winget repository](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/Microsoft/PowerToys) + +### How to test a winget installation locally: +1. Get artifacts from release CI pipeline Pipelines - Runs for PowerToys Signed YAML Release Build, or you can build one yourself by execute the + 'tools\build\build-installer.ps1' script + +2. Get the artifact hash, this is required to define winget manifest +```powershell +cd /path/to/your/directory/contains/installer +Get-FileHash -Path ".\.exe" -Algorithm SHA256 +``` + 3. Host your installer.exe - Attention: staged github release artifacts or artifacts in release pipeline is not OK in this step +You can self-host it or you can upload to a publicly available endpoint +**How to selfhost it** (A extremely simple way): +```powershell +python -m http.server 8000 +``` + +4. Download a version folder from wingetpkgs like: [version 0.92.1](https://github.com/microsoft/winget-pkgs/tree/master/manifests/m/Microsoft/PowerToys/0.92.1) +and you get **a folder contains 3 yml files** +>note: Do not put any files other than these three in this folder + +5. Modify the yml files based on your version and the self hosted artifact link, and modify the sha256 hash for the installer you'd like to use + +6. Start winget install: +```powershell +#execute as admin +winget settings --enable LocalManifestFiles +winget install --manifest "" --architecture x64 --scope user +``` \ No newline at end of file diff --git a/doc/devdocs/images/uitests/naming.png b/doc/devdocs/images/uitests/naming.png new file mode 100644 index 0000000000..6109bac59b Binary files /dev/null and b/doc/devdocs/images/uitests/naming.png differ diff --git a/doc/devdocs/readme.md b/doc/devdocs/readme.md index e4a86666ae..1709d12abe 100644 --- a/doc/devdocs/readme.md +++ b/doc/devdocs/readme.md @@ -52,7 +52,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an ## Rules - **Follow the pattern of what you already see in the code.** -- [Coding style](development/style.md). +- [Coding style](style.md). - Try to package new functionality/components into libraries that have nicely defined interfaces. - Package new functionality into classes or refactor existing functionality into a class as you extend the code. - When adding new classes/methods/changing existing code, add new unit tests or update the existing tests. @@ -67,10 +67,7 @@ Once you've discussed your proposed feature/fix/etc. with a team member, and an - When you'd like the team to take a look (even if the work is not yet fully complete) mark the PR as 'Ready For Review' so that the team can review your work and provide comments, suggestions, and request changes. It may take several cycles, but the end result will be solid, testable, conformant code that is safe for us to merge. - When the PR is approved, let the owner of the PR merge it. For community contributions, the reviewer who approved the PR can also merge it. - Use the `Squash and merge` option to merge a PR. If you don't want to squash it because there are logically different commits, use `Rebase and merge`. -- We don't close issues automatically when referenced in a PR, so after the PR is merged: - - mark the issue(s) that the PR solved with the `Resolution-Fix-Committed` label, remove the `In progress` label and if the issue is assigned to a project, move the item to the `Done` status. - - don't close the issue if it's a bug in the current released version; since users tend to not search for closed issues, we will close the resolved issues when a new version is released. - - if it's not a code fix that effects the end user, the issue can be closed (for example a fix in the build or a code refactoring and so on). +- Close issues automatically when referenced in a PR. You can use [closing keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) in the body of the PR to have GitHub automatically link your PR to the issue. ## Compiling PowerToys diff --git a/doc/images/icons/Mouse Crosshairs.png b/doc/images/icons/Mouse Crosshairs.png index 6b1dcb9c16..a2c64a72a4 100644 Binary files a/doc/images/icons/Mouse Crosshairs.png and b/doc/images/icons/Mouse Crosshairs.png differ diff --git a/doc/thirdPartyRunPlugins.md b/doc/thirdPartyRunPlugins.md index 30a297a6a7..6feabdc38c 100644 --- a/doc/thirdPartyRunPlugins.md +++ b/doc/thirdPartyRunPlugins.md @@ -49,6 +49,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi | [Definition](https://github.com/ruslanlap/PowerToysRun-Definition) | [ruslanlap](https://github.com/ruslanlap) | Lookup word definitions, phonetics, and synonyms directly in PowerToys Run. | | [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. | | [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 | ## Extending software plugins diff --git a/installer/PowerToysSetup/Resources.wxs b/installer/PowerToysSetup/Resources.wxs index 5da4db1390..b238799dd1 100644 --- a/installer/PowerToysSetup/Resources.wxs +++ b/installer/PowerToysSetup/Resources.wxs @@ -11,7 +11,7 @@ - + @@ -181,7 +181,7 @@ @@ -553,6 +553,7 @@ + diff --git a/src/common/UITestAutomation/Element/ComboBox.cs b/src/common/UITestAutomation/Element/ComboBox.cs index 5462c33910..1cac4d3ba5 100644 --- a/src/common/UITestAutomation/Element/ComboBox.cs +++ b/src/common/UITestAutomation/Element/ComboBox.cs @@ -24,5 +24,16 @@ namespace Microsoft.PowerToys.UITest { this.Find(value).Click(); } + + /// + /// Select a text item from the ComboBox. + /// + /// The text to select from the ComboBox. + public void SelectTxt(string value) + { + this.Click(); // First click to expand the ComboBox + Thread.Sleep(100); // Wait for the dropdown to appear + this.Find(value).Click(); // Find and click the text item using basic Element type + } } } diff --git a/src/common/UITestAutomation/Element/Element.cs b/src/common/UITestAutomation/Element/Element.cs index f5a8fac138..1868a9c34d 100644 --- a/src/common/UITestAutomation/Element/Element.cs +++ b/src/common/UITestAutomation/Element/Element.cs @@ -364,7 +364,7 @@ namespace Microsoft.PowerToys.UITest /// Save UI Element to a PNG file. /// /// the full path - internal void SaveToPngFile(string path) + public void SaveToPngFile(string path) { Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method SaveToPngFile with parameter: path = {path}"); this.windowsElement.GetScreenshot().SaveAsFile(path); diff --git a/src/common/UITestAutomation/Element/RadioButton.cs b/src/common/UITestAutomation/Element/RadioButton.cs new file mode 100644 index 0000000000..c88ccee79c --- /dev/null +++ b/src/common/UITestAutomation/Element/RadioButton.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. + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Represents a radio button UI element in the application. + /// + public class RadioButton : Element + { + private static readonly string ExpectedControlType = "ControlType.RadioButton"; + + /// + /// Initializes a new instance of the class. + /// + public RadioButton() + { + this.TargetControlType = RadioButton.ExpectedControlType; + } + + /// + /// Gets a value indicating whether the RadioButton is selected. + /// + public bool IsSelected => this.Selected; + + /// + /// Select the RadioButton. + /// + public void Select() + { + if (!this.IsSelected) + { + this.Click(); + } + } + } +} diff --git a/src/common/UITestAutomation/Element/TextBox.cs b/src/common/UITestAutomation/Element/TextBox.cs index 4ffb1a23e5..c2fc49e791 100644 --- a/src/common/UITestAutomation/Element/TextBox.cs +++ b/src/common/UITestAutomation/Element/TextBox.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.Threading.Tasks; + namespace Microsoft.PowerToys.UITest { /// @@ -25,8 +27,9 @@ namespace Microsoft.PowerToys.UITest /// /// The text to set. /// A value indicating whether to clear the text before setting it. Default value is true + /// Delay in milliseconds between each character. Default is 0 (no delay). /// The current TextBox instance. - public TextBox SetText(string value, bool clearText = true) + public TextBox SetText(string value, bool clearText = true, int charDelayMS = 0) { if (clearText) { @@ -39,10 +42,36 @@ namespace Microsoft.PowerToys.UITest Task.Delay(500).Wait(); } - PerformAction((actions, windowElement) => + // TODO: CmdPal bug – when inputting text, characters are swallowed too quickly. + // This should be fixed within CmdPal itself. + // Temporary workaround: introduce a delay between character inputs to avoid the issue + if (charDelayMS > 0 || EnvironmentConfig.IsInPipeline) { - windowElement.SendKeys(value); - }); + // Send text character by character with delay (if specified or in pipeline) + PerformAction((actions, windowElement) => + { + foreach (char c in value) + { + windowElement.SendKeys(c.ToString()); + if (charDelayMS > 0) + { + Task.Delay(charDelayMS).Wait(); + } + else if (EnvironmentConfig.IsInPipeline) + { + Task.Delay(50).Wait(); + } + } + }); + } + else + { + // No character delay - send all text at once (original behavior) + PerformAction((actions, windowElement) => + { + windowElement.SendKeys(value); + }); + } return this; } diff --git a/src/common/UITestAutomation/EnvironmentConfig.cs b/src/common/UITestAutomation/EnvironmentConfig.cs new file mode 100644 index 0000000000..ac0f1fa456 --- /dev/null +++ b/src/common/UITestAutomation/EnvironmentConfig.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 System; + +namespace Microsoft.PowerToys.UITest +{ + /// + /// Centralized configuration for all environment variables used in UI tests. + /// + public static class EnvironmentConfig + { + private static readonly Lazy _isInPipeline = new(() => + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("platform"))); + + private static readonly Lazy _useInstallerForTest = new(() => + { + string? envValue = Environment.GetEnvironmentVariable("useInstallerForTest") ?? + Environment.GetEnvironmentVariable("USEINSTALLERFORTEST"); + return !string.IsNullOrEmpty(envValue) && bool.TryParse(envValue, out bool result) && result; + }); + + private static readonly Lazy _platform = new(() => + Environment.GetEnvironmentVariable("platform")); + + /// + /// Gets a value indicating whether the tests are running in a CI/CD pipeline. + /// Determined by the presence of the "platform" environment variable. + /// + public static bool IsInPipeline => _isInPipeline.Value; + + /// + /// Gets a value indicating whether to use installer paths for testing. + /// Checks both "useInstallerForTest" and "USEINSTALLERFORTEST" environment variables. + /// + public static bool UseInstallerForTest => _useInstallerForTest.Value; + + /// + /// Gets the platform name from the environment variable. + /// Typically used in CI/CD pipelines to identify the build platform. + /// + public static string? Platform => _platform.Value; + } +} diff --git a/src/common/UITestAutomation/ModuleConfigData.cs b/src/common/UITestAutomation/ModuleConfigData.cs index 56b8789251..d3d33b94d3 100644 --- a/src/common/UITestAutomation/ModuleConfigData.cs +++ b/src/common/UITestAutomation/ModuleConfigData.cs @@ -32,6 +32,7 @@ namespace Microsoft.PowerToys.UITest Runner, Workspaces, PowerRename, + CommandPalette, } /// @@ -91,9 +92,7 @@ namespace Microsoft.PowerToys.UITest private ModuleConfigData() { // Check if we should use installer paths from environment variable - string? useInstallerForTestEnv = - Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST"); - UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result; + UseInstallerForTest = EnvironmentConfig.UseInstallerForTest; // Module information including executable name, window name, and optional subdirectory ModuleInfo = new Dictionary @@ -104,6 +103,7 @@ namespace Microsoft.PowerToys.UITest [PowerToysModule.Runner] = new ModuleInfo("PowerToys.exe", "PowerToys"), [PowerToysModule.Workspaces] = new ModuleInfo("PowerToys.WorkspacesEditor.exe", "Workspaces Editor"), [PowerToysModule.PowerRename] = new ModuleInfo("PowerToys.PowerRename.exe", "PowerRename", "WinUI3Apps"), + [PowerToysModule.CommandPalette] = new ModuleInfo("Microsoft.CmdPal.UI.exe", "PowerToys Command Palette", "WinUI3Apps\\CmdPal"), }; } diff --git a/src/common/UITestAutomation/SessionHelper.cs b/src/common/UITestAutomation/SessionHelper.cs index b12b1f831b..0ca3eb3ddd 100644 --- a/src/common/UITestAutomation/SessionHelper.cs +++ b/src/common/UITestAutomation/SessionHelper.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -37,6 +38,9 @@ namespace Microsoft.PowerToys.UITest private PowerToysModule scope; private string[]? commandLineArgs; + /// + /// Gets a value indicating whether to use installer paths for testing. + /// private bool UseInstallerForTest { get; } [UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "")] @@ -45,9 +49,7 @@ namespace Microsoft.PowerToys.UITest this.scope = scope; this.commandLineArgs = commandLineArgs; this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope); - string? useInstallerForTestEnv = - Environment.GetEnvironmentVariable("useInstallerForTest") ?? Environment.GetEnvironmentVariable("USEINSTALLERFORTEST"); - UseInstallerForTest = !string.IsNullOrEmpty(useInstallerForTestEnv) && bool.TryParse(useInstallerForTestEnv, out bool result) && result; + UseInstallerForTest = EnvironmentConfig.UseInstallerForTest; this.locationPath = UseInstallerForTest ? string.Empty : Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); CheckWinAppDriverAndRoot(); @@ -91,15 +93,12 @@ namespace Microsoft.PowerToys.UITest } /// - /// Exit a exe. + /// Exit a exe by Name. /// - /// The path to the application executable. - public void ExitExe(string appPath) + /// The path to the application executable. + public void ExitExeByName(string processName) { - // Exit Exe - string exeName = Path.GetFileNameWithoutExtension(appPath); - - Process[] processes = Process.GetProcessesByName(exeName); + Process[] processes = Process.GetProcessesByName(processName); foreach (Process process in processes) { try @@ -114,6 +113,18 @@ namespace Microsoft.PowerToys.UITest } } + /// + /// Exit a exe. + /// + /// The path to the application executable. + public void ExitExe(string appPath) + { + // Exit Exe + string exeName = Path.GetFileNameWithoutExtension(appPath); + + ExitExeByName(exeName); + } + /// /// Starts a new exe and takes control of it. /// @@ -122,69 +133,113 @@ namespace Microsoft.PowerToys.UITest public void StartExe(string appPath, string[]? args = null) { var opts = new AppiumOptions(); - opts.AddAdditionalCapability("app", appPath); - if (args != null && args.Length > 0) + if (scope == PowerToysModule.PowerToysSettings) { - // Build command line arguments string - string argsString = string.Join(" ", args.Select(arg => + TryLaunchPowerToysSettings(opts); + } + else if (scope == PowerToysModule.CommandPalette && UseInstallerForTest) + { + TryLaunchCommandPalette(opts); + } + else + { + opts.AddAdditionalCapability("app", appPath); + + if (args != null && args.Length > 0) { - // Quote arguments that contain spaces - if (arg.Contains(' ')) + // Build command line arguments string + string argsString = string.Join(" ", args.Select(arg => { - return $"\"{arg}\""; - } + // Quote arguments that contain spaces + if (arg.Contains(' ')) + { + return $"\"{arg}\""; + } - return arg; - })); + return arg; + })); - opts.AddAdditionalCapability("appArguments", argsString); + opts.AddAdditionalCapability("appArguments", argsString); + } } - this.Driver = NewWindowsDriver(opts); + Driver = NewWindowsDriver(opts); } private void TryLaunchPowerToysSettings(AppiumOptions opts) { - CheckWinAppDriverAndRoot(); - - var runnerProcessInfo = new ProcessStartInfo + try { - FileName = locationPath + this.runnerPath, - Verb = "runas", - Arguments = "--open-settings", - }; - - this.ExitExe(runnerProcessInfo.FileName); - this.runner = Process.Start(runnerProcessInfo); - Thread.Sleep(5000); - - if (root != null) - { - const int maxRetries = 5; - const int delayMs = 5000; - var windowName = "PowerToys Settings"; - - for (int attempt = 1; attempt <= maxRetries; attempt++) + var runnerProcessInfo = new ProcessStartInfo { - var settingsWindow = ApiHelper.FindDesktopWindowHandler( - new[] { windowName, AdministratorPrefix + windowName }); + FileName = locationPath + runnerPath, + Verb = "runas", + Arguments = "--open-settings", + }; - if (settingsWindow.Count > 0) - { - var hexHwnd = settingsWindow[0].HWnd.ToString("x"); - opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd); - return; - } + ExitExe(runnerProcessInfo.FileName); + runner = Process.Start(runnerProcessInfo); - if (attempt < maxRetries) - { - Thread.Sleep(delayMs); - } - else - { - throw new TimeoutException("Failed to find PowerToys Settings window after multiple attempts."); - } + WaitForWindowAndSetCapability(opts, "PowerToys Settings", 5000, 5); + + // Exit CmdPal UI before launching new process if use installer for test + ExitExeByName("Microsoft.CmdPal.UI"); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to launch PowerToys Settings: {ex.Message}", ex); + } + } + + private void TryLaunchCommandPalette(AppiumOptions opts) + { + try + { + // Exit any existing CmdPal UI process + ExitExeByName("Microsoft.CmdPal.UI"); + + var processStartInfo = new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = "/c start shell:appsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App", + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + }; + + var process = Process.Start(processStartInfo); + process?.WaitForExit(); + + WaitForWindowAndSetCapability(opts, "Command Palette", 5000, 10); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to launch Command Palette: {ex.Message}", ex); + } + } + + private void WaitForWindowAndSetCapability(AppiumOptions opts, string windowName, int delayMs, int maxRetries) + { + for (int attempt = 1; attempt <= maxRetries; attempt++) + { + var window = ApiHelper.FindDesktopWindowHandler( + [windowName, AdministratorPrefix + windowName]); + + if (window.Count > 0) + { + var hexHwnd = window[0].HWnd.ToString("x"); + opts.AddAdditionalCapability("appTopLevelWindow", hexHwnd); + return; + } + + if (attempt < maxRetries) + { + Thread.Sleep(delayMs); + } + else + { + throw new TimeoutException($"Failed to find {windowName} window after multiple attempts."); } } } diff --git a/src/common/UITestAutomation/UITestAutomation.csproj b/src/common/UITestAutomation/UITestAutomation.csproj index fc9da3b983..17841e0a60 100644 --- a/src/common/UITestAutomation/UITestAutomation.csproj +++ b/src/common/UITestAutomation/UITestAutomation.csproj @@ -20,6 +20,7 @@ + diff --git a/src/common/UITestAutomation/UITestBase.cs b/src/common/UITestAutomation/UITestBase.cs index 81732dd5e4..f44c62ab62 100644 --- a/src/common/UITestAutomation/UITestBase.cs +++ b/src/common/UITestAutomation/UITestBase.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium; @@ -20,8 +21,13 @@ namespace Microsoft.PowerToys.UITest public required Session Session { get; set; } + /// + /// Gets a value indicating whether the tests are running in a CI/CD pipeline. + /// public bool IsInPipeline { get; } + public string? ScreenshotDirectory { get; set; } + public static MonitorInfoData.ParamsWrapper MonitorInfoData { get; set; } = new MonitorInfoData.ParamsWrapper() { Monitors = new List() }; private readonly PowerToysModule scope; @@ -29,12 +35,11 @@ namespace Microsoft.PowerToys.UITest private readonly string[]? commandLineArgs; private SessionHelper? sessionHelper; private System.Threading.Timer? screenshotTimer; - private string? screenshotDirectory; public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings, WindowSize size = WindowSize.UnSpecified, string[]? commandLineArgs = null) { - this.IsInPipeline = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("platform")); - Console.WriteLine($"Running tests on platform: {Environment.GetEnvironmentVariable("platform")}"); + this.IsInPipeline = EnvironmentConfig.IsInPipeline; + Console.WriteLine($"Running tests on platform: {EnvironmentConfig.Platform}"); if (IsInPipeline) { NativeMethods.ChangeDisplayResolution(1920, 1080); @@ -55,14 +60,15 @@ namespace Microsoft.PowerToys.UITest [TestInitialize] public void TestInit() { + KeyboardHelper.SendKeys(Key.Win, Key.M); CloseOtherApplications(); if (IsInPipeline) { - screenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString()); - Directory.CreateDirectory(screenshotDirectory); + ScreenshotDirectory = Path.Combine(this.TestContext.TestResultsDirectory ?? string.Empty, "UITestScreenshots_" + Guid.NewGuid().ToString()); + Directory.CreateDirectory(ScreenshotDirectory); // Take screenshot every 1 second - screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, screenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000)); + screenshotTimer = new System.Threading.Timer(ScreenCapture.TimerCallback, ScreenshotDirectory, TimeSpan.Zero, TimeSpan.FromMilliseconds(1000)); // Escape Popups before starting System.Windows.Forms.SendKeys.SendWait("{ESC}"); @@ -246,6 +252,174 @@ namespace Microsoft.PowerToys.UITest return this.Session.Has(name, timeoutMS, global); } + /// + /// Finds an element using partial name matching (contains). + /// Useful for finding windows with variable titles like "filename.txt - Notepad" or "filename - Notepad". + /// + /// The class of the element, should be Element or its derived class. + /// Part of the name to search for. + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected T FindByPartialName(string partialName, int timeoutMS = 5000, bool global = false) + where T : Element, new() + { + return Session.Find(By.XPath($"//*[contains(@Name, '{partialName}')]"), timeoutMS, global); + } + + /// + /// Finds an element using partial name matching (contains). + /// + /// Part of the name to search for. + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected Element FindByPartialName(string partialName, int timeoutMS = 5000, bool global = false) + { + return FindByPartialName(partialName, timeoutMS, global); + } + + /// + /// Base method for finding elements by selector and filtering by name pattern. + /// + /// The class of the element, should be Element or its derived class. + /// The selector to find initial candidates. + /// Pattern to match against the Name attribute. Supports regex patterns. + /// The timeout in milliseconds (default is 5000). + /// Custom error message when no element is found. + /// The found element. + private T FindByNamePattern(By selector, string namePattern, int timeoutMS = 5000, bool global = false, string? errorMessage = null) + where T : Element, new() + { + var elements = Session.FindAll(selector, timeoutMS, global); + var regex = new Regex(namePattern, RegexOptions.IgnoreCase); + + foreach (var element in elements) + { + var name = element.GetAttribute("Name"); + if (!string.IsNullOrEmpty(name) && regex.IsMatch(name)) + { + return element; + } + } + + throw new NoSuchElementException(errorMessage ?? $"No element found matching pattern: {namePattern}"); + } + + /// + /// Finds an element using regular expression pattern matching. + /// + /// The class of the element, should be Element or its derived class. + /// Regular expression pattern to match against the Name attribute. + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected T FindByPattern(string pattern, int timeoutMS = 5000, bool global = false) + where T : Element, new() + { + return FindByNamePattern(By.XPath("//*[@Name]"), pattern, timeoutMS, global, $"No element found matching pattern: {pattern}"); + } + + /// + /// Finds an element using regular expression pattern matching. + /// + /// Regular expression pattern to match against the Name attribute. + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected Element FindByPattern(string pattern, int timeoutMS = 5000, bool global = false) + { + return FindByPattern(pattern, timeoutMS, global); + } + + /// + /// Finds an element by ClassName only. + /// Returns the first element found with the specified ClassName. + /// + /// The class of the element, should be Element or its derived class. + /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected T FindByClassName(string className, int timeoutMS = 5000, bool global = false) + where T : Element, new() + { + return Session.Find(By.ClassName(className), timeoutMS, global); + } + + /// + /// Finds an element by ClassName only. + /// Returns the first element found with the specified ClassName. + /// + /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected Element FindByClassName(string className, int timeoutMS = 5000, bool global = false) + { + return FindByClassName(className, timeoutMS, global); + } + + /// + /// Finds an element by ClassName and matches its Name attribute using regex pattern matching. + /// + /// The class of the element, should be Element or its derived class. + /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). + /// Pattern to match against the Name attribute. Supports regex patterns. + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected T FindByClassNameAndNamePattern(string className, string namePattern, int timeoutMS = 5000, bool global = false) + where T : Element, new() + { + return FindByNamePattern(By.ClassName(className), namePattern, timeoutMS, global, $"No element with ClassName '{className}' found matching name pattern: {namePattern}"); + } + + /// + /// Finds an element by ClassName and matches its Name attribute using regex pattern matching. + /// + /// The ClassName to search for (e.g., "Notepad", "CabinetWClass"). + /// Pattern to match against the Name attribute. Supports regex patterns. + /// The timeout in milliseconds (default is 5000). + /// The found element. + protected Element FindByClassNameAndNamePattern(string className, string namePattern, int timeoutMS = 5000, bool global = false) + { + return FindByClassNameAndNamePattern(className, namePattern, timeoutMS, global); + } + + /// + /// Finds a Notepad window regardless of whether the file extension is shown in the title. + /// Handles both "filename.txt - Notepad" and "filename - Notepad" formats. + /// Uses ClassName to efficiently find Notepad windows first, then matches the filename. + /// + /// The base filename without extension (e.g., "test" for "test.txt"). + /// The timeout in milliseconds (default is 5000). + /// The found Notepad window element. + protected Element FindNotepadWindow(string baseFileName, int timeoutMS = 5000, bool global = false) + { + string pattern = $@"^{Regex.Escape(baseFileName)}(\.\w+)?(\s*-\s*|\s+)Notepad$"; + return FindByClassNameAndNamePattern("Notepad", pattern, timeoutMS, global); + } + + /// + /// Finds an Explorer window regardless of the folder or file name display format. + /// Handles various Explorer window title formats like "FolderName", "FileName", "FolderName - File Explorer", etc. + /// Uses ClassName to efficiently find Explorer windows first, then matches the folder or file name. + /// + /// The folder or file name to search for (e.g., "Documents", "Desktop", "test.txt"). + /// The timeout in milliseconds (default is 5000). + /// The found Explorer window element. + protected Element FindExplorerWindow(string folderName, int timeoutMS = 5000, bool global = false) + { + string pattern = $@"^{Regex.Escape(folderName)}(\s*-\s*(File\s+Explorer|Windows\s+Explorer))?$"; + return FindByClassNameAndNamePattern("CabinetWClass", pattern, timeoutMS, global); + } + + /// + /// Finds an Explorer window by partial folder path. + /// Useful when the full path might be displayed in the title. + /// + /// Part of the folder path to search for. + /// The timeout in milliseconds (default is 5000). + /// The found Explorer window element. + protected Element FindExplorerByPartialPath(string partialPath, int timeoutMS = 5000, bool global = false) + { + return FindByPartialName(partialPath, timeoutMS, global); + } + /// /// Finds all elements by selector. /// Shortcut for this.Session.FindAll(by, timeoutMS) @@ -415,9 +589,9 @@ namespace Microsoft.PowerToys.UITest protected void AddScreenShotsToTestResultsDirectory() { - if (screenshotDirectory != null) + if (ScreenshotDirectory != null) { - foreach (string file in Directory.GetFiles(screenshotDirectory)) + foreach (string file in Directory.GetFiles(ScreenshotDirectory)) { this.TestContext.AddResultFile(file); } @@ -627,6 +801,23 @@ namespace Microsoft.PowerToys.UITest Console.WriteLine($"Failed to change display resolution. Error code: {result}"); } } + + // Windows API for moving windows + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + + private const uint SWPNOSIZE = 0x0001; + private const uint SWPNOZORDER = 0x0004; + + public static void MoveWindow(Element window, int x, int y) + { + var windowHandle = IntPtr.Parse(window.GetAttribute("NativeWindowHandle") ?? "0", System.Globalization.CultureInfo.InvariantCulture); + if (windowHandle != IntPtr.Zero) + { + SetWindowPos(windowHandle, IntPtr.Zero, x, y, 0, 0, SWPNOSIZE | SWPNOZORDER); + Task.Delay(500).Wait(); + } + } } } } diff --git a/src/common/UITestAutomation/VisualAssert.cs b/src/common/UITestAutomation/VisualAssert.cs index 692090440a..f08ba780d9 100644 --- a/src/common/UITestAutomation/VisualAssert.cs +++ b/src/common/UITestAutomation/VisualAssert.cs @@ -6,7 +6,11 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; +using CoenM.ImageHash; +using CoenM.ImageHash.HashAlgorithms; using Microsoft.VisualStudio.TestTools.UnitTesting; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace Microsoft.PowerToys.UITest { @@ -23,10 +27,8 @@ namespace Microsoft.PowerToys.UITest [RequiresUnreferencedCode("This method uses reflection which may not be compatible with trimming.")] public static void AreEqual(TestContext? testContext, Element element, string scenarioSubname = "") { - var pipelinePlatform = Environment.GetEnvironmentVariable("platform"); - // Perform visual validation only in the pipeline - if (string.IsNullOrEmpty(pipelinePlatform)) + if (!EnvironmentConfig.IsInPipeline) { Console.WriteLine("Skip visual validation in the local run."); return; @@ -51,11 +53,11 @@ namespace Microsoft.PowerToys.UITest if (string.IsNullOrWhiteSpace(scenarioSubname)) { - scenarioSubname = string.Join("_", callerClassName, callerName, pipelinePlatform); + scenarioSubname = string.Join("_", callerClassName, callerName, EnvironmentConfig.Platform); } else { - scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), pipelinePlatform); + scenarioSubname = string.Join("_", callerClassName, callerName, scenarioSubname.Trim(), EnvironmentConfig.Platform); } var baselineImageResourceName = callerMethod!.DeclaringType!.Assembly.GetManifestResourceNames().Where(name => name.Contains(scenarioSubname)).FirstOrDefault(); @@ -127,34 +129,75 @@ namespace Microsoft.PowerToys.UITest } /// - /// Test if two images are equal bit-by-bit + /// Test if two images are equal using ImageHash comparison /// /// baseline image /// test image /// true if are equal,otherwise false private static bool AreEqual(Bitmap baselineImage, Bitmap testImage) { - if (baselineImage.Width != testImage.Width || baselineImage.Height != testImage.Height) + try { - return false; + // Define a threshold for similarity percentage + const int SimilarityThreshold = 95; + + // Use CoenM.ImageHash for perceptual hash comparison + var hashAlgorithm = new AverageHash(); + + // Convert System.Drawing.Bitmap to SixLabors.ImageSharp.Image + using var baselineImageSharp = ConvertBitmapToImageSharp(baselineImage); + using var testImageSharp = ConvertBitmapToImageSharp(testImage); + + // Calculate hashes for both images + var baselineHash = hashAlgorithm.Hash(baselineImageSharp); + var testHash = hashAlgorithm.Hash(testImageSharp); + + // Compare hashes using CompareHash method + // Returns similarity percentage (0-100, where 100 is identical) + var similarity = CompareHash.Similarity(baselineHash, testHash); + + // Consider images equal if similarity is very high + // Allow for minor rendering differences (threshold can be adjusted) + return similarity >= SimilarityThreshold; // 95% similarity threshold } - - // WinAppDriver sometimes adds a border to the screenshot (around 2 pix width), and it is not always consistent. - // So we exclude the border when comparing the images, and usually it is the edge of the windows, won't affect the comparison. - int excludeBorderWidth = 5, excludeBorderHeight = 5; - - for (int x = excludeBorderWidth; x < baselineImage.Width - excludeBorderWidth; x++) + catch { - for (int y = excludeBorderHeight; y < baselineImage.Height - excludeBorderHeight; y++) + // Fallback to pixel-by-pixel comparison if hash comparison fails + if (baselineImage.Width != testImage.Width || baselineImage.Height != testImage.Height) { - if (!VisualHelper.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y))) + return false; + } + + // WinAppDriver sometimes adds a border to the screenshot (around 2 pix width), and it is not always consistent. + // So we exclude the border when comparing the images, and usually it is the edge of the windows, won't affect the comparison. + int excludeBorderWidth = 5, excludeBorderHeight = 5; + + for (int x = excludeBorderWidth; x < baselineImage.Width - excludeBorderWidth; x++) + { + for (int y = excludeBorderHeight; y < baselineImage.Height - excludeBorderHeight; y++) { - return false; + if (!VisualHelper.PixIsSame(baselineImage.GetPixel(x, y), testImage.GetPixel(x, y))) + { + return false; + } } } - } - return true; + return true; + } + } + + /// + /// Convert System.Drawing.Bitmap to SixLabors.ImageSharp.Image + /// + /// The bitmap to convert + /// ImageSharp Image + private static Image ConvertBitmapToImageSharp(Bitmap bitmap) + { + using var memoryStream = new MemoryStream(); + bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); + memoryStream.Position = 0; + return SixLabors.ImageSharp.Image.Load(memoryStream); } } } diff --git a/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj b/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj index cc25f7ae43..28041207fa 100644 --- a/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj +++ b/src/common/UnitTests-CommonLib/UnitTests-CommonLib.vcxproj @@ -7,6 +7,7 @@ Win32Proj UnitTestsCommonLib NativeUnitTestProject + Common.Lib.UnitTests diff --git a/src/common/interop/interop-tests/Microsoft.Interop.Tests.csproj b/src/common/interop/interop-tests/Common.Interop.UnitTests.csproj similarity index 100% rename from src/common/interop/interop-tests/Microsoft.Interop.Tests.csproj rename to src/common/interop/interop-tests/Common.Interop.UnitTests.csproj diff --git a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp index 896b362735..6af0d636ac 100644 --- a/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp +++ b/src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp @@ -112,7 +112,7 @@ private: return {}; } - static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject) + static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true) { try { @@ -122,6 +122,7 @@ private: hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); hotkey.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + hotkey.isShown = isShown; return hotkey; } catch (...) @@ -231,8 +232,10 @@ private: return false; } - void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue) + void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true) { + bool actionIsShown = true; + if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object) { return; @@ -240,9 +243,9 @@ private: const auto action = actionValue.GetObjectW(); - if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) + if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown) { - return; + actionIsShown = false; } if (action.HasKey(JSON_KEY_SHORTCUT)) @@ -250,7 +253,7 @@ private: const AdditionalAction additionalAction { actionName.c_str(), - parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT)) + parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown) }; m_additional_actions.push_back(additionalAction); @@ -259,12 +262,12 @@ private: { for (const auto& [subActionName, subAction] : action) { - process_additional_action(subActionName, subAction); + process_additional_action(subActionName, subAction, actionIsShown); } } } - void read_settings(PowerToysSettings::PowerToyValues& settings) + void read_settings(PowerToysSettings::PowerToyValues& settings) { const auto settingsObject = settings.get_raw_json(); @@ -317,9 +320,21 @@ private: { const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS); - for (const auto& [actionName, additionalAction] : additionalActions) + // Define the expected order to ensure consistent hotkey ID assignment + const std::vector expectedOrder = { + L"image-to-text", + L"paste-as-file", + L"transcode" + }; + + // Process actions in the predefined order + for (auto& actionKey : expectedOrder) { - process_additional_action(actionName, additionalAction); + if (additionalActions.HasKey(actionKey)) + { + const auto actionValue = additionalActions.GetNamedValue(actionKey); + process_additional_action(actionKey, actionValue); + } } } @@ -331,17 +346,14 @@ private: for (const auto& customAction : customActions) { const auto object = customAction.GetObjectW(); + bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false); - if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) - { - const CustomAction customActionData - { - static_cast(object.GetNamedNumber(JSON_KEY_ID)), - parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT)) - }; + const CustomAction customActionData{ + static_cast(object.GetNamedNumber(JSON_KEY_ID)), + parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown) + }; - m_custom_actions.push_back(customActionData); - } + m_custom_actions.push_back(customActionData); } } } diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/AdvancedPaste-UITests.csproj b/src/modules/AdvancedPaste/UITest-AdvancedPaste/AdvancedPaste-UITests.csproj new file mode 100644 index 0000000000..82a599d660 --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/AdvancedPaste-UITests.csproj @@ -0,0 +1,38 @@ + + + + + + {2B1505FA-132A-460B-B22B-7CC3FFAB0C5D} + Microsoft.AdvancedPaste.UITests + false + enable + Library + + + false + + + + ..\..\..\..\$(Platform)\$(Configuration)\tests\UITests-AdvancedPaste\ + + + + + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/AdvancedPasteUITest.cs b/src/modules/AdvancedPaste/UITest-AdvancedPaste/AdvancedPasteUITest.cs new file mode 100644 index 0000000000..76ff3580e4 --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/AdvancedPasteUITest.cs @@ -0,0 +1,791 @@ +// 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.Drawing; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Windows.Forms; + +using Microsoft.AdvancedPaste.UITests.Helper; +using Microsoft.CodeCoverage.Core.Reports.Coverage; +using Microsoft.PowerToys.UITest; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using OpenQA.Selenium; +using Windows.ApplicationModel.DataTransfer; +using static System.Net.Mime.MediaTypeNames; +using static System.Resources.ResXFileRef; +using static System.Runtime.InteropServices.JavaScript.JSType; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.ToolTip; + +namespace Microsoft.AdvancedPaste.UITests +{ + [TestClass] + public class AdvancedPasteUITest : UITestBase + { + private readonly string testFilesFolderPath; + private readonly string tempRTFFileName = "TempFile.rtf"; + private readonly string pasteAsPlainTextRawFileName = "PasteAsPlainTextFileRaw.rtf"; + private readonly string pasteAsPlainTextPlainFileName = "PasteAsPlainTextFilePlain.rtf"; + private readonly string pasteAsPlainTextPlainNoRepeatFileName = "PasteAsPlainTextFilePlainNoRepeat.rtf"; + private readonly string wordpadPath = @"C:\Program Files\wordpad\wordpad.exe"; + + private readonly string tempTxtFileName = "TempFile.txt"; + private readonly string pasteAsMarkdownSrcFile = "PasteAsMarkdownFile.html"; + private readonly string pasteAsMarkdownResultFile = "PasteAsMarkdownResultFile.txt"; + + private readonly string pasteAsJsonFileName = "PasteAsJsonFile.xml"; + private readonly string pasteAsJsonResultFile = "PasteAsJsonResultFile.txt"; + + private bool _notepadSettingsChanged; + + // Static constructor - runs before any instance is created + static AdvancedPasteUITest() + { + // Using the predefined settings. + // paste as plain text: win + ctrl + alt + o + // paste as markdown text: win + ctrl + alt + m + // paste as json text: win + ctrl + alt + j + CopySettingsFileBeforeTests(); + } + + public AdvancedPasteUITest() + : base(PowerToysModule.PowerToysSettings, size: WindowSize.Small) + { + Type currentTestType = typeof(AdvancedPasteUITest); + string? dirName = Path.GetDirectoryName(currentTestType.Assembly.Location); + Assert.IsNotNull(dirName, "Failed to get directory name of the current test assembly."); + + string testFilesFolder = Path.Combine(dirName, "TestFiles"); + Assert.IsTrue(Directory.Exists(testFilesFolder), $"Test files directory not found at: {testFilesFolder}"); + + testFilesFolderPath = testFilesFolder; + + // ignore the notepad settings in pipeline + _notepadSettingsChanged = true; + } + + [TestInitialize] + public void TestInitialize() + { + Session.CloseMainWindow(); + SendKeys(Key.Win, Key.M); + } + + [TestMethod] + [TestCategory("AdvancedPasteUITest")] + [TestCategory("PasteAsPlainText")] + [Ignore("Temporarily disabled due to wordpad.exe is missing in pipeline.")] + public void TestCasePasteAsPlainText() + { + // Copy some rich text(e.g word of the text is different color, another work is bold, underlined, etd.). + // Paste the text using standard Windows Ctrl + V shortcut and ensure that rich text is pasted(with all colors, formatting, etc.) + DeleteAndCopyFile(pasteAsPlainTextRawFileName, tempRTFFileName); + ContentCopyAndPasteDirectly(tempRTFFileName, isRTF: true); + + var resultWithFormatting = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempRTFFileName), + Path.Combine(testFilesFolderPath, pasteAsPlainTextRawFileName), + compareFormatting: true); + + Assert.IsTrue(resultWithFormatting.IsConsistent, "RTF files should be identical including formatting"); + + // Paste the text using Paste As Plain Text activation shortcut and ensure that plain text without any formatting is pasted. + // Paste again the text using standard Windows Ctrl + V shortcut and ensure the text is now pasted plain without formatting as well. + DeleteAndCopyFile(pasteAsPlainTextRawFileName, tempRTFFileName); + ContentCopyAndPasteWithShortcutThenPasteAgain(tempRTFFileName, isRTF: true); + resultWithFormatting = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempRTFFileName), + Path.Combine(testFilesFolderPath, pasteAsPlainTextPlainFileName), + compareFormatting: true); + Assert.IsTrue(resultWithFormatting.IsConsistent, "RTF files should be identical without formatting"); + + // Copy some rich text again. + // Open Advanced Paste window using hotkey, click Paste as Plain Text button and confirm that plain text without any formatting is pasted. + DeleteAndCopyFile(pasteAsPlainTextRawFileName, tempRTFFileName); + ContentCopyAndPasteCase3(tempRTFFileName, isRTF: true); + resultWithFormatting = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempRTFFileName), + Path.Combine(testFilesFolderPath, pasteAsPlainTextPlainNoRepeatFileName), + compareFormatting: true); + Assert.IsTrue(resultWithFormatting.IsConsistent, "RTF files should be identical without formatting"); + + // Copy some rich text again. + // Open Advanced Paste window using hotkey, press Ctrl + 1 and confirm that plain text without any formatting is pasted. + DeleteAndCopyFile(pasteAsPlainTextRawFileName, tempRTFFileName); + ContentCopyAndPasteCase4(tempRTFFileName, isRTF: true); + resultWithFormatting = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempRTFFileName), + Path.Combine(testFilesFolderPath, pasteAsPlainTextPlainNoRepeatFileName), + compareFormatting: true); + Assert.IsTrue(resultWithFormatting.IsConsistent, "RTF files should be identical without formatting"); + } + + [TestMethod] + [TestCategory("AdvancedPasteUITest")] + [TestCategory("PasteAsMarkdownCase1")] + public void TestCasePasteAsMarkdownCase1() + { + if (_notepadSettingsChanged == false) + { + ChangeNotePadSettings(); + } + + // Copy some text(e.g.some HTML text - convertible to Markdown) + // Paste the text using set hotkey and confirm that pasted text is converted to markdown + DeleteAndCopyFile(pasteAsMarkdownSrcFile, tempTxtFileName); + ContentCopyAndPasteAsMarkdownCase1(tempTxtFileName); + var result = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempTxtFileName), + Path.Combine(testFilesFolderPath, pasteAsMarkdownResultFile), + compareFormatting: true); + Assert.IsTrue(result.IsConsistent, "Paste as markdown using shortcut failed."); + } + + [TestMethod] + [TestCategory("AdvancedPasteUITest")] + [TestCategory("PasteAsMarkdownCase2")] + public void TestCasePasteAsMarkdownCase2() + { + if (_notepadSettingsChanged == false) + { + ChangeNotePadSettings(); + } + + // Copy some text(same as in the previous step or different.If nothing is coppied between steps, previously pasted Markdown text will be picked up from clipboard and converted again to nested Markdown). + // Open Advanced Paste window using hotkey, click Paste as markdown button and confirm that pasted text is converted to markdown + DeleteAndCopyFile(pasteAsMarkdownSrcFile, tempTxtFileName); + ContentCopyAndPasteAsMarkdownCase2(tempTxtFileName); + var result = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempTxtFileName), + Path.Combine(testFilesFolderPath, pasteAsMarkdownResultFile), + compareFormatting: true); + Assert.IsTrue(result.IsConsistent, "Paste as markdown using shortcut failed."); + } + + [TestMethod] + [TestCategory("AdvancedPasteUITest")] + [TestCategory("PasteAsMarkdownCase3")] + public void TestCasePasteAsMarkdownCase3() + { + if (_notepadSettingsChanged == false) + { + ChangeNotePadSettings(); + } + + // Copy some text(same as in the previous step or different.If nothing is coppied between steps, previously pasted Markdown text will be picked up from clipboard and converted again to nested Markdown). + // Open Advanced Paste window using hotkey, press Ctrl + 2 and confirm that pasted text is converted to markdown + DeleteAndCopyFile(pasteAsMarkdownSrcFile, tempTxtFileName); + ContentCopyAndPasteAsMarkdownCase3(tempTxtFileName); + var result = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempTxtFileName), + Path.Combine(testFilesFolderPath, pasteAsMarkdownResultFile), + compareFormatting: true); + Assert.IsTrue(result.IsConsistent, "Paste as markdown using shortcut failed."); + } + + [TestMethod] + [TestCategory("AdvancedPasteUITest")] + [TestCategory("PasteAsJSONCase1")] + public void TestCasePasteAsJSONCase1() + { + if (_notepadSettingsChanged == false) + { + ChangeNotePadSettings(); + } + + // Copy some XML or CSV text(or any other text, it will be converted to simple JSON object) + // Paste the text using set hotkey and confirm that pasted text is converted to JSON + DeleteAndCopyFile(pasteAsJsonFileName, tempTxtFileName); + ContentCopyAndPasteAsJsonCase1(tempTxtFileName); + var result = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempTxtFileName), + Path.Combine(testFilesFolderPath, pasteAsJsonResultFile), + compareFormatting: true); + Assert.IsTrue(result.IsConsistent, "Paste as Json using shortcut failed."); + } + + [TestMethod] + [TestCategory("AdvancedPasteUITest")] + [TestCategory("PasteAsJSONCase2")] + public void TestCasePasteAsJSONCase2() + { + if (_notepadSettingsChanged == false) + { + ChangeNotePadSettings(); + } + + // Copy some text(same as in the previous step or different.If nothing is coppied between steps, previously pasted JSON text will be picked up from clipboard and converted again to nested JSON). + // Open Advanced Paste window using hotkey, click Paste as markdown button and confirm that pasted text is converted to markdown + DeleteAndCopyFile(pasteAsJsonFileName, tempTxtFileName); + ContentCopyAndPasteAsJsonCase2(tempTxtFileName); + var result = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempTxtFileName), + Path.Combine(testFilesFolderPath, pasteAsJsonResultFile), + compareFormatting: true); + Assert.IsTrue(result.IsConsistent, "Paste as Json using shortcut failed."); + } + + [TestMethod] + [TestCategory("AdvancedPasteUITest")] + [TestCategory("PasteAsJSONCase3")] + public void TestCasePasteAsJSONCase3() + { + if (_notepadSettingsChanged == false) + { + ChangeNotePadSettings(); + } + + // Copy some text(same as in the previous step or different.If nothing is coppied between steps, previously pasted JSON text will be picked up from clipboard and converted again to nested JSON). + // Open Advanced Paste window using hotkey, press Ctrl + 3 and confirm that pasted text is converted to markdown + DeleteAndCopyFile(pasteAsJsonFileName, tempTxtFileName); + ContentCopyAndPasteAsJsonCase3(tempTxtFileName); + var result = FileReader.CompareRtfFiles( + Path.Combine(testFilesFolderPath, tempTxtFileName), + Path.Combine(testFilesFolderPath, pasteAsJsonResultFile), + compareFormatting: true); + Assert.IsTrue(result.IsConsistent, "Paste as Json using shortcut failed."); + } + + /* + * Clipboard History + - [] Open Settings and Enable clipboard history (if not enabled already). Open Advanced Paste window with hotkey, click Clipboard history and try deleting some entry. Check OS clipboard history (Win+V), and confirm that the same entry no longer exist. + - [] Open Advanced Paste window with hotkey, click Clipboard history, and click any entry (but first). Observe that entry is put on top of clipboard history. Check OS clipboard history (Win+V), and confirm that the same entry is on top of the clipboard. + - [] Open Settings and Disable clipboard history. Open Advanced Paste window with hotkey and observe that Clipboard history button is disabled. + * Disable Advanced Paste, try different Advanced Paste hotkeys and confirm that it's disabled and nothing happens. + */ + private void TestCaseClipboardHistory() + { + } + + private void ContentCopyAndPasteDirectly(string fileName, bool isRTF = false) + { + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.V); + Thread.Sleep(1000); + this.SendKeys(Key.Backspace); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + process.Kill(true); + } + + private void ContentCopyAndPasteWithShortcutThenPasteAgain(string fileName, bool isRTF = false) + { + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + this.SendKeys(Key.Win, Key.LCtrl, Key.Alt, Key.O); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.V); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + process.Kill(true); + } + + private void ContentCopyAndPasteCase3(string fileName, bool isRTF = false) + { + // Copy some rich text again. + // Open Advanced Paste window using hotkey, click Paste as Plain Text button and confirm that plain text without any formatting is pasted. + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + // Open Advanced Paste window using hotkey + this.SendKeys(Key.Win, Key.Shift, Key.V); + Thread.Sleep(15000); + + // Click Paste as Plain Text button and confirm that plain text without any formatting is pasted. + var apWind = this.Find("Advanced Paste", global: true); + apWind.Find("Paste as plain text").Click(); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + process.Kill(true); + } + + private void ContentCopyAndPasteCase4(string fileName, bool isRTF = false) + { + // Copy some rich text again. + // Open Advanced Paste window using hotkey, press Ctrl + 1 and confirm that plain text without any formatting is pasted. + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + // Open Advanced Paste window using hotkey + this.SendKeys(Key.Win, Key.Shift, Key.V); + Thread.Sleep(1000); + + // press Ctrl + 1 and confirm that plain text without any formatting is pasted. + this.SendKeys(Key.LCtrl, Key.Num1); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + process.Kill(true); + } + + private void ContentCopyAndPasteAsMarkdownCase1(string fileName, bool isRTF = false) + { + // Copy some rich text again. + // Open Advanced Paste window using hotkey, press Ctrl + 1 and confirm that plain text without any formatting is pasted. + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + this.SendKeys(Key.Win, Key.LCtrl, Key.Alt, Key.M); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + window.Close(); + } + + private void ContentCopyAndPasteAsMarkdownCase2(string fileName, bool isRTF = false) + { + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + // Open Advanced Paste window using hotkey + this.SendKeys(Key.Win, Key.Shift, Key.V); + Thread.Sleep(15000); + + // click Paste as markdown button and confirm that pasted text is converted to markdown + var apWind = this.Find("Advanced Paste", global: true); + apWind.Find("Paste as markdown").Click(); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + window.Close(); + } + + private void ContentCopyAndPasteAsMarkdownCase3(string fileName, bool isRTF = false) + { + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + // Open Advanced Paste window using hotkey + this.SendKeys(Key.Win, Key.Shift, Key.V); + Thread.Sleep(15000); + + this.SendKeys(Key.LCtrl, Key.Num2); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + window.Close(); + } + + private void ContentCopyAndPasteAsJsonCase1(string fileName, bool isRTF = false) + { + // Copy some rich text again. + // Open Advanced Paste window using hotkey, press Ctrl + 1 and confirm that plain text without any formatting is pasted. + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + this.SendKeys(Key.Win, Key.LCtrl, Key.Alt, Key.J); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + window.Close(); + } + + private void ContentCopyAndPasteAsJsonCase2(string fileName, bool isRTF = false) + { + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + // Open Advanced Paste window using hotkey + this.SendKeys(Key.Win, Key.Shift, Key.V); + Thread.Sleep(15000); + + // click Paste as markdown button and confirm that pasted text is converted to markdown + var apWind = this.Find("Advanced Paste", global: true); + apWind.Find("Paste as JSON").Click(); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + window.Close(); + } + + private void ContentCopyAndPasteAsJsonCase3(string fileName, bool isRTF = false) + { + string tempFile = Path.Combine(testFilesFolderPath, fileName); + + Process process = Process.Start(isRTF ? wordpadPath : "notepad.exe", tempFile); + if (process == null) + { + throw new InvalidOperationException($"Failed to start {(isRTF ? "WordPad" : "Notepad")}."); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle(Path.GetFileName(tempFile), isRTF); + + window.Click(); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.A); + Thread.Sleep(1000); + this.SendKeys(Key.LCtrl, Key.C); + Thread.Sleep(1000); + this.SendKeys(Key.Delete); + Thread.Sleep(1000); + + // Open Advanced Paste window using hotkey + this.SendKeys(Key.Win, Key.Shift, Key.V); + Thread.Sleep(15000); + + this.SendKeys(Key.LCtrl, Key.Num3); + Thread.Sleep(1000); + + this.SendKeys(Key.LCtrl, Key.S); + Thread.Sleep(1000); + + window.Close(); + } + + private string DeleteAndCopyFile(string sourceFileName, string destinationFileName) + { + string sourcePath = Path.Combine(testFilesFolderPath, sourceFileName); + string destinationPath = Path.Combine(testFilesFolderPath, destinationFileName); + + // Check if source file exists + if (!File.Exists(sourcePath)) + { + throw new FileNotFoundException($"Source file not found: {sourcePath}"); + } + + // Delete destination file if it exists + if (File.Exists(destinationPath)) + { + try + { + File.Delete(destinationPath); + } + catch (IOException ex) + { + throw new IOException($"Failed to delete file {destinationPath}. The file may be in use: {ex.Message}", ex); + } + } + + // Copy the source file to the destination + try + { + File.Copy(sourcePath, destinationPath); + } + catch (IOException ex) + { + throw new IOException($"Failed to copy file from {sourcePath} to {destinationPath}: {ex.Message}", ex); + } + + return destinationPath; + } + + private void ChangeNotePadSettings() + { + Process process = Process.Start("notepad.exe"); + if (process == null) + { + throw new InvalidOperationException($"Failed to start Notepad.exe"); + } + + Thread.Sleep(15000); + + var window = FindWindowWithFlexibleTitle("Untitled", false); + + window.Find("Settings").Click(); + var combobox = window.Find("Opening files"); + combobox.SelectTxt("Open in a new window"); + + window.Find("When Notepad starts").Click(); + + window.Find("Open a new window").Select(); + + _notepadSettingsChanged = true; + window.Close(); + } + + /// + /// Finds a window with flexible title matching, trying multiple title variations + /// + /// The base title to search for + /// Whether the window is a WordPad window + /// The found Window element or throws an exception if not found + private Window FindWindowWithFlexibleTitle(string baseTitle, bool isRTF) + { + Window? window = null; + string appType = isRTF ? "WordPad" : "Notepad"; + + // Try different title variations + string[] titleVariations = new string[] + { + baseTitle + (isRTF ? " - WordPad" : " - Notepad"), // With suffix + baseTitle, // Without suffix + Path.GetFileNameWithoutExtension(baseTitle) + (isRTF ? " - WordPad" : " - Notepad"), // Without extension, with suffix + Path.GetFileNameWithoutExtension(baseTitle), // Without extension, without suffix + }; + + Exception? lastException = null; + + foreach (string title in titleVariations) + { + try + { + window = this.Find(title, global: true); + if (window != null) + { + return window; + } + } + catch (Exception ex) + { + // Save the exception, but continue trying other variations + lastException = ex; + } + } + + // If we couldn't find the window with any variation, throw an exception with details + throw new InvalidOperationException( + $"Failed to find {appType} window with title containing '{baseTitle}'. "); + } + + private static void CopySettingsFileBeforeTests() + { + try + { + // Determine the assembly location and test files path + string? assemblyLocation = Path.GetDirectoryName(typeof(AdvancedPasteUITest).Assembly.Location); + if (assemblyLocation == null) + { + Debug.WriteLine("ERROR: Failed to get assembly location"); + return; + } + + string testFilesFolder = Path.Combine(assemblyLocation, "TestFiles"); + if (!Directory.Exists(testFilesFolder)) + { + Debug.WriteLine($"ERROR: Test files directory not found at: {testFilesFolder}"); + return; + } + + // Settings file source path + string settingsFileName = "settings.json"; + string sourceSettingsPath = Path.Combine(testFilesFolder, settingsFileName); + + // Make sure the source file exists + if (!File.Exists(sourceSettingsPath)) + { + Debug.WriteLine($"ERROR: Settings file not found at: {sourceSettingsPath}"); + return; + } + + // Determine the target directory in %LOCALAPPDATA% + string targetDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Microsoft", + "PowerToys", + "AdvancedPaste"); + + // Create the directory if it doesn't exist + if (!Directory.Exists(targetDirectory)) + { + Directory.CreateDirectory(targetDirectory); + } + + string targetSettingsPath = Path.Combine(targetDirectory, settingsFileName); + + // Copy the file and overwrite if it exists + File.Copy(sourceSettingsPath, targetSettingsPath, true); + + Debug.WriteLine($"Successfully copied settings file from {sourceSettingsPath} to {targetSettingsPath}"); + } + catch (Exception ex) + { + Debug.WriteLine($"ERROR copying settings file: {ex.Message}"); + } + } + } +} diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/Helper/FileReader.cs b/src/modules/AdvancedPaste/UITest-AdvancedPaste/Helper/FileReader.cs new file mode 100644 index 0000000000..d711fe010a --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/Helper/FileReader.cs @@ -0,0 +1,85 @@ +// 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; +using System.Windows.Forms; + +namespace Microsoft.AdvancedPaste.UITests.Helper; + +public class FileReader +{ + public static string ReadContent(string filePath) + { + try + { + return File.ReadAllText(filePath); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to read file: {ex.Message}", ex); + } + } + + public static string ReadRTFPlainText(string filePath) + { + try + { + using (var rtb = new System.Windows.Forms.RichTextBox()) + { + rtb.Rtf = File.ReadAllText(filePath); + return rtb.Text; + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to read plain text from file: {ex.Message}", ex); + } + } + + /// + /// Compares the contents of two RTF files to check if they are consistent. + /// + /// Path to the first RTF file + /// Path to the second RTF file + /// If true, compares the raw RTF content (including formatting). + /// If false, compares only the plain text content. + /// + /// A tuple containing: (bool isConsistent, string firstContent, string secondContent) + /// - isConsistent: true if the files are consistent according to the comparison method + /// - firstContent: the content of the first file + /// - secondContent: the content of the second file + /// + public static (bool IsConsistent, string FirstContent, string SecondContent) CompareRtfFiles( + string firstFilePath, + string secondFilePath, + bool compareFormatting = false) + { + try + { + string firstContent, secondContent; + + if (compareFormatting) + { + // Compare raw RTF content (including formatting) + firstContent = ReadContent(firstFilePath); + secondContent = ReadContent(secondFilePath); + } + else + { + // Compare only the plain text content + firstContent = ReadRTFPlainText(firstFilePath); + secondContent = ReadRTFPlainText(secondFilePath); + } + + bool isConsistent = string.Equals(firstContent, secondContent, StringComparison.Ordinal); + return (isConsistent, firstContent, secondContent); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to compare RTF files: {ex.Message}", ex); + } + } +} diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsJsonFile.xml b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsJsonFile.xml new file mode 100644 index 0000000000..90f0a1b454 --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsJsonFile.xml @@ -0,0 +1,6 @@ + + Tove + Jani + Reminder + Don't forget me this weekend! + \ No newline at end of file diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsJsonResultFile.txt b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsJsonResultFile.txt new file mode 100644 index 0000000000..2bea5fd966 --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsJsonResultFile.txt @@ -0,0 +1,8 @@ +{ + "note": { + "to": "Tove", + "from": "Jani", + "heading": "Reminder", + "body": "Don't forget me this weekend!" + } +} \ No newline at end of file diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsMarkdownFile.html b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsMarkdownFile.html new file mode 100644 index 0000000000..097b3d4d2b --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsMarkdownFile.html @@ -0,0 +1,12 @@ + + + + + +

The title Attribute

+ +

Mouse over this paragraph, to display the title attribute as a tooltip.

+ + + + \ No newline at end of file diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsMarkdownResultFile.txt b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsMarkdownResultFile.txt new file mode 100644 index 0000000000..a383bfdb1b --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsMarkdownResultFile.txt @@ -0,0 +1,3 @@ +## The title Attribute + +Mouse over this paragraph, to display the title attribute as a tooltip. \ No newline at end of file diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFilePlain.rtf b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFilePlain.rtf new file mode 100644 index 0000000000..c0d8a0402b Binary files /dev/null and b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFilePlain.rtf differ diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFilePlainNoRepeat.rtf b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFilePlainNoRepeat.rtf new file mode 100644 index 0000000000..67f5ed3dce Binary files /dev/null and b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFilePlainNoRepeat.rtf differ diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFileRaw.rtf b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFileRaw.rtf new file mode 100644 index 0000000000..be2ac272dd Binary files /dev/null and b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/PasteAsPlainTextFileRaw.rtf differ diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json new file mode 100644 index 0000000000..31ad05c701 --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/TestFiles/settings.json @@ -0,0 +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 diff --git a/src/modules/AdvancedPaste/UITest-AdvancedPaste/UITestAdvancedPaste.md b/src/modules/AdvancedPaste/UITest-AdvancedPaste/UITestAdvancedPaste.md new file mode 100644 index 0000000000..202ee43494 --- /dev/null +++ b/src/modules/AdvancedPaste/UITest-AdvancedPaste/UITestAdvancedPaste.md @@ -0,0 +1,41 @@ +## [Advanced Paste](tests-checklist-template-advanced-paste-section.md) + NOTES: + When using Advanced Paste, make sure that window focused while starting/using Advanced paste is text editor or has text input field focused (e.g. Word). + * Paste As Plain Text + - [x] Copy some rich text (e.g word of the text is different color, another work is bold, underlined, etd.). + - [x] Paste the text using standard Windows Ctrl + V shortcut and ensure that rich text is pasted (with all colors, formatting, etc.) + - [x] Paste the text using Paste As Plain Text activation shortcut and ensure that plain text without any formatting is pasted. + - [x] Paste again the text using standard Windows Ctrl + V shortcut and ensure the text is now pasted plain without formatting as well. + - [x] Copy some rich text again. + - [x] Open Advanced Paste window using hotkey, click Paste as Plain Text button and confirm that plain text without any formatting is pasted. + - [x] Copy some rich text again. + - [x] Open Advanced Paste window using hotkey, press Ctrl + 1 and confirm that plain text without any formatting is pasted. + * Paste As Markdown + - [] Open Settings and set Paste as Markdown directly hotkey + - [x] Copy some text (e.g. some HTML text - convertible to Markdown) + - [x] Paste the text using set hotkey and confirm that pasted text is converted to markdown + - [x] Copy some text (same as in the previous step or different. If nothing is coppied between steps, previously pasted Markdown text will be picked up from clipboard and converted again to nested Markdown). + - [x] Open Advanced Paste window using hotkey, click Paste as markdown button and confirm that pasted text is converted to markdown + - [x] Copy some text (same as in the previous step or different. If nothing is coppied between steps, previously pasted Markdown text will be picked up from clipboard and converted again to nested Markdown). + - [x] Open Advanced Paste window using hotkey, press Ctrl + 2 and confirm that pasted text is converted to markdown + * Paste As JSON + - [] Open Settings and set Paste as JSON directly hotkey + - [x] Copy some XML or CSV text (or any other text, it will be converted to simple JSON object) + - [x] Paste the text using set hotkey and confirm that pasted text is converted to JSON + - [x] Copy some text (same as in the previous step or different. If nothing is coppied between steps, previously pasted JSON text will be picked up from clipboard and converted again to nested JSON). + - [x] Open Advanced Paste window using hotkey, click Paste as markdown button and confirm that pasted text is converted to markdown + - [x] Copy some text (same as in the previous step or different. If nothing is coppied between steps, previously pasted JSON text will be picked up from clipboard and converted again to nested JSON). + - [x] Open Advanced Paste window using hotkey, press Ctrl + 3 and confirm that pasted text is converted to markdown + * Paste as custom format using AI + - [] Open Settings, navigate to Enable Paste with AI and set OpenAI key. + - [] Copy some text to clipboard. Any text. + - [] Open Advanced Paste window using hotkey, and confirm that Custom intput text box is now enabled. Write "Insert smiley after every word" and press Enter. Observe that result preview shows coppied text with smileys between words. Press Enter to paste the result and observe that it is pasted. + - [] Open Advanced Paste window using hotkey. Input some query (any, feel free to play around) and press Enter. When result is shown, click regenerate button, to see if new result is generated. Select one of the results and paste. Observe that correct result is pasted. + - [] Create few custom actions. Set up hotkey for custom actions and confirm they work. Enable/disable custom actions and confirm that the change is reflected in Advanced Paste UI - custom action is not listed. Try different ctrl + in-app shortcuts for custom actions. Try moving custom actions up/down and confirm that the change is reflected in Advanced Paste UI. + - [] Open Settings and disable Custom format preview. Open Advanced Paste window with hotkey, enter some query and press enter. Observe that result is now pasted right away, without showing the preview first. + - [] Open Settings and Disable Enable Paste with AI. Open Advanced Paste window with hotkey and observe that Custom Input text box is now disabled. + * Clipboard History + - [] Open Settings and Enable clipboard history (if not enabled already). Open Advanced Paste window with hotkey, click Clipboard history and try deleting some entry. Check OS clipboard history (Win+V), and confirm that the same entry no longer exist. + - [] Open Advanced Paste window with hotkey, click Clipboard history, and click any entry (but first). Observe that entry is put on top of clipboard history. Check OS clipboard history (Win+V), and confirm that the same entry is on top of the clipboard. + - [] Open Settings and Disable clipboard history. Open Advanced Paste window with hotkey and observe that Clipboard history button is disabled. + * Disable Advanced Paste, try different Advanced Paste hotkeys and confirm that it's disabled and nothing happens. \ No newline at end of file diff --git a/src/modules/Hosts/Hosts.FuzzTests/Hosts.FuzzTests.csproj b/src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj similarity index 100% rename from src/modules/Hosts/Hosts.FuzzTests/Hosts.FuzzTests.csproj rename to src/modules/Hosts/Hosts.FuzzTests/HostsEditor.FuzzTests.csproj diff --git a/src/modules/Hosts/Hosts.FuzzTests/OneFuzzConfig.json b/src/modules/Hosts/Hosts.FuzzTests/OneFuzzConfig.json index cafb475fc6..6a5b9883f1 100644 --- a/src/modules/Hosts/Hosts.FuzzTests/OneFuzzConfig.json +++ b/src/modules/Hosts/Hosts.FuzzTests/OneFuzzConfig.json @@ -4,8 +4,8 @@ { "fuzzer": { "$type": "libfuzzerDotNet", - "dll": "Hosts.FuzzTests.dll", - "class": "Hosts.FuzzTests.FuzzTests", + "dll": "HostsEditor.FuzzTests.dll", + "class": "HostsEditor.FuzzTests.FuzzTests", "method": "FuzzValidIPv4", "FuzzingTargetBinaries": [ "PowerToys.Hosts.dll" @@ -35,8 +35,8 @@ // the DLL and PDB files // you will need to add any other files required // (globs are supported) - "Hosts.FuzzTests.dll", - "Hosts.FuzzTests.pdb", + "HostsEditor.FuzzTests.dll", + "HostsEditor.FuzzTests.pdb", "Microsoft.Windows.SDK.NET.dll", "WinRT.Runtime.dll" ], @@ -45,8 +45,8 @@ { "fuzzer": { "$type": "libfuzzerDotNet", - "dll": "Hosts.FuzzTests.dll", - "class": "Hosts.FuzzTests.FuzzTests", + "dll": "HostsEditor.FuzzTests.dll", + "class": "HostsEditor.FuzzTests.FuzzTests", "method": "FuzzValidIPv6", "FuzzingTargetBinaries": [ "PowerToys.Hosts.dll" @@ -76,8 +76,8 @@ // the DLL and PDB files // you will need to add any other files required // (globs are supported) - "Hosts.FuzzTests.dll", - "Hosts.FuzzTests.pdb", + "HostsEditor.FuzzTests.dll", + "HostsEditor.FuzzTests.pdb", "Microsoft.Windows.SDK.NET.dll", "WinRT.Runtime.dll" ], @@ -86,8 +86,8 @@ { "fuzzer": { "$type": "libfuzzerDotNet", - "dll": "Hosts.FuzzTests.dll", - "class": "Hosts.FuzzTests.FuzzTests", + "dll": "HostsEditor.FuzzTests.dll", + "class": "HostsEditor.FuzzTests.FuzzTests", "method": "FuzzValidHosts", "FuzzingTargetBinaries": [ "PowerToys.Hosts.dll" @@ -117,8 +117,8 @@ // the DLL and PDB files // you will need to add any other files required // (globs are supported) - "Hosts.FuzzTests.dll", - "Hosts.FuzzTests.pdb", + "HostsEditor.FuzzTests.dll", + "HostsEditor.FuzzTests.pdb", "Microsoft.Windows.SDK.NET.dll", "WinRT.Runtime.dll" ], @@ -127,8 +127,8 @@ { "fuzzer": { "$type": "libfuzzerDotNet", - "dll": "Hosts.FuzzTests.dll", - "class": "Hosts.FuzzTests.FuzzTests", + "dll": "HostsEditor.FuzzTests.dll", + "class": "HostsEditor.FuzzTests.FuzzTests", "method": "FuzzWriteAsync", "FuzzingTargetBinaries": [ "PowerToys.Hosts.dll" @@ -160,8 +160,8 @@ // (globs are supported) "Castle.Core.dll", "CommunityToolkit.Mvvm.dll", - "Hosts.FuzzTests.dll", - "Hosts.FuzzTests.pdb", + "HostsEditor.FuzzTests.dll", + "HostsEditor.FuzzTests.pdb", "Microsoft.Windows.SDK.NET.dll", "Moq.dll", "System.IO.Abstractions.dll", diff --git a/src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj b/src/modules/Hosts/Hosts.Tests/HostsEditor.UnitTests.csproj similarity index 100% rename from src/modules/Hosts/Hosts.Tests/Hosts.Tests.csproj rename to src/modules/Hosts/Hosts.Tests/HostsEditor.UnitTests.csproj diff --git a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs index 8eaa37a348..81052fd101 100644 --- a/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs +++ b/src/modules/Hosts/Hosts.Tests/HostsServiceTest.cs @@ -298,5 +298,34 @@ namespace Hosts.Tests var hidden = fileSystem.FileInfo.New(service.HostsFilePath).Attributes.HasFlag(FileAttributes.Hidden); Assert.IsTrue(hidden); } + + [TestMethod] + public async Task NoLeadingSpaces_Disabled_RemovesIndent() + { + var content = + @"10.1.1.1 host host.local # comment +10.1.1.2 host2 host2.local # another comment +"; + + var expected = + @"10.1.1.1 host host.local # comment +10.1.1.2 host2 host2.local # another comment +# 10.1.1.30 host30 host30.local # new entry +"; + + var fs = new CustomMockFileSystem(); + var settings = new Mock(); + settings.Setup(s => s.NoLeadingSpaces).Returns(true); + var svc = new HostsService(fs, settings.Object, _elevationHelper.Object); + fs.AddFile(svc.HostsFilePath, new MockFileData(content)); + + var data = await svc.ReadAsync(); + var entries = data.Entries.ToList(); + entries.Add(new Entry(0, "10.1.1.30", "host30 host30.local", "new entry", false)); + await svc.WriteAsync(data.AdditionalLines, entries); + + var result = fs.GetFile(svc.HostsFilePath); + Assert.AreEqual(expected, result.TextContents); + } } } diff --git a/src/modules/Hosts/Hosts.UITests/Hosts.UITests.csproj b/src/modules/Hosts/Hosts.UITests/HostsEditor.UITests.csproj similarity index 100% rename from src/modules/Hosts/Hosts.UITests/Hosts.UITests.csproj rename to src/modules/Hosts/Hosts.UITests/HostsEditor.UITests.csproj diff --git a/src/modules/Hosts/Hosts/Settings/UserSettings.cs b/src/modules/Hosts/Hosts/Settings/UserSettings.cs index 3530a3f74b..75da5d214d 100644 --- a/src/modules/Hosts/Hosts/Settings/UserSettings.cs +++ b/src/modules/Hosts/Hosts/Settings/UserSettings.cs @@ -26,6 +26,8 @@ namespace Hosts.Settings private bool _loopbackDuplicates; + public bool NoLeadingSpaces { get; private set; } + public bool LoopbackDuplicates { get => _loopbackDuplicates; @@ -88,6 +90,7 @@ namespace Hosts.Settings AdditionalLinesPosition = (HostsAdditionalLinesPosition)settings.Properties.AdditionalLinesPosition; Encoding = (HostsEncoding)settings.Properties.Encoding; LoopbackDuplicates = settings.Properties.LoopbackDuplicates; + NoLeadingSpaces = settings.Properties.NoLeadingSpaces; } retry = false; diff --git a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs index b07eb8f93c..83aa3544b1 100644 --- a/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs +++ b/src/modules/Hosts/HostsUILib/Helpers/HostsService.cs @@ -157,7 +157,7 @@ namespace HostsUILib.Helpers { lineBuilder.Append('#').Append(' '); } - else if (anyDisabled) + else if (anyDisabled && !_userSettings.NoLeadingSpaces) { lineBuilder.Append(' ').Append(' '); } diff --git a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs index 21a8e6fa36..46c7a7dab5 100644 --- a/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs +++ b/src/modules/Hosts/HostsUILib/Settings/IUserSettings.cs @@ -19,5 +19,7 @@ namespace HostsUILib.Settings event EventHandler LoopbackDuplicatesChanged; public delegate void OpenSettingsFunction(); + + public bool NoLeadingSpaces { get; } } } diff --git a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp index 25a95f4d39..05670742ec 100644 --- a/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp +++ b/src/modules/MouseUtils/MouseHighlighter/MouseHighlighter.cpp @@ -5,6 +5,7 @@ #include "MouseHighlighter.h" #include "trace.h" #include +#include #ifdef COMPOSITION namespace winrt @@ -49,6 +50,9 @@ private: void BringToFront(); HHOOK m_mouseHook = NULL; static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam) noexcept; + // Helpers for spotlight overlay + float GetDpiScale() const; + void UpdateSpotlightMask(float cx, float cy, float radius, bool show); static constexpr auto m_className = L"MouseHighlighter"; static constexpr auto m_windowTitle = L"PowerToys Mouse Highlighter"; @@ -67,7 +71,14 @@ private: winrt::CompositionSpriteShape m_leftPointer{ nullptr }; winrt::CompositionSpriteShape m_rightPointer{ nullptr }; winrt::CompositionSpriteShape m_alwaysPointer{ nullptr }; - winrt::CompositionSpriteShape m_spotlightPointer{ nullptr }; + // Spotlight overlay (mask with soft feathered edge) + winrt::SpriteVisual m_overlay{ nullptr }; + winrt::CompositionMaskBrush m_spotlightMask{ nullptr }; + winrt::CompositionRadialGradientBrush m_spotlightMaskGradient{ nullptr }; + winrt::CompositionColorBrush m_spotlightSource{ nullptr }; + winrt::CompositionColorGradientStop m_maskStopCenter{ nullptr }; + winrt::CompositionColorGradientStop m_maskStopInner{ nullptr }; + winrt::CompositionColorGradientStop m_maskStopOuter{ nullptr }; bool m_leftPointerEnabled = true; bool m_rightPointerEnabled = true; @@ -123,6 +134,35 @@ bool Highlighter::CreateHighlighter() m_shape.RelativeSizeAdjustment({ 1.0f, 1.0f }); m_root.Children().InsertAtTop(m_shape); + // Create spotlight overlay (soft feather, DPI-aware) + m_overlay = m_compositor.CreateSpriteVisual(); + m_overlay.RelativeSizeAdjustment({ 1.0f, 1.0f }); + m_spotlightSource = m_compositor.CreateColorBrush(m_alwaysColor); + m_spotlightMaskGradient = m_compositor.CreateRadialGradientBrush(); + m_spotlightMaskGradient.MappingMode(winrt::CompositionMappingMode::Absolute); + // Center region fully transparent + m_maskStopCenter = m_compositor.CreateColorGradientStop(); + m_maskStopCenter.Offset(0.0f); + m_maskStopCenter.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + // Inner edge of feather (still transparent) + m_maskStopInner = m_compositor.CreateColorGradientStop(); + m_maskStopInner.Offset(0.995f); // will be updated per-radius + m_maskStopInner.Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + // Outer edge (opaque mask -> overlay visible) + m_maskStopOuter = m_compositor.CreateColorGradientStop(); + m_maskStopOuter.Offset(1.0f); + m_maskStopOuter.Color(winrt::Windows::UI::ColorHelper::FromArgb(255, 255, 255, 255)); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopCenter); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopInner); + m_spotlightMaskGradient.ColorStops().Append(m_maskStopOuter); + + m_spotlightMask = m_compositor.CreateMaskBrush(); + m_spotlightMask.Source(m_spotlightSource); + m_spotlightMask.Mask(m_spotlightMaskGradient); + m_overlay.Brush(m_spotlightMask); + m_overlay.IsVisible(false); + m_root.Children().InsertAtTop(m_overlay); + return true; } catch (...) @@ -165,12 +205,8 @@ void Highlighter::AddDrawingPoint(MouseButton button) // always if (m_spotlightMode) { - float borderThickness = static_cast(std::hypot(GetSystemMetrics(SM_CXVIRTUALSCREEN), GetSystemMetrics(SM_CYVIRTUALSCREEN))); - circleGeometry.Radius({ static_cast(borderThickness / 2.0 + m_radius), static_cast(borderThickness / 2.0 + m_radius) }); - circleShape.FillBrush(nullptr); - circleShape.StrokeBrush(m_compositor.CreateColorBrush(m_alwaysColor)); - circleShape.StrokeThickness(borderThickness); - m_spotlightPointer = circleShape; + UpdateSpotlightMask(static_cast(pt.x), static_cast(pt.y), m_radius, true); + return; } else { @@ -209,20 +245,14 @@ void Highlighter::UpdateDrawingPointPosition(MouseButton button) } else { - // always + // always / spotlight idle if (m_spotlightMode) { - if (m_spotlightPointer) - { - m_spotlightPointer.Offset({ static_cast(pt.x), static_cast(pt.y) }); - } + UpdateSpotlightMask(static_cast(pt.x), static_cast(pt.y), m_radius, true); } - else + else if (m_alwaysPointer) { - if (m_alwaysPointer) - { - m_alwaysPointer.Offset({ static_cast(pt.x), static_cast(pt.y) }); - } + m_alwaysPointer.Offset({ static_cast(pt.x), static_cast(pt.y) }); } } } @@ -266,9 +296,9 @@ void Highlighter::ClearDrawingPoint() { if (m_spotlightMode) { - if (m_spotlightPointer) + if (m_overlay) { - m_spotlightPointer.StrokeBrush().as().Color(winrt::Windows::UI::ColorHelper::FromArgb(0, 0, 0, 0)); + m_overlay.IsVisible(false); } } else @@ -421,7 +451,10 @@ void Highlighter::StopDrawing() m_leftPointer = nullptr; m_rightPointer = nullptr; m_alwaysPointer = nullptr; - m_spotlightPointer = nullptr; + if (m_overlay) + { + m_overlay.IsVisible(false); + } ShowWindow(m_hwnd, SW_HIDE); UnhookWindowsHookEx(m_mouseHook); ClearDrawing(); @@ -452,6 +485,16 @@ void Highlighter::ApplySettings(MouseHighlighterSettings settings) m_rightPointerEnabled = false; } + // Keep spotlight overlay color updated + if (m_spotlightSource) + { + m_spotlightSource.Color(m_alwaysColor); + } + if (!m_spotlightMode && m_overlay) + { + m_overlay.IsVisible(false); + } + if (instance->m_visible) { instance->StopDrawing(); @@ -563,6 +606,43 @@ void Highlighter::Terminate() } } +float Highlighter::GetDpiScale() const +{ + return static_cast(GetDpiForWindow(m_hwnd)) / 96.0f; +} + +// Update spotlight radial mask center/radius with DPI-aware feather +void Highlighter::UpdateSpotlightMask(float cx, float cy, float radius, bool show) +{ + if (!m_spotlightMaskGradient) + { + return; + } + + m_spotlightMaskGradient.EllipseCenter({ cx, cy }); + m_spotlightMaskGradient.EllipseRadius({ radius, radius }); + + const float dpiScale = GetDpiScale(); + // Target a very fine edge: ~1 physical pixel, convert to DIPs: 1 / dpiScale + const float featherDip = 1.0f / (dpiScale > 0.0f ? dpiScale : 1.0f); + const float safeRadius = (std::max)(radius, 1.0f); + const float featherRel = (std::min)(0.25f, featherDip / safeRadius); + + if (m_maskStopInner) + { + m_maskStopInner.Offset((std::max)(0.0f, 1.0f - featherRel)); + } + + if (m_spotlightSource) + { + m_spotlightSource.Color(m_alwaysColor); + } + if (m_overlay) + { + m_overlay.IsVisible(show); + } +} + #pragma region MouseHighlighter_API void MouseHighlighterApplySettings(MouseHighlighterSettings settings) diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp index 937e9bfca3..61e292d7ee 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.cpp @@ -27,6 +27,73 @@ struct InclusiveCrosshairs void SwitchActivationMode(); void ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRuntimeObjects); +public: + // Allow external callers to request a position update (thread-safe enqueue) + static void RequestUpdatePosition() + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([]() { + if (instance != nullptr) + { + instance->UpdateCrosshairsPosition(); + } + }); + } + } + + static void EnsureOn() + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([]() { + if (instance != nullptr && !instance->m_drawing) + { + instance->StartDrawing(); + } + }); + } + } + + static void EnsureOff() + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([]() { + if (instance != nullptr && instance->m_drawing) + { + instance->StopDrawing(); + } + }); + } + } + + static void SetExternalControl(bool enabled) + { + if (instance != nullptr) + { + auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue(); + dispatcherQueue.TryEnqueue([enabled]() { + if (instance != nullptr) + { + instance->m_externalControl = enabled; + if (enabled && instance->m_mouseHook) + { + UnhookWindowsHookEx(instance->m_mouseHook); + instance->m_mouseHook = NULL; + } + else if (!enabled && instance->m_drawing && !instance->m_mouseHook) + { + instance->m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, instance->m_hinstance, 0); + } + } + }); + } + } + private: enum class MouseButton { @@ -69,6 +136,7 @@ private: bool m_drawing = false; bool m_destroyed = false; bool m_hiddenCursor = false; + bool m_externalControl = false; void SetAutoHideTimer() noexcept; // Configurable Settings @@ -264,9 +332,12 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP if (nCode >= 0) { MSLLHOOKSTRUCT* hookData = reinterpret_cast(lParam); - if (wParam == WM_MOUSEMOVE) + if (instance && !instance->m_externalControl) { - instance->UpdateCrosshairsPosition(); + if (wParam == WM_MOUSEMOVE) + { + instance->UpdateCrosshairsPosition(); + } } } return CallNextHookEx(0, nCode, wParam, lParam); @@ -527,6 +598,26 @@ bool InclusiveCrosshairsIsEnabled() return (InclusiveCrosshairs::instance != nullptr); } +void InclusiveCrosshairsRequestUpdatePosition() +{ + InclusiveCrosshairs::RequestUpdatePosition(); +} + +void InclusiveCrosshairsEnsureOn() +{ + InclusiveCrosshairs::EnsureOn(); +} + +void InclusiveCrosshairsEnsureOff() +{ + InclusiveCrosshairs::EnsureOff(); +} + +void InclusiveCrosshairsSetExternalControl(bool enabled) +{ + InclusiveCrosshairs::SetExternalControl(enabled); +} + int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings) { Logger::info("Starting a crosshairs instance."); diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h index 43456a4326..a6618d85bf 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h +++ b/src/modules/MouseUtils/MousePointerCrosshairs/InclusiveCrosshairs.h @@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable(); bool InclusiveCrosshairsIsEnabled(); void InclusiveCrosshairsSwitch(); void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings); +void InclusiveCrosshairsRequestUpdatePosition(); +void InclusiveCrosshairsEnsureOn(); +void InclusiveCrosshairsEnsureOff(); +void InclusiveCrosshairsSetExternalControl(bool enabled); diff --git a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp index d2273c7efd..3dcee0d6a4 100644 --- a/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp +++ b/src/modules/MouseUtils/MousePointerCrosshairs/dllmain.cpp @@ -4,6 +4,15 @@ #include "trace.h" #include "InclusiveCrosshairs.h" #include "common/utils/color.h" +#include +#include +#include +#include + +extern void InclusiveCrosshairsRequestUpdatePosition(); +extern void InclusiveCrosshairsEnsureOn(); +extern void InclusiveCrosshairsEnsureOff(); +extern void InclusiveCrosshairsSetExternalControl(bool enabled); // Non-Localizable strings namespace @@ -11,6 +20,7 @@ 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_GLIDING_ACTIVATION_SHORTCUT[] = L"gliding_cursor_activation_shortcut"; const wchar_t JSON_KEY_CROSSHAIRS_COLOR[] = L"crosshairs_color"; const wchar_t JSON_KEY_CROSSHAIRS_OPACITY[] = L"crosshairs_opacity"; const wchar_t JSON_KEY_CROSSHAIRS_RADIUS[] = L"crosshairs_radius"; @@ -21,13 +31,15 @@ namespace const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled"; const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length"; const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate"; + const wchar_t JSON_KEY_GLIDE_TRAVEL_SPEED[] = L"gliding_travel_speed"; + const wchar_t JSON_KEY_GLIDE_DELAY_SPEED[] = L"gliding_delay_speed"; } extern "C" IMAGE_DOS_HEADER __ImageBase; HMODULE m_hModule; -BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) +BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) { m_hModule = hModule; switch (ul_reason_for_call) @@ -57,8 +69,46 @@ private: // The PowerToy state. bool m_enabled = false; - // Hotkey to invoke the module - HotkeyEx m_hotkey; + // Additional hotkeys (legacy API) to support multiple shortcuts + Hotkey m_activationHotkey{}; // Crosshairs toggle + Hotkey m_glidingHotkey{}; // Gliding cursor state machine + + // Shared state for worker threads (decoupled from this lifetime) + struct State + { + std::atomic stopX{ false }; + std::atomic stopY{ false }; + + // positions and speeds + int currentXPos{ 0 }; + int currentYPos{ 0 }; + int currentXSpeed{ 0 }; // pixels per base window + int currentYSpeed{ 0 }; // pixels per base window + int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan + + // Fractional accumulators to spread movement across 10ms ticks + double xFraction{ 0.0 }; + double yFraction{ 0.0 }; + + // Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings) + int fastHSpeed{ 30 }; // pixels per base window + int slowHSpeed{ 5 }; // pixels per base window + int fastVSpeed{ 30 }; // pixels per base window + int slowVSpeed{ 5 }; // pixels per base window + }; + + std::shared_ptr m_state; + + // Worker threads + std::thread m_xThread; + std::thread m_yThread; + + // Gliding cursor state machine + std::atomic m_glideState{ 0 }; // 0..4 like the AHK script + + // Timer configuration: 10ms tick, speeds are defined per 200ms base window + static constexpr int kTimerTickMs = 10; + static constexpr int kBaseSpeedTickMs = 200; // mapping period for configured pixel counts // Mouse Pointer Crosshairs specific settings InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings; @@ -68,12 +118,17 @@ public: MousePointerCrosshairs() { LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName); + m_state = std::make_shared(); init_settings(); }; // Destroy the powertoy and free memory virtual void destroy() override { + StopXTimer(); + StopYTimer(); + // Release shared state so worker threads (if any) exit when weak_ptr lock fails + m_state.reset(); delete this; } @@ -107,9 +162,7 @@ public: // 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 - { - } + 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 @@ -143,6 +196,9 @@ public: { m_enabled = false; Trace::EnableMousePointerCrosshairs(false); + StopXTimer(); + StopYTimer(); + m_glideState = 0; InclusiveCrosshairsDisable(); } @@ -158,15 +214,249 @@ public: return false; } - virtual std::optional GetHotkeyEx() override + // Legacy multi-hotkey support (like CropAndLock) + virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override { - return m_hotkey; + if (buffer && buffer_size >= 2) + { + buffer[0] = m_activationHotkey; // Crosshairs toggle + buffer[1] = m_glidingHotkey; // Gliding cursor toggle + } + return 2; } - virtual void OnHotkeyEx() override + virtual bool on_hotkey(size_t hotkeyId) override { - InclusiveCrosshairsSwitch(); + if (!m_enabled) + { + return false; + } + + if (hotkeyId == 0) + { + InclusiveCrosshairsSwitch(); + return true; + } + if (hotkeyId == 1) + { + HandleGlidingHotkey(); + return true; + } + return false; } + +private: + static void LeftClick() + { + INPUT inputs[2]{}; + inputs[0].type = INPUT_MOUSE; + inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN; + inputs[1].type = INPUT_MOUSE; + inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP; + SendInput(2, inputs, sizeof(INPUT)); + } + + // Stateless helpers operating on shared State + static void PositionCursorX(const std::shared_ptr& s) + { + int screenW = GetSystemMetrics(SM_CXVIRTUALSCREEN); + int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN); + s->currentYPos = screenH / 2; + + // Distribute movement over 10ms ticks to match pixels-per-base-window speeds + const double perTick = (static_cast(s->currentXSpeed) * kTimerTickMs) / static_cast(kBaseSpeedTickMs); + s->xFraction += perTick; + int step = static_cast(s->xFraction); + if (step > 0) + { + s->xFraction -= step; + s->currentXPos += step; + } + + s->xPosSnapshot = s->currentXPos; + if (s->currentXPos >= screenW) + { + s->currentXPos = 0; + s->currentXSpeed = s->fastHSpeed; + s->xPosSnapshot = 0; + s->xFraction = 0.0; // reset fractional remainder on wrap + } + SetCursorPos(s->currentXPos, s->currentYPos); + // Ensure overlay crosshairs follow immediately + InclusiveCrosshairsRequestUpdatePosition(); + } + + static void PositionCursorY(const std::shared_ptr& s) + { + int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN); + // Keep X at snapshot + // Use s->xPosSnapshot captured during X pass + + // Distribute movement over 10ms ticks to match pixels-per-base-window speeds + const double perTick = (static_cast(s->currentYSpeed) * kTimerTickMs) / static_cast(kBaseSpeedTickMs); + s->yFraction += perTick; + int step = static_cast(s->yFraction); + if (step > 0) + { + s->yFraction -= step; + s->currentYPos += step; + } + + if (s->currentYPos >= screenH) + { + s->currentYPos = 0; + s->currentYSpeed = s->fastVSpeed; + s->yFraction = 0.0; // reset fractional remainder on wrap + } + SetCursorPos(s->xPosSnapshot, s->currentYPos); + // Ensure overlay crosshairs follow immediately + InclusiveCrosshairsRequestUpdatePosition(); + } + + void StartXTimer() + { + auto s = m_state; + if (!s) + { + return; + } + s->stopX = false; + std::weak_ptr wp = s; + m_xThread = std::thread([wp]() { + while (true) + { + auto sp = wp.lock(); + if (!sp || sp->stopX.load()) + { + break; + } + PositionCursorX(sp); + std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs)); + } + }); + } + + void StopXTimer() + { + auto s = m_state; + if (s) + { + s->stopX = true; + } + if (m_xThread.joinable()) + { + m_xThread.join(); + } + } + + void StartYTimer() + { + auto s = m_state; + if (!s) + { + return; + } + s->stopY = false; + std::weak_ptr wp = s; + m_yThread = std::thread([wp]() { + while (true) + { + auto sp = wp.lock(); + if (!sp || sp->stopY.load()) + { + break; + } + PositionCursorY(sp); + std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs)); + } + }); + } + + void StopYTimer() + { + auto s = m_state; + if (s) + { + s->stopY = true; + } + if (m_yThread.joinable()) + { + m_yThread.join(); + } + } + + void HandleGlidingHotkey() + { + auto s = m_state; + if (!s) + { + return; + } + // Simulate the AHK state machine + int state = m_glideState.load(); + switch (state) + { + case 0: + { + // Ensure crosshairs on (do not toggle off if already on) + InclusiveCrosshairsEnsureOn(); + // Disable internal mouse hook so we control position updates explicitly + InclusiveCrosshairsSetExternalControl(true); + + s->currentXPos = 0; + s->currentXSpeed = s->fastHSpeed; + s->xFraction = 0.0; + s->yFraction = 0.0; + int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2; + SetCursorPos(0, y); + InclusiveCrosshairsRequestUpdatePosition(); + m_glideState = 1; + StartXTimer(); + break; + } + case 1: + { + // Slow horizontal + s->currentXSpeed = s->slowHSpeed; + m_glideState = 2; + break; + } + case 2: + { + // Stop horizontal, start vertical (fast) + StopXTimer(); + s->currentYSpeed = s->fastVSpeed; + s->currentYPos = 0; + s->yFraction = 0.0; + SetCursorPos(s->xPosSnapshot, s->currentYPos); + InclusiveCrosshairsRequestUpdatePosition(); + m_glideState = 3; + StartYTimer(); + break; + } + case 3: + { + // Slow vertical + s->currentYSpeed = s->slowVSpeed; + m_glideState = 4; + break; + } + case 4: + default: + { + // Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state + StopYTimer(); + m_glideState = 0; + LeftClick(); + InclusiveCrosshairsEnsureOff(); + InclusiveCrosshairsSetExternalControl(false); + s->xFraction = 0.0; + s->yFraction = 0.0; + break; + } + } + } + // Load the settings file. void init_settings() { @@ -192,37 +482,44 @@ public: { try { - // Parse HotKey + // Parse primary activation HotKey (for centralized hook) auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT); auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject); - m_hotkey = HotkeyEx(); - if (hotkey.win_pressed()) - { - m_hotkey.modifiersMask |= MOD_WIN; - } - if (hotkey.ctrl_pressed()) - { - m_hotkey.modifiersMask |= MOD_CONTROL; - } - - if (hotkey.shift_pressed()) - { - m_hotkey.modifiersMask |= MOD_SHIFT; - } - - if (hotkey.alt_pressed()) - { - m_hotkey.modifiersMask |= MOD_ALT; - } - - m_hotkey.vkCode = hotkey.get_code(); + // Map to legacy Hotkey for multi-hotkey API + 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 Mouse Pointer Crosshairs activation shortcut"); } try + { + // Parse Gliding Cursor HotKey + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT); + auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject); + m_glidingHotkey.win = hotkey.win_pressed(); + m_glidingHotkey.ctrl = hotkey.ctrl_pressed(); + m_glidingHotkey.shift = hotkey.shift_pressed(); + m_glidingHotkey.alt = hotkey.alt_pressed(); + m_glidingHotkey.key = static_cast(hotkey.get_code()); + } + catch (...) + { + // note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut + // both need to be kept in sync! + Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+."); + m_glidingHotkey.win = true; + m_glidingHotkey.alt = true; + m_glidingHotkey.ctrl = false; + m_glidingHotkey.shift = false; + m_glidingHotkey.key = VK_OEM_PERIOD; + } + try { // Parse Opacity auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY); @@ -272,7 +569,6 @@ public: { throw std::runtime_error("Invalid Radius value"); } - } catch (...) { @@ -291,7 +587,6 @@ public: { throw std::runtime_error("Invalid Thickness value"); } - } catch (...) { @@ -320,7 +615,7 @@ public: { // Parse border size auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE); - int value = static_cast (jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); + int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); if (value >= 0) { inclusiveCrosshairsSettings.crosshairsBorderSize = value; @@ -383,20 +678,86 @@ public: { Logger::warn("Failed to initialize auto activate from settings. Will use default value"); } + try + { + // Parse Travel speed (fast speed mapping) + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_TRAVEL_SPEED); + int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); + if (value >= 5 && value <= 60) + { + m_state->fastHSpeed = value; + m_state->fastVSpeed = value; + } + else if (value < 5) + { + m_state->fastHSpeed = 5; m_state->fastVSpeed = 5; + } + else + { + m_state->fastHSpeed = 60; m_state->fastVSpeed = 60; + } + } + catch (...) + { + Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25."); + if (m_state) + { + m_state->fastHSpeed = 25; + m_state->fastVSpeed = 25; + } + } + try + { + // Parse Delay speed (slow speed mapping) + auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED); + int value = static_cast(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); + if (value >= 5 && value <= 60) + { + m_state->slowHSpeed = value; + m_state->slowVSpeed = value; + } + else if (value < 5) + { + m_state->slowHSpeed = 5; m_state->slowVSpeed = 5; + } + else + { + m_state->slowHSpeed = 60; m_state->slowVSpeed = 60; + } + } + catch (...) + { + Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5."); + if (m_state) + { + m_state->slowHSpeed = 5; + m_state->slowVSpeed = 5; + } + } } else { Logger::info("Mouse Pointer Crosshairs settings are empty"); } - if (!m_hotkey.modifiersMask) + + if (m_activationHotkey.key == 0) { - Logger::info("Mouse Pointer Crosshairs is going to use default shortcut"); - m_hotkey.modifiersMask = MOD_WIN | MOD_ALT; - m_hotkey.vkCode = 0x50; // P key + m_activationHotkey.win = true; + m_activationHotkey.alt = true; + m_activationHotkey.ctrl = false; + m_activationHotkey.shift = false; + m_activationHotkey.key = 'P'; + } + if (m_glidingHotkey.key == 0) + { + m_glidingHotkey.win = true; + m_glidingHotkey.alt = true; + m_glidingHotkey.ctrl = false; + m_glidingHotkey.shift = false; + m_glidingHotkey.key = VK_OEM_PERIOD; } m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings; } - }; extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() diff --git a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp index 33030fbdfb..29d7a781ae 100644 --- a/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp +++ b/src/modules/MouseWithoutBorders/ModuleInterface/dllmain.cpp @@ -556,6 +556,61 @@ public: return m_enabled; } + virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override + { + constexpr size_t num_hotkeys = 4; // We have 4 hotkeys + + if (hotkeys && buffer_size >= num_hotkeys) + { + PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME); + + // Cache the raw JSON object to avoid multiple parsing + json::JsonObject root_json = values.get_raw_json(); + json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{}); + + size_t hotkey_index = 0; + + // Helper lambda to extract hotkey from JSON properties + auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey { + if (properties_json.HasKey(property_name)) + { + try + { + json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name); + + // Extract hotkey properties directly from JSON + bool win = hotkey_json.GetNamedBoolean(L"win", false); + bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false); + bool alt = hotkey_json.GetNamedBoolean(L"alt", false); + bool shift = hotkey_json.GetNamedBoolean(L"shift", false); + unsigned char key = static_cast( + hotkey_json.GetNamedNumber(L"code", 0)); + + return { win, ctrl, shift, alt, key }; + } + catch (...) + { + // If parsing individual hotkey fails, use defaults + return { false, false, false, false, 0 }; + } + } + else + { + // Property doesn't exist, use defaults + return { false, false, false, false, 0 }; + } + }; + + // Extract all hotkeys using the optimized helper + hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse + hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine + hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs + hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect + } + + return num_hotkeys; + } + void launch_add_firewall_process() { Logger::trace(L"Starting Process to add firewall rule"); diff --git a/src/modules/PowerOCR/PowerOCR-UITests/PowerOCR.UITests.csproj b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCR.UITests.csproj new file mode 100644 index 0000000000..c2a51bb332 --- /dev/null +++ b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCR.UITests.csproj @@ -0,0 +1,19 @@ + + + + PowerOCR.UITests + enable + enable + Library + false + + + + ..\..\..\..\$(Platform)\$(Configuration)\tests\PowerOCR.UITests\ + + + + + + + diff --git a/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.cs new file mode 100644 index 0000000000..926729542a --- /dev/null +++ b/src/modules/PowerOCR/PowerOCR-UITests/PowerOCRTests.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.PowerToys.UITest; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Microsoft.PowerToys.UITest.UITestBase; + +namespace PowerOCR.UITests; + +[TestClass] +public class PowerOCRTests : UITestBase +{ + public PowerOCRTests() + : base(PowerToysModule.PowerToysSettings, WindowSize.Medium) + { + } + + [TestInitialize] + public void TestInitialize() + { + if (FindAll("Text Extractor").Count == 0) + { + // Expand Advanced list-group if needed + Find("System Tools").Click(); + } + + Find("Text Extractor").Click(); + + Find("Enable Text Extractor").Toggle(true); + + SendKeys(Key.Win, Key.D); + } + + [TestMethod("PowerOCR.DetectTextExtractor")] + [TestCategory("PowerOCR Detection")] + public void DetectTextExtractorTest() + { + try + { + SendKeys(Key.Win, Key.Shift, Key.T); + + Thread.Sleep(5000); + + var textExtractorWindow = Find("TextExtractor", 10000, true); + + Assert.IsNotNull(textExtractorWindow, "TextExtractor window should be found after hotkey activation"); + + Console.WriteLine("✓ TextExtractor window detected successfully after hotkey activation"); + + SendKeys(Key.Esc); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to detect TextExtractor window: {ex.Message}"); + Assert.Fail("TextExtractor window was not found after hotkey activation"); + } + } +} diff --git a/src/modules/Workspaces/WorkspacesEditorUITest/WorkspacesEditorUITest.csproj b/src/modules/Workspaces/WorkspacesEditorUITest/Workspaces.Editor.UITests.csproj similarity index 100% rename from src/modules/Workspaces/WorkspacesEditorUITest/WorkspacesEditorUITest.csproj rename to src/modules/Workspaces/WorkspacesEditorUITest/Workspaces.Editor.UITests.csproj diff --git a/src/modules/Workspaces/WorkspacesLib.UnitTests/WorkspacesLibUnitTests.vcxproj b/src/modules/Workspaces/WorkspacesLib.UnitTests/WorkspacesLibUnitTests.vcxproj index e3d18f54f3..14f87ef729 100644 --- a/src/modules/Workspaces/WorkspacesLib.UnitTests/WorkspacesLibUnitTests.vcxproj +++ b/src/modules/Workspaces/WorkspacesLib.UnitTests/WorkspacesLibUnitTests.vcxproj @@ -5,7 +5,7 @@ {A85D4D9F-9A39-4B5D-8B5A-9F2D5C9A8B4C} Win32Proj WorkspacesLibUnitTests - WorkspacesLibUnitTests + Workspaces.Lib.UnitTests
diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props index 664b2d678a..d364f7da8b 100644 --- a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props +++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props @@ -12,6 +12,6 @@ - + diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/ExecuteActionCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/ExecuteActionCommand.cs similarity index 89% rename from src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/ExecuteActionCommand.cs rename to src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/ExecuteActionCommand.cs index 2a3c895e9b..bf523d5792 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/ExecuteActionCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/ExecuteActionCommand.cs @@ -9,11 +9,11 @@ using Windows.AI.Actions.Hosting; namespace Microsoft.CmdPal.Ext.Indexer.Commands; -internal sealed partial class ExecuteActionCommand : InvokableCommand +public sealed partial class ExecuteActionCommand : InvokableCommand { private readonly ActionInstance actionInstance; - internal ExecuteActionCommand(ActionInstance actionInstance) + public ExecuteActionCommand(ActionInstance actionInstance) { this.actionInstance = actionInstance; this.Name = actionInstance.DisplayInfo.Description; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenInConsoleCommand.cs similarity index 61% rename from src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs rename to src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenInConsoleCommand.cs index cd9c5ce94b..37b82422d0 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenInConsoleCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenInConsoleCommand.cs @@ -6,28 +6,29 @@ using System.ComponentModel; using System.Diagnostics; using System.IO; using ManagedCommon; -using Microsoft.CmdPal.Ext.Indexer.Data; -using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Common.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; -namespace Microsoft.CmdPal.Ext.Indexer.Commands; +namespace Microsoft.CmdPal.Common.Commands; -internal sealed partial class OpenInConsoleCommand : InvokableCommand +public partial class OpenInConsoleCommand : InvokableCommand { - private readonly IndexerItem _item; + internal static IconInfo OpenInConsoleIcon { get; } = new("\uE756"); - internal OpenInConsoleCommand(IndexerItem item) + private readonly string _path; + + public OpenInConsoleCommand(string fullPath) { - this._item = item; + this._path = fullPath; this.Name = Resources.Indexer_Command_OpenPathInConsole; - this.Icon = new IconInfo("\uE756"); + this.Icon = OpenInConsoleIcon; } public override CommandResult Invoke() { using (var process = new Process()) { - process.StartInfo.WorkingDirectory = Path.GetDirectoryName(_item.FullPath); + process.StartInfo.WorkingDirectory = Path.GetDirectoryName(_path); process.StartInfo.FileName = "cmd.exe"; try @@ -36,10 +37,10 @@ internal sealed partial class OpenInConsoleCommand : InvokableCommand } catch (Win32Exception ex) { - Logger.LogError($"Unable to open {_item.FullPath}", ex); + Logger.LogError($"Unable to open '{_path}'", ex); } } - return CommandResult.GoHome(); + return CommandResult.Dismiss(); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenPropertiesCommand.cs similarity index 70% rename from src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs rename to src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenPropertiesCommand.cs index d07bbdca80..b4833dc913 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenPropertiesCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenPropertiesCommand.cs @@ -6,17 +6,17 @@ using System; using System.Runtime.InteropServices; using ManagedCommon; using ManagedCsWin32; -using Microsoft.CmdPal.Ext.Indexer.Data; -using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; -using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Common.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.Win32.UI.WindowsAndMessaging; -namespace Microsoft.CmdPal.Ext.Indexer.Commands; +namespace Microsoft.CmdPal.Common.Commands; -internal sealed partial class OpenPropertiesCommand : InvokableCommand +public partial class OpenPropertiesCommand : InvokableCommand { - private readonly IndexerItem _item; + internal static IconInfo OpenPropertiesIcon { get; } = new("\uE90F"); + + private readonly string _path; private static unsafe bool ShowFileProperties(string filename) { @@ -31,7 +31,7 @@ internal sealed partial class OpenPropertiesCommand : InvokableCommand LpVerb = propertiesPtr, LpFile = filenamePtr, Show = (int)SHOW_WINDOW_CMD.SW_SHOW, - FMask = NativeHelpers.SEEMASKINVOKEIDLIST, + FMask = global::Windows.Win32.PInvoke.SEE_MASK_INVOKEIDLIST, }; return Shell32.ShellExecuteEx(ref info); @@ -43,24 +43,24 @@ internal sealed partial class OpenPropertiesCommand : InvokableCommand } } - internal OpenPropertiesCommand(IndexerItem item) + public OpenPropertiesCommand(string fullPath) { - this._item = item; + this._path = fullPath; this.Name = Resources.Indexer_Command_OpenProperties; - this.Icon = new IconInfo("\uE90F"); + this.Icon = OpenPropertiesIcon; } public override CommandResult Invoke() { try { - ShowFileProperties(_item.FullPath); + ShowFileProperties(_path); } catch (Exception ex) { Logger.LogError("Error showing file properties: ", ex); } - return CommandResult.GoHome(); + return CommandResult.Dismiss(); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenWithCommand.cs similarity index 70% rename from src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs rename to src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenWithCommand.cs index 2c1875d3d7..33bd83a20c 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Indexer/Commands/OpenWithCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Commands/OpenWithCommand.cs @@ -4,17 +4,17 @@ using System.Runtime.InteropServices; using ManagedCsWin32; -using Microsoft.CmdPal.Ext.Indexer.Data; -using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils; -using Microsoft.CmdPal.Ext.Indexer.Properties; +using Microsoft.CmdPal.Common.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.Win32.UI.WindowsAndMessaging; -namespace Microsoft.CmdPal.Ext.Indexer.Commands; +namespace Microsoft.CmdPal.Common.Commands; -internal sealed partial class OpenWithCommand : InvokableCommand +public partial class OpenWithCommand : InvokableCommand { - private readonly IndexerItem _item; + internal static IconInfo OpenWithIcon { get; } = new("\uE7AC"); + + private readonly string _path; private static unsafe bool OpenWith(string filename) { @@ -29,7 +29,7 @@ internal sealed partial class OpenWithCommand : InvokableCommand LpVerb = verbPtr, LpFile = filenamePtr, Show = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL, - FMask = NativeHelpers.SEEMASKINVOKEIDLIST, + FMask = global::Windows.Win32.PInvoke.SEE_MASK_INVOKEIDLIST, }; return Shell32.ShellExecuteEx(ref info); @@ -41,16 +41,16 @@ internal sealed partial class OpenWithCommand : InvokableCommand } } - internal OpenWithCommand(IndexerItem item) + public OpenWithCommand(string fullPath) { - this._item = item; + this._path = fullPath; this.Name = Resources.Indexer_Command_OpenWith; - this.Icon = new IconInfo("\uE7AC"); + this.Icon = OpenWithIcon; } public override CommandResult Invoke() { - OpenWith(_item.FullPath); + OpenWith(_path); return CommandResult.GoHome(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/DiagnosticsHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/DiagnosticsHelper.cs new file mode 100644 index 0000000000..d2e9ddbcb3 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/DiagnosticsHelper.cs @@ -0,0 +1,66 @@ +// 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.Common.Helpers; + +/// +/// Provides utility methods for building diagnostic and error messages. +/// +public static class DiagnosticsHelper +{ + /// + /// Builds a comprehensive exception message with timestamp and detailed diagnostic information. + /// + /// The exception that occurred. + /// A hint about which extension caused the exception to help with debugging. + /// A string containing the exception details, timestamp, and source information for diagnostic purposes. + public static string BuildExceptionMessage(Exception exception, string? extensionHint) + { + var locationHint = string.IsNullOrWhiteSpace(extensionHint) ? "application" : $"'{extensionHint}' extension"; + + // let's try to get a message from the exception or inferred it from the HRESULT + // to show at least something + var message = exception.Message; + if (string.IsNullOrWhiteSpace(message)) + { + var temp = Marshal.GetExceptionForHR(exception.HResult)?.Message; + if (!string.IsNullOrWhiteSpace(temp)) + { + message = temp + $" (inferred from HRESULT 0x{exception.HResult:X8})"; + } + } + + if (string.IsNullOrWhiteSpace(message)) + { + message = "[No message available]"; + } + + // note: keep date time kind and format consistent with the log + return $""" + ============================================================ + 😢 An unexpected error occurred in the {locationHint}. + + Summary: + Message: {message} + Type: {exception.GetType().FullName} + Source: {exception.Source ?? "N/A"} + Time: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fffffff} + HRESULT: 0x{exception.HResult:X8} ({exception.HResult}) + + Stack Trace: + {exception.StackTrace ?? "[No stack trace available]"} + + ------------------ Full Exception Details ------------------ + {exception} + + ℹ️ If you need further assistance, please include this information in your support request. + ℹ️ Before sending, take a quick look to make sure it doesn't contain any personal or sensitive information. + ============================================================ + + """; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs index 25ff815a69..76de2729d0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/ExtensionHostInstance.cs @@ -24,7 +24,7 @@ public partial class ExtensionHostInstance /// The log message to send public void LogMessage(ILogMessage message) { - if (Host != null) + if (Host is not null) { _ = Task.Run(async () => { @@ -47,7 +47,7 @@ public partial class ExtensionHostInstance public void ShowStatus(IStatusMessage message, StatusContext context) { - if (Host != null) + if (Host is not null) { _ = Task.Run(async () => { @@ -64,7 +64,7 @@ public partial class ExtensionHostInstance public void HideStatus(IStatusMessage message) { - if (Host != null) + if (Host is not null) { _ = Task.Run(async () => { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/InterlockedBoolean.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/InterlockedBoolean.cs new file mode 100644 index 0000000000..8113ef9990 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/InterlockedBoolean.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.Threading; + +namespace Microsoft.CmdPal.Common.Helpers; + +/// +/// Thread-safe boolean implementation using atomic operations +/// +public struct InterlockedBoolean(bool initialValue = false) +{ + private int _value = initialValue ? 1 : 0; + + /// + /// Gets or sets the boolean value atomically + /// + public bool Value + { + get => Volatile.Read(ref _value) == 1; + set => Interlocked.Exchange(ref _value, value ? 1 : 0); + } + + /// + /// Atomically sets the value to true + /// + /// True if the value was previously false, false if it was already true + public bool Set() + { + return Interlocked.Exchange(ref _value, 1) == 0; + } + + /// + /// Atomically sets the value to false + /// + /// True if the value was previously true, false if it was already false + public bool Clear() + { + return Interlocked.Exchange(ref _value, 0) == 1; + } + + public override int GetHashCode() => Value.GetHashCode(); + + public override string ToString() => Value.ToString(); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/SupersedingAsyncGate.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/SupersedingAsyncGate.cs new file mode 100644 index 0000000000..9313ba6755 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/SupersedingAsyncGate.cs @@ -0,0 +1,139 @@ +// 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 Microsoft.CmdPal.Common.Helpers; + +/// +/// An async gate that ensures only one operation runs at a time. +/// If ExecuteAsync is called while already executing, it cancels the current execution +/// and starts the operation again (superseding behavior). +/// +public class SupersedingAsyncGate : IDisposable +{ + private readonly Func _action; + private readonly Lock _lock = new(); + private int _callId; + private TaskCompletionSource? _currentTcs; + private CancellationTokenSource? _currentCancellationSource; + private Task? _executingTask; + + public SupersedingAsyncGate(Func action) + { + ArgumentNullException.ThrowIfNull(action); + _action = action; + } + + /// + /// Executes the configured action. If another execution is running, this call will + /// cancel the current execution and restart the operation. + /// + /// Optional external cancellation token + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + TaskCompletionSource tcs; + + lock (_lock) + { + _currentCancellationSource?.Cancel(); + _currentTcs?.TrySetException(new OperationCanceledException("Superseded by newer call")); + + tcs = new(); + _currentTcs = tcs; + _callId++; + + var shouldStartExecution = _executingTask is null; + if (shouldStartExecution) + { + _executingTask = Task.Run(ExecuteLoop, CancellationToken.None); + } + } + + await using var ctr = cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)); + await tcs.Task; + } + + private async Task ExecuteLoop() + { + try + { + while (true) + { + TaskCompletionSource? currentTcs; + CancellationTokenSource? currentCts; + int currentCallId; + + lock (_lock) + { + currentTcs = _currentTcs; + currentCallId = _callId; + + if (currentTcs is null) + { + break; + } + + _currentCancellationSource?.Dispose(); + _currentCancellationSource = new(); + currentCts = _currentCancellationSource; + } + + try + { + await _action(currentCts.Token); + CompleteIfCurrent(currentTcs, currentCallId, static t => t.TrySetResult(true)); + } + catch (OperationCanceledException) + { + CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.TrySetCanceled(currentCts.Token)); + } + catch (Exception ex) + { + CompleteIfCurrent(currentTcs, currentCallId, tcs => tcs.TrySetException(ex)); + } + } + } + finally + { + lock (_lock) + { + _currentTcs = null; + _currentCancellationSource?.Dispose(); + _currentCancellationSource = null; + _executingTask = null; + } + } + } + + private void CompleteIfCurrent( + TaskCompletionSource candidate, + int id, + Action> complete) + { + lock (_lock) + { + if (_currentTcs == candidate && _callId == id) + { + complete(candidate); + _currentTcs = null; + } + } + } + + public void Dispose() + { + lock (_lock) + { + _currentCancellationSource?.Cancel(); + _currentCancellationSource?.Dispose(); + _currentTcs?.TrySetException(new ObjectDisposedException(nameof(SupersedingAsyncGate))); + _currentTcs = null; + } + + GC.SuppressFinalize(this); + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj index 0112da1b0b..27509d0e5b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj @@ -28,7 +28,24 @@ + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt index 996bbd7153..61e89b68c4 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt @@ -9,3 +9,7 @@ GetWindowRect GetMonitorInfo SetWindowPos MonitorFromWindow + +SHOW_WINDOW_CMD +ShellExecuteEx +SEE_MASK_INVOKEIDLIST \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..c2f81dd683 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// 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.Common.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()] + internal 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)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Common.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)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Open path in console. + /// + internal static string Indexer_Command_OpenPathInConsole { + get { + return ResourceManager.GetString("Indexer_Command_OpenPathInConsole", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Properties. + /// + internal static string Indexer_Command_OpenProperties { + get { + return ResourceManager.GetString("Indexer_Command_OpenProperties", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open with. + /// + internal static string Indexer_Command_OpenWith { + get { + return ResourceManager.GetString("Indexer_Command_OpenWith", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Show in folder. + /// + internal static string Indexer_Command_ShowInFolder { + get { + return ResourceManager.GetString("Indexer_Command_ShowInFolder", resourceCulture); + } + } + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.resx new file mode 100644 index 0000000000..14e62fb4c2 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Properties/Resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Open path in console + + + Properties + + + Open with + + + Show in folder + + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IRunHistoryService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IRunHistoryService.cs new file mode 100644 index 0000000000..703ad9f1ff --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IRunHistoryService.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.Collections.Generic; + +namespace Microsoft.CmdPal.Common.Services; + +public interface IRunHistoryService +{ + /// + /// Gets the run history. + /// + /// A list of run history items. + IReadOnlyList GetRunHistory(); + + /// + /// Clears the run history. + /// + void ClearRunHistory(); + + /// + /// Adds a run history item. + /// + /// The run history item to add. + void AddRunHistoryItem(string item); +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs index 3a828a3e5d..8a93aee51d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/AppExtensionHost.cs @@ -36,7 +36,7 @@ public abstract partial class AppExtensionHost : IExtensionHost public IAsyncAction HideStatus(IStatusMessage? message) { - if (message == null) + if (message is null) { return Task.CompletedTask.AsAsyncAction(); } @@ -55,7 +55,7 @@ public abstract partial class AppExtensionHost : IExtensionHost public IAsyncAction LogMessage(ILogMessage? message) { - if (message == null) + if (message is null) { return Task.CompletedTask.AsAsyncAction(); } @@ -80,7 +80,7 @@ public abstract partial class AppExtensionHost : IExtensionHost try { var vm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault(); - if (vm != null) + if (vm is not null) { StatusMessages.Remove(vm); } @@ -113,7 +113,7 @@ public abstract partial class AppExtensionHost : IExtensionHost { // If this message is already in the list of messages, just bring it to the top var oldVm = StatusMessages.Where(messageVM => messageVM.Model.Unsafe == message).FirstOrDefault(); - if (oldVm != null) + if (oldVm is not null) { Task.Factory.StartNew( () => @@ -142,7 +142,7 @@ public abstract partial class AppExtensionHost : IExtensionHost public IAsyncAction ShowStatus(IStatusMessage? message, StatusContext context) { - if (message == null) + if (message is null) { return Task.CompletedTask.AsAsyncAction(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs index f506c127f2..c01cb13730 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandBarViewModel.cs @@ -2,7 +2,6 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.ViewModels.Messages; @@ -35,13 +34,13 @@ public partial class CommandBarViewModel : ObservableObject, [NotifyPropertyChangedFor(nameof(HasPrimaryCommand))] public partial CommandItemViewModel? PrimaryCommand { get; set; } - public bool HasPrimaryCommand => PrimaryCommand != null && PrimaryCommand.ShouldBeVisible; + public bool HasPrimaryCommand => PrimaryCommand is not null && PrimaryCommand.ShouldBeVisible; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasSecondaryCommand))] public partial CommandItemViewModel? SecondaryCommand { get; set; } - public bool HasSecondaryCommand => SecondaryCommand != null; + public bool HasSecondaryCommand => SecondaryCommand is not null; [ObservableProperty] public partial bool ShouldShowContextMenu { get; set; } = false; @@ -58,14 +57,14 @@ public partial class CommandBarViewModel : ObservableObject, private void SetSelectedItem(ICommandBarContext? value) { - if (value != null) + if (value is not null) { PrimaryCommand = value.PrimaryCommand; value.PropertyChanged += SelectedItemPropertyChanged; } else { - if (SelectedItem != null) + if (SelectedItem is not null) { SelectedItem.PropertyChanged -= SelectedItemPropertyChanged; } @@ -88,7 +87,7 @@ public partial class CommandBarViewModel : ObservableObject, private void UpdateContextItems() { - if (SelectedItem == null) + if (SelectedItem is null) { SecondaryCommand = null; ShouldShowContextMenu = false; @@ -127,13 +126,13 @@ public partial class CommandBarViewModel : ObservableObject, public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key) { var keybindings = SelectedItem?.Keybindings(); - if (keybindings != null) + if (keybindings is not null) { // Does the pressed key match any of the keybindings? var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0); if (keybindings.TryGetValue(pressedKeyChord, out var matchedItem)) { - return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled; + return matchedItem is not null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled; } } @@ -142,7 +141,7 @@ public partial class CommandBarViewModel : ObservableObject, private ContextKeybindingResult PerformCommand(CommandItemViewModel? command) { - if (command == null) + if (command is null) { return ContextKeybindingResult.Unhandled; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs index 60fc815a52..4b25f68e0a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandContextItemViewModel.cs @@ -2,11 +2,14 @@ // 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 Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; +using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel { private readonly KeyChord nullKeyChord = new(0, 0, 0); @@ -17,7 +20,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem public KeyChord? RequestedShortcut { get; private set; } - public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord); + public bool HasRequestedShortcut => RequestedShortcut is not null && (RequestedShortcut.Value != nullKeyChord); public override void InitializeProperties() { @@ -29,7 +32,7 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem base.InitializeProperties(); var contextItem = Model.Unsafe; - if (contextItem == null) + if (contextItem is null) { return; // throw? } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs index 75a8eb9a56..4f589a4e2f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.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.Diagnostics.CodeAnalysis; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; @@ -9,6 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBarContext { public ExtensionObject Model => _commandItemModel; @@ -66,7 +68,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa { get { - List l = _defaultCommandContextItem == null ? + List l = _defaultCommandContextItem is null ? new() : [_defaultCommandContextItem]; @@ -98,7 +100,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } var model = _commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; } @@ -126,7 +128,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } var model = _commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; } @@ -134,7 +136,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa Command.InitializeProperties(); var listIcon = model.Icon; - if (listIcon != null) + if (listIcon is not null) { _listItemIcon = new(listIcon); _listItemIcon.InitializeProperties(); @@ -170,13 +172,13 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa } var model = _commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; } var more = model.MoreCommands; - if (more != null) + if (more is not null) { MoreCommands = more .Select(item => @@ -298,7 +300,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa protected virtual void FetchProperty(string propertyName) { var model = this._commandItemModel.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -306,13 +308,17 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa switch (propertyName) { case nameof(Command): - if (Command != null) + if (Command is not null) { Command.PropertyChanged -= Command_PropertyChanged; } Command = new(model.Command, PageContext); Command.InitializeProperties(); + + // Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command + // or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK. + _itemTitle = model.Title; UpdateProperty(nameof(Name)); UpdateProperty(nameof(Title)); UpdateProperty(nameof(Icon)); @@ -333,12 +339,12 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa case nameof(model.MoreCommands): var more = model.MoreCommands; - if (more != null) + if (more is not null) { var newContextMenu = more .Select(item => { - if (item is CommandContextItem contextItem) + if (item is ICommandContextItem contextItem) { return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel; } @@ -385,6 +391,14 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa switch (propertyName) { case nameof(Command.Name): + // Extensions based on Command Palette SDK < 0.3 CommandItem class won't notify when Title changes because Command + // or Command.Name change. This is a workaround to ensure that the Title is always up-to-date for extensions with old SDK. + var model = _commandItemModel.Unsafe; + if (model is not null) + { + _itemTitle = model.Title; + } + UpdateProperty(nameof(Title)); UpdateProperty(nameof(Name)); break; @@ -416,7 +430,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa Command.SafeCleanup(); var model = _commandItemModel.Unsafe; - if (model != null) + if (model is not null) { model.PropChanged -= Model_PropChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs index 6e48cef382..30a85045d3 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandViewModel.cs @@ -44,7 +44,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel } var model = Model.Unsafe; - if (model == null) + if (model is null) { return; } @@ -67,13 +67,13 @@ public partial class CommandViewModel : ExtensionObjectViewModel } var model = Model.Unsafe; - if (model == null) + if (model is null) { return; } var ico = model.Icon; - if (ico != null) + if (ico is not null) { Icon = new(ico); Icon.InitializeProperties(); @@ -98,7 +98,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel protected void FetchProperty(string propertyName) { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -125,7 +125,7 @@ public partial class CommandViewModel : ExtensionObjectViewModel Icon = new(null); // necessary? var model = Model.Unsafe; - if (model != null) + if (model is not null) { model.PropChanged -= Model_PropChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs index 45cd18f4dd..c653357ccd 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ConfirmResultViewModel.cs @@ -25,7 +25,7 @@ public partial class ConfirmResultViewModel(IConfirmationArgs _args, WeakReferen public override void InitializeProperties() { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs index 16611a31ac..0c0f7c7c12 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs @@ -14,7 +14,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Core.ViewModels; -public partial class ContentPageViewModel : PageViewModel, ICommandBarContext +public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarContext { private readonly ExtensionObject _model; @@ -28,7 +28,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext public DetailsViewModel? Details { get; private set; } [MemberNotNullWhen(true, nameof(Details))] - public bool HasDetails => Details != null; + public bool HasDetails => Details is not null; /////// ICommandBarContext /////// public IEnumerable MoreCommands => Commands.Skip(1); @@ -67,7 +67,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext foreach (var item in newItems) { var viewModel = ViewModelFromContent(item, PageContext); - if (viewModel != null) + if (viewModel is not null) { viewModel.InitializeProperties(); newContent.Add(viewModel); @@ -104,7 +104,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext base.InitializeProperties(); var model = _model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -113,7 +113,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext .ToList() .Select(item => { - if (item is CommandContextItem contextItem) + if (item is ICommandContextItem contextItem) { return new CommandContextItemViewModel(contextItem, PageContext); } @@ -133,7 +133,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext }); var extensionDetails = model.Details; - if (extensionDetails != null) + if (extensionDetails is not null) { Details = new(extensionDetails, PageContext); Details.InitializeProperties(); @@ -156,7 +156,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext base.FetchProperty(propertyName); var model = this._model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -166,13 +166,13 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext case nameof(Commands): var more = model.Commands; - if (more != null) + if (more is not null) { var newContextMenu = more .ToList() .Select(item => { - if (item is CommandContextItem contextItem) + if (item is ICommandContextItem contextItem) { return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel; } @@ -216,7 +216,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext break; case nameof(Details): var extensionDetails = model.Details; - Details = extensionDetails != null ? new(extensionDetails, PageContext) : null; + Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null; UpdateDetails(); break; } @@ -248,7 +248,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext [RelayCommand] private void InvokePrimaryCommand(ContentPageViewModel page) { - if (PrimaryCommand != null) + if (PrimaryCommand is not null) { WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model)); } @@ -258,7 +258,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext [RelayCommand] private void InvokeSecondaryCommand(ContentPageViewModel page) { - if (SecondaryCommand != null) + if (SecondaryCommand is not null) { WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model)); } @@ -285,7 +285,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext Content.Clear(); var model = _model.Unsafe; - if (model != null) + if (model is not null) { model.ItemsChanged -= Model_ItemsChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs index bcc414859a..02af0aa67e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContextMenuViewModel.cs @@ -8,14 +8,12 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; -using Microsoft.Diagnostics.Utilities; using Windows.System; namespace Microsoft.CmdPal.Core.ViewModels; public partial class ContextMenuViewModel : ObservableObject, - IRecipient, - IRecipient + IRecipient { public ICommandBarContext? SelectedItem { @@ -43,7 +41,6 @@ public partial class ContextMenuViewModel : ObservableObject, public ContextMenuViewModel() { WeakReferenceMessenger.Default.Register(this); - WeakReferenceMessenger.Default.Register(this); } public void Receive(UpdateCommandBarMessage message) @@ -51,19 +48,9 @@ public partial class ContextMenuViewModel : ObservableObject, SelectedItem = message.ViewModel; } - public void Receive(OpenContextMenuMessage message) - { - FilterOnTop = message.ContextMenuFilterLocation == ContextMenuFilterLocation.Top; - - ResetContextMenu(); - - OnPropertyChanging(nameof(FilterOnTop)); - OnPropertyChanged(nameof(FilterOnTop)); - } - public void UpdateContextItems() { - if (SelectedItem != null) + if (SelectedItem is not null) { if (SelectedItem.MoreCommands.Count() > 1) { @@ -80,14 +67,14 @@ public partial class ContextMenuViewModel : ObservableObject, return; } - if (SelectedItem == null) + if (SelectedItem is null) { return; } _lastSearchText = searchText; - if (CurrentContextMenu == null) + if (CurrentContextMenu is null) { ListHelpers.InPlaceUpdateList(FilteredItems, []); return; @@ -136,7 +123,7 @@ public partial class ContextMenuViewModel : ObservableObject, /// that have a shortcut key set. public Dictionary Keybindings() { - if (CurrentContextMenu == null) + if (CurrentContextMenu is null) { return []; } @@ -152,7 +139,7 @@ public partial class ContextMenuViewModel : ObservableObject, public ContextKeybindingResult? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key) { var keybindings = Keybindings(); - if (keybindings != null) + if (keybindings is not null) { // Does the pressed key match any of the keybindings? var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0); @@ -192,7 +179,7 @@ public partial class ContextMenuViewModel : ObservableObject, ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]); } - private void ResetContextMenu() + public void ResetContextMenu() { while (ContextMenuStack.Count > 1) { @@ -202,7 +189,7 @@ public partial class ContextMenuViewModel : ObservableObject, OnPropertyChanging(nameof(CurrentContextMenu)); OnPropertyChanged(nameof(CurrentContextMenu)); - if (CurrentContextMenu != null) + if (CurrentContextMenu is not null) { ListHelpers.InPlaceUpdateList(FilteredItems, [.. CurrentContextMenu!]); } @@ -210,7 +197,7 @@ public partial class ContextMenuViewModel : ObservableObject, public ContextKeybindingResult InvokeCommand(CommandItemViewModel? command) { - if (command == null) + if (command is null) { return ContextKeybindingResult.Unhandled; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs index b85aeaba81..11a67603e9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsCommandsViewModel.cs @@ -22,7 +22,7 @@ public partial class DetailsCommandsViewModel( { base.InitializeProperties(); var model = _dataModel.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs index 390459f26c..9739220b65 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsElementViewModel.cs @@ -16,7 +16,7 @@ public abstract partial class DetailsElementViewModel(IDetailsElement _detailsEl public override void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs index e7aa9b67af..427fcd170e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsLinkViewModel.cs @@ -18,7 +18,7 @@ public partial class DetailsLinkViewModel( public Uri? Link { get; private set; } - public bool IsLink => Link != null; + public bool IsLink => Link is not null; public bool IsText => !IsLink; @@ -26,14 +26,14 @@ public partial class DetailsLinkViewModel( { base.InitializeProperties(); var model = _dataModel.Unsafe; - if (model == null) + if (model is null) { return; } Text = model.Text ?? string.Empty; Link = model.Link; - if (string.IsNullOrEmpty(Text) && Link != null) + if (string.IsNullOrEmpty(Text) && Link is not null) { Text = Link.ToString(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs index 803585c1ce..747a0a74c9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsTagsViewModel.cs @@ -22,7 +22,7 @@ public partial class DetailsTagsViewModel( { base.InitializeProperties(); var model = _dataModel.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs index 034e247519..a381cfda6b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/DetailsViewModel.cs @@ -26,7 +26,7 @@ public partial class DetailsViewModel(IDetails _details, WeakReference new DetailsTagsViewModel(element, this.PageContext), _ => null, }; - if (vm != null) + if (vm is not null) { vm.InitializeProperties(); Metadata.Add(vm); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs index 704947c3a8..cad1af9d4d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public interface IContextItemViewModel { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs index 70b143864c..5f4b4436f2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconDataViewModel.cs @@ -16,7 +16,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData // If the extension previously gave us a Data, then died, the data will // throw if we actually try to read it, but the pointer itself won't be // null, so this is relatively safe. - public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe != null; + public bool HasIcon => !string.IsNullOrEmpty(Icon) || Data.Unsafe is not null; // Locally cached properties from IIconData. public string Icon { get; private set; } = string.Empty; @@ -36,7 +36,7 @@ public partial class IconDataViewModel : ObservableObject, IIconData public void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs index 21ddbe99d9..aebe9b03aa 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IconInfoViewModel.cs @@ -26,7 +26,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo public bool HasIcon(bool light) => IconForTheme(light).HasIcon; - public bool IsSet => _model.Unsafe != null; + public bool IsSet => _model.Unsafe is not null; IIconData? IIconInfo.Dark => Dark; @@ -43,7 +43,7 @@ public partial class IconInfoViewModel : ObservableObject, IIconInfo public void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs index 682bf4daea..ad1aebe2d1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListItemViewModel.cs @@ -27,7 +27,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference Details != null; + public bool HasDetails => Details is not null; public override void InitializeProperties() { @@ -40,7 +40,7 @@ public partial class ListItemViewModel(IListItem model, WeakReference? ItemsUpdated; @@ -56,10 +57,13 @@ public partial class ListViewModel : PageViewModel, IDisposable public CommandItemViewModel EmptyContent { get; private set; } + public bool IsMainPage { get; init; } + private bool _isDynamic; private Task? _initializeItemsTask; private CancellationTokenSource? _cancellationTokenSource; + private CancellationTokenSource? _fetchItemsCancellationTokenSource; private ListItemViewModel? _lastSelectedItem; @@ -119,29 +123,45 @@ public partial class ListViewModel : PageViewModel, IDisposable ItemsUpdated?.Invoke(this, EventArgs.Empty); UpdateEmptyContent(); - _isLoading = false; + _isLoading.Clear(); } } //// Run on background thread, from InitializeAsync or Model_ItemsChanged private void FetchItems() { + // Cancel any previous FetchItems operation + _fetchItemsCancellationTokenSource?.Cancel(); + _fetchItemsCancellationTokenSource?.Dispose(); + _fetchItemsCancellationTokenSource = new CancellationTokenSource(); + + var cancellationToken = _fetchItemsCancellationTokenSource.Token; + // TEMPORARY: just plop all the items into a single group // see 9806fe5d8 for the last commit that had this with sections _isFetching = true; + // Collect all the items into new viewmodels + Collection newViewModels = []; + try { + // Check for cancellation before starting expensive operations + cancellationToken.ThrowIfCancellationRequested(); + var newItems = _model.Unsafe!.GetItems(); - // Collect all the items into new viewmodels - Collection newViewModels = []; + // Check for cancellation after getting items from extension + cancellationToken.ThrowIfCancellationRequested(); // TODO we can probably further optimize this by also keeping a // HashSet of every ExtensionObject we currently have, and only // building new viewmodels for the ones we haven't already built. foreach (var item in newItems) { + // Check for cancellation during item processing + cancellationToken.ThrowIfCancellationRequested(); + ListItemViewModel viewModel = new(item, new(this)); // If an item fails to load, silently ignore it. @@ -151,25 +171,57 @@ public partial class ListViewModel : PageViewModel, IDisposable } } + // Check for cancellation before initializing first twenty items + cancellationToken.ThrowIfCancellationRequested(); + var firstTwenty = newViewModels.Take(20); foreach (var item in firstTwenty) { + cancellationToken.ThrowIfCancellationRequested(); item?.SafeInitializeProperties(); } // Cancel any ongoing search _cancellationTokenSource?.Cancel(); + // Check for cancellation before updating the list + cancellationToken.ThrowIfCancellationRequested(); + + List removedItems = []; lock (_listLock) { // Now that we have new ViewModels for everything from the // extension, smartly update our list of VMs - ListHelpers.InPlaceUpdateList(Items, newViewModels); + ListHelpers.InPlaceUpdateList(Items, newViewModels, out removedItems); + + // DO NOT ThrowIfCancellationRequested AFTER THIS! If you do, + // you'll clean up list items that we've now transferred into + // .Items + } + + // If we removed items, we need to clean them up, to remove our event handlers + foreach (var removedItem in removedItems) + { + removedItem.SafeCleanup(); } // TODO: Iterate over everything in Items, and prune items from the // cache if we don't need them anymore } + catch (OperationCanceledException) + { + // Cancellation is expected, don't treat as error + + // However, if we were cancelled, we didn't actually add these items to + // our Items list. Before we release them to the GC, make sure we clean + // them up + foreach (var vm in newViewModels) + { + vm.SafeCleanup(); + } + + return; + } catch (Exception ex) { // TODO: Move this within the for loop, so we can catch issues with individual items @@ -219,7 +271,7 @@ public partial class ListViewModel : PageViewModel, IDisposable } ItemsUpdated?.Invoke(this, EventArgs.Empty); - _isLoading = false; + _isLoading.Clear(); }); } @@ -295,11 +347,11 @@ public partial class ListViewModel : PageViewModel, IDisposable [RelayCommand] private void InvokeItem(ListItemViewModel? item) { - if (item != null) + if (item is not null) { WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model)); } - else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe != null) + else if (ShowEmptyContent && EmptyContent.PrimaryCommand?.Model.Unsafe is not null) { WeakReferenceMessenger.Default.Send(new( EmptyContent.PrimaryCommand.Command.Model, @@ -311,14 +363,14 @@ public partial class ListViewModel : PageViewModel, IDisposable [RelayCommand] private void InvokeSecondaryCommand(ListItemViewModel? item) { - if (item != null) + if (item is not null) { - if (item.SecondaryCommand != null) + if (item.SecondaryCommand is not null) { WeakReferenceMessenger.Default.Send(new(item.SecondaryCommand.Command.Model, item.Model)); } } - else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe != null) + else if (ShowEmptyContent && EmptyContent.SecondaryCommand?.Model.Unsafe is not null) { WeakReferenceMessenger.Default.Send(new( EmptyContent.SecondaryCommand.Command.Model, @@ -329,12 +381,12 @@ public partial class ListViewModel : PageViewModel, IDisposable [RelayCommand] private void UpdateSelectedItem(ListItemViewModel? item) { - if (_lastSelectedItem != null) + if (_lastSelectedItem is not null) { _lastSelectedItem.PropertyChanged -= SelectedItemPropertyChanged; } - if (item != null) + if (item is not null) { SetSelectedItem(item); } @@ -370,6 +422,7 @@ public partial class ListViewModel : PageViewModel, IDisposable } TextToSuggest = item.TextToSuggest; + WeakReferenceMessenger.Default.Send(new(item.TextToSuggest)); }); _lastSelectedItem = item; @@ -379,7 +432,7 @@ public partial class ListViewModel : PageViewModel, IDisposable private void SelectedItemPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { var item = _lastSelectedItem; - if (item == null) + if (item is null) { return; } @@ -423,6 +476,8 @@ public partial class ListViewModel : PageViewModel, IDisposable WeakReferenceMessenger.Default.Send(); + WeakReferenceMessenger.Default.Send(new(string.Empty)); + TextToSuggest = string.Empty; }); } @@ -432,7 +487,7 @@ public partial class ListViewModel : PageViewModel, IDisposable base.InitializeProperties(); var model = _model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -459,26 +514,43 @@ public partial class ListViewModel : PageViewModel, IDisposable public void LoadMoreIfNeeded() { var model = this._model.Unsafe; - if (model == null) + if (model is null) { return; } - if (model.HasMoreItems && !_isLoading) + if (!_isLoading.Set()) { - _isLoading = true; - _ = Task.Run(() => + return; + + // NOTE: May miss newly available items until next scroll if model + // state changes between our check and this reset + } + + _ = Task.Run(() => + { + // Execute all COM calls on background thread to avoid reentrancy issues with UI + // with the UI thread when COM starts inner message pump + try { - try + if (model.HasMoreItems) { model.LoadMore(); + + // _isLoading flag will be set as a result of LoadMore, + // which must raise ItemsChanged to end the loading. } - catch (Exception ex) + else { - ShowException(ex, model.Name); + _isLoading.Clear(); } - }); - } + } + catch (Exception ex) + { + _isLoading.Clear(); + ShowException(ex, model.Name); + } + }); } protected override void FetchProperty(string propertyName) @@ -486,7 +558,7 @@ public partial class ListViewModel : PageViewModel, IDisposable base.FetchProperty(propertyName); var model = this._model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -517,7 +589,7 @@ public partial class ListViewModel : PageViewModel, IDisposable private void UpdateEmptyContent() { UpdateProperty(nameof(ShowEmptyContent)); - if (!ShowEmptyContent || EmptyContent.Model.Unsafe == null) + if (!ShowEmptyContent || EmptyContent.Model.Unsafe is null) { return; } @@ -537,6 +609,10 @@ public partial class ListViewModel : PageViewModel, IDisposable _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; + + _fetchItemsCancellationTokenSource?.Cancel(); + _fetchItemsCancellationTokenSource?.Dispose(); + _fetchItemsCancellationTokenSource = null; } protected override void UnsafeCleanup() @@ -547,6 +623,7 @@ public partial class ListViewModel : PageViewModel, IDisposable EmptyContent = new(new(null), PageContext); // necessary? _cancellationTokenSource?.Cancel(); + _fetchItemsCancellationTokenSource?.Cancel(); lock (_listLock) { @@ -565,7 +642,7 @@ public partial class ListViewModel : PageViewModel, IDisposable } var model = _model.Unsafe; - if (model != null) + if (model is not null) { model.ItemsChanged -= Model_ItemsChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs index 9ebff20304..969bf60aea 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/LogMessageViewModel.cs @@ -22,7 +22,7 @@ public partial class LogMessageViewModel : ExtensionObjectViewModel public override void InitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateSuggestionMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateSuggestionMessage.cs new file mode 100644 index 0000000000..7e27056c4c --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateSuggestionMessage.cs @@ -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. + +namespace Microsoft.CmdPal.Core.ViewModels.Messages; + +public record UpdateSuggestionMessage(string TextToSuggest) +{ +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj index 1508994524..014a2a39e1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Microsoft.CmdPal.Core.ViewModels.csproj @@ -45,16 +45,4 @@ - - - - diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs index 552971b96c..046c9fae93 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs @@ -5,6 +5,7 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Core.ViewModels.Models; using Microsoft.CommandPalette.Extensions; @@ -45,7 +46,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext [ObservableProperty] public partial AppExtensionHost ExtensionHost { get; private set; } - public bool HasStatusMessage => MostRecentStatusMessage != null; + public bool HasStatusMessage => MostRecentStatusMessage is not null; [ObservableProperty] [NotifyPropertyChangedFor(nameof(HasStatusMessage))] @@ -132,7 +133,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext public override void InitializeProperties() { var page = _pageModel.Unsafe; - if (page == null) + if (page is null) { return; // throw? } @@ -177,7 +178,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext protected virtual void FetchProperty(string propertyName) { var model = this._pageModel.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -223,9 +224,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext extensionHint ??= ExtensionHost.GetExtensionDisplayName() ?? Title; Task.Factory.StartNew( () => - { - ErrorMessage += $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n"; - }, + { + var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint); + ErrorMessage += message; + }, CancellationToken.None, TaskCreationOptions.None, Scheduler); @@ -240,7 +242,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext ExtensionHost.StatusMessages.CollectionChanged -= StatusMessages_CollectionChanged; var model = _pageModel.Unsafe; - if (model != null) + if (model is not null) { model.PropChanged -= Model_PropChanged; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModelFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModelFactory.cs deleted file mode 100644 index a30a2bd76b..0000000000 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModelFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.CommandPalette.Extensions; - -namespace Microsoft.CmdPal.Core.ViewModels; - -public class PageViewModelFactory : IPageViewModelFactoryService -{ - private readonly TaskScheduler _scheduler; - - public PageViewModelFactory(TaskScheduler scheduler) - { - _scheduler = scheduler; - } - - public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, AppExtensionHost host) - { - return page switch - { - IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested }, - IContentPage contentPage => new ContentPageViewModel(contentPage, _scheduler, host), - _ => null, - }; - } -} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs index 40ea290dd4..4ddcfb22e7 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ProgressViewModel.cs @@ -24,7 +24,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel public override void InitializeProperties() { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -50,7 +50,7 @@ public partial class ProgressViewModel : ExtensionObjectViewModel protected virtual void FetchProperty(string propertyName) { var model = this.Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs index c6858f490d..8d896bd341 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs @@ -2,11 +2,13 @@ // 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 Microsoft.CmdPal.Core.ViewModels; using Microsoft.CommandPalette.Extensions; namespace Microsoft.CmdPal.Core.ViewModels; +[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem { } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs index 170103b09c..6c660d52f2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ShellViewModel.cs @@ -13,7 +13,8 @@ using Microsoft.CommandPalette.Extensions; namespace Microsoft.CmdPal.Core.ViewModels; public partial class ShellViewModel : ObservableObject, - IRecipient + IRecipient, + IRecipient { private readonly IRootPageService _rootPageService; private readonly IAppHostService _appHostService; @@ -77,6 +78,7 @@ public partial class ShellViewModel : ObservableObject, // Register to receive messages WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); } [RelayCommand] @@ -118,7 +120,7 @@ public partial class ShellViewModel : ObservableObject, ////LoadedState = ViewModelLoadedState.Loading; if (!viewModel.IsInitialized - && viewModel.InitializeCommand != null) + && viewModel.InitializeCommand is not null) { _ = Task.Run(async () => { @@ -183,7 +185,7 @@ public partial class ShellViewModel : ObservableObject, private void PerformCommand(PerformCommandMessage message) { var command = message.Command.Unsafe; - if (command == null) + if (command is null) { return; } @@ -203,7 +205,7 @@ public partial class ShellViewModel : ObservableObject, // Construct our ViewModel of the appropriate type and pass it the UI Thread context. var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host); - if (pageViewModel == null) + if (pageViewModel is null) { Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}"); throw new NotSupportedException(); @@ -238,7 +240,7 @@ public partial class ShellViewModel : ObservableObject, // TODO GH #525 This needs more better locking. lock (_invokeLock) { - if (_handleInvokeTask != null) + if (_handleInvokeTask is not null) { // do nothing - a command is already doing a thing } @@ -278,7 +280,7 @@ public partial class ShellViewModel : ObservableObject, private void UnsafeHandleCommandResult(ICommandResult? result) { - if (result == null) + if (result is null) { // No result, nothing to do. return; @@ -358,6 +360,11 @@ public partial class ShellViewModel : ObservableObject, WeakReferenceMessenger.Default.Send(new(withAnimation, focusSearch)); } + public void Receive(HandleCommandResultMessage message) + { + UnsafeHandleCommandResult(message.Result.Unsafe); + } + private void OnUIThread(Action action) { _ = Task.Factory.StartNew( diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs index fb8b333637..2c78ff407e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/StatusMessageViewModel.cs @@ -17,7 +17,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel public ProgressViewModel? Progress { get; private set; } - public bool HasProgress => Progress != null; + public bool HasProgress => Progress is not null; public StatusMessageViewModel(IStatusMessage message, WeakReference context) : base(context) @@ -28,7 +28,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel public override void InitializeProperties() { var model = Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -36,7 +36,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel Message = model.Message; State = model.State; var modelProgress = model.Progress; - if (modelProgress != null) + if (modelProgress is not null) { Progress = new(modelProgress, this.PageContext); Progress.InitializeProperties(); @@ -61,7 +61,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel protected virtual void FetchProperty(string propertyName) { var model = this.Model.Unsafe; - if (model == null) + if (model is null) { return; // throw? } @@ -76,7 +76,7 @@ public partial class StatusMessageViewModel : ExtensionObjectViewModel break; case nameof(Progress): var modelProgress = model.Progress; - if (modelProgress != null) + if (modelProgress is not null) { Progress = new(modelProgress, this.PageContext); Progress.InitializeProperties(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs index 98ea66f4e8..5287cf441c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/TagViewModel.cs @@ -28,7 +28,7 @@ public partial class TagViewModel(ITag _tag, WeakReference context public override void InitializeProperties() { var model = _tagModel.Unsafe; - if (model == null) + if (model is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs index 131d633940..642e5ad4a9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AliasManager.cs @@ -35,7 +35,7 @@ public partial class AliasManager : ObservableObject try { var topLevelCommand = _topLevelCommandManager.LookupCommand(alias.CommandId); - if (topLevelCommand != null) + if (topLevelCommand is not null) { WeakReferenceMessenger.Default.Send(); @@ -88,7 +88,7 @@ public partial class AliasManager : ObservableObject } // If we already have _this exact alias_, do nothing - if (newAlias != null && + if (newAlias is not null && _aliases.TryGetValue(newAlias.SearchPrefix, out var existingAlias)) { if (existingAlias.CommandId == commandId) @@ -113,7 +113,7 @@ public partial class AliasManager : ObservableObject _aliases.Remove(alias.SearchPrefix); } - if (newAlias != null) + if (newAlias is not null) { AddAlias(newAlias); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs index 89f0cdf2c8..3a11c50a59 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs @@ -21,8 +21,12 @@ public partial class AppStateModel : ObservableObject /////////////////////////////////////////////////////////////////////////// // STATE HERE + // Make sure that you make the setters public (JsonSerializer.Deserialize will fail silently otherwise)! + // Make sure that any new types you add are added to JsonSerializationContext! public RecentCommandsManager RecentCommands { get; set; } = new(); + public List RunHistory { get; set; } = []; + // END SETTINGS /////////////////////////////////////////////////////////////////////////// @@ -51,7 +55,7 @@ public partial class AppStateModel : ObservableObject var loaded = JsonSerializer.Deserialize(jsonContent, JsonSerializationContext.Default.AppStateModel); - Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse"); + Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse"); return loaded ?? new(); } @@ -86,7 +90,7 @@ public partial class AppStateModel : ObservableObject { foreach (var item in newSettings) { - savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null; + savedSettings[item.Key] = item.Value?.DeepClone(); } var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options); @@ -121,20 +125,4 @@ public partial class AppStateModel : ObservableObject // now, the settings is just next to the exe return Path.Combine(directory, "state.json"); } - - // [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] - // private static readonly JsonSerializerOptions _serializerOptions = new() - // { - // WriteIndented = true, - // Converters = { new JsonStringEnumConverter() }, - // }; - - // private static readonly JsonSerializerOptions _deserializerOptions = new() - // { - // PropertyNameCaseInsensitive = true, - // IncludeFields = true, - // AllowTrailingCommas = true, - // PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate, - // ReadCommentHandling = JsonCommentHandling.Skip, - // }; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs index 852babe4b7..59903d7ed8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandProviderWrapper.cs @@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.UI.ViewModels; public sealed class CommandProviderWrapper { - public bool IsExtension => Extension != null; + public bool IsExtension => Extension is not null; private readonly bool isValid; @@ -188,14 +188,14 @@ public sealed class CommandProviderWrapper return topLevelViewModel; }; - if (commands != null) + if (commands is not null) { TopLevelItems = commands .Select(c => makeAndAdd(c, false)) .ToArray(); } - if (fallbacks != null) + if (fallbacks is not null) { FallbackItems = fallbacks .Select(c => makeAndAdd(c, true)) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs index a23cb4621e..2c2eafc44c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandSettingsViewModel.cs @@ -3,11 +3,11 @@ // See the LICENSE file in the project root for more information. using ManagedCommon; +using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Models; -using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CommandPalette.Extensions; -namespace Microsoft.CmdPal.Core.ViewModels; +namespace Microsoft.CmdPal.UI.ViewModels; public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread) { @@ -18,20 +18,20 @@ public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, public bool Initialized { get; private set; } public bool HasSettings => - _model.Unsafe != null && // We have a settings model AND - (!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page + _model.Unsafe is not null && // We have a settings model AND + (!Initialized || SettingsPage is not null); // we weren't initialized, OR we were, and we do have a settings page private void UnsafeInitializeProperties() { var model = _model.Unsafe; - if (model == null) + if (model is null) { return; } - if (model.SettingsPage is IContentPage page) + if (model.SettingsPage is not null) { - SettingsPage = new(page, mainThread, provider.ExtensionHost); + SettingsPage = new CommandPaletteContentPageViewModel(model.SettingsPage, mainThread, provider.ExtensionHost); SettingsPage.InitializeProperties(); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs index 44bcb49cb3..4ab993d84a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs @@ -30,7 +30,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase public override ICommandResult SubmitForm(string inputs, string data) { var dataInput = JsonNode.Parse(data)?.AsObject(); - if (dataInput == null) + if (dataInput is null) { return CommandResult.KeepOpen(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs index acb04889fb..90dea58e5c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/LogMessagesPage.cs @@ -23,7 +23,7 @@ public partial class LogMessagesPage : ListPage private void LogMessages_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { - if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null) + if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems is not null) { foreach (var item in e.NewItems) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs index 5be7c872c3..781371a866 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/MainListPage.cs @@ -6,8 +6,10 @@ using System.Collections.Immutable; using System.Collections.Specialized; using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; +using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Ext.Apps; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; @@ -29,9 +31,13 @@ public partial class MainListPage : DynamicListPage, private bool _includeApps; private bool _filteredItemsIncludesApps; + private InterlockedBoolean _refreshRunning; + private InterlockedBoolean _refreshRequested; + public MainListPage(IServiceProvider serviceProvider) { Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png"); + PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder; _serviceProvider = serviceProvider; _tlcManager = _serviceProvider.GetService()!; @@ -55,6 +61,7 @@ public partial class MainListPage : DynamicListPage, var settings = _serviceProvider.GetService()!; settings.SettingsChanged += SettingsChangedHandler; HotReloadSettings(settings); + _includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId); IsLoading = true; } @@ -82,18 +89,47 @@ public partial class MainListPage : DynamicListPage, private void ReapplySearchInBackground() { - _ = Task.Run(() => + _refreshRequested.Set(); + if (!_refreshRunning.Set()) { - try + return; + } + + _ = Task.Run(RunRefreshLoop); + } + + private void RunRefreshLoop() + { + try + { + do { + _refreshRequested.Clear(); + lock (_tlcManager.TopLevelCommands) + { + if (_filteredItemsIncludesApps == _includeApps) + { + break; + } + } + var currentSearchText = SearchText; UpdateSearchText(currentSearchText, currentSearchText); } - catch (Exception e) + while (_refreshRequested.Value); + } + catch (Exception e) + { + Logger.LogError("Failed to reload search", e); + } + finally + { + _refreshRunning.Clear(); + if (_refreshRequested.Value && _refreshRunning.Set()) { - Logger.LogError("Failed to reload search", e); + _ = Task.Run(RunRefreshLoop); } - }); + } } public override IListItem[] GetItems() @@ -125,6 +161,15 @@ public partial class MainListPage : DynamicListPage, var aliases = _serviceProvider.GetService()!; if (aliases.CheckAlias(newSearch)) { + if (_filteredItemsIncludesApps != _includeApps) + { + lock (_tlcManager.TopLevelCommands) + { + _filteredItemsIncludesApps = _includeApps; + _filteredItems = null; + } + } + return; } } @@ -137,6 +182,7 @@ public partial class MainListPage : DynamicListPage, // Cleared out the filter text? easy. Reset _filteredItems, and bail out. if (string.IsNullOrEmpty(newSearch)) { + _filteredItemsIncludesApps = _includeApps; _filteredItems = null; RaiseItemsChanged(commands.Count); return; @@ -157,13 +203,18 @@ public partial class MainListPage : DynamicListPage, // If we don't have any previous filter results to work with, start // with a list of all our commands & apps. - if (_filteredItems == null) + if (_filteredItems is null) { _filteredItems = commands; _filteredItemsIncludesApps = _includeApps; if (_includeApps) { IEnumerable apps = AllAppsCommandProvider.Page.GetItems(); + var appIds = apps.Select(app => app.Command.Id).ToArray(); + + // Remove any top level pinned apps and use the apps from AllAppsCommandProvider.Page.GetItems() + // since they contain details. + _filteredItems = _filteredItems.Where(item => item.Command is not AppCommand); _filteredItems = _filteredItems.Concat(apps); } } @@ -263,7 +314,7 @@ public partial class MainListPage : DynamicListPage, { nameMatch, descriptionMatch, - isFallback ? 1 : 0, // Always give fallbacks a chance... + isFallback ? 1 : 0, // Always give fallbacks a chance }; var max = scores.Max(); @@ -273,8 +324,7 @@ public partial class MainListPage : DynamicListPage, // above "git" from "whatever" max = max + extensionTitleMatch; - // ... but downweight them - var matchSomething = (max / (isFallback ? 3 : 1)) + var matchSomething = max + (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0)); // If we matched title, subtitle, or alias (something real), then diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs index c1f3f64612..62301714e9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs @@ -98,7 +98,7 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase public override CommandResult SubmitForm(string payload) { var formInput = JsonNode.Parse(payload)?.AsObject(); - if (formInput == null) + if (formInput is null) { return CommandResult.KeepOpen(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs index 8cfa9658d4..6bdb8a7330 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionPage.cs @@ -14,7 +14,7 @@ public partial class NewExtensionPage : ContentPage public override IContent[] GetContent() { - return _resultForm != null ? [_resultForm] : [_inputForm]; + return _resultForm is not null ? [_resultForm] : [_inputForm]; } public NewExtensionPage() @@ -28,13 +28,13 @@ public partial class NewExtensionPage : ContentPage private void FormSubmitted(NewExtensionFormBase sender, NewExtensionFormBase? args) { - if (_resultForm != null) + if (_resultForm is not null) { _resultForm.FormSubmitted -= FormSubmitted; } _resultForm = args; - if (_resultForm != null) + if (_resultForm is not null) { _resultForm.FormSubmitted += FormSubmitted; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs index a5af351fd4..ac7fe624e5 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/OpenSettingsCommand.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.Messaging; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs index 313685f6f2..bd3cee3159 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/QuitAction.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.Messaging; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs index 77efb05a73..88024efe2f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/ReloadExtensionsCommand.cs @@ -4,6 +4,7 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs index d561a0e00f..9b2234fb16 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.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.Diagnostics.CodeAnalysis; using System.Text.Json; using AdaptiveCards.ObjectModel.WinUI3; using AdaptiveCards.Templating; @@ -96,6 +97,9 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference item.Hotkey == null); + _commandHotkeys.RemoveAll(item => item.Hotkey is null); foreach (var item in _commandHotkeys) { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/OpenSettingsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs similarity index 80% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/OpenSettingsMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs index e4b02b5c0c..c699ab427a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/OpenSettingsMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/OpenSettingsMessage.cs @@ -2,7 +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. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.Messages; public record OpenSettingsMessage() { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/QuitMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs similarity index 87% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/QuitMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs index 12b9cec827..ae65782336 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/QuitMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/QuitMessage.cs @@ -2,7 +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. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.ViewModels.Messages; /// /// Message which closes the application. Used by via . diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/ReloadCommandsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs similarity index 81% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/ReloadCommandsMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs index a553568f50..cba0fa3f56 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/ReloadCommandsMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/ReloadCommandsMessage.cs @@ -2,7 +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. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.ViewModels.Messages; public record ReloadCommandsMessage() { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateFallbackItemsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs similarity index 81% rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateFallbackItemsMessage.cs rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs index 8a913f7a3f..08e65c2213 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/Messages/UpdateFallbackItemsMessage.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateFallbackItemsMessage.cs @@ -2,7 +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. -namespace Microsoft.CmdPal.Core.ViewModels.Messages; +namespace Microsoft.CmdPal.UI.ViewModels.Messages; public record UpdateFallbackItemsMessage() { diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs index 6d59aa66b4..f0a14ab7db 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.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 ManagedCommon; using Microsoft.CmdPal.Common.Services; using Microsoft.CommandPalette.Extensions; using Windows.ApplicationModel; @@ -89,7 +90,7 @@ public partial class ExtensionService : IExtensionService, IDisposable }).Result; var isExtension = isCmdPalExtensionResult.IsExtension; var extension = isCmdPalExtensionResult.Extension; - if (isExtension && extension != null) + if (isExtension && extension is not null) { CommandPaletteHost.Instance.DebugLog($"Installed new extension app {extension.DisplayName}"); @@ -151,7 +152,7 @@ public partial class ExtensionService : IExtensionService, IDisposable { var (cmdPalProvider, classId) = await GetCmdPalExtensionPropertiesAsync(extension); - return new(cmdPalProvider != null && classId.Count != 0, extension); + return new(cmdPalProvider is not null && classId.Count != 0, extension); } } @@ -236,7 +237,7 @@ public partial class ExtensionService : IExtensionService, IDisposable { var (cmdPalProvider, classIds) = await GetCmdPalExtensionPropertiesAsync(extension); - if (cmdPalProvider == null || classIds.Count == 0) + if (cmdPalProvider is null || classIds.Count == 0) { return []; } @@ -287,9 +288,17 @@ public partial class ExtensionService : IExtensionService, IDisposable var installedExtensions = await GetInstalledExtensionsAsync(); foreach (var installedExtension in installedExtensions) { - if (installedExtension.IsRunning()) + Logger.LogDebug($"Signaling dispose to {installedExtension.ExtensionUniqueId}"); + try { - installedExtension.SignalDispose(); + if (installedExtension.IsRunning()) + { + installedExtension.SignalDispose(); + } + } + catch (Exception ex) + { + Logger.LogError($"Failed to send dispose signal to extension {installedExtension.ExtensionUniqueId}", ex); } } } @@ -343,12 +352,12 @@ public partial class ExtensionService : IExtensionService, IDisposable { var propSetList = new List(); var singlePropertySet = GetSubPropertySet(activationPropSet, CreateInstanceProperty); - if (singlePropertySet != null) + if (singlePropertySet is not null) { var classId = GetProperty(singlePropertySet, ClassIdProperty); // If the instance has a classId as a single string, then it's only supporting a single instance. - if (classId != null) + if (classId is not null) { propSetList.Add(classId); } @@ -356,7 +365,7 @@ public partial class ExtensionService : IExtensionService, IDisposable else { var propertySetArray = GetSubPropertySetArray(activationPropSet, CreateInstanceProperty); - if (propertySetArray != null) + if (propertySetArray is not null) { foreach (var prop in propertySetArray) { @@ -366,7 +375,7 @@ public partial class ExtensionService : IExtensionService, IDisposable } var classId = GetProperty(propertySet, ClassIdProperty); - if (classId != null) + if (classId is not null) { propSetList.Add(classId); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs index 4c1339ae7d..1deb9cdf74 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.Designer.cs @@ -321,6 +321,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties { } } + /// + /// Looks up a localized string similar to Search for apps, files and commands.... + /// + public static string builtin_main_list_page_searchbar_placeholder { + get { + return ResourceManager.GetString("builtin_main_list_page_searchbar_placeholder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Creates a project for a new Command Palette extension. /// diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx index ce4061a58a..0d341e3981 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Properties/Resources.resx @@ -227,4 +227,7 @@ Disabled + + Search for apps, files and commands... + \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs index ec33bf4216..1e20040d57 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettings.cs @@ -35,7 +35,7 @@ public class ProviderSettings public void Connect(CommandProviderWrapper wrapper) { ProviderId = wrapper.ProviderId; - IsBuiltin = wrapper.Extension == null; + IsBuiltin = wrapper.Extension is null; ProviderDisplayName = wrapper.DisplayName; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs index 3c8e402364..714b3ca805 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ProviderSettingsViewModel.cs @@ -7,7 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Core.ViewModels; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Properties; using Microsoft.Extensions.DependencyInjection; @@ -33,7 +33,7 @@ public partial class ProviderSettingsViewModel( Resources.builtin_disabled_extension; [MemberNotNullWhen(true, nameof(Extension))] - public bool IsFromExtension => _provider.Extension != null; + public bool IsFromExtension => _provider.Extension is not null; public IExtensionWrapper? Extension => _provider.Extension; @@ -76,7 +76,7 @@ public partial class ProviderSettingsViewModel( { get { - if (_provider.Settings == null) + if (_provider.Settings is null) { return false; } @@ -100,7 +100,7 @@ public partial class ProviderSettingsViewModel( { get { - if (_provider.Settings == null) + if (_provider.Settings is null) { return null; } @@ -126,7 +126,7 @@ public partial class ProviderSettingsViewModel( { get { - if (field == null) + if (field is null) { field = BuildTopLevelViewModels(); } @@ -149,7 +149,7 @@ public partial class ProviderSettingsViewModel( { get { - if (field == null) + if (field is null) { field = BuildFallbackViewModels(); } @@ -173,7 +173,7 @@ public partial class ProviderSettingsViewModel( private void InitializeSettingsPage() { - if (_provider.Settings == null) + if (_provider.Settings is null) { return; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs index 8551b5f964..9135c9588a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs @@ -30,7 +30,7 @@ public partial class RecentCommandsManager : ObservableObject // These numbers are vaguely scaled so that "VS" will make "Visual Studio" the // match after one use. // Usually it has a weight of 84, compared to 109 for the VS cmd prompt - if (entry.Item != null) + if (entry.Item is not null) { var index = entry.Index; @@ -61,7 +61,7 @@ public partial class RecentCommandsManager : ObservableObject var entry = History .Where(item => item.CommandId == commandId) .FirstOrDefault(); - if (entry == null) + if (entry is null) { var newitem = new HistoryItem() { CommandId = commandId, Uses = 1 }; History.Insert(0, newitem); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs index 587a8e4f62..b0d10a5285 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs @@ -95,7 +95,7 @@ public partial class SettingsModel : ObservableObject var loaded = JsonSerializer.Deserialize(jsonContent, JsonSerializationContext.Default.SettingsModel); - Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse"); + Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse"); return loaded ?? new(); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index b4f6542c93..f55a322792 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -9,9 +9,10 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; +using Microsoft.CmdPal.Common.Helpers; using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.Core.ViewModels; -using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Extensions.DependencyInjection; @@ -20,7 +21,8 @@ namespace Microsoft.CmdPal.UI.ViewModels; public partial class TopLevelCommandManager : ObservableObject, IRecipient, - IPageContext + IPageContext, + IDisposable { private readonly IServiceProvider _serviceProvider; private readonly TaskScheduler _taskScheduler; @@ -28,6 +30,7 @@ public partial class TopLevelCommandManager : ObservableObject, private readonly List _builtInCommands = []; private readonly List _extensionCommandProviders = []; private readonly Lock _commandProvidersLock = new(); + private readonly SupersedingAsyncGate _reloadCommandsGate; TaskScheduler IPageContext.Scheduler => _taskScheduler; @@ -36,6 +39,7 @@ public partial class TopLevelCommandManager : ObservableObject, _serviceProvider = serviceProvider; _taskScheduler = _serviceProvider.GetService()!; WeakReferenceMessenger.Default.Register(this); + _reloadCommandsGate = new(ReloadAllCommandsAsyncCore); } public ObservableCollection TopLevelCommands { get; set; } = []; @@ -144,46 +148,10 @@ public partial class TopLevelCommandManager : ObservableObject, /// an awaitable task private async Task UpdateCommandsForProvider(CommandProviderWrapper sender, IItemsChangedEventArgs args) { - // Work on a clone of the list, so that we can just do one atomic - // update to the actual observable list at the end - List clone = [.. TopLevelCommands]; - List newItems = []; - var startIndex = -1; - var firstCommand = sender.TopLevelItems[0]; - var commandsToRemove = sender.TopLevelItems.Length + sender.FallbackItems.Length; - - // Tricky: all Commands from a single provider get added to the - // top-level list all together, in a row. So if we find just the first - // one, we can slice it out and insert the new ones there. - for (var i = 0; i < clone.Count; i++) - { - var wrapper = clone[i]; - try - { - var isTheSame = wrapper == firstCommand; - if (isTheSame) - { - startIndex = i; - break; - } - } - catch - { - } - } - WeakReference weakSelf = new(this); - - // Fetch the new items await sender.LoadTopLevelCommands(_serviceProvider, weakSelf); - var settings = _serviceProvider.GetService()!; - - foreach (var i in sender.TopLevelItems) - { - newItems.Add(i); - } - + List newItems = [.. sender.TopLevelItems]; foreach (var i in sender.FallbackItems) { if (i.IsEnabled) @@ -192,25 +160,60 @@ public partial class TopLevelCommandManager : ObservableObject, } } - // Slice out the old commands - if (startIndex != -1) + // modify the TopLevelCommands under shared lock; event if we clone it, we don't want + // TopLevelCommands to get modified while we're working on it. Otherwise, we might + // out clone would be stale at the end of this method. + lock (TopLevelCommands) { - clone.RemoveRange(startIndex, commandsToRemove); - } - else - { - // ... or, just stick them at the end (this is unexpected) - startIndex = clone.Count; + // Work on a clone of the list, so that we can just do one atomic + // update to the actual observable list at the end + // TODO: just added a lock around all of this anyway, but keeping the clone + // while looking on some other ways to improve this; can be removed later. + List clone = [.. TopLevelCommands]; + + var startIndex = FindIndexForFirstProviderItem(clone, sender.ProviderId); + clone.RemoveAll(item => item.CommandProviderId == sender.ProviderId); + clone.InsertRange(startIndex, newItems); + + ListHelpers.InPlaceUpdateList(TopLevelCommands, clone); } - // add the new commands into the list at the place we found the old ones - clone.InsertRange(startIndex, newItems); + return; - // now update the actual observable list with the new contents - ListHelpers.InPlaceUpdateList(TopLevelCommands, clone); + static int FindIndexForFirstProviderItem(List topLevelItems, string providerId) + { + // Tricky: all Commands from a single provider get added to the + // top-level list all together, in a row. So if we find just the first + // one, we can slice it out and insert the new ones there. + for (var i = 0; i < topLevelItems.Count; i++) + { + var wrapper = topLevelItems[i]; + try + { + if (providerId == wrapper.CommandProviderId) + { + return i; + } + } + catch + { + } + } + + // If we didn't find any, then we just append the new commands to the end of the list. + return topLevelItems.Count; + } } public async Task ReloadAllCommandsAsync() + { + // gate ensures that the reload is serialized and if multiple calls + // request a reload, only the first and the last one will be executed. + // this should be superseded with a cancellable version. + await _reloadCommandsGate.ExecuteAsync(CancellationToken.None); + } + + private async Task ReloadAllCommandsAsyncCore(CancellationToken cancellationToken) { IsLoading = true; var extensionService = _serviceProvider.GetService()!; @@ -246,7 +249,7 @@ public partial class TopLevelCommandManager : ObservableObject, _extensionCommandProviders.Clear(); } - if (extensions != null) + if (extensions is not null) { await StartExtensionsAndGetCommands(extensions); } @@ -280,7 +283,7 @@ public partial class TopLevelCommandManager : ObservableObject, var startTasks = extensions.Select(StartExtensionWithTimeoutAsync); // Wait for all extensions to start - var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper != null).Select(w => w!).ToList(); + var wrappers = (await Task.WhenAll(startTasks)).Where(wrapper => wrapper is not null).Select(w => w!).ToList(); lock (_commandProvidersLock) { @@ -290,7 +293,7 @@ public partial class TopLevelCommandManager : ObservableObject, // Load the commands from the providers in parallel var loadTasks = wrappers.Select(LoadCommandsWithTimeoutAsync); - var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results != null).Select(r => r!).ToList(); + var commandSets = (await Task.WhenAll(loadTasks)).Where(results => results is not null).Select(r => r!).ToList(); lock (TopLevelCommands) { @@ -407,8 +410,8 @@ public partial class TopLevelCommandManager : ObservableObject, void IPageContext.ShowException(Exception ex, string? extensionHint) { - var errorMessage = $"A bug occurred in {$"the \"{extensionHint}\"" ?? "an unknown's"} extension's code:\n{ex.Message}\n{ex.Source}\n{ex.StackTrace}\n\n"; - CommandPaletteHost.Instance.Log(errorMessage); + var message = DiagnosticsHelper.BuildExceptionMessage(ex, extensionHint ?? "TopLevelCommandManager"); + CommandPaletteHost.Instance.Log(message); } internal bool IsProviderActive(string id) @@ -419,4 +422,10 @@ public partial class TopLevelCommandManager : ObservableObject, || _extensionCommandProviders.Any(wrapper => wrapper.Id == id && wrapper.IsActive); } } + + public void Dispose() + { + _reloadCommandsGate.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs index dabfdca3f6..e73f5b09ba 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelViewModel.cs @@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.Messaging; using ManagedCommon; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.ViewModels.Messages; using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -47,6 +48,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem public CommandItemViewModel ItemViewModel => _commandItemViewModel; + public string CommandProviderId => _commandProviderId; + ////// ICommandItem public string Title => _commandItemViewModel.Title; @@ -63,9 +66,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem { return item as IContextItem; } + else if (item is CommandContextItemViewModel commandItem) + { + return commandItem.Model.Unsafe; + } else { - return ((CommandContextItemViewModel)item).Model.Unsafe; + return null; } }).ToArray(); @@ -233,7 +240,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem private void FetchAliasFromAliasManager() { var am = _serviceProvider.GetService(); - if (am != null) + if (am is not null) { var commandAlias = am.AliasFromId(Id); if (commandAlias is not null) @@ -247,7 +254,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem private void UpdateHotkey() { var hotkey = _settings.CommandHotkeys.Where(hk => hk.CommandId == Id).FirstOrDefault(); - if (hotkey != null) + if (hotkey is not null) { _hotkey = hotkey.Hotkey; } @@ -257,12 +264,12 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem { List tags = []; - if (Hotkey != null) + if (Hotkey is not null) { tags.Add(new Tag() { Text = Hotkey.ToString() }); } - if (Alias != null) + if (Alias is not null) { tags.Add(new Tag() { Text = Alias.SearchPrefix }); } @@ -347,4 +354,9 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem { return new PerformCommandMessage(this.CommandViewModel.Model, new Core.ViewModels.Models.ExtensionObject(this)); } + + public override string ToString() + { + return $"{nameof(TopLevelViewModel)}: {Id} ({Title}) - display: {DisplayTitle} - fallback: {IsFallback} - enabled: {IsEnabled}"; + } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 36c742ba7f..ec3604621d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -64,6 +64,9 @@ public partial class App : Application this.InitializeComponent(); + // Ensure types used in XAML are preserved for AOT compilation + TypePreservation.PreserveTypes(); + NativeEventWaiter.WaitForEventLoop( "Local\\PowerToysCmdPal-ExitEvent-eb73f6be-3f22-4b36-aee3-62924ba40bfd", () => { @@ -98,10 +101,13 @@ public partial class App : Application // Built-in Commands. Order matters - this is the order they'll be presented by default. var allApps = new AllAppsCommandProvider(); + var files = new IndexerCommandsProvider(); + files.SuppressFallbackWhen(ShellCommandsProvider.SuppressFileFallbackIf); services.AddSingleton(allApps); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(files); services.AddSingleton(); services.AddSingleton(); @@ -144,6 +150,7 @@ public partial class App : Application services.AddSingleton(state); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml index 0edcf04a9c..aa14b0878a 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml @@ -41,13 +41,18 @@ + + + + Opened="ContextMenuFlyout_Opened" + ShouldConstrainToRootBounds="False" + SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}"> @@ -59,8 +64,24 @@ + + + + + @@ -76,44 +97,44 @@ - - - - - - - - - - - - - - + @@ -169,8 +185,8 @@ Padding="6,4,4,4" x:Load="{x:Bind IsLoaded, Mode=OneWay}" AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}" + Click="SecondaryButton_Clicked" Style="{StaticResource SubtleButtonStyle}" - Tapped="SecondaryButton_Tapped" Visibility="{x:Bind ViewModel.HasSecondaryCommand, Mode=OneWay}"> - + - + @@ -199,13 +206,26 @@ diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs index 208024fdcc..f4a0dc3d43 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs @@ -5,11 +5,11 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.CmdPal.Core.ViewModels; using Microsoft.CmdPal.Core.ViewModels.Messages; +using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.Views; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; -using Microsoft.UI.Xaml.Input; using Windows.System; namespace Microsoft.CmdPal.UI.Controls; @@ -49,7 +49,7 @@ public sealed partial class CommandBar : UserControl, return; } - if (message.Element == null) + if (message.Element is null) { _ = DispatcherQueue.TryEnqueue( () => @@ -113,34 +113,23 @@ public sealed partial class CommandBar : UserControl, } [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")] - private void PrimaryButton_Tapped(object sender, TappedRoutedEventArgs e) + private void PrimaryButton_Clicked(object sender, RoutedEventArgs e) { ViewModel.InvokePrimaryCommand(); } [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")] - private void SecondaryButton_Tapped(object sender, TappedRoutedEventArgs e) + private void SecondaryButton_Clicked(object sender, RoutedEventArgs e) { ViewModel.InvokeSecondaryCommand(); } - private void PageIcon_Tapped(object sender, TappedRoutedEventArgs e) - { - if (CurrentPageViewModel?.StatusMessages.Count > 0) - { - StatusMessagesFlyout.ShowAt( - placementTarget: IconRoot, - showOptions: new FlyoutShowOptions() { ShowMode = FlyoutShowMode.Standard }); - } - } - - private void SettingsIcon_Tapped(object sender, TappedRoutedEventArgs e) + private void SettingsIcon_Clicked(object sender, RoutedEventArgs e) { WeakReferenceMessenger.Default.Send(); - e.Handled = true; } - private void MoreCommandsButton_Tapped(object sender, TappedRoutedEventArgs e) + private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e) { WeakReferenceMessenger.Default.Send(new OpenContextMenuMessage(null, null, null, ContextMenuFilterLocation.Bottom)); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs index 78805f00b2..3301326883 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentFormControl.xaml.cs @@ -44,7 +44,7 @@ public sealed partial class ContentFormControl : UserControl // 5% BODGY: if we set this multiple times over the lifetime of the app, // then the second call will explode, because "CardOverrideStyles is already the child of another element". // SO only set this once. - if (_renderer.OverrideStyles == null) + if (_renderer.OverrideStyles is null) { _renderer.OverrideStyles = CardOverrideStyles; } @@ -55,19 +55,19 @@ public sealed partial class ContentFormControl : UserControl private void AttachViewModel(ContentFormViewModel? vm) { - if (_viewModel != null) + if (_viewModel is not null) { _viewModel.PropertyChanged -= ViewModel_PropertyChanged; } _viewModel = vm; - if (_viewModel != null) + if (_viewModel is not null) { _viewModel.PropertyChanged += ViewModel_PropertyChanged; var c = _viewModel.Card; - if (c != null) + if (c is not null) { DisplayCard(c); } @@ -76,7 +76,7 @@ public sealed partial class ContentFormControl : UserControl private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { - if (ViewModel == null) + if (ViewModel is null) { return; } @@ -84,7 +84,7 @@ public sealed partial class ContentFormControl : UserControl if (e.PropertyName == nameof(ViewModel.Card)) { var c = ViewModel.Card; - if (c != null) + if (c is not null) { DisplayCard(c); } @@ -95,7 +95,7 @@ public sealed partial class ContentFormControl : UserControl { _renderedCard = _renderer.RenderAdaptiveCard(result.AdaptiveCard); ContentGrid.Children.Clear(); - if (_renderedCard.FrameworkElement != null) + if (_renderedCard.FrameworkElement is not null) { ContentGrid.Children.Add(_renderedCard.FrameworkElement); @@ -148,7 +148,7 @@ public sealed partial class ContentFormControl : UserControl // Recursively check children var result = FindFirstFocusableElement(child); - if (result != null) + if (result is not null) { return result; } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.cs index 1e12b12ebd..211d28b410 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContentIcon.cs @@ -2,9 +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.Runtime.InteropServices; +using System.Diagnostics; using CommunityToolkit.WinUI; -using ManagedCommon; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -37,14 +36,18 @@ public partial class ContentIcon : FontIcon { if (this.FindDescendants().OfType().FirstOrDefault() is Grid grid && Content is not null) { - try + if (grid.Children.Contains(Content)) { - grid.Children.Add(Content); + return; } - catch (COMException ex) + + if (Content is FrameworkElement element && element.Parent is not null) { - Logger.LogError(ex.ToString()); + Debug.Assert(false, $"IconBoxElement Content is already parented to {element.Parent.GetType().Name}"); + return; } + + grid.Children.Add(Content); } } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml index 27ac608240..f3c4e5413e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml @@ -112,7 +112,7 @@ + Fill="{ThemeResource MenuFlyoutSeparatorBackground}" /> @@ -131,7 +131,7 @@ ItemClick="CommandsDropdown_ItemClick" ItemTemplateSelector="{StaticResource ContextItemTemplateSelector}" ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}" - KeyDown="CommandsDropdown_KeyDown" + PreviewKeyDown="CommandsDropdown_PreviewKeyDown" SelectionMode="Single"> + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml.cs new file mode 100644 index 0000000000..43ba496712 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyCharPresenter.xaml.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 System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Documents; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; + +namespace Microsoft.PowerToys.Settings.UI.Controls; + +public sealed partial class KeyCharPresenter : Control +{ + public KeyCharPresenter() + { + DefaultStyleKey = typeof(KeyCharPresenter); + } + + public object Content + { + get => (object)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyCharPresenter), new PropertyMetadata(default(string))); +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs deleted file mode 100644 index 9d323c636d..0000000000 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Markup; -using Windows.System; - -namespace Microsoft.PowerToys.Settings.UI.Controls -{ - [TemplatePart(Name = KeyPresenter, Type = typeof(ContentPresenter))] - [TemplateVisualState(Name = "Normal", GroupName = "CommonStates")] - [TemplateVisualState(Name = "Disabled", GroupName = "CommonStates")] - [TemplateVisualState(Name = "Default", GroupName = "StateStates")] - [TemplateVisualState(Name = "Error", GroupName = "StateStates")] - public sealed partial class KeyVisual : Control - { - private const string KeyPresenter = "KeyPresenter"; - private KeyVisual _keyVisual; - private ContentPresenter _keyPresenter; - - public object Content - { - get => (object)GetValue(ContentProperty); - set => SetValue(ContentProperty, value); - } - - public static readonly DependencyProperty ContentProperty = DependencyProperty.Register("Content", typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged)); - - public VisualType VisualType - { - get => (VisualType)GetValue(VisualTypeProperty); - set => SetValue(VisualTypeProperty, value); - } - - public static readonly DependencyProperty VisualTypeProperty = DependencyProperty.Register("VisualType", typeof(VisualType), typeof(KeyVisual), new PropertyMetadata(default(VisualType), OnSizeChanged)); - - public bool IsError - { - get => (bool)GetValue(IsErrorProperty); - set => SetValue(IsErrorProperty, value); - } - - public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsErrorChanged)); - - public KeyVisual() - { - this.DefaultStyleKey = typeof(KeyVisual); - this.Style = GetStyleSize("TextKeyVisualStyle"); - } - - protected override void OnApplyTemplate() - { - IsEnabledChanged -= KeyVisual_IsEnabledChanged; - _keyVisual = (KeyVisual)this; - _keyPresenter = (ContentPresenter)_keyVisual.GetTemplateChild(KeyPresenter); - Update(); - SetEnabledState(); - SetErrorState(); - IsEnabledChanged += KeyVisual_IsEnabledChanged; - base.OnApplyTemplate(); - } - - private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((KeyVisual)d).Update(); - } - - private static void OnSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((KeyVisual)d).Update(); - } - - private static void OnIsErrorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - ((KeyVisual)d).SetErrorState(); - } - - private void Update() - { - if (_keyVisual == null) - { - return; - } - - if (_keyVisual.Content != null) - { - if (_keyVisual.Content.GetType() == typeof(string)) - { - _keyVisual.Style = GetStyleSize("TextKeyVisualStyle"); - _keyVisual._keyPresenter.Content = _keyVisual.Content; - } - else - { - _keyVisual.Style = GetStyleSize("IconKeyVisualStyle"); - - switch ((int)_keyVisual.Content) - { - /* We can enable other glyphs in the future - case 13: // The Enter key or button. - _keyVisual._keyPresenter.Content = "\uE751"; break; - - case 8: // The Back key or button. - _keyVisual._keyPresenter.Content = "\uE750"; break; - - case 16: // The right Shift key or button. - case 160: // The left Shift key or button. - case 161: // The Shift key or button. - _keyVisual._keyPresenter.Content = "\uE752"; break; */ - - case 38: _keyVisual._keyPresenter.Content = "\uE0E4"; break; // The Up Arrow key or button. - case 40: _keyVisual._keyPresenter.Content = "\uE0E5"; break; // The Down Arrow key or button. - case 37: _keyVisual._keyPresenter.Content = "\uE0E2"; break; // The Left Arrow key or button. - case 39: _keyVisual._keyPresenter.Content = "\uE0E3"; break; // The Right Arrow key or button. - - case 91: // The left Windows key - case 92: // The right Windows key - PathIcon winIcon = XamlReader.Load(@"") as PathIcon; - Viewbox winIconContainer = new Viewbox(); - winIconContainer.Child = winIcon; - winIconContainer.HorizontalAlignment = HorizontalAlignment.Center; - winIconContainer.VerticalAlignment = VerticalAlignment.Center; - - double iconDimensions = GetIconSize(); - winIconContainer.Height = iconDimensions; - winIconContainer.Width = iconDimensions; - _keyVisual._keyPresenter.Content = winIconContainer; - break; - default: _keyVisual._keyPresenter.Content = ((VirtualKey)_keyVisual.Content).ToString(); break; - } - } - } - } - - public Style GetStyleSize(string styleName) - { - if (VisualType == VisualType.Small) - { - return (Style)App.Current.Resources["Small" + styleName]; - } - else if (VisualType == VisualType.SmallOutline) - { - return (Style)App.Current.Resources["SmallOutline" + styleName]; - } - else if (VisualType == VisualType.TextOnly) - { - return (Style)App.Current.Resources["Only" + styleName]; - } - else - { - return (Style)App.Current.Resources["Default" + styleName]; - } - } - - public double GetIconSize() - { - if (VisualType == VisualType.Small || VisualType == VisualType.SmallOutline) - { - return (double)App.Current.Resources["SmallIconSize"]; - } - else - { - return (double)App.Current.Resources["DefaultIconSize"]; - } - } - - private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) - { - SetEnabledState(); - } - - private void SetErrorState() - { - VisualStateManager.GoToState(this, IsError ? "Error" : "Default", true); - } - - private void SetEnabledState() - { - VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true); - } - } - - public enum VisualType - { - Small, - SmallOutline, - TextOnly, - Large, - } -} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml index 00192a215a..931286ceaf 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml @@ -1,66 +1,70 @@  + xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls"> - 16 - 12 - + x:Key="AccentKeyVisualStyle" + BasedOn="{StaticResource DefaultKeyVisualStyle}" + TargetType="local:KeyVisual"> - - - - - - - - - - - \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs new file mode 100644 index 0000000000..b638c32f2b --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml.cs @@ -0,0 +1,180 @@ +// 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 Windows.System; + +namespace Microsoft.PowerToys.Settings.UI.Controls +{ + [TemplatePart(Name = KeyPresenter, Type = typeof(KeyCharPresenter))] + [TemplateVisualState(Name = NormalState, GroupName = "CommonStates")] + [TemplateVisualState(Name = DisabledState, GroupName = "CommonStates")] + [TemplateVisualState(Name = InvalidState, GroupName = "CommonStates")] + public sealed partial class KeyVisual : Control + { + private const string KeyPresenter = "KeyPresenter"; + private const string NormalState = "Normal"; + private const string DisabledState = "Disabled"; + private const string InvalidState = "Invalid"; + private KeyCharPresenter _keyPresenter; + + public object Content + { + get => (object)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(KeyVisual), new PropertyMetadata(default(string), OnContentChanged)); + + public bool IsInvalid + { + get => (bool)GetValue(IsInvalidProperty); + set => SetValue(IsInvalidProperty, value); + } + + public static readonly DependencyProperty IsInvalidProperty = DependencyProperty.Register(nameof(IsInvalid), typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnIsInvalidChanged)); + + public bool RenderKeyAsGlyph + { + get => (bool)GetValue(RenderKeyAsGlyphProperty); + set => SetValue(RenderKeyAsGlyphProperty, value); + } + + public static readonly DependencyProperty RenderKeyAsGlyphProperty = DependencyProperty.Register(nameof(RenderKeyAsGlyph), typeof(bool), typeof(KeyVisual), new PropertyMetadata(false, OnContentChanged)); + + public KeyVisual() + { + this.DefaultStyleKey = typeof(KeyVisual); + } + + protected override void OnApplyTemplate() + { + IsEnabledChanged -= KeyVisual_IsEnabledChanged; + _keyPresenter = (KeyCharPresenter)this.GetTemplateChild(KeyPresenter); + Update(); + SetVisualStates(); + IsEnabledChanged += KeyVisual_IsEnabledChanged; + base.OnApplyTemplate(); + } + + private static void OnContentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((KeyVisual)d).SetVisualStates(); + } + + private static void OnIsInvalidChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((KeyVisual)d).SetVisualStates(); + } + + private void SetVisualStates() + { + if (this != null) + { + if (IsInvalid) + { + VisualStateManager.GoToState(this, InvalidState, true); + } + else if (!IsEnabled) + { + VisualStateManager.GoToState(this, DisabledState, true); + } + else + { + VisualStateManager.GoToState(this, NormalState, true); + } + } + } + + private void Update() + { + if (Content == null) + { + return; + } + + if (Content is string key) + { + switch (key) + { + case "Copilot": + _keyPresenter.Style = (Style)Application.Current.Resources["CopilotKeyCharPresenterStyle"]; + break; + + case "Office": + _keyPresenter.Style = (Style)Application.Current.Resources["OfficeKeyCharPresenterStyle"]; + break; + + default: + _keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"]; + break; + } + + return; + } + + if (Content is int keyCode) + { + VirtualKey virtualKey = (VirtualKey)keyCode; + switch (virtualKey) + { + case VirtualKey.Enter: + SetGlyphOrText("\uE751", virtualKey); + break; + + case VirtualKey.Back: + SetGlyphOrText("\uE750", virtualKey); + break; + + case VirtualKey.Shift: + case (VirtualKey)160: // Left Shift + case (VirtualKey)161: // Right Shift + SetGlyphOrText("\uE752", virtualKey); + break; + + case VirtualKey.Up: + _keyPresenter.Content = "\uE0E4"; + break; + + case VirtualKey.Down: + _keyPresenter.Content = "\uE0E5"; + break; + + case VirtualKey.Left: + _keyPresenter.Content = "\uE0E2"; + break; + + case VirtualKey.Right: + _keyPresenter.Content = "\uE0E3"; + break; + + case VirtualKey.LeftWindows: + case VirtualKey.RightWindows: + _keyPresenter.Style = (Style)Application.Current.Resources["WindowsKeyCharPresenterStyle"]; + break; + } + } + } + + private void SetGlyphOrText(string glyph, VirtualKey key) + { + if (RenderKeyAsGlyph) + { + _keyPresenter.Content = glyph; + _keyPresenter.Style = (Style)Application.Current.Resources["GlyphKeyCharPresenterStyle"]; + } + else + { + _keyPresenter.Content = key.ToString(); + _keyPresenter.Style = (Style)Application.Current.Resources["DefaultKeyCharPresenterStyle"]; + } + } + + private void KeyVisual_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e) + { + SetVisualStates(); + } + } +} diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl/OOBEPageControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl.xaml similarity index 100% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl/OOBEPageControl.xaml rename to src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl.xaml diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl/OOBEPageControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl.xaml.cs similarity index 100% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl/OOBEPageControl.xaml.cs rename to src/settings-ui/Settings.UI/SettingsXAML/Controls/OOBEPageControl.xaml.cs diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml index c115d1febe..09b2d7d26a 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/PowerAccentShortcutControl.xaml @@ -31,8 +31,7 @@ VerticalAlignment="Center" AutomationProperties.AccessibilityView="Raw" Content="{Binding}" - IsTabStop="False" - VisualType="SmallOutline" /> + IsTabStop="False" /> diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsGroup/SettingsGroup.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsGroup/SettingsGroup.xaml.cs similarity index 100% rename from src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsGroup/SettingsGroup.cs rename to src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsGroup/SettingsGroup.xaml.cs diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml index a49c93a518..118c9b7ca5 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/SettingsPageControl/SettingsPageControl.xaml @@ -6,20 +6,12 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls" - xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters" Loaded="UserControl_Loaded" mc:Ignorable="d"> - 1000 1020 - - @@ -62,7 +54,7 @@ MaxWidth="160" HorizontalAlignment="Left" VerticalAlignment="Top" - CornerRadius="4"> + CornerRadius="{StaticResource OverlayCornerRadius}"> @@ -113,7 +105,7 @@ MaxWidth="{StaticResource PageMaxWidth}" AutomationProperties.Name="{x:Bind SecondaryLinksHeader}" Orientation="Vertical" - Visibility="{x:Bind SecondaryLinks.Count, Converter={StaticResource doubleToVisibilityConverter}}"> + Visibility="{x:Bind SecondaryLinks.Count, Converter={StaticResource DoubleToVisibilityConverter}}"> - - - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs index e33127572d..5b21743d5f 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutControl.xaml.cs @@ -3,18 +3,33 @@ // See the LICENSE file in the project root for more information. using System; - +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using CommunityToolkit.WinUI; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; +using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events; +using Microsoft.PowerToys.Settings.UI.Services; +using Microsoft.PowerToys.Settings.UI.Views; +using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.Windows.ApplicationModel.Resources; using Windows.System; namespace Microsoft.PowerToys.Settings.UI.Controls { + public enum ShortcutControlSource + { + SettingsPage, + ConflictWindow, + } + public sealed partial class ShortcutControl : UserControl, IDisposable { private readonly UIntPtr ignoreKeyEventFlag = (UIntPtr)0x5555; @@ -33,8 +48,14 @@ namespace Microsoft.PowerToys.Settings.UI.Controls public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register("Enabled", typeof(bool), typeof(ShortcutControl), null); public static readonly DependencyProperty HotkeySettingsProperty = DependencyProperty.Register("HotkeySettings", typeof(HotkeySettings), typeof(ShortcutControl), null); - public static readonly DependencyProperty AllowDisableProperty = DependencyProperty.Register("AllowDisable", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnAllowDisableChanged)); + public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutControl), new PropertyMetadata(false, OnHasConflictChanged)); + public static readonly DependencyProperty TooltipProperty = DependencyProperty.Register("Tooltip", typeof(string), typeof(ShortcutControl), new PropertyMetadata(null, OnTooltipChanged)); + + // Dependency property to track the source/context of the ShortcutControl + public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(ShortcutControlSource), typeof(ShortcutControl), new PropertyMetadata(ShortcutControlSource.SettingsPage)); + + private static ResourceLoader resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; private static void OnAllowDisableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { @@ -50,14 +71,75 @@ namespace Microsoft.PowerToys.Settings.UI.Controls return; } - var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; - var newValue = (bool)(e?.NewValue ?? false); var text = newValue ? resourceLoader.GetString("Activation_Shortcut_With_Disable_Description") : resourceLoader.GetString("Activation_Shortcut_Description"); description.Text = text; } + private static void OnHasConflictChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as ShortcutControl; + if (control == null) + { + return; + } + + control.UpdateKeyVisualStyles(); + + // Check if conflict was resolved (had conflict before, no conflict now) + var oldValue = (bool)(e.OldValue ?? false); + var newValue = (bool)(e.NewValue ?? false); + + // General conflict resolution telemetry (for all sources) + if (oldValue && !newValue) + { + // Determine the actual source based on the control's context + var actualSource = DetermineControlSource(control); + + // Conflict was resolved - send general telemetry + PowerToysTelemetry.Log.WriteEvent(new ShortcutConflictResolvedEvent() + { + Source = actualSource.ToString(), + }); + } + } + + private static ShortcutControlSource DetermineControlSource(ShortcutControl control) + { + // Walk up the visual tree to find the parent window/container + DependencyObject parent = control; + while (parent != null) + { + parent = VisualTreeHelper.GetParent(parent); + + // Check if we're in a ShortcutConflictWindow + if (parent != null && parent.GetType().Name == "ShortcutConflictWindow") + { + return ShortcutControlSource.ConflictWindow; + } + + if (parent != null && (parent.GetType().Name == "MainWindow" || parent.GetType().Name == "ShellPage")) + { + return ShortcutControlSource.SettingsPage; + } + } + + // Fallback to the explicitly set value or default + return ShortcutControlSource.ConflictWindow; + } + + private static void OnTooltipChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as ShortcutControl; + if (control == null) + { + return; + } + + control.UpdateTooltip(); + } + private ShortcutDialogContentControl c = new ShortcutDialogContentControl(); private ContentDialog shortcutDialog; @@ -67,6 +149,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set => SetValue(AllowDisableProperty, value); } + public bool HasConflict + { + get => (bool)GetValue(HasConflictProperty); + set => SetValue(HasConflictProperty, value); + } + + public string Tooltip + { + get => (string)GetValue(TooltipProperty); + set => SetValue(TooltipProperty, value); + } + + public ShortcutControlSource Source + { + get => (ShortcutControlSource)GetValue(SourceProperty); + set => SetValue(SourceProperty, value); + } + public bool Enabled { get @@ -101,15 +201,54 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { if (hotkeySettings != value) { + // Unsubscribe from old settings + if (hotkeySettings != null) + { + hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged; + } + hotkeySettings = value; SetValue(HotkeySettingsProperty, value); - PreviewKeysControl.ItemsSource = HotkeySettings.GetKeysList(); - AutomationProperties.SetHelpText(EditButton, HotkeySettings.ToString()); - c.Keys = HotkeySettings.GetKeysList(); + + // Subscribe to new settings + if (hotkeySettings != null) + { + hotkeySettings.PropertyChanged += OnHotkeySettingsPropertyChanged; + + // Update UI based on conflict properties + UpdateConflictStatusFromHotkeySettings(); + } + + SetKeys(); + c.Keys = HotkeySettings?.GetKeysList(); } } } + private void OnHotkeySettingsPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(HotkeySettings.HasConflict) || + e.PropertyName == nameof(HotkeySettings.ConflictDescription)) + { + UpdateConflictStatusFromHotkeySettings(); + } + } + + private void UpdateConflictStatusFromHotkeySettings() + { + if (hotkeySettings != null) + { + // Update the ShortcutControl's conflict properties from HotkeySettings + HasConflict = hotkeySettings.HasConflict; + Tooltip = hotkeySettings.HasConflict ? hotkeySettings.ConflictDescription : null; + } + else + { + HasConflict = false; + Tooltip = null; + } + } + public ShortcutControl() { InitializeComponent(); @@ -118,8 +257,6 @@ namespace Microsoft.PowerToys.Settings.UI.Controls this.Unloaded += ShortcutControl_Unloaded; this.Loaded += ShortcutControl_Loaded; - var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; - // We create the Dialog in C# because doing it in XAML is giving WinUI/XAML Island bugs when using dark theme. shortcutDialog = new ContentDialog { @@ -139,6 +276,29 @@ namespace Microsoft.PowerToys.Settings.UI.Controls OnAllowDisableChanged(this, null); } + private void UpdateKeyVisualStyles() + { + if (PreviewKeysControl?.ItemsSource != null) + { + // Force refresh of the ItemsControl to update KeyVisual styles + var items = PreviewKeysControl.ItemsSource; + PreviewKeysControl.ItemsSource = null; + PreviewKeysControl.ItemsSource = items; + } + } + + private void UpdateTooltip() + { + if (!string.IsNullOrEmpty(Tooltip)) + { + ToolTipService.SetToolTip(EditButton, Tooltip); + } + else + { + ToolTipService.SetToolTip(EditButton, null); + } + } + private void ShortcutControl_Unloaded(object sender, RoutedEventArgs e) { shortcutDialog.PrimaryButtonClick -= ShortcutDialog_PrimaryButtonClick; @@ -150,6 +310,12 @@ namespace Microsoft.PowerToys.Settings.UI.Controls App.GetSettingsWindow().Activated -= ShortcutDialog_SettingsWindow_Activated; } + // Unsubscribe from HotkeySettings property changes + if (hotkeySettings != null) + { + hotkeySettings.PropertyChanged -= OnHotkeySettingsPropertyChanged; + } + // Dispose the HotkeySettingsControlHook object to terminate the hook threads when the textbox is unloaded hook?.Dispose(); @@ -171,6 +337,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { App.GetSettingsWindow().Activated += ShortcutDialog_SettingsWindow_Activated; } + + // Initialize tooltip when loaded + UpdateTooltip(); } private void KeyEventHandler(int key, bool matchValue, int matchValueCode) @@ -305,6 +474,8 @@ namespace Microsoft.PowerToys.Settings.UI.Controls KeyEventHandler(key, true, key); c.Keys = internalSettings.GetKeysList(); + c.ConflictMessage = string.Empty; + c.HasConflict = false; if (internalSettings.GetKeysList().Count == 0) { @@ -339,12 +510,74 @@ namespace Microsoft.PowerToys.Settings.UI.Controls else { EnableKeys(); + if (lastValidSettings.IsValid()) + { + if (string.Equals(lastValidSettings.ToString(), hotkeySettings.ToString(), StringComparison.OrdinalIgnoreCase)) + { + c.HasConflict = hotkeySettings.HasConflict; + c.ConflictMessage = hotkeySettings.ConflictDescription; + } + else + { + // Check for conflicts with the new hotkey settings + CheckForConflicts(lastValidSettings); + } + } } } c.IsWarningAltGr = internalSettings.Ctrl && internalSettings.Alt && !internalSettings.Win && (internalSettings.Code > 0); } + private void CheckForConflicts(HotkeySettings settings) + { + void UpdateUIForConflict(bool hasConflict, HotkeyConflictResponse hotkeyConflictResponse) + { + DispatcherQueue.TryEnqueue(() => + { + if (hasConflict) + { + // Build conflict message from all conflicts - only show module names + var conflictingModules = new HashSet(); + + foreach (var conflict in hotkeyConflictResponse.AllConflicts) + { + if (!string.IsNullOrEmpty(conflict.ModuleName)) + { + conflictingModules.Add(conflict.ModuleName); + } + } + + if (conflictingModules.Count > 0) + { + var moduleNames = conflictingModules.ToArray(); + var conflictMessage = moduleNames.Length == 1 + ? $"Conflict detected with {moduleNames[0]}" + : $"Conflicts detected with: {string.Join(", ", moduleNames)}"; + + c.ConflictMessage = conflictMessage; + } + else + { + c.ConflictMessage = "Conflict detected with unknown module"; + } + + c.HasConflict = true; + } + else + { + c.ConflictMessage = string.Empty; + c.HasConflict = false; + } + }); + } + + HotkeyConflictHelper.CheckHotkeyConflict( + settings, + ShellPage.SendDefaultIPCMessage, + UpdateUIForConflict); + } + private void EnableKeys() { shortcutDialog.IsPrimaryButtonEnabled = true; @@ -419,6 +652,9 @@ namespace Microsoft.PowerToys.Settings.UI.Controls c.Keys = null; c.Keys = HotkeySettings.GetKeysList(); + c.HasConflict = hotkeySettings.HasConflict; + c.ConflictMessage = hotkeySettings.ConflictDescription; + // 92 means the Win key. The logic is: warning should be visible if the shortcut contains Alt AND contains Ctrl AND NOT contains Win. // Additional key must be present, as this is a valid, previously used shortcut shown at dialog open. Check for presence of non-modifier-key is not necessary therefore c.IsWarningAltGr = c.Keys.Contains("Ctrl") && c.Keys.Contains("Alt") && !c.Keys.Contains(92); @@ -433,23 +669,36 @@ namespace Microsoft.PowerToys.Settings.UI.Controls hotkeySettings = null; SetValue(HotkeySettingsProperty, hotkeySettings); - PreviewKeysControl.ItemsSource = HotkeySettings.GetKeysList(); + SetKeys(); lastValidSettings = hotkeySettings; - - AutomationProperties.SetHelpText(EditButton, HotkeySettings.ToString()); shortcutDialog.Hide(); + + // Send RequestAllConflicts IPC to update the UI after changed hotkey settings. + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); } private void ShortcutDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) { if (ComboIsValid(lastValidSettings)) { - HotkeySettings = lastValidSettings with { }; + if (c.HasConflict) + { + lastValidSettings = lastValidSettings with { HasConflict = true }; + } + else + { + lastValidSettings = lastValidSettings with { HasConflict = false }; + } + + HotkeySettings = lastValidSettings; } - PreviewKeysControl.ItemsSource = hotkeySettings.GetKeysList(); - AutomationProperties.SetHelpText(EditButton, HotkeySettings.ToString()); + SetKeys(); + + // Send RequestAllConflicts IPC to update the UI after changed hotkey settings. + GlobalHotkeyConflictManager.Instance?.RequestAllConflicts(); + shortcutDialog.Hide(); } @@ -462,9 +711,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls var empty = new HotkeySettings(); HotkeySettings = empty; - - PreviewKeysControl.ItemsSource = HotkeySettings.GetKeysList(); - AutomationProperties.SetHelpText(EditButton, HotkeySettings.ToString()); + SetKeys(); shortcutDialog.Hide(); } @@ -525,5 +772,22 @@ namespace Microsoft.PowerToys.Settings.UI.Controls Dispose(disposing: true); GC.SuppressFinalize(this); } + + private void SetKeys() + { + var keys = HotkeySettings?.GetKeysList(); + + if (keys != null && keys.Count > 0) + { + VisualStateManager.GoToState(this, "Configured", true); + PreviewKeysControl.ItemsSource = keys; + AutomationProperties.SetHelpText(EditButton, HotkeySettings.ToString()); + } + else + { + VisualStateManager.GoToState(this, "Normal", true); + AutomationProperties.SetHelpText(EditButton, resourceLoader.GetString("ConfigureShortcut")); + } + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml index 8765a3d4b3..13033344ab 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml @@ -14,9 +14,6 @@ - - - + Style="{StaticResource AccentKeyVisualStyle}" /> @@ -51,20 +51,25 @@ Orientation="Vertical" Spacing="8"> - - + - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs index 5d44f7c451..8907f12415 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutDialogContentControl.xaml.cs @@ -11,6 +11,24 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { public sealed partial class ShortcutDialogContentControl : UserControl { + public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutDialogContentControl), new PropertyMetadata(default(string))); + public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty HasConflictProperty = DependencyProperty.Register("HasConflict", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); + public static readonly DependencyProperty ConflictMessageProperty = DependencyProperty.Register("ConflictMessage", typeof(string), typeof(ShortcutDialogContentControl), new PropertyMetadata(string.Empty)); + + public bool HasConflict + { + get => (bool)GetValue(HasConflictProperty); + set => SetValue(HasConflictProperty, value); + } + + public string ConflictMessage + { + get => (string)GetValue(ConflictMessageProperty); + set => SetValue(ConflictMessageProperty, value); + } + public ShortcutDialogContentControl() { this.InitializeComponent(); @@ -22,22 +40,16 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set { SetValue(KeysProperty, value); } } - public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(SettingsPageControl), new PropertyMetadata(default(string))); - public bool IsError { get => (bool)GetValue(IsErrorProperty); set => SetValue(IsErrorProperty, value); } - public static readonly DependencyProperty IsErrorProperty = DependencyProperty.Register("IsError", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); - public bool IsWarningAltGr { get => (bool)GetValue(IsWarningAltGrProperty); set => SetValue(IsWarningAltGrProperty, value); } - - public static readonly DependencyProperty IsWarningAltGrProperty = DependencyProperty.Register("IsWarningAltGr", typeof(bool), typeof(ShortcutDialogContentControl), new PropertyMetadata(false)); } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml index 23f2d3cc2b..ea3be0bff8 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml @@ -16,6 +16,7 @@ + Style="{StaticResource DefaultKeyVisualStyle}" /> + + + + + + + + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs index ed18669eba..c3829e3984 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml.cs @@ -17,7 +17,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set { SetValue(TextProperty, value); } } - public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); public List Keys { @@ -25,11 +25,40 @@ namespace Microsoft.PowerToys.Settings.UI.Controls set { SetValue(KeysProperty, value); } } - public static readonly DependencyProperty KeysProperty = DependencyProperty.Register("Keys", typeof(List), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + public static readonly DependencyProperty KeysProperty = DependencyProperty.Register(nameof(Keys), typeof(List), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(default(string))); + + public LabelPlacement LabelPlacement + { + get { return (LabelPlacement)GetValue(LabelPlacementProperty); } + set { SetValue(LabelPlacementProperty, value); } + } + + public static readonly DependencyProperty LabelPlacementProperty = DependencyProperty.Register(nameof(LabelPlacement), typeof(LabelPlacement), typeof(ShortcutWithTextLabelControl), new PropertyMetadata(defaultValue: LabelPlacement.After, OnIsLabelPlacementChanged)); public ShortcutWithTextLabelControl() { this.InitializeComponent(); } + + private static void OnIsLabelPlacementChanged(DependencyObject d, DependencyPropertyChangedEventArgs newValue) + { + if (d is ShortcutWithTextLabelControl labelControl) + { + if (labelControl.LabelPlacement == LabelPlacement.Before) + { + VisualStateManager.GoToState(labelControl, "LabelBefore", true); + } + else + { + VisualStateManager.GoToState(labelControl, "LabelAfter", true); + } + } + } + } + + public enum LabelPlacement + { + Before, + After, } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml index 163922236e..dd8a40fb7e 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/AppsListPage.xaml @@ -7,17 +7,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters" xmlns:viewModels="using:Microsoft.PowerToys.Settings.UI.ViewModels" mc:Ignorable="d"> - - - - - @@ -89,7 +80,7 @@ VerticalAlignment="Center" FontSize="16" Glyph="" - Visibility="{x:Bind IsLocked, Converter={StaticResource BoolToInvertedVisibilityConverter}, ConverterParameter=True, Mode=OneWay}"> + Visibility="{x:Bind IsLocked, Converter={StaticResource ReverseBoolToVisibilityConverter}, ConverterParameter=True, Mode=OneWay}"> diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml index 67d8030b16..a5e6f2de40 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml @@ -21,7 +21,6 @@ - @@ -110,7 +109,7 @@ diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml index b04c800bca..20815cd81c 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeOverview.xaml @@ -20,33 +20,56 @@ - + + Margin="0,24,0,0" + Style="{StaticResource BodyStrongTextBlockStyle}" /> + + + + + + + - - - + + + + + + + + + - - - - - - - + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs index 1b2524eee8..15fcea6452 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeWhatsNew.xaml.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Linq; using System.Net; @@ -17,9 +18,11 @@ using CommunityToolkit.WinUI.UI.Controls; using global::PowerToys.GPOWrapper; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; +using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts; using Microsoft.PowerToys.Settings.UI.OOBE.Enums; using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel; using Microsoft.PowerToys.Settings.UI.SerializationContext; +using Microsoft.PowerToys.Settings.UI.Services; using Microsoft.PowerToys.Settings.UI.Views; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Xaml.Controls; @@ -27,12 +30,54 @@ using Microsoft.UI.Xaml.Navigation; namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { - public sealed partial class OobeWhatsNew : Page + public sealed partial class OobeWhatsNew : Page, INotifyPropertyChanged { public OobePowerToysModule ViewModel { get; set; } + private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData(); + public bool ShowDataDiagnosticsInfoBar => GetShowDataDiagnosticsInfoBar(); + public AllHotkeyConflictsData AllHotkeyConflictsData + { + get => _allHotkeyConflictsData; + set + { + if (_allHotkeyConflictsData != value) + { + _allHotkeyConflictsData = value; + OnPropertyChanged(nameof(AllHotkeyConflictsData)); + OnPropertyChanged(nameof(HasConflicts)); + } + } + } + + public bool HasConflicts + { + get + { + if (AllHotkeyConflictsData == null) + { + return false; + } + + int count = 0; + if (AllHotkeyConflictsData.InAppConflicts != null) + { + count += AllHotkeyConflictsData.InAppConflicts.Count; + } + + if (AllHotkeyConflictsData.SystemConflicts != null) + { + count += AllHotkeyConflictsData.SystemConflicts.Count; + } + + return count > 0; + } + } + + public event PropertyChangedEventHandler PropertyChanged; + /// /// Initializes a new instance of the class. /// @@ -40,7 +85,27 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views { this.InitializeComponent(); ViewModel = new OobePowerToysModule(OobeShellPage.OobeShellHandler.Modules[(int)PowerToysModules.WhatsNew]); - DataContext = ViewModel; + DataContext = this; + + // Subscribe to hotkey conflict updates + if (GlobalHotkeyConflictManager.Instance != null) + { + GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated; + GlobalHotkeyConflictManager.Instance.RequestAllConflicts(); + } + } + + private void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e) + { + this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, () => + { + AllHotkeyConflictsData = e.Conflicts ?? new AllHotkeyConflictsData(); + }); + } + + private void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private bool GetShowDataDiagnosticsInfoBar() @@ -184,6 +249,12 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Views protected override void OnNavigatedFrom(NavigationEventArgs e) { ViewModel.LogClosingModuleEvent(); + + // Unsubscribe from conflict updates when leaving the page + if (GlobalHotkeyConflictManager.Instance != null) + { + GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated; + } } private void ReleaseNotesMarkdown_LinkClicked(object sender, LinkClickedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml index 4a5f4233de..f277350fbc 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/OobeWindow.xaml @@ -14,4 +14,4 @@ - + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Themes/Generic.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Themes/Generic.xaml index b1c5f79256..6a68895c50 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Themes/Generic.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Themes/Generic.xaml @@ -2,7 +2,7 @@ - - + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs index a395ac767b..8442262688 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs @@ -31,6 +31,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs index b38fffc59e..2e22da3120 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml.cs @@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel = new AlwaysOnTopViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml index 5c4a09a9c4..34305e3529 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdNotFoundPage.xaml @@ -10,13 +10,6 @@ xmlns:ui="using:CommunityToolkit.WinUI" AutomationProperties.LandmarkType="Main" mc:Ignorable="d"> - - - - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs index a9a016b80e..fb3a97e309 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CmdPalPage.xaml.cs @@ -26,6 +26,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views ShellPage.SendDefaultIPCMessage, DispatcherQueue); DataContext = ViewModel; + Loaded += (s, e) => ViewModel.OnPageLoaded(); InitializeComponent(); } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml index 5295cf2df4..7ac03ead81 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ColorPickerPage.xaml @@ -14,14 +14,8 @@ d:DataContext="{d:DesignInstance Type=viewModels:ColorPickerViewModel}" AutomationProperties.LandmarkType="Main" mc:Ignorable="d"> - - - - - - ViewModel.OnPageLoaded(); } /// diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs index 66e3652da8..d769650dd1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/CropAndLockPage.xaml.cs @@ -19,6 +19,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel = new CropAndLockViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; InitializeComponent(); + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml index 347ea02b0e..e5b800cda1 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml @@ -9,497 +9,357 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls" xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters" - xmlns:viewModels="using:Microsoft.PowerToys.Settings.UI.ViewModels" + xmlns:viewmodels="using:Microsoft.PowerToys.Settings.UI.ViewModels" AutomationProperties.LandmarkType="Main" DataContext="DashboardViewModel" mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 16 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file 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 394b1d6de6..bf792e2b75 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/DashboardPage.xaml.cs @@ -5,10 +5,10 @@ using System; using System.Threading; using System.Threading.Tasks; - using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.OOBE.Enums; using Microsoft.PowerToys.Settings.UI.OOBE.Views; using Microsoft.PowerToys.Settings.UI.ViewModels; using Microsoft.UI.Xaml; @@ -39,6 +39,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel = new DashboardViewModel( SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; + + Loaded += (s, e) => ViewModel.OnPageLoaded(); } public void RefreshEnabledState() @@ -46,14 +48,23 @@ namespace Microsoft.PowerToys.Settings.UI.Views ViewModel.ModuleEnabledChangedOnSettingsPage(); } - private void SWVersionButtonClicked(object sender, RoutedEventArgs e) - { - ViewModel.SWVersionButtonClicked(); - } - private void DashboardListItemClick(object sender, RoutedEventArgs e) { ViewModel.DashboardListItemClick(sender); } + + private void WhatsNewButton_Click(object sender, RoutedEventArgs e) + { + if (App.GetOobeWindow() == null) + { + App.SetOobeWindow(new OobeWindow(PowerToysModules.WhatsNew)); + } + else + { + App.GetOobeWindow().SetAppWindow(PowerToysModules.WhatsNew); + } + + App.GetOobeWindow().Activate(); + } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs index c224c42683..61865c89fa 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/FancyZonesPage.xaml.cs @@ -19,6 +19,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views var settingsUtils = new SettingsUtils(); ViewModel = new FancyZonesViewModel(settingsUtils, SettingsRepository.GetInstance(settingsUtils), SettingsRepository.GetInstance(settingsUtils), ShellPage.SendDefaultIPCMessage); DataContext = ViewModel; + Loaded += (s, e) => ViewModel.OnPageLoaded(); } private void OpenColorsSettings_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml index 51118fea10..1a3c640ddf 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml @@ -12,7 +12,6 @@ mc:Ignorable="d"> - @@ -268,7 +267,10 @@ - + + + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml index f6e7a3fddb..b816fccf09 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/KeyboardManagerPage.xaml @@ -14,22 +14,23 @@ - - - - + - + + + + + {29B91A80-0590-4B1F-89B8-4F8812A7F116} + Microsoft.Settings.UITests + false + enable + Library + + + false + + + + ..\..\..\$(Platform)\$(Configuration)\tests\UITests-Settings\ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/BugReportTool/BugReportTool/RegistryUtils.cpp b/tools/BugReportTool/BugReportTool/RegistryUtils.cpp index eb576690e8..10913a55bd 100644 --- a/tools/BugReportTool/BugReportTool/RegistryUtils.cpp +++ b/tools/BugReportTool/BugReportTool/RegistryUtils.cpp @@ -35,6 +35,8 @@ namespace { HKEY_CLASSES_ROOT, L".qoi\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" }, { HKEY_CLASSES_ROOT, L".gcode\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" }, { HKEY_CLASSES_ROOT, L".gcode\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" }, + { HKEY_CLASSES_ROOT, L".bgcode\\shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}" }, + { HKEY_CLASSES_ROOT, L".bgcode\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" }, { HKEY_CLASSES_ROOT, L".stl\\shellex\\{E357FCCD-A995-4576-B01F-234630154E96}" } }; diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1 index 28e6939760..f185ddfb0f 100644 --- a/tools/build/build-installer.ps1 +++ b/tools/build/build-installer.ps1 @@ -4,46 +4,74 @@ Build and package PowerToys (CmdPal and installer) for a specific platform and c .DESCRIPTION This script automates the end-to-end build and packaging process for PowerToys, including: -- Restoring and building all necessary solutions (CmdPal, BugReportTool, StylesReportTool, etc.) +- Restoring and building all necessary solutions (CmdPal, BugReportTool, etc.) - Cleaning up old output - Signing generated .msix packages - Building the WiX-based MSI and bootstrapper installers It is designed to work in local development. +The cert used to sign the packages is generated by .PARAMETER Platform -Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'arm64'. +Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'x64'. .PARAMETER Configuration Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'. +.PARAMETER PerUser +Specifies whether to build a per-user installer (true) or machine-wide installer (false). Default is true (per-user). + .EXAMPLE .\build-installer.ps1 Runs the installer build pipeline for ARM64 Release (default). .EXAMPLE .\build-installer.ps1 -Platform x64 -Configuration Release -Runs the pipeline for x64 Debug. +Runs the pipeline for x64 Release. + +.EXAMPLE +.\build-installer.ps1 -Platform x64 -Configuration Release -PerUser false +Runs the pipeline for x64 Release with machine-wide installer. .NOTES -- Requires MSBuild, WiX Toolset, and Git to be installed and accessible from your environment. - Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell). - Generated MSIX files will be signed using cert-sign-package.ps1. - This script will clean previous outputs under the build directories and installer directory (except *.exe files). - First time run need admin permission to trust the certificate. -- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/UserSetup +- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/User[Machine]Setup relative to the solution root directory. -- The installer can't be run right after the build, I need to copy it to another file before it can be run. +- To run the full installation in other machines, call "./cert-management.ps1" to export the cert used to sign the packages. + And trust the cert in the target machine. #> - param ( - [string]$Platform = 'arm64', - [string]$Configuration = 'Release' + [string]$Platform = 'x64', + [string]$Configuration = 'Release', + [string]$PerUser = 'true' ) -$repoRoot = Resolve-Path "$PSScriptRoot\..\.." -Set-Location $repoRoot +# Find the PowerToys repository root automatically +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$repoRoot = $scriptDir + +# Navigate up from the script location to find the repo root +# Script is typically in tools\build, so go up two levels +while ($repoRoot -and -not (Test-Path (Join-Path $repoRoot "PowerToys.sln"))) { + $parentDir = Split-Path -Parent $repoRoot + if ($parentDir -eq $repoRoot) { + # Reached the root of the drive, PowerToys.sln not found + Write-Error "Could not find PowerToys repository root. Make sure this script is in the PowerToys repository." + exit 1 + } + $repoRoot = $parentDir +} + +if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "PowerToys.sln"))) { + Write-Error "Could not locate PowerToys.sln. Please ensure this script is run from within the PowerToys repository." + exit 1 +} + +Write-Host "PowerToys repository root detected: $repoRoot" function RunMSBuild { param ( @@ -53,8 +81,9 @@ function RunMSBuild { $base = @( $Solution - "/p:Platform=`"$Platform`"" + "/p:Platform=$Platform" "/p:Configuration=$Configuration" + "/p:CIBuild=true" '/verbosity:normal' '/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly' '/nologo' @@ -62,13 +91,18 @@ function RunMSBuild { $cmd = $base + ($ExtraArgs -split ' ') Write-Host ("[MSBUILD] {0} {1}" -f $Solution, ($cmd -join ' ')) - & msbuild.exe @cmd - - if ($LASTEXITCODE -ne 0) { - Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs) - exit $LASTEXITCODE + + # Run MSBuild from the repository root directory + Push-Location $repoRoot + try { + & msbuild.exe @cmd + if ($LASTEXITCODE -ne 0) { + Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs) + exit $LASTEXITCODE + } + } finally { + Pop-Location } - } function RestoreThenBuild { @@ -81,9 +115,9 @@ function RestoreThenBuild { } Write-Host ("Make sure wix is installed and available") -& "$PSScriptRoot\ensure-wix.ps1" +& (Join-Path $PSScriptRoot "ensure-wix.ps1") -Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1}" -f $Platform, $Configuration) +Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1} PerUser={2}" -f $Platform, $Configuration, $PerUser) Write-Host '' $cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal" @@ -93,7 +127,7 @@ if (Test-Path $cmdpalOutputPath) { Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore } -RestoreThenBuild '.\PowerToys.sln' +RestoreThenBuild 'PowerToys.sln' $msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration" $msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix | @@ -101,22 +135,27 @@ Select-Object -ExpandProperty FullName if ($msixFiles.Count) { Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; ')) - & "$PSScriptRoot\cert-sign-package.ps1" -TargetPaths $msixFiles + & (Join-Path $PSScriptRoot "cert-sign-package.ps1") -TargetPaths $msixFiles } else { Write-Warning "[SIGN] No .msix files found in $msixSearchRoot" } -RestoreThenBuild '.\tools\BugReportTool\BugReportTool.sln' -RestoreThenBuild '.\tools\StylesReportTool\StylesReportTool.sln' +RestoreThenBuild 'tools\BugReportTool\BugReportTool.sln' +RestoreThenBuild 'tools\StylesReportTool\StylesReportTool.sln' Write-Host '[CLEAN] installer (keep *.exe)' -git clean -xfd -e '*.exe' -- .\installer\ | Out-Null +Push-Location $repoRoot +try { + git clean -xfd -e '*.exe' -- .\installer\ | Out-Null +} finally { + Pop-Location +} -RunMSBuild '.\installer\PowerToysSetup.sln' '/t:restore /p:RestorePackagesConfig=true' +RunMSBuild 'installer\PowerToysSetup.sln' '/t:restore /p:RestorePackagesConfig=true' -RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysInstaller /p:PerUser=true' +RunMSBuild 'installer\PowerToysSetup.sln' "/m /t:PowerToysInstaller /p:PerUser=$PerUser" -RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysBootstrapper /p:PerUser=true' +RunMSBuild 'installer\PowerToysSetup.sln' "/m /t:PowerToysBootstrapper /p:PerUser=$PerUser" -Write-Host '[PIPELINE] Completed' \ No newline at end of file +Write-Host '[PIPELINE] Completed' diff --git a/tools/build/cert-management.ps1 b/tools/build/cert-management.ps1 index ed7031c1e9..f92146f730 100644 --- a/tools/build/cert-management.ps1 +++ b/tools/build/cert-management.ps1 @@ -152,4 +152,37 @@ function Export-CertificateFiles { if (-not $CerPath -and -not $PfxPath) { Write-Warning "No output path specified. Nothing was exported." } +} + +# Main execution when script is run directly +if ($MyInvocation.InvocationName -ne '.') { + Write-Host "=== PowerToys Certificate Management ===" -ForegroundColor Green + Write-Host "" + + # Ensure certificate exists and is trusted + Write-Host "Checking for existing certificate or creating new one..." -ForegroundColor Yellow + $cert = EnsureCertificate + + if ($cert) { + # Export the certificate to a .cer file + $exportPath = Join-Path (Get-Location) "PowerToys-CodeSigning.cer" + Write-Host "" + Write-Host "Exporting certificate..." -ForegroundColor Yellow + Export-CertificateFiles -Certificate $cert -CerPath $exportPath + + Write-Host "" + Write-Host "=== IMPORTANT NOTES ===" -ForegroundColor Red + Write-Host "The certificate has been exported to: $exportPath" -ForegroundColor White + Write-Host "" + Write-Host "To use this certificate for code signing, you need to:" -ForegroundColor Yellow + Write-Host "1. Import this certificate into 'Trusted People' store" -ForegroundColor White + Write-Host "2. Import this certificate into 'Trusted Root Certification Authorities' store" -ForegroundColor White + Write-Host "Certificate Details:" -ForegroundColor Green + Write-Host "Subject: $($cert.Subject)" -ForegroundColor White + Write-Host "Thumbprint: $($cert.Thumbprint)" -ForegroundColor White + Write-Host "Valid Until: $($cert.NotAfter)" -ForegroundColor White + } else { + Write-Error "Failed to create or find certificate. Please check the error messages above." + exit 1 + } } \ No newline at end of file