diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index bd875d9d0f..766142a67b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -50,6 +50,7 @@ body: - Always on Top - Awake - ColorPicker + - Crop and Lock - FancyZones - FancyZones Editor - File Locksmith diff --git a/.github/ISSUE_TEMPLATE/translation_issue.yml b/.github/ISSUE_TEMPLATE/translation_issue.yml index 1330d336d5..d84b2eaa52 100644 --- a/.github/ISSUE_TEMPLATE/translation_issue.yml +++ b/.github/ISSUE_TEMPLATE/translation_issue.yml @@ -24,6 +24,7 @@ body: - Always on Top - Awake - ColorPicker + - Crop and Lock - FancyZones - FancyZones Editor - File Locksmith diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt index 76ecf535e0..b3ad904e7c 100644 --- a/.github/actions/spell-check/allow/names.txt +++ b/.github/actions/spell-check/allow/names.txt @@ -15,6 +15,8 @@ edwinzap Essey Garside Gershaft +Gokce +Guo hallatore Harmath Hemmerlein @@ -22,10 +24,13 @@ Huynh Jaswal jefflord Kamra +Kantarci Karthick +kevinguo Krigun Luecking Mahalingam +Mikhayelyan mshtang Myrvold naveensrinivasan @@ -33,6 +38,7 @@ nVidia Ponten Pooja robmen +robmikh Schoen skycommand snickler diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 3a7b44a779..e8af95b8ed 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -674,6 +674,7 @@ hcblack HCERTSTORE HCRYPTHASH HCRYPTPROV +hcursor hcwhite hdc hdrop @@ -742,7 +743,7 @@ hstring hsv htcfreek HTCLIENT -HTHUMBNAIL +hthumbnail HTOUCHINPUT HTTRANSPARENT HVal @@ -1529,6 +1530,7 @@ RECTDESTINATION RECTL rectp rects +RECTSOURCE redirectedfrom Redist redistributable @@ -1566,6 +1568,9 @@ Removelnk renamable RENAMEONCOLLISION Renamer +reparent +reparented +reparenting reparse reportbug requery @@ -1998,6 +2003,7 @@ unregistering unremapped unsubscribe unvirtualized +unwide UOffset UOI Updatelayout diff --git a/.github/actions/spell-check/patterns.txt b/.github/actions/spell-check/patterns.txt index 5d9877ade7..5959fbe8fc 100644 --- a/.github/actions/spell-check/patterns.txt +++ b/.github/actions/spell-check/patterns.txt @@ -115,12 +115,14 @@ TestCase\("[^"]+" \\Registry \\registry \\reinstall +\\release \\Resize \\resource \\Resources \\restart \\restore \\result +\\robmikh \\rotating \\runner \\runtimes diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index e590f9cff4..d5c6ff120b 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -33,6 +33,9 @@ "PowerToys.ColorPickerUI.dll", "PowerToys.ColorPickerUI.exe", + "PowerToys.CropAndLockModuleInterface.dll", + "PowerToys.CropAndLock.exe", + "PowerToys.PowerOCRModuleInterface.dll", "PowerToys.PowerOCR.dll", "PowerToys.PowerOCR.exe", diff --git a/.pipelines/ci/templates/build-powertoys-ci.yml b/.pipelines/ci/templates/build-powertoys-ci.yml index 6017f864af..b180fb50d0 100644 --- a/.pipelines/ci/templates/build-powertoys-ci.yml +++ b/.pipelines/ci/templates/build-powertoys-ci.yml @@ -1,7 +1,7 @@ parameters: configuration: 'Release' platform: '' - additionalBuildArguments: '-m' + additionalBuildArguments: '/p:RestorePackagesConfig=true -m' jobs: - job: Build${{ parameters.platform }}${{ parameters.configuration }} diff --git a/.pipelines/ci/templates/build-powertoys-installer.yml b/.pipelines/ci/templates/build-powertoys-installer.yml index 3335a5048c..ab99af8e54 100644 --- a/.pipelines/ci/templates/build-powertoys-installer.yml +++ b/.pipelines/ci/templates/build-powertoys-installer.yml @@ -1,7 +1,7 @@ parameters: configuration: 'Release' platform: '' - additionalBuildArguments: '-m' + additionalBuildArguments: '/p:RestorePackagesConfig=true -m' jobs: - job: Build${{ parameters.platform }}${{ parameters.configuration }} diff --git a/.pipelines/ci/templates/build-powertoys-steps.yml b/.pipelines/ci/templates/build-powertoys-steps.yml index 5b187423ad..43358f3cc0 100644 --- a/.pipelines/ci/templates/build-powertoys-steps.yml +++ b/.pipelines/ci/templates/build-powertoys-steps.yml @@ -55,21 +55,9 @@ steps: packageType: sdk version: '7.x' -- task: NuGetToolInstaller@1 - displayName: Ensure NuGet Installer - - task: VisualStudioTestPlatformInstaller@1 displayName: Ensure VSTest Platform -- task: NuGetCommand@2 - displayName: Restore NuGet packages for PowerToys.sln - inputs: - command: restore - feedsToUse: config - configPath: NuGet.config - restoreSolution: PowerToys.sln - restoreDirectory: '$(Build.SourcesDirectory)\packages' - - task: VSBuild@1 displayName: 'Build PowerToys.sln' inputs: @@ -77,18 +65,9 @@ steps: vsVersion: 17.0 platform: '$(BuildPlatform)' configuration: '$(BuildConfiguration)' - msbuildArgs: ${{ parameters.additionalBuildArguments }} + msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} maximumCpuCount: true -- task: NuGetCommand@2 - displayName: Restore NuGet packages for BugReportTool.sln - inputs: - command: restore - feedsToUse: config - configPath: NuGet.config - restoreSolution: tools\BugReportTool\BugReportTool.sln - restoreDirectory: '$(Build.SourcesDirectory)\tools\BugReportTool\packages' - - task: VSBuild@1 displayName: 'Build BugReportTool.sln' inputs: @@ -96,18 +75,9 @@ steps: vsVersion: 17.0 platform: '$(BuildPlatform)' configuration: '$(BuildConfiguration)' - msbuildArgs: ${{ parameters.additionalBuildArguments }} + msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} maximumCpuCount: true -- task: NuGetCommand@2 - displayName: Restore NuGet packages for WebcamReportTool.sln - inputs: - command: restore - feedsToUse: config - configPath: NuGet.config - restoreSolution: tools\WebcamReportTool\WebcamReportTool.sln - restoreDirectory: '$(Build.SourcesDirectory)\tools\WebcamReportTool\packages' - - task: VSBuild@1 displayName: 'Build WebcamReportTool.sln' inputs: @@ -115,18 +85,9 @@ steps: vsVersion: 17.0 platform: '$(BuildPlatform)' configuration: '$(BuildConfiguration)' - msbuildArgs: ${{ parameters.additionalBuildArguments }} + msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} maximumCpuCount: true -- task: NuGetCommand@2 - displayName: Restore NuGet packages for StylesReportTool.sln - inputs: - command: restore - feedsToUse: config - configPath: NuGet.config - restoreSolution: tools\StylesReportTool\StylesReportTool.sln - restoreDirectory: '$(Build.SourcesDirectory)\tools\StylesReportTool\packages' - - task: VSBuild@1 displayName: 'Build StylesReportTool.sln' inputs: @@ -134,18 +95,9 @@ steps: vsVersion: 17.0 platform: '$(BuildPlatform)' configuration: '$(BuildConfiguration)' - msbuildArgs: ${{ parameters.additionalBuildArguments }} + msbuildArgs: -restore ${{ parameters.additionalBuildArguments }} maximumCpuCount: true -- task: NuGetCommand@2 - displayName: Restore NuGet packages for PowerToysSetup.sln - inputs: - command: restore - feedsToUse: config - configPath: NuGet.config - restoreSolution: installer\PowerToysSetup.sln - restoreDirectory: '$(Build.SourcesDirectory)\installer\packages' - - task: PowerShell@2 displayName: Download and install WiX 3.14 development build inputs: @@ -159,7 +111,7 @@ steps: vsVersion: 17.0 platform: '$(BuildPlatform)' configuration: '$(BuildConfiguration)' - msbuildArgs: /t:PowerToysInstaller ${{ parameters.additionalBuildArguments }} + msbuildArgs: /t:PowerToysInstaller -restore ${{ parameters.additionalBuildArguments }} maximumCpuCount: true - task: VSBuild@1 @@ -180,15 +132,6 @@ steps: script: git clean -xfd -e *exe -- .\installer\ pwsh: true -- task: NuGetCommand@2 - displayName: Restore NuGet packages for PowerToysSetup.sln - inputs: - command: restore - feedsToUse: config - configPath: NuGet.config - restoreSolution: installer\PowerToysSetup.sln - restoreDirectory: '$(Build.SourcesDirectory)\installer\packages' - - task: VSBuild@1 displayName: 'Build PowerToys per-user MSI' inputs: @@ -196,7 +139,7 @@ steps: vsVersion: 17.0 platform: '$(BuildPlatform)' configuration: '$(BuildConfiguration)' - msbuildArgs: /t:PowerToysInstaller ${{ parameters.additionalBuildArguments }} /p:PerUser=true + msbuildArgs: /t:PowerToysInstaller -restore ${{ parameters.additionalBuildArguments }} /p:PerUser=true maximumCpuCount: true - task: VSBuild@1 diff --git a/.pipelines/installer-steps.yml b/.pipelines/installer-steps.yml index 4c28143ef7..d8c256d86c 100644 --- a/.pipelines/installer-steps.yml +++ b/.pipelines/installer-steps.yml @@ -18,7 +18,7 @@ steps: inputs: solution: "**/installer/PowerToysSetup.sln" vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog /t:PowerToysSetupCustomActions /p:RunBuildEvents=true /p:PerUser=${{parameters.perUserArg}} + msbuildArgs: -restore /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog /t:PowerToysSetupCustomActions /p:RunBuildEvents=true /p:PerUser=${{parameters.perUserArg}} platform: $(BuildPlatform) configuration: $(BuildConfiguration) clean: true diff --git a/.pipelines/release.yml b/.pipelines/release.yml index a104998068..7c889c2fe7 100644 --- a/.pipelines/release.yml +++ b/.pipelines/release.yml @@ -71,7 +71,7 @@ jobs: packageType: sdk version: '7.x' - - task: NuGetAuthenticate@0 + - task: NuGetAuthenticate@1 - task: NuGetToolInstaller@1 displayName: Use NuGet Installer latest @@ -82,14 +82,6 @@ jobs: # - Webcam report tool # - Installer # - Bootstrapper Installer - - task: NuGetCommand@2 - displayName: NuGet restore solutions dependencies - inputs: - command: restore - restoreSolution: '**/*.sln' - selectOrConfig: config - nugetConfigPath: .pipelines/release-nuget.config - - task: PowerShell@2 displayName: Download and install WiX 3.14 development build inputs: @@ -136,7 +128,7 @@ jobs: inputs: solution: '**\PowerToys.sln' vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog + msbuildArgs: -restore /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog platform: $(BuildPlatform) configuration: $(BuildConfiguration) clean: true @@ -147,7 +139,7 @@ jobs: inputs: solution: '**/tools/BugReportTool/BugReportTool.sln' vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog + msbuildArgs: -restore /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog platform: $(BuildPlatform) configuration: $(BuildConfiguration) clean: true @@ -158,7 +150,7 @@ jobs: inputs: solution: '**/tools/WebcamReportTool/WebcamReportTool.sln' vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog + msbuildArgs: -restore /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog platform: $(BuildPlatform) configuration: $(BuildConfiguration) clean: true @@ -169,7 +161,7 @@ jobs: inputs: solution: '**/tools/StylesReportTool/StylesReportTool.sln' vsVersion: 17.0 - msbuildArgs: /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog + msbuildArgs: -restore /p:RestorePackagesConfig=true /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:CIBuild=true /bl:$(Build.SourcesDirectory)\msbuild.binlog platform: $(BuildPlatform) configuration: $(BuildConfiguration) clean: true @@ -397,14 +389,6 @@ jobs: script: git clean -xfd -e *exe -- .\installer\ pwsh: true - - task: NuGetCommand@2 - displayName: NuGet restore solutions dependencies - inputs: - command: restore - restoreSolution: 'installer/*.sln' - selectOrConfig: config - nugetConfigPath: .pipelines/release-nuget.config - - template: installer-steps.yml parameters: versionNumber: ${{ parameters.versionNumber }} diff --git a/COMMUNITY.md b/COMMUNITY.md index 6f831031c8..2edd40657d 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -107,6 +107,10 @@ Randy contributed Registry Preview and some very early conversations about keybo Find My Mouse is based on Raymond Chen's SuperSonar. +### [@robmikh](https://github.com/robmikh) - Robert Mikhayelyan + +Crop And Lock is based on the original work of Robert Mikhayelyan, with Program Manager support from [@kevinguo305](https://github.com/kevinguo305) - Kevin Guo. + ### Microsoft InVEST team This amazing team helped PowerToys develop PowerToys Run and Keyboard manager as well as update our Settings to v2. @alekhyareddy28, @arjunbalgovind, @jyuwono @laviusmotileng-ms, @ryanbodrug-microsoft, @saahmedm, @somil55, @traies, @udit3333 @@ -152,8 +156,7 @@ Other contributors: - [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Product Manager - [@jaimecbernardo](https://github.com/jaimecbernardo) - Jaime Bernardo - Dev lead - [@donlaci](https://github.com/donlaci) - Laszlo Nemeth - Dev +- [@gokcekantarci](https://github.com/gokcekantarci) - Gokce Kantarci - Dev - [@SeraphimaZykova](https://github.com/SeraphimaZykova) - Seraphima Zykova - Dev -- [@sosssego](https://github.com/sosssego) - Frederico Moron - Dev - [@stefansjfw](https://github.com/stefansjfw) - Stefan Markovic - Dev -- [@taras-janea](https://github.com/taras-janea) - Taras Sich - Dev - [@yuyoyuppe](https://github.com/yuyoyuppe) - Andrey Nekrasov - Dev diff --git a/PowerToys.sln b/PowerToys.sln index f636803aa1..01508db104 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -530,6 +530,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plu EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests", "src\modules\launcher\Plugins\Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests\Community.PowerToys.Run.Plugin.ValueGenerator.UnitTests.csproj", "{90F9FA90-2C20-4004-96E6-F3B78151F5A5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CropAndLock", "CropAndLock", "{3B227528-4BA6-4CAF-B44A-A10C78A64849}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLock", "src\modules\CropAndLock\CropAndLock\CropAndLock.vcxproj", "{F5E1146E-B7B3-4E11-85FD-270A500BD78C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLockModuleInterface", "src\modules\CropAndLock\CropAndLockModuleInterface\CropAndLockModuleInterface.vcxproj", "{3157FA75-86CF-4EE2-8F62-C43F776493C6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2276,6 +2282,30 @@ Global {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x64.Build.0 = Release|x64 {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x86.ActiveCfg = Release|x64 {90F9FA90-2C20-4004-96E6-F3B78151F5A5}.Release|x86.Build.0 = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|ARM64.Build.0 = Debug|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x64.ActiveCfg = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x64.Build.0 = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x86.ActiveCfg = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Debug|x86.Build.0 = Debug|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|ARM64.ActiveCfg = Release|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|ARM64.Build.0 = Release|ARM64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x64.ActiveCfg = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x64.Build.0 = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x86.ActiveCfg = Release|x64 + {F5E1146E-B7B3-4E11-85FD-270A500BD78C}.Release|x86.Build.0 = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|ARM64.Build.0 = Debug|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x64.ActiveCfg = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x64.Build.0 = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x86.ActiveCfg = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Debug|x86.Build.0 = Debug|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|ARM64.ActiveCfg = Release|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|ARM64.Build.0 = Release|ARM64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x64.ActiveCfg = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x64.Build.0 = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x86.ActiveCfg = Release|x64 + {3157FA75-86CF-4EE2-8F62-C43F776493C6}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2467,6 +2497,9 @@ Global {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} + {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/README.md b/README.md index cab666b120..ce589d18d7 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,13 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline | | Current utilities: | | |--------------|--------------------|--------------| | [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | -| [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) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [Peek](https://aka.ms/PowerToysOverview_Peek) | -| [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [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) | -| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) | +| [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [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) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | +| [Peek](https://aka.ms/PowerToysOverview_Peek) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [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) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | +| [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) | ## Installing and running Microsoft PowerToys diff --git a/doc/devdocs/modules/keyboardmanager/keyboardmanagerui.md b/doc/devdocs/modules/keyboardmanager/keyboardmanagerui.md index 3542385029..1f55198b4b 100644 --- a/doc/devdocs/modules/keyboardmanager/keyboardmanagerui.md +++ b/doc/devdocs/modules/keyboardmanager/keyboardmanagerui.md @@ -1,12 +1,12 @@ # Keyboard Manager UI ## Table of Contents: -1. [C++ XAML Islands](#c---xaml-islands) +1. [C++ XAML Islands](#c-xaml-islands) 1. [Debugging exceptions in XAML Islands](#debugging-exceptions-in-xaml-islands) 2. [Build times](#build-times) 3. [Setting custom backgrounds for Xaml Controls using brushes](#setting-custom-backgrounds-for-xaml-controls-using-brushes) 2. [UI Structure](#ui-structure) -3. [EditKeyboardWindow/EditShortcutsWindow](#editkeyboardwindow-editshortcutswindow) +3. [EditKeyboardWindow / EditShortcutsWindow](#editkeyboardwindow--editshortcutswindow) 1. [OK and Cancel button](#ok-and-cancel-button) 2. [Delete button](#delete-button) 3. [Handling common modifiers in EditKeyboardWindow](#handling-common-modifiers-in-editkeyboardwindow) @@ -51,7 +51,7 @@ Since ComboBoxes are added dynamically, handlers have been added which [update t When the `EditKeyboardWindow`/`EditShortcutsWindow` is created, [we iterate through the remappings](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/EditKeyboardWindow.cpp#L254-L262) stored in `KeyboardManagerState` and add rows to the UI Grid. For both the windows we have `static` buffers [`singleKeyRemapBuffer`](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/SingleKeyRemapControl.h#L39-L40) and [`shortcutRemapBuffer`](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/ShortcutControl.h#L42-L43) which store the corresponding key/shortcuts as per the selections in the UI if they are valid with no warnings. -## EditKeyboardWindow/EditShortcutsWindow +## EditKeyboardWindow / EditShortcutsWindow ### OK and Cancel button [On pressing the OK button](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/EditKeyboardWindow.cpp#L66-L89) in `EditKeyboardWindow`, first the [`CheckIfRemappingsAreValid` method](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/LoadingAndSavingRemappingHelper.cpp#L10-L44) is executed which performs basic validity checks on the current remappings in the remap buffer (`static SingleKeyRemapControl::singleKeyRemapBuffer`), such as if there are no NULL columns and none of the source keys are repeated. All other validity checks are assumed to happen while the user adds the remapping. If this is found to be invalid a ContentDialog is displayed which shows that some remappings are invalid and if the user proceeds only the valid ones will be applied. If it is valid [`GetOrphanedKeys`](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/LoadingAndSavingRemappingHelper.cpp#L46-L75) is executed which checks if any keys are orphaned (i.e. the key has been remapped and no other key has been remapped to it, so there is no way to send that key code), and a dialog is shown for notifying the user with a list of orphaned keys. After this the settings are [applied by adding it to the `KeyboardManagerState.singleKeyReMap` member](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/LoadingAndSavingRemappingHelper.cpp#L102-L164) and they are saved to the JSON file. `EditShortcutsWindow` differs slightly from this, as there is no orphaned keys check, and [on pressing OK](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/EditShortcutsWindow.cpp#L32-L47) both the global and app-specific shortcuts are validated and [updated](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/ui/LoadingAndSavingRemappingHelper.cpp#L166-L223). diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index 289b36feb0..579fe3ba6b 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1005,7 +1005,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) } processes.resize(bytes / sizeof(processes[0])); - std::array processesToTerminate = { + std::array processesToTerminate = { L"PowerToys.PowerLauncher.exe", L"PowerToys.Settings.exe", L"PowerToys.Awake.exe", @@ -1032,6 +1032,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) L"PowerToys.MouseWithoutBorders.exe", L"PowerToys.MouseWithoutBordersHelper.exe", L"PowerToys.MouseWithoutBordersService.exe", + L"PowerToys.CropAndLock.exe", L"PowerToys.exe", }; diff --git a/src/Update/PowerToys.Update.cpp b/src/Update/PowerToys.Update.cpp index d19647623d..1e16598c43 100644 --- a/src/Update/PowerToys.Update.cpp +++ b/src/Update/PowerToys.Update.cpp @@ -73,6 +73,9 @@ std::optional ObtainInstaller(bool& isUpToDate) return std::nullopt; } + // Cleanup old updates before downloading the latest + updating::cleanup_updates(); + auto downloaded_installer = download_new_version(std::get(*new_version_info)).get(); if (!downloaded_installer) { diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index cb5ac55089..46ccf722d3 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -16,6 +16,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getConfiguredColorPickerEnabledValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredCropAndLockEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredCropAndLockEnabledValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredFancyZonesEnabledValue() { return static_cast(powertoys_gpo::getConfiguredFancyZonesEnabledValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index d9e9896caf..d11feb7c3a 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -10,6 +10,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue(); static GpoRuleConfigured GetConfiguredAwakeEnabledValue(); static GpoRuleConfigured GetConfiguredColorPickerEnabledValue(); + static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue(); static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue(); static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue(); static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue(); diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 9bec67ccf1..5e26390d7e 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -14,6 +14,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredAlwaysOnTopEnabledValue(); static GpoRuleConfigured GetConfiguredAwakeEnabledValue(); static GpoRuleConfigured GetConfiguredColorPickerEnabledValue(); + static GpoRuleConfigured GetConfiguredCropAndLockEnabledValue(); static GpoRuleConfigured GetConfiguredFancyZonesEnabledValue(); static GpoRuleConfigured GetConfiguredFileLocksmithEnabledValue(); static GpoRuleConfigured GetConfiguredSvgPreviewEnabledValue(); diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index f5dd5dc8da..1535a3aa87 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -74,6 +74,11 @@ namespace CommonSharedConstants // Path to the event used to show Peek const wchar_t SHOW_PEEK_SHARED_EVENT[] = L"Local\\ShowPeekEvent"; + // Path to the events used by CropAndLock + const wchar_t CROP_AND_LOCK_REPARENT_EVENT[] = L"Local\\PowerToysCropAndLockReparentEvent-6060860a-76a1-44e8-8d0e-6355785e9c36"; + const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434"; + const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a"; + // Max DWORD for key code to disable keys. const DWORD VK_DISABLED = 0x100; } diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index eeb7274331..8498f9bf4b 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -59,7 +59,8 @@ struct LogSettings inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt"; inline const static std::string hostsLoggerName = "hosts"; inline const static std::wstring hostsLogPath = L"Logs\\hosts-log.txt"; - inline const static std::string registryPreviewLoggerName = "registrypreview"; + inline const static std::string registryPreviewLoggerName = "registrypreview"; + inline const static std::string cropAndLockLoggerName = "crop-and-lock"; inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.txt"; inline const static int retention = 30; std::wstring logLevel; diff --git a/src/common/updating/updating.cpp b/src/common/updating/updating.cpp index c0e194403b..361e77bc4f 100644 --- a/src/common/updating/updating.cpp +++ b/src/common/updating/updating.cpp @@ -1,6 +1,7 @@ #include "pch.h" #include +#include #include #include @@ -185,4 +186,48 @@ namespace updating co_return download_success ? installer_download_path : std::nullopt; } + void cleanup_updates() + { + auto update_dir = updating::get_pending_updates_path(); + if (std::filesystem::exists(update_dir)) + { + // Msi and exe files + for (const auto& entry : std::filesystem::directory_iterator(update_dir)) + { + auto entryPath = entry.path().wstring(); + std::transform(entryPath.begin(), entryPath.end(), entryPath.begin(), ::towlower); + + if (entryPath.ends_with(L".msi") || entryPath.ends_with(L".exe")) + { + std::error_code err; + std::filesystem::remove(entry, err); + if (err.value()) + { + Logger::warn("Failed to delete installer file {}. {}", entry.path().string(), err.message()); + } + } + } + } + + // Log files + auto rootPath{ PTSettingsHelper::get_root_save_folder_location() }; + auto currentVersion = left_trim(get_product_version(), L"v"); + if (std::filesystem::exists(rootPath)) + { + for (const auto& entry : std::filesystem::directory_iterator(rootPath)) + { + auto entryPath = entry.path().wstring(); + std::transform(entryPath.begin(), entryPath.end(), entryPath.begin(), ::towlower); + if (entry.is_regular_file() && entryPath.ends_with(L".log") && entryPath.find(currentVersion) == std::string::npos) + { + std::error_code err; + std::filesystem::remove(entry, err); + if (err.value()) + { + Logger::warn("Failed to delete log file {}. {}", entry.path().string(), err.message()); + } + } + } + } + } } diff --git a/src/common/updating/updating.h b/src/common/updating/updating.h index 11ab3b95fd..155002361f 100644 --- a/src/common/updating/updating.h +++ b/src/common/updating/updating.h @@ -28,6 +28,7 @@ namespace updating std::future> download_new_version(const new_version_download_info& new_version); std::filesystem::path get_pending_updates_path(); std::future> get_github_version_info_async(const bool prerelease = false); + void cleanup_updates(); // non-localized constexpr inline std::wstring_view INSTALLER_FILENAME_PATTERN = L"powertoyssetup"; diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index fa937718fb..3da87d62ae 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -22,6 +22,7 @@ namespace powertoys_gpo { const std::wstring POLICY_CONFIGURE_ENABLED_ALWAYS_ON_TOP = L"ConfigureEnabledUtilityAlwaysOnTop"; const std::wstring POLICY_CONFIGURE_ENABLED_AWAKE = L"ConfigureEnabledUtilityAwake"; const std::wstring POLICY_CONFIGURE_ENABLED_COLOR_PICKER = L"ConfigureEnabledUtilityColorPicker"; + const std::wstring POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK = L"ConfigureEnabledUtilityCropAndLock"; const std::wstring POLICY_CONFIGURE_ENABLED_FANCYZONES = L"ConfigureEnabledUtilityFancyZones"; const std::wstring POLICY_CONFIGURE_ENABLED_FILE_LOCKSMITH = L"ConfigureEnabledUtilityFileLocksmith"; const std::wstring POLICY_CONFIGURE_ENABLED_SVG_PREVIEW = L"ConfigureEnabledUtilityFileExplorerSVGPreview"; @@ -129,6 +130,11 @@ namespace powertoys_gpo { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_COLOR_PICKER); } + inline gpo_rule_configured_t getConfiguredCropAndLockEnabledValue() + { + return getConfiguredValue(POLICY_CONFIGURE_ENABLED_CROP_AND_LOCK); + } + inline gpo_rule_configured_t getConfiguredFancyZonesEnabledValue() { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_FANCYZONES); diff --git a/src/common/utils/string_utils.h b/src/common/utils/string_utils.h index 27bf64b15b..cd209eb5aa 100644 --- a/src/common/utils/string_utils.h +++ b/src/common/utils/string_utils.h @@ -54,3 +54,12 @@ inline void replace_chars(std::basic_string& s, std::replace(begin(s), end(s), c, replacement_char); } } + +inline std::string unwide(const std::wstring& wide) +{ + std::string result(wide.length(), 0); + std::transform(begin(wide), end(wide), result.begin(), [](const wchar_t c) { + return static_cast(c); + }); + return result; +} diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index 2f45a8ab29..74b319de2c 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -1,17 +1,18 @@ - + - + + @@ -51,6 +52,16 @@ + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index e70495cc71..8d5e858414 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -1,7 +1,7 @@ - + PowerToys PowerToys @@ -13,6 +13,7 @@ PowerToys version 0.68.0 or later PowerToys version 0.69.0 or later PowerToys version 0.70.0 or later + PowerToys version 0.73.0 or later This policy configures the enabled state for a PowerToys utility. @@ -67,6 +68,7 @@ If this setting is disabled, experimentation is not allowed. Always On Top: Configure enabled state Awake: Configure enabled state Color Picker: Configure enabled state + Crop And Lock: Configure enabled state FancyZones: Configure enabled state File Locksmith: Configure enabled state SVG file preview: Configure enabled state diff --git a/src/modules/CropAndLock/CropAndLock/ChildWindow.cpp b/src/modules/CropAndLock/CropAndLock/ChildWindow.cpp new file mode 100644 index 0000000000..12f3e0261f --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ChildWindow.cpp @@ -0,0 +1,56 @@ +#include "pch.h" +#include "ChildWindow.h" + +namespace util +{ + using namespace robmikh::common::desktop; + using namespace robmikh::common::desktop::controls; +} + +const std::wstring ChildWindow::ClassName = L"CropAndLock.ChildWindow"; +std::once_flag ChildWindowClassRegistration; + +void ChildWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +ChildWindow::ChildWindow(int width, int height, HWND parent) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(ChildWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = 0; + auto style = WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; + + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), L"", style, + 0, 0, width, height, parent, nullptr, instance, this)); + WINRT_ASSERT(m_window); + + ShowWindow(m_window, SW_SHOW); + UpdateWindow(m_window); +} + +LRESULT ChildWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} diff --git a/src/modules/CropAndLock/CropAndLock/ChildWindow.h b/src/modules/CropAndLock/CropAndLock/ChildWindow.h new file mode 100644 index 0000000000..3fa213676a --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ChildWindow.h @@ -0,0 +1,11 @@ +#pragma once +#include + +struct ChildWindow : robmikh::common::desktop::DesktopWindow +{ + static const std::wstring ClassName; + ChildWindow(int width, int height, HWND parent); + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); +private: + static void RegisterWindowClass(); +}; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.rc b/src/modules/CropAndLock/CropAndLock/CropAndLock.rc new file mode 100644 index 0000000000..a0dbc35e4e --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.rc @@ -0,0 +1,105 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO +FILEVERSION FILE_VERSION +PRODUCTVERSION PRODUCT_VERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG +FILEFLAGS VS_FF_DEBUG +#else +FILEFLAGS 0x0L +#endif +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset + END +END + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "icon1.ico" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj new file mode 100644 index 0000000000..3c3e2f26f1 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj @@ -0,0 +1,170 @@ + + + + + true + true + true + true + 15.0 + {f5e1146e-b7b3-4e11-85fd-270a500bd78c} + Win32Proj + CropAndLock + 10.0.20348.0 + 10.0.19041.0 + + + + + Debug + ARM64 + + + Release + ARM64 + + + Debug + x64 + + + Release + x64 + + + + Application + v143 + v142 + v143 + v143 + Unicode + Spectre + + + true + true + + + false + true + false + + + + + + + + + + + + + + + PowerToys.$(MSBuildProjectName) + ..\..\..\..\$(Platform)\$(Configuration)\ + + + + _CONSOLE;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions) + Level4 + %(AdditionalOptions) /bigobj + $(SolutionDir)src\common\Telemetry;$(SolutionDir)src;%(AdditionalIncludeDirectories) + + + Windows + shell32.lib;dwmapi.lib;DbgHelp.lib;gdi32.lib;Shcore.lib;%(AdditionalDependencies) + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + MultiThreadedDebug + MultiThreadedDebug + + + false + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + MultiThreaded + MultiThreaded + + + true + true + false + + + + + + + + + + + + + + Create + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters new file mode 100644 index 0000000000..98752f066d --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLock.vcxproj.filters @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/CropAndLockWindow.h b/src/modules/CropAndLock/CropAndLock/CropAndLockWindow.h new file mode 100644 index 0000000000..609303e36a --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/CropAndLockWindow.h @@ -0,0 +1,10 @@ +#pragma once + +struct CropAndLockWindow +{ + virtual ~CropAndLockWindow() {} + + virtual HWND Handle() = 0; + virtual void CropAndLock(HWND windowToCrop, RECT cropRect) = 0; + virtual void OnClosed(std::function callback) = 0; +}; diff --git a/src/modules/CropAndLock/CropAndLock/DisplaysUtil.h b/src/modules/CropAndLock/CropAndLock/DisplaysUtil.h new file mode 100644 index 0000000000..b01def102b --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/DisplaysUtil.h @@ -0,0 +1,25 @@ +#pragma once + +inline RECT ComputeAllDisplaysUnion(std::vector const& infos) +{ + RECT result = {}; + result.left = LONG_MAX; + result.top = LONG_MAX; + result.right = LONG_MIN; + result.bottom = LONG_MIN; + for (auto&& info : infos) + { + auto rect = info.Rect(); + result.left = std::min(result.left, rect.left); + result.top = std::min(result.top, rect.top); + result.right = std::max(result.right, rect.right); + result.bottom = std::max(result.bottom, rect.bottom); + } + return result; +} + +inline RECT ComputeAllDisplaysUnion() +{ + auto infos = robmikh::common::desktop::DisplayInfo::GetAllDisplays(); + return ComputeAllDisplaysUnion(infos); +} diff --git a/src/modules/CropAndLock/CropAndLock/ModuleConstants.h b/src/modules/CropAndLock/CropAndLock/ModuleConstants.h new file mode 100644 index 0000000000..8cc452bb12 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ModuleConstants.h @@ -0,0 +1,6 @@ +#pragma once + +namespace NonLocalizable +{ + const inline wchar_t ModuleKey[] = L"CropAndLock"; +} \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/OverlayWindow.cpp b/src/modules/CropAndLock/CropAndLock/OverlayWindow.cpp new file mode 100644 index 0000000000..12f4598d74 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/OverlayWindow.cpp @@ -0,0 +1,356 @@ +#include "pch.h" +#include "OverlayWindow.h" + +namespace winrt +{ + using namespace Windows::UI; + using namespace Windows::UI::Composition; +} + +namespace util +{ + using namespace robmikh::common::desktop; +} + +const std::wstring OverlayWindow::ClassName = L"CropAndLock.OverlayWindow"; +const float OverlayWindow::BorderThickness = 5; +std::once_flag OverlayWindowClassRegistration; + +bool IsPointWithinRect(POINT const& point, RECT const& rect) +{ + return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom; +} + +void OverlayWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(instance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +OverlayWindow::OverlayWindow( + winrt::Compositor const& compositor, + HWND windowToCrop, + std::function windowCropped) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(OverlayWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = WS_EX_NOREDIRECTIONBITMAP | WS_EX_TOOLWINDOW | WS_EX_LAYERED | WS_EX_TOPMOST; + auto style = WS_POPUP; + + // Get the union of all displays + auto displaysRect = ComputeAllDisplaysUnion(); + + // Create our window + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), L"", style, + displaysRect.left, displaysRect.top, displaysRect.right - displaysRect.left, displaysRect.bottom - displaysRect.top, nullptr, nullptr, instance, this)); + WINRT_ASSERT(m_window); + + // Load cursors + m_standardCursor.reset(winrt::check_pointer(LoadCursorW(nullptr, IDC_ARROW))); + m_crosshairCursor.reset(winrt::check_pointer(LoadCursorW(nullptr, IDC_CROSS))); + m_cursorType = CursorType::Standard; + + // Setup the visual tree + m_compositor = compositor; + m_target = CreateWindowTarget(m_compositor); + m_rootVisual = m_compositor.CreateContainerVisual(); + m_shadeVisual = m_compositor.CreateSpriteVisual(); + m_windowAreaVisual = m_compositor.CreateContainerVisual(); + m_selectionVisual = m_compositor.CreateSpriteVisual(); + + m_target.Root(m_rootVisual); + auto children = m_rootVisual.Children(); + children.InsertAtBottom(m_shadeVisual); + children.InsertAtTop(m_windowAreaVisual); + m_windowAreaVisual.Children().InsertAtTop(m_selectionVisual); + + m_rootVisual.RelativeSizeAdjustment({ 1, 1 }); + m_shadeBrush = m_compositor.CreateNineGridBrush(); + m_shadeBrush.IsCenterHollow(true); + m_shadeBrush.Source(m_compositor.CreateColorBrush(winrt::Color{ 255, 0, 0, 0 })); + m_shadeVisual.Brush(m_shadeBrush); + m_shadeVisual.Opacity(0.6f); + m_shadeVisual.RelativeSizeAdjustment({ 1, 1 }); + auto selectionBrush = m_compositor.CreateNineGridBrush(); + selectionBrush.SetInsets(BorderThickness); + selectionBrush.IsCenterHollow(true); + selectionBrush.Source(m_compositor.CreateColorBrush(winrt::Color{ 255, 255, 0, 0 })); + m_selectionVisual.Brush(selectionBrush); + + WINRT_VERIFY(windowToCrop != nullptr); + m_currentWindow = windowToCrop; + SetupOverlay(); + m_windowCropped = windowCropped; + + ShowWindow(m_window, SW_SHOW); + UpdateWindow(m_window); + SetForegroundWindow(m_window); +} + +void OverlayWindow::SetupOverlay() +{ + ResetCrop(); + + // Get the client bounds of the target window + auto windowBounds = ClientAreaInScreenSpace(m_currentWindow); + + // Get the union of all displays + auto displaysRect = ComputeAllDisplaysUnion(); + + // Before we can use the window bounds, we need to + // shift the origin to the top-left most point. + m_currentWindowAreaBounds.left = windowBounds.left - displaysRect.left; + m_currentWindowAreaBounds.top = windowBounds.top - displaysRect.top; + m_currentWindowAreaBounds.right = m_currentWindowAreaBounds.left + (windowBounds.right - windowBounds.left); + m_currentWindowAreaBounds.bottom = m_currentWindowAreaBounds.top + (windowBounds.bottom - windowBounds.top); + + auto windowLeft = static_cast(m_currentWindowAreaBounds.left); + auto windowTop = static_cast(m_currentWindowAreaBounds.top); + auto windowWidth = static_cast(windowBounds.right - windowBounds.left); + auto windowHeight = static_cast(windowBounds.bottom - windowBounds.top); + + // Change the shade brush to match the window bounds + // We need to make sure the values are non-negative, as they are invalid insets. We + // can sometimes get negative values for the left and top when windows are maximized. + m_shadeBrush.LeftInset(std::max(windowLeft, 0.0f)); + m_shadeBrush.TopInset(std::max(windowTop, 0.0f)); + m_shadeBrush.RightInset(std::max(static_cast(displaysRect.right - windowBounds.right), 0.0f)); + m_shadeBrush.BottomInset(std::max(static_cast(displaysRect.bottom - windowBounds.bottom), 0.0f)); + + // Change the window area visual to match the window bounds + m_windowAreaVisual.Offset({ windowLeft, windowTop, 0 }); + m_windowAreaVisual.Size({ windowWidth, windowHeight }); + + // Reset the selection visual + m_selectionVisual.Offset({ 0, 0, 0 }); + m_selectionVisual.Size({ 0, 0 }); +} + +LRESULT OverlayWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + break; + case WM_SETCURSOR: + return OnSetCursor(); + case WM_KEYUP: + { + auto key = static_cast(wparam); + if (key == VK_ESCAPE) + { + DestroyWindow(m_window); + } + } + break; + case WM_LBUTTONDOWN: + { + auto xPos = GET_X_LPARAM(lparam); + auto yPos = GET_Y_LPARAM(lparam); + OnLeftButtonDown(xPos, yPos); + } + break; + case WM_LBUTTONUP: + { + auto xPos = GET_X_LPARAM(lparam); + auto yPos = GET_Y_LPARAM(lparam); + OnLeftButtonUp(xPos, yPos); + } + break; + case WM_MOUSEMOVE: + { + auto xPos = GET_X_LPARAM(lparam); + auto yPos = GET_Y_LPARAM(lparam); + OnMouseMove(xPos, yPos); + } + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} + +void OverlayWindow::ResetCrop() +{ + m_cropStatus = CropStatus::None; + m_startPosition = {}; + m_cropRect = {}; +} + +bool OverlayWindow::OnSetCursor() +{ + switch (m_cursorType) + { + case CursorType::Standard: + SetCursor(m_standardCursor.get()); + return true; + case CursorType::Crosshair: + SetCursor(m_crosshairCursor.get()); + return true; + default: + return false; + } +} + +void OverlayWindow::OnLeftButtonDown(int x, int y) +{ + if (m_cropStatus == CropStatus::None) + { + if (!IsPointWithinRect({ x, y }, m_currentWindowAreaBounds)) + { + DestroyWindow(m_window); + return; + } + + m_cropStatus = CropStatus::Ongoing; + + x -= m_currentWindowAreaBounds.left; + y -= m_currentWindowAreaBounds.top; + + m_selectionVisual.Offset({ x - BorderThickness, y - BorderThickness, 0 }); + m_startPosition = { x, y }; + } +} + +void OverlayWindow::OnLeftButtonUp(int x, int y) +{ + if (m_cropStatus == CropStatus::Ongoing) + { + m_cropStatus = CropStatus::Completed; + m_cursorType = CursorType::Standard; + + // For debugging, it's easier if the window doesn't block the screen after this point + ShowWindow(m_window, SW_HIDE); + + if (x < m_currentWindowAreaBounds.left) + { + x = m_currentWindowAreaBounds.left; + } + else if (x > m_currentWindowAreaBounds.right) + { + x = m_currentWindowAreaBounds.right; + } + + if (y < m_currentWindowAreaBounds.top) + { + y = m_currentWindowAreaBounds.top; + } + else if (y > m_currentWindowAreaBounds.bottom) + { + y = m_currentWindowAreaBounds.bottom; + } + + x -= m_currentWindowAreaBounds.left; + y -= m_currentWindowAreaBounds.top; + + // Compute our crop rect + if (x < m_startPosition.x) + { + m_cropRect.left = x; + m_cropRect.right = m_startPosition.x; + } + else + { + m_cropRect.left = m_startPosition.x; + m_cropRect.right = x; + } + if (y < m_startPosition.y) + { + m_cropRect.top = y; + m_cropRect.bottom = m_startPosition.y; + } + else + { + m_cropRect.top = m_startPosition.y; + m_cropRect.bottom = y; + } + + // Exit if the rect is empty + if (m_cropRect.right - m_cropRect.left == 0 || m_cropRect.bottom - m_cropRect.top == 0) + { + DestroyWindow(m_window); + return; + } + + // Fire the callback + if (m_windowCropped != nullptr) + { + m_windowCropped(m_currentWindow, m_cropRect); + } + DestroyWindow(m_window); + } +} + +void OverlayWindow::OnMouseMove(int x, int y) +{ + if (m_cropStatus == CropStatus::None) + { + if (IsPointWithinRect({ x, y }, m_currentWindowAreaBounds)) + { + m_cursorType = CursorType::Crosshair; + } + else + { + m_cursorType = CursorType::Standard; + } + } + else if (m_cropStatus == CropStatus::Ongoing) + { + if (x < m_currentWindowAreaBounds.left) + { + x = m_currentWindowAreaBounds.left; + } + else if (x > m_currentWindowAreaBounds.right) + { + x = m_currentWindowAreaBounds.right; + } + + if (y < m_currentWindowAreaBounds.top) + { + y = m_currentWindowAreaBounds.top; + } + else if (y > m_currentWindowAreaBounds.bottom) + { + y = m_currentWindowAreaBounds.bottom; + } + + x -= m_currentWindowAreaBounds.left; + y -= m_currentWindowAreaBounds.top; + + auto offset = m_selectionVisual.Offset(); + auto size = m_selectionVisual.Size(); + + if (x < m_startPosition.x) + { + offset.x = x - BorderThickness; + size.x = (m_startPosition.x - x) + (2 * BorderThickness); + } + else + { + size.x = (x - m_startPosition.x) + (2 * BorderThickness); + } + + if (y < m_startPosition.y) + { + offset.y = y - BorderThickness; + size.y = (m_startPosition.y - y) + (2 * BorderThickness); + } + else + { + size.y = (y - m_startPosition.y) + (2 * BorderThickness); + } + + m_selectionVisual.Offset(offset); + m_selectionVisual.Size(size); + } +} diff --git a/src/modules/CropAndLock/CropAndLock/OverlayWindow.h b/src/modules/CropAndLock/CropAndLock/OverlayWindow.h new file mode 100644 index 0000000000..67d59c56a4 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/OverlayWindow.h @@ -0,0 +1,58 @@ +#pragma once +#include + +struct OverlayWindow : robmikh::common::desktop::DesktopWindow +{ + static const std::wstring ClassName; + OverlayWindow( + winrt::Windows::UI::Composition::Compositor const& compositor, + HWND windowToCrop, + std::function windowCropped); + ~OverlayWindow() { m_windowCropped = nullptr; DestroyWindow(m_window); } + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); + +private: + enum class CursorType + { + Standard, + Crosshair, + }; + + enum class CropStatus + { + None, + Ongoing, + Completed, + }; + + static const float BorderThickness; + static void RegisterWindowClass(); + + void SetupOverlay(); + void ResetCrop(); + bool OnSetCursor(); + void OnLeftButtonDown(int x, int y); + void OnLeftButtonUp(int x, int y); + void OnMouseMove(int x, int y); + +private: + std::function m_windowCropped; + winrt::Windows::UI::Composition::Compositor m_compositor{ nullptr }; + winrt::Windows::UI::Composition::CompositionTarget m_target{ nullptr }; + winrt::Windows::UI::Composition::ContainerVisual m_rootVisual{ nullptr }; + winrt::Windows::UI::Composition::SpriteVisual m_shadeVisual{ nullptr }; + winrt::Windows::UI::Composition::ContainerVisual m_windowAreaVisual{ nullptr }; + winrt::Windows::UI::Composition::SpriteVisual m_selectionVisual{ nullptr }; + winrt::Windows::UI::Composition::CompositionNineGridBrush m_shadeBrush{ nullptr }; + + HWND m_currentWindow = nullptr; + RECT m_currentWindowAreaBounds = {}; + + CropStatus m_cropStatus = CropStatus::None; + POINT m_startPosition = {}; + RECT m_cropRect = {}; + + CursorType m_cursorType = CursorType::Standard; + wil::unique_hcursor m_standardCursor; + wil::unique_hcursor m_crosshairCursor; +}; diff --git a/src/modules/CropAndLock/CropAndLock/PropertySheet.props b/src/modules/CropAndLock/CropAndLock/PropertySheet.props new file mode 100644 index 0000000000..b0c622690f --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/PropertySheet.props @@ -0,0 +1,16 @@ + + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp new file mode 100644 index 0000000000..cdf9fb3eb0 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.cpp @@ -0,0 +1,160 @@ +#include "pch.h" +#include "ReparentCropAndLockWindow.h" + +const std::wstring ReparentCropAndLockWindow::ClassName = L"CropAndLock.ReparentCropAndLockWindow"; +std::once_flag ReparentCropAndLockWindowClassRegistration; + +void ReparentCropAndLockWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.hbrBackground = reinterpret_cast(COLOR_WINDOW + 1); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +ReparentCropAndLockWindow::ReparentCropAndLockWindow(std::wstring const& titleString, int width, int height) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(ReparentCropAndLockWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = 0; + auto style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN; + style &= ~(WS_MAXIMIZEBOX | WS_THICKFRAME); + + RECT rect = { 0, 0, width, height}; + winrt::check_bool(AdjustWindowRectEx(&rect, style, false, exStyle)); + auto adjustedWidth = rect.right - rect.left; + auto adjustedHeight = rect.bottom - rect.top; + + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), titleString.c_str(), style, + CW_USEDEFAULT, CW_USEDEFAULT, adjustedWidth, adjustedHeight, nullptr, nullptr, instance, this)); + WINRT_ASSERT(m_window); + + m_childWindow = std::make_unique(width, height, m_window); +} + +ReparentCropAndLockWindow::~ReparentCropAndLockWindow() +{ + DisconnectTarget(); + DestroyWindow(m_window); +} + +LRESULT ReparentCropAndLockWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + if (m_closedCallback != nullptr && !m_destroyed) + { + m_destroyed = true; + m_closedCallback(m_window); + } + break; + case WM_MOUSEACTIVATE: + if (m_currentTarget != nullptr && GetForegroundWindow() != m_currentTarget) + { + SetForegroundWindow(m_currentTarget); + } + return MA_NOACTIVATE; + case WM_ACTIVATE: + if (static_cast(wparam) == WA_ACTIVE) + { + if (m_currentTarget != nullptr) + { + SetForegroundWindow(m_currentTarget); + } + } + break; + case WM_DPICHANGED: + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} + +void ReparentCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect) +{ + DisconnectTarget(); + m_currentTarget = windowToCrop; + + // Adjust the crop rect to be in the window space as reported by win32k + RECT windowRect = {}; + winrt::check_bool(GetWindowRect(m_currentTarget, &windowRect)); + auto clientRect = ClientAreaInScreenSpace(m_currentTarget); + auto diffX = clientRect.left - windowRect.left; + auto diffY = clientRect.top - windowRect.top; + auto adjustedCropRect = cropRect; + adjustedCropRect.left += diffX; + adjustedCropRect.top += diffY; + adjustedCropRect.right += diffX; + adjustedCropRect.bottom += diffY; + cropRect = adjustedCropRect; + + // Save the previous position of the target so that we can restore it. + m_previousPosition = { windowRect.left, windowRect.top }; + auto newX = adjustedCropRect.left + windowRect.left; + auto newY = adjustedCropRect.top + windowRect.top; + + auto monitor = winrt::check_pointer(MonitorFromWindow(m_currentTarget, MONITOR_DEFAULTTONULL)); + uint32_t dpiX = 0; + uint32_t dpiY = 0; + winrt::check_hresult(GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY)); + uint32_t dpi = dpiX > dpiY ? dpiX : dpiY; + + // Reconfigure our window + auto width = cropRect.right - cropRect.left; + auto height = cropRect.bottom - cropRect.top; + windowRect = { newX, newY, newX + width, newY + height }; + auto exStyle = static_cast(GetWindowLongPtrW(m_window, GWL_EXSTYLE)); + auto style = static_cast(GetWindowLongPtrW(m_window, GWL_STYLE)); + winrt::check_bool(AdjustWindowRectExForDpi(&windowRect, style, false, exStyle, dpi)); + auto adjustedWidth = windowRect.right - windowRect.left; + auto adjustedHeight = windowRect.bottom - windowRect.top; + + winrt::check_bool(SetWindowPos(m_window, HWND_TOPMOST, windowRect.left, windowRect.top, adjustedWidth, adjustedHeight, SWP_SHOWWINDOW | SWP_NOACTIVATE)); + winrt::check_bool(SetWindowPos(m_childWindow->m_window, nullptr, 0, 0, width, height, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE)); + + // Reparent the target window + SetParent(m_currentTarget, m_childWindow->m_window); + auto targetStyle = GetWindowLongPtrW(m_currentTarget, GWL_STYLE); + targetStyle |= WS_CHILD; + SetWindowLongPtrW(m_currentTarget, GWL_STYLE, targetStyle); + auto x = -cropRect.left; + auto y = -cropRect.top; + winrt::check_bool(SetWindowPos(m_currentTarget, nullptr, x, y, 0, 0, SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOZORDER)); +} + +void ReparentCropAndLockWindow::Hide() +{ + DisconnectTarget(); + ShowWindow(m_window, SW_HIDE); +} + +void ReparentCropAndLockWindow::DisconnectTarget() +{ + if (m_currentTarget != nullptr) + { + if (!IsWindow(m_currentTarget)) + { + // The child window was closed by other means? + m_currentTarget = nullptr; + return; + } + winrt::check_bool(SetWindowPos(m_currentTarget, nullptr, m_previousPosition.x, m_previousPosition.y, 0, 0, SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED)); + SetParent(m_currentTarget, nullptr); + auto targetStyle = static_cast(GetWindowLongPtrW(m_currentTarget, GWL_STYLE)); + targetStyle &= ~WS_CHILD; + SetWindowLongPtrW(m_currentTarget, GWL_STYLE, targetStyle); + m_currentTarget = nullptr; + } +} diff --git a/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.h b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.h new file mode 100644 index 0000000000..4734340e34 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ReparentCropAndLockWindow.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include "CropAndLockWindow.h" +#include "ChildWindow.h" + +struct ReparentCropAndLockWindow : robmikh::common::desktop::DesktopWindow, CropAndLockWindow +{ + static const std::wstring ClassName; + ReparentCropAndLockWindow(std::wstring const& titleString, int width, int height); + ~ReparentCropAndLockWindow() override; + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); + + HWND Handle() override { return m_window; } + void CropAndLock(HWND windowToCrop, RECT cropRect) override; + void OnClosed(std::function callback) override { m_closedCallback = callback; } + +private: + static void RegisterWindowClass(); + + void Hide(); + void DisconnectTarget(); + +private: + HWND m_currentTarget = nullptr; + POINT m_previousPosition = {}; + std::unique_ptr m_childWindow; + bool m_destroyed = false; + std::function m_closedCallback; +}; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/SettingsWindow.h b/src/modules/CropAndLock/CropAndLock/SettingsWindow.h new file mode 100644 index 0000000000..88489601ee --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/SettingsWindow.h @@ -0,0 +1,7 @@ +#pragma once + +enum class CropAndLockType +{ + Reparent, + Thumbnail, +}; diff --git a/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.cpp b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.cpp new file mode 100644 index 0000000000..454e8d5abe --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.cpp @@ -0,0 +1,181 @@ +#include "pch.h" +#include "ThumbnailCropAndLockWindow.h" + +const std::wstring ThumbnailCropAndLockWindow::ClassName = L"CropAndLock.ThumbnailCropAndLockWindow"; +std::once_flag ThumbnailCropAndLockWindowClassRegistration; + +float ComputeScaleFactor(RECT const& windowRect, RECT const& contentRect) +{ + auto windowWidth = static_cast(windowRect.right - windowRect.left); + auto windowHeight = static_cast(windowRect.bottom - windowRect.top); + auto contentWidth = static_cast(contentRect.right - contentRect.left); + auto contentHeight = static_cast(contentRect.bottom - contentRect.top); + + auto windowRatio = windowWidth / windowHeight; + auto contentRatio = contentWidth / contentHeight; + + auto scaleFactor = windowWidth / contentWidth; + if (windowRatio > contentRatio) + { + scaleFactor = windowHeight / contentHeight; + } + + return scaleFactor; +} + +RECT ComputeDestRect(RECT const& windowRect, RECT const& contentRect) +{ + auto scaleFactor = ComputeScaleFactor(windowRect, contentRect); + + auto windowWidth = static_cast(windowRect.right - windowRect.left); + auto windowHeight = static_cast(windowRect.bottom - windowRect.top); + auto contentWidth = static_cast(contentRect.right - contentRect.left) * scaleFactor; + auto contentHeight = static_cast(contentRect.bottom - contentRect.top) * scaleFactor; + + auto remainingWidth = windowWidth - contentWidth; + auto remainingHeight = windowHeight - contentHeight; + + auto left = static_cast(remainingWidth / 2.0f); + auto top = static_cast(remainingHeight / 2.0f); + auto right = left + static_cast(contentWidth); + auto bottom = top + static_cast(contentHeight); + + return RECT{ left, top, right, bottom }; +} + +void ThumbnailCropAndLockWindow::RegisterWindowClass() +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + WNDCLASSEXW wcex = {}; + wcex.cbSize = sizeof(wcex); + wcex.style = CS_HREDRAW | CS_VREDRAW; + wcex.lpfnWndProc = WndProc; + wcex.hInstance = instance; + wcex.hIcon = LoadIconW(instance, IDI_APPLICATION); + wcex.hCursor = LoadCursorW(nullptr, IDC_ARROW); + wcex.hbrBackground = static_cast(GetStockObject(BLACK_BRUSH)); + wcex.lpszClassName = ClassName.c_str(); + wcex.hIconSm = LoadIconW(wcex.hInstance, IDI_APPLICATION); + winrt::check_bool(RegisterClassExW(&wcex)); +} + +ThumbnailCropAndLockWindow::ThumbnailCropAndLockWindow(std::wstring const& titleString, int width, int height) +{ + auto instance = winrt::check_pointer(GetModuleHandleW(nullptr)); + + std::call_once(ThumbnailCropAndLockWindowClassRegistration, []() { RegisterWindowClass(); }); + + auto exStyle = 0; + auto style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN; + + RECT rect = { 0, 0, width, height}; + winrt::check_bool(AdjustWindowRectEx(&rect, style, false, exStyle)); + auto adjustedWidth = rect.right - rect.left; + auto adjustedHeight = rect.bottom - rect.top; + + winrt::check_bool(CreateWindowExW(exStyle, ClassName.c_str(), titleString.c_str(), style, + CW_USEDEFAULT, CW_USEDEFAULT, adjustedWidth, adjustedHeight, nullptr, nullptr, instance, this)); + WINRT_ASSERT(m_window); +} + +ThumbnailCropAndLockWindow::~ThumbnailCropAndLockWindow() +{ + DisconnectTarget(); + DestroyWindow(m_window); +} + +LRESULT ThumbnailCropAndLockWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) +{ + switch (message) + { + case WM_DESTROY: + if (m_closedCallback != nullptr && !m_destroyed) + { + m_destroyed = true; + m_closedCallback(m_window); + } + break; + case WM_SIZE: + case WM_SIZING: + { + if (m_thumbnail != nullptr) + { + RECT clientRect = {}; + winrt::check_bool(GetClientRect(m_window, &clientRect)); + + m_destRect = ComputeDestRect(clientRect, m_sourceRect); + + DWM_THUMBNAIL_PROPERTIES properties = {}; + properties.dwFlags = DWM_TNP_RECTDESTINATION; + properties.rcDestination = m_destRect; + winrt::check_hresult(DwmUpdateThumbnailProperties(m_thumbnail.get(), &properties)); + } + } + break; + default: + return base_type::MessageHandler(message, wparam, lparam); + } + return 0; +} + +void ThumbnailCropAndLockWindow::CropAndLock(HWND windowToCrop, RECT cropRect) +{ + DisconnectTarget(); + m_currentTarget = windowToCrop; + + // Adjust the crop rect to be in the window space as reported by the DWM + RECT windowRect = {}; + winrt::check_hresult(DwmGetWindowAttribute(m_currentTarget, DWMWA_EXTENDED_FRAME_BOUNDS, reinterpret_cast(&windowRect), sizeof(windowRect))); + auto clientRect = ClientAreaInScreenSpace(m_currentTarget); + auto diffX = clientRect.left - windowRect.left; + auto diffY = clientRect.top - windowRect.top; + auto adjustedCropRect = cropRect; + adjustedCropRect.left += diffX; + adjustedCropRect.top += diffY; + adjustedCropRect.right += diffX; + adjustedCropRect.bottom += diffY; + cropRect = adjustedCropRect; + + // Resize our window + auto width = cropRect.right - cropRect.left; + auto height = cropRect.bottom - cropRect.top; + windowRect = { 0, 0, width, height }; + auto exStyle = static_cast(GetWindowLongPtrW(m_window, GWL_EXSTYLE)); + auto style = static_cast(GetWindowLongPtrW(m_window, GWL_STYLE)); + winrt::check_bool(AdjustWindowRectEx(&windowRect, style, false, exStyle)); + auto adjustedWidth = windowRect.right - windowRect.left; + auto adjustedHeight = windowRect.bottom - windowRect.top; + winrt::check_bool(SetWindowPos(m_window, HWND_TOPMOST, 0, 0, adjustedWidth, adjustedHeight, SWP_NOMOVE | SWP_SHOWWINDOW)); + + // Setup the thumbnail + winrt::check_hresult(DwmRegisterThumbnail(m_window, m_currentTarget, m_thumbnail.addressof())); + + clientRect = {}; + winrt::check_bool(GetClientRect(m_window, &clientRect)); + m_destRect = clientRect; + m_sourceRect = cropRect; + + DWM_THUMBNAIL_PROPERTIES properties = {}; + properties.dwFlags = DWM_TNP_SOURCECLIENTAREAONLY | DWM_TNP_VISIBLE | DWM_TNP_OPACITY | DWM_TNP_RECTDESTINATION | DWM_TNP_RECTSOURCE; + properties.fSourceClientAreaOnly = false; + properties.fVisible = true; + properties.opacity = 255; + properties.rcDestination = m_destRect; + properties.rcSource = m_sourceRect; + winrt::check_hresult(DwmUpdateThumbnailProperties(m_thumbnail.get(), &properties)); +} + +void ThumbnailCropAndLockWindow::Hide() +{ + DisconnectTarget(); + ShowWindow(m_window, SW_HIDE); +} + +void ThumbnailCropAndLockWindow::DisconnectTarget() +{ + if (m_currentTarget != nullptr) + { + m_thumbnail.reset(); + m_currentTarget = nullptr; + } +} diff --git a/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.h b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.h new file mode 100644 index 0000000000..c75fe8c94a --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ThumbnailCropAndLockWindow.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include "CropAndLockWindow.h" + +struct ThumbnailCropAndLockWindow : robmikh::common::desktop::DesktopWindow, CropAndLockWindow +{ + static const std::wstring ClassName; + ThumbnailCropAndLockWindow(std::wstring const& titleString, int width, int height); + ~ThumbnailCropAndLockWindow() override; + LRESULT MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam); + + HWND Handle() override { return m_window; } + void CropAndLock(HWND windowToCrop, RECT cropRect) override; + void OnClosed(std::function callback) override { m_closedCallback = callback; } + +private: + static void RegisterWindowClass(); + + void Hide(); + void DisconnectTarget(); + +private: + HWND m_currentTarget = nullptr; + POINT m_previousPosition = {}; + + unique_hthumbnail m_thumbnail; + RECT m_destRect = {}; + RECT m_sourceRect = {}; + + bool m_destroyed = false; + std::function m_closedCallback; +}; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/ThumbnailUtil.h b/src/modules/CropAndLock/CropAndLock/ThumbnailUtil.h new file mode 100644 index 0000000000..d919869a35 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/ThumbnailUtil.h @@ -0,0 +1,3 @@ +#pragma once + +typedef wil::unique_any unique_hthumbnail; \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/WindowRectUtil.h b/src/modules/CropAndLock/CropAndLock/WindowRectUtil.h new file mode 100644 index 0000000000..8b59fdac32 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/WindowRectUtil.h @@ -0,0 +1,14 @@ +#pragma once + +inline RECT ClientAreaInScreenSpace(HWND window) +{ + POINT clientOrigin = { 0, 0 }; + winrt::check_bool(ClientToScreen(window, &clientOrigin)); + RECT windowBounds = {}; + winrt::check_bool(GetClientRect(window, &windowBounds)); + windowBounds.left += clientOrigin.x; + windowBounds.top += clientOrigin.y; + windowBounds.right += clientOrigin.x; + windowBounds.bottom += clientOrigin.y; + return windowBounds; +} diff --git a/src/modules/CropAndLock/CropAndLock/app.manifest b/src/modules/CropAndLock/CropAndLock/app.manifest new file mode 100644 index 0000000000..92f5a62639 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/app.manifest @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/modules/CropAndLock/CropAndLock/icon1.ico b/src/modules/CropAndLock/CropAndLock/icon1.ico new file mode 100644 index 0000000000..5d06b9f285 Binary files /dev/null and b/src/modules/CropAndLock/CropAndLock/icon1.ico differ diff --git a/src/modules/CropAndLock/CropAndLock/main.cpp b/src/modules/CropAndLock/CropAndLock/main.cpp new file mode 100644 index 0000000000..5945cd63ed --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/main.cpp @@ -0,0 +1,270 @@ +#include "pch.h" +#include "SettingsWindow.h" +#include "OverlayWindow.h" +#include "CropAndLockWindow.h" +#include "ThumbnailCropAndLockWindow.h" +#include "ReparentCropAndLockWindow.h" +#include +#include +#include +#include +#include +#include "ModuleConstants.h" +#include +#include "trace.h" + +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") + +namespace winrt +{ + using namespace Windows::Foundation; + using namespace Windows::Foundation::Numerics; + using namespace Windows::UI; + using namespace Windows::UI::Composition; +} + +namespace util +{ + using namespace robmikh::common::desktop; +} + +const std::wstring instanceMutexName = L"Local\\PowerToys_CropAndLock_InstanceMutex"; +bool m_running = true; + +int WINAPI wWinMain(_In_ HINSTANCE, _In_opt_ HINSTANCE, _In_ PWSTR lpCmdLine, _In_ int) +{ + // Initialize COM + winrt::init_apartment(winrt::apartment_type::single_threaded); + + // Initialize logger automatic logging of exceptions. + LoggerHelpers::init_logger(NonLocalizable::ModuleKey, L"", LogSettings::cropAndLockLoggerName); + InitUnhandledExceptionHandler(); + + if (powertoys_gpo::getConfiguredCropAndLockEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) + { + Logger::warn(L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); + return 0; + } + + // Before we do anything, check to see if we're already running. If we are, + // the hotkey won't register and we'll fail. Instead, we should tell the user + // to kill the other instance and exit this one. + auto mutex = CreateMutex(nullptr, true, instanceMutexName.c_str()); + if (mutex == nullptr) + { + Logger::error(L"Failed to create mutex. {}", get_last_error_or_default(GetLastError())); + } + + if (GetLastError() == ERROR_ALREADY_EXISTS) + { + // CropAndLock is already open. + return 1; + } + + std::wstring pid = std::wstring(lpCmdLine); + if (pid.empty()) + { + Logger::warn(L"Tried to run Crop And Lock as a standalone."); + MessageBoxW(nullptr, L"CropAndLock can't run as a standalone. Start it from PowerToys.", L"CropAndLock", MB_ICONERROR); + return 1; + } + + auto mainThreadId = GetCurrentThreadId(); + ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) { + if (err != ERROR_SUCCESS) + { + Logger::error(L"Failed to wait for parent process exit. {}", get_last_error_or_default(err)); + } + else + { + Logger::trace(L"PowerToys runner exited."); + } + + Logger::trace(L"Exiting CropAndLock"); + PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); + }); + + // NOTE: reparenting a window with a different DPI context has consequences. + // See https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setparent#remarks + // for more info. + winrt::check_bool(SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)); + + // Create the DispatcherQueue that the compositor needs to run + auto controller = util::CreateDispatcherQueueControllerForCurrentThread(); + + // Setup Composition + auto compositor = winrt::Compositor(); + + // Create our overlay window + std::unique_ptr overlayWindow; + + // Keep a list of our cropped windows + std::vector> croppedWindows; + + // Handles and thread for the events sent from runner + HANDLE m_reparent_event_handle; + HANDLE m_thumbnail_event_handle; + HANDLE m_exit_event_handle; + std::thread m_event_triggers_thread; + + std::function removeWindowCallback = [&](HWND windowHandle) + { + if (!m_running) + { + // If we're not running, the reference to croppedWindows might no longer be valid and cause a crash at exit time, due to being called by destructors after wWinMain returns. + return; + } + + auto pos = std::find_if(croppedWindows.begin(), croppedWindows.end(), [windowHandle](auto window) { return window->Handle() == windowHandle; }); + if (pos != croppedWindows.end()) + { + croppedWindows.erase(pos); + } + }; + + std::function ProcessCommand = [&](CropAndLockType mode) + { + std::function windowCroppedCallback = [&, mode](HWND targetWindow, RECT cropRect) { + auto targetInfo = util::WindowInfo(targetWindow); + // TODO: Fix WindowInfo.h to not contain the null char at the end. + auto nullCharIndex = std::wstring::npos; + do + { + nullCharIndex = targetInfo.Title.rfind(L'\0'); + if (nullCharIndex != std::wstring::npos) + { + targetInfo.Title.erase(nullCharIndex); + } + } while (nullCharIndex != std::wstring::npos); + + std::wstringstream titleStream; + titleStream << targetInfo.Title << L" (Cropped)"; + auto title = titleStream.str(); + + std::shared_ptr croppedWindow; + switch (mode) + { + case CropAndLockType::Reparent: + croppedWindow = std::make_shared(title, 800, 600); + Logger::trace(L"Creating a reparent window"); + Trace::CropAndLock::CreateReparentWindow(); + break; + case CropAndLockType::Thumbnail: + croppedWindow = std::make_shared(title, 800, 600); + Logger::trace(L"Creating a thumbnail window"); + Trace::CropAndLock::CreateThumbnailWindow(); + break; + default: + return; + } + croppedWindow->CropAndLock(targetWindow, cropRect); + croppedWindow->OnClosed(removeWindowCallback); + croppedWindows.push_back(croppedWindow); + }; + + overlayWindow.reset(); + + // Get the current window with focus + auto foregroundWindow = GetForegroundWindow(); + if (foregroundWindow != nullptr) + { + bool match = false; + for (auto&& croppedWindow : croppedWindows) + { + if (foregroundWindow == croppedWindow->Handle()) + { + match = true; + break; + } + } + if (!match) + { + overlayWindow = std::make_unique(compositor, foregroundWindow, windowCroppedCallback); + } + } + }; + + // Start a thread to listen on the events. + m_reparent_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); + m_thumbnail_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT); + m_exit_event_handle = CreateEventW(nullptr, false, false, CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); + if (!m_reparent_event_handle || !m_reparent_event_handle || !m_exit_event_handle) + { + Logger::warn(L"Failed to create events. {}", get_last_error_or_default(GetLastError())); + return 1; + } + + m_event_triggers_thread = std::thread([&]() { + MSG msg; + HANDLE event_handles[3] = {m_reparent_event_handle, m_thumbnail_event_handle, m_exit_event_handle}; + while (m_running) + { + DWORD dwEvt = MsgWaitForMultipleObjects(3, event_handles, false, INFINITE, QS_ALLINPUT); + if (!m_running) + { + break; + } + switch (dwEvt) + { + case WAIT_OBJECT_0: + { + // Reparent Event + bool enqueueSucceeded = controller.DispatcherQueue().TryEnqueue([&]() { + ProcessCommand(CropAndLockType::Reparent); + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to reparent a window."); + } + break; + } + case WAIT_OBJECT_0 + 1: + { + // Thumbnail Event + bool enqueueSucceeded = controller.DispatcherQueue().TryEnqueue([&]() { + ProcessCommand(CropAndLockType::Thumbnail); + }); + if (!enqueueSucceeded) + { + Logger::error("Couldn't enqueue message to thumbnail a window."); + } + break; + } + case WAIT_OBJECT_0 + 2: + { + // Exit Event + Logger::trace(L"Received an exit event."); + PostThreadMessage(mainThreadId, WM_QUIT, 0, 0); + break; + } + case WAIT_OBJECT_0 + 3: + if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + break; + default: + break; + } + } + }); + + // Message pump + MSG msg = {}; + while (GetMessageW(&msg, nullptr, 0, 0)) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + m_running = false; + // Needed to unblock MsgWaitForMultipleObjects one last time + SetEvent(m_reparent_event_handle); + CloseHandle(m_reparent_event_handle); + CloseHandle(m_thumbnail_event_handle); + CloseHandle(m_exit_event_handle); + m_event_triggers_thread.join(); + + return util::ShutdownDispatcherQueueControllerAndWait(controller, static_cast(msg.wParam)); +} diff --git a/src/modules/CropAndLock/CropAndLock/packages.config b/src/modules/CropAndLock/CropAndLock/packages.config new file mode 100644 index 0000000000..abf15d0beb --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/pch.cpp b/src/modules/CropAndLock/CropAndLock/pch.cpp new file mode 100644 index 0000000000..bcb5590be1 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/CropAndLock/CropAndLock/pch.h b/src/modules/CropAndLock/CropAndLock/pch.h new file mode 100644 index 0000000000..fe9e5e635e --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/pch.h @@ -0,0 +1,78 @@ +#pragma once + +// Collision from minWinDef min/max and std +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN + +// Windows +#include +#include + +// Must come before C++/WinRT +#include + +// WinRT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// WIL +#include + +// DirectX +#include +#include +#include +#include + +// DWM +#include + +// Shell +#include + +// STL +#include +#include +#include +#include +#include +#include +#include + +// robmikh.common +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Helpers +#include "DisplaysUtil.h" +#include "ThumbnailUtil.h" +#include "WindowRectUtil.h" + +// PowerToys +#include +#include + +// Application resources +#include "resource.h" +#define MAIN_ICON MAKEINTRESOURCEW(IDI_ICON1) \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLock/resource.h b/src/modules/CropAndLock/CropAndLock/resource.h new file mode 100644 index 0000000000..19dd3a8e12 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/resource.h @@ -0,0 +1,26 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by CropAndLock.rc +// +#define IDI_ICON1 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys CropAndLock" +#define INTERNAL_NAME "PowerToys.CropAndLock" +#define ORIGINAL_FILENAME "PowerToys.CropAndLock.exe" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/CropAndLock/CropAndLock/trace.cpp b/src/modules/CropAndLock/CropAndLock/trace.cpp new file mode 100644 index 0000000000..fb5dd802c5 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/trace.cpp @@ -0,0 +1,95 @@ +#include "pch.h" +#include "trace.h" + +// Telemetry strings should not be localized. +#define LoggingProviderKey "Microsoft.PowerToys" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + LoggingProviderKey, + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::RegisterProvider() noexcept +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() noexcept +{ + TraceLoggingUnregister(g_hProvider); +} + +void Trace::CropAndLock::Enable(bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_EnableCropAndLock", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +void Trace::CropAndLock::ActivateReparent() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_ActivateReparent", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::CropAndLock::ActivateThumbnail() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_ActivateThumbnail", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::CropAndLock::CreateReparentWindow() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_CreateReparentWindow", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::CropAndLock::CreateThumbnailWindow() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "CropAndLock_CreateThumbnailWindow", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +// Event to send settings telemetry. +void Trace::CropAndLock::SettingsTelemetry(PowertoyModuleIface::Hotkey& reparentHotkey, PowertoyModuleIface::Hotkey& thumbnailHotkey) noexcept +{ + std::wstring hotKeyStrReparent = + std::wstring(reparentHotkey.win ? L"Win + " : L"") + + std::wstring(reparentHotkey.ctrl ? L"Ctrl + " : L"") + + std::wstring(reparentHotkey.shift ? L"Shift + " : L"") + + std::wstring(reparentHotkey.alt ? L"Alt + " : L"") + + std::wstring(L"VK ") + std::to_wstring(reparentHotkey.key); + + std::wstring hotKeyStrThumbnail = + std::wstring(thumbnailHotkey.win ? L"Win + " : L"") + + std::wstring(thumbnailHotkey.ctrl ? L"Ctrl + " : L"") + + std::wstring(thumbnailHotkey.shift ? L"Shift + " : L"") + + std::wstring(thumbnailHotkey.alt ? L"Alt + " : L"") + + std::wstring(L"VK ") + std::to_wstring(thumbnailHotkey.key); + + TraceLoggingWrite( + g_hProvider, + "CropAndLock_Settings", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingWideString(hotKeyStrReparent.c_str(), "ReparentHotKey"), + TraceLoggingWideString(hotKeyStrThumbnail.c_str(), "ThumbnailHotkey") + ); +} diff --git a/src/modules/CropAndLock/CropAndLock/trace.h b/src/modules/CropAndLock/CropAndLock/trace.h new file mode 100644 index 0000000000..f7e1903ee6 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLock/trace.h @@ -0,0 +1,20 @@ +#pragma once +#include + +class Trace +{ +public: + static void RegisterProvider() noexcept; + static void UnregisterProvider() noexcept; + + class CropAndLock + { + public: + static void Enable(bool enabled) noexcept; + static void ActivateReparent() noexcept; + static void ActivateThumbnail() noexcept; + static void CreateReparentWindow() noexcept; + static void CreateThumbnailWindow() noexcept; + static void SettingsTelemetry(PowertoyModuleIface::Hotkey&, PowertoyModuleIface::Hotkey&) noexcept; + }; +}; diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.rc b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.rc new file mode 100644 index 0000000000..5fa3c8b90d --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.rc @@ -0,0 +1,40 @@ +#include +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +#include "winres.h" +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO +FILEVERSION FILE_VERSION +PRODUCTVERSION PRODUCT_VERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG +FILEFLAGS VS_FF_DEBUG +#else +FILEFLAGS 0x0L +#endif +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset + END +END diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj new file mode 100644 index 0000000000..e32228c44d --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj @@ -0,0 +1,118 @@ + + + + + 16.0 + Win32Proj + {3157fa75-86cf-4ee2-8f62-c43f776493c6} + CropAndLockModuleInterface + CropAndLockModuleInterface + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + ..\..\..\..\$(Platform)\$(Configuration)\ + PowerToys.CropAndLockModuleInterface + + + + ..\;$(SolutionDir)src\common\Telemetry;$(SolutionDir)src;%(AdditionalIncludeDirectories) + + + $(OutDir)$(TargetName)$(TargetExt) + %(AdditionalDependencies) + + + + + true + _DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + + + Windows + true + false + + + + + true + true + true + NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + + + Windows + true + true + true + false + + + + + + + + + + + + + + + + Create + + + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj.filters b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj.filters new file mode 100644 index 0000000000..c249fbe065 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/CropAndLockModuleInterface.vcxproj.filters @@ -0,0 +1,53 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + + + Resource Files + + + + + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp b/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp new file mode 100644 index 0000000000..c313d63cd7 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/dllmain.cpp @@ -0,0 +1,334 @@ +#include "pch.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace NonLocalizable +{ + const wchar_t ModulePath[] = L"PowerToys.CropAndLock.exe"; +} + +namespace +{ + const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; + const wchar_t JSON_KEY_WIN[] = L"win"; + const wchar_t JSON_KEY_ALT[] = L"alt"; + const wchar_t JSON_KEY_CTRL[] = L"ctrl"; + const wchar_t JSON_KEY_SHIFT[] = L"shift"; + const wchar_t JSON_KEY_CODE[] = L"code"; + const wchar_t JSON_KEY_REPARENT_HOTKEY[] = L"reparent-hotkey"; + const wchar_t JSON_KEY_THUMBNAIL_HOTKEY[] = L"thumbnail-hotkey"; + const wchar_t JSON_KEY_VALUE[] = L"value"; +} + +BOOL APIENTRY DllMain( HMODULE /*hModule*/, + DWORD ul_reason_for_call, + LPVOID /*lpReserved*/ + ) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; + +} + +class CropAndLockModuleInterface : public PowertoyModuleIface +{ +public: + // Return the localized display name of the powertoy + virtual PCWSTR get_name() override + { + return app_name.c_str(); + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return app_key.c_str(); + } + + // Return the configured status for the gpo policy for the module + virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override + { + return powertoys_gpo::getConfiguredCropAndLockEnabledValue(); + } + + // Return JSON with the configuration options. + // These are the settings shown on the settings page along with their current values. + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + // Create a Settings object. + PowerToysSettings::Settings settings(hinstance, get_name()); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + // Passes JSON with the configuration settings for the powertoy. + // This is called when the user hits Save on the settings page. + virtual void set_config(const wchar_t* config) override + { + try + { + // Parse the input JSON string. + PowerToysSettings::PowerToyValues values = + PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + + parse_hotkey(values); + + values.save_to_settings_file(); + } + catch (std::exception&) + { + // Improper JSON. + } + } + + virtual bool on_hotkey(size_t hotkeyId) override + { + if (m_enabled) + { + Logger::trace(L"CropAndLock hotkey pressed"); + if (!is_process_running()) + { + Enable(); + } + + if (hotkeyId == 0) { // Same order as set by get_hotkeys + SetEvent(m_reparent_event_handle); + Trace::CropAndLock::ActivateReparent(); + } + if (hotkeyId == 1) { // Same order as set by get_hotkeys + SetEvent(m_thumbnail_event_handle); + Trace::CropAndLock::ActivateThumbnail(); + } + + return true; + } + + return false; + } + + virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override + { + if (hotkeys && buffer_size >= 2) + { + hotkeys[0] = m_reparent_hotkey; + hotkeys[1] = m_thumbnail_hotkey; + } + return 2; + } + + // Enable the powertoy + virtual void enable() + { + Logger::info("CropAndLock enabling"); + Enable(); + } + + // Disable the powertoy + virtual void disable() + { + Logger::info("CropAndLock disabling"); + Disable(true); + } + + // Returns if the powertoy is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + // Destroy the powertoy and free memory + virtual void destroy() override + { + Disable(false); + delete this; + } + + virtual void send_settings_telemetry() override + { + Logger::info("Send settings telemetry"); + Trace::CropAndLock::SettingsTelemetry(m_reparent_hotkey, m_thumbnail_hotkey); + } + + CropAndLockModuleInterface() + { + app_name = L"CropAndLock"; + app_key = NonLocalizable::ModuleKey; + LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::cropAndLockLoggerName); + + m_reparent_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); + m_thumbnail_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_THUMBNAIL_EVENT); + m_exit_event_handle = CreateDefaultEvent(CommonSharedConstants::CROP_AND_LOCK_EXIT_EVENT); + + init_settings(); + } + +private: + void Enable() + { + m_enabled = true; + + // Log telemetry + Trace::CropAndLock::Enable(true); + + // Pass the PID. + unsigned long powertoys_pid = GetCurrentProcessId(); + std::wstring executable_args = L""; + executable_args.append(std::to_wstring(powertoys_pid)); + + ResetEvent(m_reparent_event_handle); + ResetEvent(m_thumbnail_event_handle); + ResetEvent(m_exit_event_handle); + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; + sei.lpFile = NonLocalizable::ModulePath; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + if (ShellExecuteExW(&sei) == false) + { + Logger::error(L"Failed to start CropAndLock"); + auto message = get_last_error_message(GetLastError()); + if (message.has_value()) + { + Logger::error(message.value()); + } + } + else + { + m_hProcess = sei.hProcess; + } + + } + + void Disable(bool const traceEvent) + { + m_enabled = false; + + // We can't just kill the process, since Crop and Lock might need to release any reparented windows first. + SetEvent(m_exit_event_handle); + + ResetEvent(m_reparent_event_handle); + ResetEvent(m_thumbnail_event_handle); + + // Log telemetry + if (traceEvent) + { + Trace::CropAndLock::Enable(false); + } + + if (m_hProcess) + { + m_hProcess = nullptr; + } + + } + + void parse_hotkey(PowerToysSettings::PowerToyValues& settings) + { + auto settingsObject = settings.get_raw_json(); + if (settingsObject.GetView().Size()) + { + try + { + Hotkey _temp_reparent; + auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_REPARENT_HOTKEY).GetNamedObject(JSON_KEY_VALUE); + _temp_reparent.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); + _temp_reparent.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); + _temp_reparent.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); + _temp_reparent.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); + _temp_reparent.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + m_reparent_hotkey = _temp_reparent; + } + catch (...) + { + Logger::error("Failed to initialize CropAndLock reparent shortcut from settings. Value will keep unchanged."); + } + try + { + Hotkey _temp_thumbnail; + auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_THUMBNAIL_HOTKEY).GetNamedObject(JSON_KEY_VALUE); + _temp_thumbnail.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN); + _temp_thumbnail.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT); + _temp_thumbnail.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); + _temp_thumbnail.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); + _temp_thumbnail.key = static_cast(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); + m_thumbnail_hotkey = _temp_thumbnail; + } + catch (...) + { + Logger::error("Failed to initialize CropAndLock thumbnail shortcut from settings. Value will keep unchanged."); + } + } + else + { + Logger::info("CropAndLock settings are empty"); + } + } + + bool is_process_running() + { + return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT; + } + + void init_settings() + { + try + { + // Load and parse the settings file for this PowerToy. + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(get_key()); + + parse_hotkey(settings); + } + catch (std::exception&) + { + Logger::warn(L"An exception occurred while loading the settings file"); + // Error while loading from the settings file. Let default values stay as they are. + } + } + + std::wstring app_name; + std::wstring app_key; //contains the non localized key of the powertoy + + bool m_enabled = false; + HANDLE m_hProcess = nullptr; + + // TODO: actual default hotkey setting in line with other PowerToys. + Hotkey m_reparent_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'R' }; + Hotkey m_thumbnail_hotkey = { .win = true, .ctrl = true, .shift = true, .alt = false, .key = 'T' }; + + HANDLE m_reparent_event_handle; + HANDLE m_thumbnail_event_handle; + HANDLE m_exit_event_handle; + +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new CropAndLockModuleInterface(); +} diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/packages.config b/src/modules/CropAndLock/CropAndLockModuleInterface/packages.config new file mode 100644 index 0000000000..c92dd4bf0c --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/pch.cpp b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.cpp new file mode 100644 index 0000000000..64b7eef6d6 --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/pch.h b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.h new file mode 100644 index 0000000000..0df2e08a6f --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/pch.h @@ -0,0 +1,12 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/src/modules/CropAndLock/CropAndLockModuleInterface/resource.h b/src/modules/CropAndLock/CropAndLockModuleInterface/resource.h new file mode 100644 index 0000000000..c2d7221f0e --- /dev/null +++ b/src/modules/CropAndLock/CropAndLockModuleInterface/resource.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by CropAndLockModuleInterface.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys CropAndLockModuleInterface" +#define INTERNAL_NAME "PowerToys.CropAndLockModuleInterface" +#define ORIGINAL_FILENAME "PowerToys.CropAndLockModuleInterface.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj index 6c17cc5179..d20445e02d 100644 --- a/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj +++ b/src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj @@ -52,22 +52,6 @@ 0436 - - - PublicResXFileCodeGenerator - Resources.Designer.cs - Designer - - - - - - True - True - Resources.resx - - - diff --git a/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/MainWindow.xaml b/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/MainWindow.xaml index 4040b0c4e3..0d8a08da38 100644 --- a/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/MainWindow.xaml +++ b/src/modules/MeasureTool/MeasureToolUI/MeasureToolXAML/MainWindow.xaml @@ -26,7 +26,7 @@ LightTintColor="#F3F3F3" LightTintOpacity="0" /> - + @@ -249,67 +249,90 @@ + Spacing="8"> + Style="{StaticResource ToggleButtonRadioButtonStyle}"> + + + - + + Style="{StaticResource ToggleButtonRadioButtonStyle}"> + + + - + + Style="{StaticResource ToggleButtonRadioButtonStyle}"> + + + - + + Style="{StaticResource ToggleButtonRadioButtonStyle}"> + + + - + - + diff --git a/src/modules/MeasureTool/MeasureToolUI/Properties/Resources.Designer.cs b/src/modules/MeasureTool/MeasureToolUI/Properties/Resources.Designer.cs deleted file mode 100644 index 2bc4426978..0000000000 --- a/src/modules/MeasureTool/MeasureToolUI/Properties/Resources.Designer.cs +++ /dev/null @@ -1,108 +0,0 @@ -//------------------------------------------------------------------------------ -// -// 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 PowerToys.MeasureToolUI.Properties { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PowerToys.MeasureToolUI.Properties.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Bounds (Ctrl+1). - /// - public static string Bounds { - get { - return ResourceManager.GetString("Bounds", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Close (Esc). - /// - public static string Close { - get { - return ResourceManager.GetString("Close", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Horizontal spacing (Ctrl+3). - /// - public static string HorizontalSpacing { - get { - return ResourceManager.GetString("HorizontalSpacing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Spacing (Ctrl+2). - /// - public static string Spacing { - get { - return ResourceManager.GetString("Spacing", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Vertical spacing (Ctrl+4). - /// - public static string VerticalSpacing { - get { - return ResourceManager.GetString("VerticalSpacing", resourceCulture); - } - } - } -} diff --git a/src/modules/MeasureTool/MeasureToolUI/Properties/Resources.resx b/src/modules/MeasureTool/MeasureToolUI/Properties/Resources.resx deleted file mode 100644 index 8ace3823a3..0000000000 --- a/src/modules/MeasureTool/MeasureToolUI/Properties/Resources.resx +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - Bounds (Ctrl+1) - - - Spacing (Ctrl+2) - - - Horizontal spacing (Ctrl+3) - - - Vertical spacing (Ctrl+4) - - - Close (Esc) - - \ No newline at end of file diff --git a/src/modules/MeasureTool/MeasureToolUI/Strings/en-us/Resources.resw b/src/modules/MeasureTool/MeasureToolUI/Strings/en-us/Resources.resw index b959f3e2b5..b33f9d43f0 100644 --- a/src/modules/MeasureTool/MeasureToolUI/Strings/en-us/Resources.resw +++ b/src/modules/MeasureTool/MeasureToolUI/Strings/en-us/Resources.resw @@ -39,4 +39,31 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Bounds (Ctrl+1) + + + Bounds (Ctrl+1) + + + Spacing (Ctrl+2) + + + Spacing (Ctrl+2) + + + Horizontal spacing (Ctrl+3) + + + Horizontal spacing (Ctrl+3) + + + Vertical spacing (Ctrl+4) + + + Vertical spacing (Ctrl+4) + + + Close (Esc) + \ No newline at end of file diff --git a/src/modules/alwaysontop/AlwaysOnTop/trace.cpp b/src/modules/alwaysontop/AlwaysOnTop/trace.cpp index 591f56d7f7..ae32849bbf 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/trace.cpp +++ b/src/modules/alwaysontop/AlwaysOnTop/trace.cpp @@ -52,4 +52,4 @@ void Trace::AlwaysOnTop::UnpinWindow() noexcept EventUnpinWindowKey, ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); -} \ No newline at end of file +} diff --git a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/DefaultLayouts.cpp b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/DefaultLayouts.cpp index 56871183bc..bb8ea21f4c 100644 --- a/src/modules/fancyzones/FancyZonesLib/FancyZonesData/DefaultLayouts.cpp +++ b/src/modules/fancyzones/FancyZonesLib/FancyZonesData/DefaultLayouts.cpp @@ -171,5 +171,20 @@ LayoutData DefaultLayouts::GetDefaultLayout(MonitorConfigurationType type) const return iter->second; } + switch (type) + { + case MonitorConfigurationType::Horizontal: + return LayoutData{}; + case MonitorConfigurationType::Vertical: + return LayoutData{ + .uuid = GUID_NULL, + .type = FancyZonesDataTypes::ZoneSetLayoutType::Rows, + .showSpacing = DefaultValues::ShowSpacing, + .spacing = DefaultValues::Spacing, + .zoneCount = DefaultValues::ZoneCount, + .sensitivityRadius = DefaultValues::SensitivityRadius + }; + } + return LayoutData{}; } diff --git a/src/modules/fancyzones/FancyZonesTests/UnitTests/DefaultLayoutsTests.Spec.cpp b/src/modules/fancyzones/FancyZonesTests/UnitTests/DefaultLayoutsTests.Spec.cpp index 601145897e..c42679ee68 100644 --- a/src/modules/fancyzones/FancyZonesTests/UnitTests/DefaultLayoutsTests.Spec.cpp +++ b/src/modules/fancyzones/FancyZonesTests/UnitTests/DefaultLayoutsTests.Spec.cpp @@ -96,15 +96,21 @@ namespace FancyZonesUnitTests .sensitivityRadius = DefaultValues::SensitivityRadius }; + LayoutData rows{ + .uuid = GUID_NULL, + .type = FancyZonesDataTypes::ZoneSetLayoutType::Rows, + .showSpacing = DefaultValues::ShowSpacing, + .spacing = DefaultValues::Spacing, + .zoneCount = DefaultValues::ZoneCount, + .sensitivityRadius = DefaultValues::SensitivityRadius + }; + Assert::IsTrue(priorityGrid == DefaultLayouts::instance().GetDefaultLayout(MonitorConfigurationType::Horizontal)); - Assert::IsTrue(priorityGrid == DefaultLayouts::instance().GetDefaultLayout(MonitorConfigurationType::Vertical)); + Assert::IsTrue(rows == DefaultLayouts::instance().GetDefaultLayout(MonitorConfigurationType::Vertical)); } TEST_METHOD (DefaultLayoutsNoFile) { - // test - DefaultLayouts::instance().LoadData(); - LayoutData priorityGrid{ .uuid = GUID_NULL, .type = FancyZonesDataTypes::ZoneSetLayoutType::PriorityGrid, @@ -114,8 +120,20 @@ namespace FancyZonesUnitTests .sensitivityRadius = DefaultValues::SensitivityRadius }; + LayoutData rows{ + .uuid = GUID_NULL, + .type = FancyZonesDataTypes::ZoneSetLayoutType::Rows, + .showSpacing = DefaultValues::ShowSpacing, + .spacing = DefaultValues::Spacing, + .zoneCount = DefaultValues::ZoneCount, + .sensitivityRadius = DefaultValues::SensitivityRadius + }; + + // test + DefaultLayouts::instance().LoadData(); + Assert::IsTrue(priorityGrid == DefaultLayouts::instance().GetDefaultLayout(MonitorConfigurationType::Horizontal)); - Assert::IsTrue(priorityGrid == DefaultLayouts::instance().GetDefaultLayout(MonitorConfigurationType::Vertical)); + Assert::IsTrue(rows == DefaultLayouts::instance().GetDefaultLayout(MonitorConfigurationType::Vertical)); } }; } \ No newline at end of file diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs index faec50e2aa..d0fa03ea09 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/App.xaml.cs @@ -78,8 +78,6 @@ namespace FancyZonesEditor _themeManager = new ThemeManager(this); - var parseResult = FancyZonesEditorIO.ParseParams(); - RunnerHelper.WaitForPowerToysRunner(PowerToysPID, () => { Logger.LogInfo("Runner exited"); @@ -87,27 +85,8 @@ namespace FancyZonesEditor Application.Current.Dispatcher.Invoke(Application.Current.Shutdown); }); - if (!parseResult.Result) - { - Logger.LogError(ParsingErrorReportTag + ": " + parseResult.Message + "; " + ParsingErrorDataTag + ": " + parseResult.MalformedData); - MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); - } + var parseResult = FancyZonesEditorIO.ParseParams(); - parseResult = FancyZonesEditorIO.ParseAppliedLayouts(); - if (!parseResult.Result) - { - Logger.LogError(ParsingErrorReportTag + ": " + parseResult.Message + "; " + ParsingErrorDataTag + ": " + parseResult.MalformedData); - MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); - } - - parseResult = FancyZonesEditorIO.ParseCustomLayouts(); - if (!parseResult.Result) - { - Logger.LogError(ParsingErrorReportTag + ": " + parseResult.Message + "; " + ParsingErrorDataTag + ": " + parseResult.MalformedData); - MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); - } - - parseResult = FancyZonesEditorIO.ParseLayoutHotkeys(); if (!parseResult.Result) { Logger.LogError(ParsingErrorReportTag + ": " + parseResult.Message + "; " + ParsingErrorDataTag + ": " + parseResult.MalformedData); @@ -121,6 +100,13 @@ namespace FancyZonesEditor MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); } + parseResult = FancyZonesEditorIO.ParseCustomLayouts(); + if (!parseResult.Result) + { + Logger.LogError(ParsingErrorReportTag + ": " + parseResult.Message + "; " + ParsingErrorDataTag + ": " + parseResult.MalformedData); + MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); + } + parseResult = FancyZonesEditorIO.ParseDefaultLayouts(); if (!parseResult.Result) { @@ -128,6 +114,20 @@ namespace FancyZonesEditor MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); } + parseResult = FancyZonesEditorIO.ParseLayoutHotkeys(); + if (!parseResult.Result) + { + Logger.LogError(ParsingErrorReportTag + ": " + parseResult.Message + "; " + ParsingErrorDataTag + ": " + parseResult.MalformedData); + MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); + } + + parseResult = FancyZonesEditorIO.ParseAppliedLayouts(); + if (!parseResult.Result) + { + Logger.LogError(ParsingErrorReportTag + ": " + parseResult.Message + "; " + ParsingErrorDataTag + ": " + parseResult.MalformedData); + MessageBox.Show(parseResult.Message, FancyZonesEditor.Properties.Resources.Error_Parsing_Data_Title, MessageBoxButton.OK); + } + MainWindowSettingsModel settings = ((App)Current).MainWindowSettings; settings.UpdateSelectedLayoutModel(); diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs b/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs index 4a29c81194..908d5fe32f 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/EditorWindow.cs @@ -28,7 +28,7 @@ namespace FancyZonesEditor MainWindowSettingsModel settings = ((App)Application.Current).MainWindowSettings; settings.SetAppliedModel(model); - App.Overlay.SetLayoutSettings(App.Overlay.Monitors[App.Overlay.CurrentDesktop], model); + App.Overlay.Monitors[App.Overlay.CurrentDesktop].SetLayoutSettings(model); } App.FancyZonesEditorIO.SerializeLayoutTemplates(); diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs index 89616f6b43..5c2eaeee36 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/MainWindow.xaml.cs @@ -296,7 +296,7 @@ namespace FancyZonesEditor model.Persist(); - App.Overlay.SetLayoutSettings(App.Overlay.Monitors[App.Overlay.CurrentDesktop], model); + App.Overlay.Monitors[App.Overlay.CurrentDesktop].SetLayoutSettings(model); App.FancyZonesEditorIO.SerializeAppliedLayouts(); App.FancyZonesEditorIO.SerializeCustomLayouts(); } @@ -318,7 +318,7 @@ namespace FancyZonesEditor if (mainEditor.CurrentDataContext is LayoutModel model) { _settings.SetAppliedModel(model); - App.Overlay.SetLayoutSettings(App.Overlay.Monitors[App.Overlay.CurrentDesktop], model); + App.Overlay.Monitors[App.Overlay.CurrentDesktop].SetLayoutSettings(model); App.FancyZonesEditorIO.SerializeAppliedLayouts(); App.FancyZonesEditorIO.SerializeCustomLayouts(); } @@ -366,7 +366,7 @@ namespace FancyZonesEditor _backup = new CanvasLayoutModel(canvas); } - _defaultLayoutsBackup = new List(MainWindowSettingsModel.DefaultLayouts.DefaultLayouts); + _defaultLayoutsBackup = new List(MainWindowSettingsModel.DefaultLayouts.Layouts); Keyboard.ClearFocus(); EditLayoutDialogTitle.Text = string.Format(CultureInfo.CurrentCulture, Properties.Resources.Edit_Template, ((LayoutModel)dataContext).Name); @@ -480,7 +480,7 @@ namespace FancyZonesEditor // update current settings if (model == _settings.AppliedModel) { - App.Overlay.SetLayoutSettings(App.Overlay.Monitors[App.Overlay.CurrentDesktop], model); + App.Overlay.Monitors[App.Overlay.CurrentDesktop].SetLayoutSettings(model); } App.FancyZonesEditorIO.SerializeAppliedLayouts(); @@ -528,7 +528,7 @@ namespace FancyZonesEditor { if (monitor.Settings.ZonesetUuid == model.Uuid) { - App.Overlay.SetLayoutSettings(monitor, _settings.BlankModel); + monitor.SetLayoutSettings(_settings.BlankModel); } } diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/DefaultLayoutsModel.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/DefaultLayoutsModel.cs index 5b3167acc1..770c5ec626 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/DefaultLayoutsModel.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/DefaultLayoutsModel.cs @@ -13,7 +13,7 @@ namespace FancyZonesEditor.Models { private static int Count { get; } = Enum.GetValues(typeof(MonitorConfigurationType)).Length; - public List DefaultLayouts { get; } = new List(Count); + public List Layouts { get; } = new List(Count); public DefaultLayoutsModel() { @@ -23,30 +23,39 @@ namespace FancyZonesEditor.Models public void Reset(MonitorConfigurationType type) { - Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.PriorityGrid], type); + switch (type) + { + case MonitorConfigurationType.Horizontal: + Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.PriorityGrid], type); + break; + case MonitorConfigurationType.Vertical: + Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.Rows], type); + break; + } } public void Reset(string uuid) { - for (int i = 0; i < Count; i++) + if (Layouts[(int)MonitorConfigurationType.Horizontal].Uuid == uuid) { - if (DefaultLayouts[i].Uuid == uuid) - { - Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.PriorityGrid], (MonitorConfigurationType)i); - break; - } + Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.PriorityGrid], MonitorConfigurationType.Horizontal); + } + + if (Layouts[(int)MonitorConfigurationType.Vertical].Uuid == uuid) + { + Set(MainWindowSettingsModel.TemplateModels[(int)LayoutType.Rows], MonitorConfigurationType.Vertical); } } public void Set(LayoutModel layout, MonitorConfigurationType type) { - if (DefaultLayouts.Count <= (int)type) + if (Layouts.Count <= (int)type) { - DefaultLayouts.Insert((int)type, layout); + Layouts.Insert((int)type, layout); } else { - DefaultLayouts[(int)type] = layout; + Layouts[(int)type] = layout; } FirePropertyChanged(); diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs index a587414213..b7b244cdf2 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/LayoutModel.cs @@ -150,7 +150,7 @@ namespace FancyZonesEditor.Models { get { - return MainWindowSettingsModel.DefaultLayouts.DefaultLayouts[(int)MonitorConfigurationType.Horizontal].Uuid == this.Uuid; + return MainWindowSettingsModel.DefaultLayouts.Layouts[(int)MonitorConfigurationType.Horizontal].Uuid == this.Uuid; } } @@ -158,7 +158,7 @@ namespace FancyZonesEditor.Models { get { - return MainWindowSettingsModel.DefaultLayouts.DefaultLayouts[(int)MonitorConfigurationType.Horizontal].Uuid != this.Uuid; + return MainWindowSettingsModel.DefaultLayouts.Layouts[(int)MonitorConfigurationType.Horizontal].Uuid != this.Uuid; } } @@ -166,7 +166,7 @@ namespace FancyZonesEditor.Models { get { - return MainWindowSettingsModel.DefaultLayouts.DefaultLayouts[(int)MonitorConfigurationType.Vertical].Uuid == this.Uuid; + return MainWindowSettingsModel.DefaultLayouts.Layouts[(int)MonitorConfigurationType.Vertical].Uuid == this.Uuid; } } @@ -174,7 +174,7 @@ namespace FancyZonesEditor.Models { get { - return MainWindowSettingsModel.DefaultLayouts.DefaultLayouts[(int)MonitorConfigurationType.Vertical].Uuid != this.Uuid; + return MainWindowSettingsModel.DefaultLayouts.Layouts[(int)MonitorConfigurationType.Vertical].Uuid != this.Uuid; } } diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/MainWindowSettingsModel.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/MainWindowSettingsModel.cs index 2d5cdd3456..4cea41f04d 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/MainWindowSettingsModel.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/MainWindowSettingsModel.cs @@ -81,7 +81,7 @@ namespace FancyZonesEditor // set default layouts DefaultLayouts.Set(priorityGridModel, MonitorConfigurationType.Horizontal); - DefaultLayouts.Set(priorityGridModel, MonitorConfigurationType.Vertical); + DefaultLayouts.Set(rowsModel, MonitorConfigurationType.Vertical); } // IsShiftKeyPressed - is the shift key currently being held down @@ -261,11 +261,6 @@ namespace FancyZonesEditor } } - if (foundModel == null) - { - foundModel = TemplateModels[(int)LayoutType.PriorityGrid]; - } - SetSelectedModel(foundModel); SetAppliedModel(foundModel); FirePropertyChanged(nameof(IsCustomLayoutActive)); diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Models/Monitor.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Models/Monitor.cs index 489a2b27e0..ab0096862d 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Models/Monitor.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Models/Monitor.cs @@ -14,21 +14,45 @@ namespace FancyZonesEditor.Models { public LayoutOverlayWindow Window { get; private set; } - public LayoutSettings Settings { get; set; } - public Device Device { get; set; } + public LayoutSettings Settings + { + get + { + if (_settings != null) + { + return _settings; + } + + return DefaultLayoutSettings; + } + + set + { + _settings = value; + } + } + + public bool IsInitialized + { + get + { + return _settings != null; + } + } + + public MonitorConfigurationType MonitorConfigurationType + { + get + { + return Device.MonitorSize.Width > Device.MonitorSize.Height ? MonitorConfigurationType.Horizontal : MonitorConfigurationType.Vertical; + } + } + public Monitor(Rect workArea, Size monitorSize) { Window = new LayoutOverlayWindow(); - Settings = new LayoutSettings(); - - // provide a good default for vertical monitors - if (monitorSize.Height > monitorSize.Width) - { - Settings.Type = LayoutType.Rows; - } - Device = new Device(workArea, monitorSize); if (App.DebugMode) @@ -54,6 +78,8 @@ namespace FancyZonesEditor.Models Device = new Device(monitorName, monitorInstanceId, monitorSerialNumber, virtualDesktop, dpi, workArea, monitorSize); } + private LayoutSettings _settings; + public void Scale(double scaleFactor) { Device.Scale(scaleFactor); @@ -64,5 +90,48 @@ namespace FancyZonesEditor.Models Window.Width = workArea.Width; Window.Height = workArea.Height; } + + public void SetLayoutSettings(LayoutModel model) + { + if (model == null) + { + return; + } + + if (_settings == null) + { + _settings = new LayoutSettings(); + } + + _settings.ZonesetUuid = model.Uuid; + _settings.Type = model.Type; + _settings.SensitivityRadius = model.SensitivityRadius; + _settings.ZoneCount = model.TemplateZoneCount; + + if (model is GridLayoutModel grid) + { + _settings.ShowSpacing = grid.ShowSpacing; + _settings.Spacing = grid.Spacing; + } + else + { + _settings.ShowSpacing = false; + _settings.Spacing = 0; + } + } + + private LayoutSettings DefaultLayoutSettings + { + get + { + LayoutSettings settings = new LayoutSettings(); + if (MonitorConfigurationType == MonitorConfigurationType.Vertical) + { + settings.Type = LayoutType.Rows; + } + + return settings; + } + } } } diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs index 5677fb565f..1e55c3f61f 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Overlay.cs @@ -180,30 +180,6 @@ namespace FancyZonesEditor } } - public void SetLayoutSettings(Monitor monitor, LayoutModel model) - { - if (model == null) - { - return; - } - - monitor.Settings.ZonesetUuid = model.Uuid; - monitor.Settings.Type = model.Type; - monitor.Settings.SensitivityRadius = model.SensitivityRadius; - monitor.Settings.ZoneCount = model.TemplateZoneCount; - - if (model is GridLayoutModel grid) - { - monitor.Settings.ShowSpacing = grid.ShowSpacing; - monitor.Settings.Spacing = grid.Spacing; - } - else - { - monitor.Settings.ShowSpacing = false; - monitor.Settings.Spacing = 0; - } - } - public void OpenEditor(LayoutModel model) { Logger.LogTrace(); diff --git a/src/modules/fancyzones/editor/FancyZonesEditor/Utils/FancyZonesEditorIO.cs b/src/modules/fancyzones/editor/FancyZonesEditor/Utils/FancyZonesEditorIO.cs index 2ddf1cafaf..604c8bf33e 100644 --- a/src/modules/fancyzones/editor/FancyZonesEditor/Utils/FancyZonesEditorIO.cs +++ b/src/modules/fancyzones/editor/FancyZonesEditor/Utils/FancyZonesEditorIO.cs @@ -1159,15 +1159,17 @@ namespace FancyZonesEditor.Utils foreach (var layout in layouts) { + LayoutModel defaultLayoutModel = null; + MonitorConfigurationType type = JsonTagToMonitorConfigurationType(layout.MonitorConfiguration); + if (layout.Layout.Uuid != null && layout.Layout.Uuid != string.Empty) { - MonitorConfigurationType type = JsonTagToMonitorConfigurationType(layout.MonitorConfiguration); - foreach (var customLayout in MainWindowSettingsModel.CustomModels) { if (customLayout.Uuid == layout.Layout.Uuid) { MainWindowSettingsModel.DefaultLayouts.Set(customLayout, type); + defaultLayoutModel = customLayout; break; } } @@ -1175,8 +1177,28 @@ namespace FancyZonesEditor.Utils else { LayoutType layoutType = JsonTagToLayoutType(layout.Layout.Type); - MonitorConfigurationType type = JsonTagToMonitorConfigurationType(layout.MonitorConfiguration); - MainWindowSettingsModel.DefaultLayouts.Set(MainWindowSettingsModel.TemplateModels[(int)layoutType], type); + defaultLayoutModel = MainWindowSettingsModel.TemplateModels[(int)layoutType]; + defaultLayoutModel.TemplateZoneCount = layout.Layout.ZoneCount; + defaultLayoutModel.SensitivityRadius = layout.Layout.SensitivityRadius; + + if (defaultLayoutModel is GridLayoutModel gridDefaultLayoutModel) + { + gridDefaultLayoutModel.ShowSpacing = layout.Layout.ShowSpacing; + gridDefaultLayoutModel.Spacing = layout.Layout.Spacing; + } + + MainWindowSettingsModel.DefaultLayouts.Set(defaultLayoutModel, type); + } + + if (defaultLayoutModel != null) + { + foreach (Monitor monitor in App.Overlay.Monitors) + { + if (!monitor.IsInitialized && monitor.MonitorConfigurationType == type) + { + monitor.SetLayoutSettings(defaultLayoutModel); + } + } } } diff --git a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs index 0d4220dae9..d89bb200a2 100644 --- a/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs +++ b/src/modules/launcher/Plugins/Microsoft.Plugin.WindowWalker/Components/OpenWindows.cs @@ -40,7 +40,7 @@ namespace Microsoft.Plugin.WindowWalker.Components /// /// Gets the list of all open windows /// - internal List Windows => windows; + internal List Windows => new List(windows); /// /// Gets an instance property of this class that makes sure that diff --git a/src/modules/powerrename/PowerRenameContextMenu/pch.h b/src/modules/powerrename/PowerRenameContextMenu/pch.h index 885d5d62e4..ce11aa6d9d 100644 --- a/src/modules/powerrename/PowerRenameContextMenu/pch.h +++ b/src/modules/powerrename/PowerRenameContextMenu/pch.h @@ -7,6 +7,7 @@ #ifndef PCH_H #define PCH_H +#define NOMINMAX // add headers that you want to pre-compile here #include "framework.h" diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl index c488560308..c6b86bfb77 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.idl @@ -14,6 +14,7 @@ namespace PowerRenameUI ExplorerItemsSource ExplorerItems { get; }; Windows.Foundation.Collections.IObservableVector SearchRegExShortcuts { get; }; Windows.Foundation.Collections.IObservableVector DateTimeShortcuts { get; }; + Windows.Foundation.Collections.IObservableVector CounterShortcuts { get; }; String OriginalCount; String RenamedCount; diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml index 6b75375566..cecbeea866 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml @@ -323,6 +323,8 @@ + + @@ -364,7 +366,45 @@ - + + + + + + + + + + + + + + + + + @@ -391,9 +431,9 @@ HorizontalAlignment="Stretch" AutomationProperties.LabeledBy="{Binding ElementName=ApplyToLabel}" SelectedIndex="0"> - Filename + extension - Filename only - Extension only + + + +#include #include #include #include @@ -133,8 +134,7 @@ namespace winrt::PowerRenameUI::implementation GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE::MDT_EFFECTIVE_DPI, &x_dpi, &x_dpi); UINT window_dpi = GetDpiForWindow(m_window); - int width = 1400; - int height = 800; + const auto& [width, height] = CSettingsInstance().GetLastWindowSize(); winrt::Windows::Graphics::RectInt32 rect; // Scale window size @@ -196,6 +196,13 @@ namespace winrt::PowerRenameUI::implementation m_dateTimeShortcuts.Append(winrt::make(L"$ff", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_MilliSeconds2D").ValueAsString())); m_dateTimeShortcuts.Append(winrt::make(L"$f", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_MilliSeconds1D").ValueAsString())); + m_CounterShortcuts = winrt::single_threaded_observable_vector(); + m_CounterShortcuts.Append(winrt::make(L"${}", manager.MainResourceMap().GetValue(L"Resources/CounterCheatSheet_Simple").ValueAsString())); + m_CounterShortcuts.Append(winrt::make(L"${start=10}", manager.MainResourceMap().GetValue(L"Resources/CounterCheatSheet_Start").ValueAsString())); + m_CounterShortcuts.Append(winrt::make(L"${increment=5}", manager.MainResourceMap().GetValue(L"Resources/CounterCheatSheet_Increment").ValueAsString())); + m_CounterShortcuts.Append(winrt::make(L"${padding=8}", manager.MainResourceMap().GetValue(L"Resources/CounterCheatSheet_Padding").ValueAsString())); + m_CounterShortcuts.Append(winrt::make(L"${increment=3,padding=4,start=900}", manager.MainResourceMap().GetValue(L"Resources/CounterCheatSheet_Complex").ValueAsString())); + InitializeComponent(); listView_ExplorerItems().ApplyTemplate(); @@ -278,6 +285,23 @@ namespace winrt::PowerRenameUI::implementation InitAutoComplete(); SearchReplaceChanged(); InvalidateItemListViewState(); + + SizeChanged({ this, &MainWindow::OnSizeChanged }); + Closed({ this, &MainWindow::OnClosed }); + } + + void MainWindow::OnSizeChanged(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::WindowSizeChangedEventArgs const& /*args*/) + { + const auto appWindow = + Microsoft::UI::Windowing::AppWindow::GetFromWindowId(Microsoft::UI::GetWindowIdFromWindow(m_window)); + const auto [width, height] = appWindow.Size(); + + CSettingsInstance().UpdateLastWindowSize(static_cast(width), static_cast(height)); + } + + void MainWindow::OnClosed(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::WindowEventArgs const&) + { + CSettingsInstance().Save(); } void MainWindow::InvalidateItemListViewState() diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h index 70759e3ff7..ed9d6a6c2b 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.h @@ -74,6 +74,9 @@ namespace winrt::PowerRenameUI::implementation MainWindow(); + void OnSizeChanged(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::WindowSizeChangedEventArgs const&); + void OnClosed(winrt::Windows::Foundation::IInspectable const&, winrt::Microsoft::UI::Xaml::WindowEventArgs const&); + void InvalidateItemListViewState(); Windows::Foundation::Collections::IObservableVector SearchMRU() { return m_searchMRUList; } @@ -81,6 +84,8 @@ namespace winrt::PowerRenameUI::implementation PowerRenameUI::ExplorerItemsSource ExplorerItems() { return m_explorerItems; } winrt::Windows::Foundation::Collections::IObservableVector SearchRegExShortcuts() { return m_searchRegExShortcuts; } winrt::Windows::Foundation::Collections::IObservableVector DateTimeShortcuts() { return m_dateTimeShortcuts; } + winrt::Windows::Foundation::Collections::IObservableVector CounterShortcuts() { return m_CounterShortcuts; } + hstring OriginalCount(); void OriginalCount(hstring value); hstring RenamedCount(); @@ -93,6 +98,7 @@ namespace winrt::PowerRenameUI::implementation void ShowRenamed(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e); private: + winrt::Windows::Foundation::Size m_lastWindowSize; bool m_allSelected; winrt::Windows::Foundation::Collections::IObservableVector m_searchMRUList; @@ -100,6 +106,7 @@ namespace winrt::PowerRenameUI::implementation PowerRenameUI::ExplorerItemsSource m_explorerItems; winrt::Windows::Foundation::Collections::IObservableVector m_searchRegExShortcuts; winrt::Windows::Foundation::Collections::IObservableVector m_dateTimeShortcuts; + winrt::Windows::Foundation::Collections::IObservableVector m_CounterShortcuts; // Used by PowerRenameManagerEvents HRESULT OnRename(_In_ IPowerRenameItem* renameItem); diff --git a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw index c54f9910a3..a07c601c69 100644 --- a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw +++ b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw @@ -150,6 +150,24 @@ Replace with + + Replace using advanced counter syntax. + + + A simple counter that you can use anywhere in a replace string. + + + A counter with a customized start value. + + + A counter with a customized increment value. + + + A counter with a customized number of leading padding zeroes. + + + A counter showing multiple customized properties. + Replace using file creation date and time @@ -336,6 +354,13 @@ File name contains invalid character(s): > < | " : ? * \ / - - + + Filename + extension + + + Filename only + + + Extension only + diff --git a/src/modules/powerrename/dll/pch.h b/src/modules/powerrename/dll/pch.h index 27ab94b42f..2d9c5fb252 100644 --- a/src/modules/powerrename/dll/pch.h +++ b/src/modules/powerrename/dll/pch.h @@ -3,6 +3,7 @@ #include "targetver.h" #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +#define NOMINMAX // Windows Header Files: #include #include diff --git a/src/modules/powerrename/lib/Enumerating.cpp b/src/modules/powerrename/lib/Enumerating.cpp new file mode 100644 index 0000000000..1ea275720c --- /dev/null +++ b/src/modules/powerrename/lib/Enumerating.cpp @@ -0,0 +1,46 @@ +#include + +#include "Enumerating.h" + +#include + +std::vector parseEnumOptions(const std::wstring& replaceWith) +{ + static const std::wregex enumStartRegex(LR"(start=(\d+))"); + static const std::wregex enumIncrementRegex(LR"(increment=(-?\d+))"); + static const std::wregex enumPaddingRegex(LR"(padding=(\d+))"); + + std::string buf; + std::vector options; + std::wregex enumGroupRegex(LR"(\$\{.*?\})"); + for (std::wsregex_iterator i{ begin(replaceWith), end(replaceWith), enumGroupRegex }, end; i != end; ++i) + { + std::wsmatch match = *i; + std::wstring matchString = match.str(); + + EnumOptions option; + option.replaceStrSpan.offset = match.position(); + option.replaceStrSpan.length = match.length(); + + std::wsmatch subMatch; + if (std::regex_search(matchString, subMatch, enumStartRegex)) + { + buf = unwide(subMatch[1].str()); + std::from_chars(buf.data(), buf.data() + buf.size(), option.start.emplace()); + } + if (std::regex_search(matchString, subMatch, enumIncrementRegex)) + { + buf = unwide(subMatch[1].str()); + std::from_chars(buf.data(), buf.data() + buf.size(), option.increment.emplace()); + } + if (std::regex_search(matchString, subMatch, enumPaddingRegex)) + { + buf = unwide(subMatch[1].str()); + std::from_chars(buf.data(), buf.data() + buf.size(), option.padding.emplace()); + } + + options.emplace_back(std::move(option)); + } + + return options; +} diff --git a/src/modules/powerrename/lib/Enumerating.h b/src/modules/powerrename/lib/Enumerating.h new file mode 100644 index 0000000000..7318daa66d --- /dev/null +++ b/src/modules/powerrename/lib/Enumerating.h @@ -0,0 +1,55 @@ +#pragma once + +#include "pch.h" + +struct EnumSpan +{ + size_t offset = 0; + size_t length = 0; +}; + +struct EnumOptions +{ + std::optional start; + std::optional increment; + std::optional padding; + + EnumSpan replaceStrSpan; + + std::strong_ordering operator<=>(const EnumOptions& rhs) const noexcept + { + return std::make_tuple(start, increment, padding) <=> std::make_tuple(rhs.start, rhs.increment, rhs.padding); + } + + bool operator==(const EnumOptions& rhs) const noexcept + { + return std::make_tuple(start, increment, padding) == std::make_tuple(rhs.start, rhs.increment, rhs.padding); + } +}; + +std::vector parseEnumOptions(const std::wstring& replaceWith); + +struct Enumerator +{ + inline Enumerator(EnumOptions options) : + start{ options.start.value_or(0) }, increment{ options.increment.value_or(1) }, padding{ options.padding.value_or(0) }, replaceStrSpan{ options.replaceStrSpan } + { + } + + inline int32_t enumerate(const unsigned long index) const { return start + static_cast(index * increment); } + + inline size_t printTo(wchar_t* buf, const size_t bufSize, const unsigned long index) const + { + const int32_t enumeratedIndex = enumerate(index); + wchar_t format[32]; + swprintf_s(format, sizeof(format) / sizeof(wchar_t), L"%%0%ud", padding); + return swprintf_s(buf, bufSize, format, enumeratedIndex); + } + + EnumSpan replaceStrSpan; + +private: + int start; + int increment; + uint32_t padding; +}; diff --git a/src/modules/powerrename/lib/Helpers.cpp b/src/modules/powerrename/lib/Helpers.cpp index ecd823e4d7..73991c8e9b 100644 --- a/src/modules/powerrename/lib/Helpers.cpp +++ b/src/modules/powerrename/lib/Helpers.cpp @@ -188,7 +188,7 @@ HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR sour { hr = StringCchCopy(result, cchMax, source); } - } + } else if (flags & Capitalized) { if (!(flags & ExtensionOnly)) @@ -234,15 +234,13 @@ HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR sour return hr; } -bool isFileTimeUsed(_In_ PCWSTR source) +bool isFileTimeUsed(_In_ PCWSTR source) { bool used = false; - std::wstring patterns[] = { L"(([^\\$]|^)(\\$\\$)*)\\$Y", L"(([^\\$]|^)(\\$\\$)*)\\$M", L"(([^\\$]|^)(\\$\\$)*)\\$D", - L"(([^\\$]|^)(\\$\\$)*)\\$h", L"(([^\\$]|^)(\\$\\$)*)\\$m", L"(([^\\$]|^)(\\$\\$)*)\\$s", L"(([^\\$]|^)(\\$\\$)*)\\$f" }; - size_t patternsLength = ARRAYSIZE(patterns); - for (size_t i = 0; !used && i < patternsLength; i++) + static const std::array patterns = { std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$Y" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$M" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$D" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$h" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$m" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$s" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$f" } }; + for (size_t i = 0; !used && i < patterns.size(); i++) { - if (std::regex_search(source, std::wregex(patterns[i]))) + if (std::regex_search(source, patterns[i])) { used = true; } @@ -253,7 +251,7 @@ bool isFileTimeUsed(_In_ PCWSTR source) HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SYSTEMTIME fileTime) { std::locale::global(std::locale("")); - HRESULT hr = E_INVALIDARG; + HRESULT hr = E_INVALIDARG; if (source && wcslen(source) > 0) { std::wstring res(source); @@ -274,17 +272,17 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", (fileTime.wYear % 10)); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$Y"), replaceTerm); - + GetDateFormatEx(localeName, NULL, &fileTime, L"MMMM", formattedDate, MAX_PATH, NULL); formattedDate[0] = towupper(formattedDate[0]); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMMM"), replaceTerm); - + GetDateFormatEx(localeName, NULL, &fileTime, L"MMM", formattedDate, MAX_PATH, NULL); formattedDate[0] = towupper(formattedDate[0]); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MMM"), replaceTerm); - + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMonth); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$MM"), replaceTerm); @@ -295,7 +293,7 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY formattedDate[0] = towupper(formattedDate[0]); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$DDDD"), replaceTerm); - + GetDateFormatEx(localeName, NULL, &fileTime, L"ddd", formattedDate, MAX_PATH, NULL); formattedDate[0] = towupper(formattedDate[0]); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", formattedDate); @@ -328,10 +326,10 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%03d"), L"$01", fileTime.wMilliseconds); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$fff"), replaceTerm); - StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds/10); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wMilliseconds / 10); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$ff"), replaceTerm); - StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds/100); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wMilliseconds / 100); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$f"), replaceTerm); hr = StringCchCopy(result, cchMax, res.c_str()); diff --git a/src/modules/powerrename/lib/PowerRenameEnum.cpp b/src/modules/powerrename/lib/PowerRenameEnum.cpp index c1de60d4fe..15a8b6e05c 100644 --- a/src/modules/powerrename/lib/PowerRenameEnum.cpp +++ b/src/modules/powerrename/lib/PowerRenameEnum.cpp @@ -102,7 +102,10 @@ HRESULT CPowerRenameEnum::_ParseEnumItems(_In_ IEnumShellItems* pesi, _In_ int d l->Compare(r, SICHINT_DISPLAY, &res); return res < 0; }; - std::sort(begin(items), end(items), cmpShellItems); + + // We need to sort only the first layer, because later ones are enumerated correctly + if (depth == 0) + std::sort(begin(items), end(items), cmpShellItems); for (const auto& item : items) { diff --git a/src/modules/powerrename/lib/PowerRenameInterfaces.h b/src/modules/powerrename/lib/PowerRenameInterfaces.h index 6fcc25368d..974fee5425 100644 --- a/src/modules/powerrename/lib/PowerRenameInterfaces.h +++ b/src/modules/powerrename/lib/PowerRenameInterfaces.h @@ -58,7 +58,7 @@ public: IFACEMETHOD(PutFlags)(_In_ DWORD flags) = 0; IFACEMETHOD(PutFileTime)(_In_ SYSTEMTIME fileTime) = 0; IFACEMETHOD(ResetFileTime)() = 0; - IFACEMETHOD(Replace)(_In_ PCWSTR source, _Outptr_ PWSTR* result) = 0; + IFACEMETHOD(Replace)(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex) = 0; }; interface __declspec(uuid("C7F59201-4DE1-4855-A3A2-26FC3279C8A5")) IPowerRenameItem : public IUnknown diff --git a/src/modules/powerrename/lib/PowerRenameLib.vcxproj b/src/modules/powerrename/lib/PowerRenameLib.vcxproj index be76c5f5ab..e330dfb3e2 100644 --- a/src/modules/powerrename/lib/PowerRenameLib.vcxproj +++ b/src/modules/powerrename/lib/PowerRenameLib.vcxproj @@ -31,6 +31,7 @@ + @@ -47,6 +48,7 @@ + diff --git a/src/modules/powerrename/lib/PowerRenameManager.cpp b/src/modules/powerrename/lib/PowerRenameManager.cpp index 379e349e02..b6641374ba 100644 --- a/src/modules/powerrename/lib/PowerRenameManager.cpp +++ b/src/modules/powerrename/lib/PowerRenameManager.cpp @@ -923,7 +923,7 @@ DWORD WINAPI CPowerRenameManager::s_regexWorkerThread(_In_ void* pv) winrt::check_hresult(pwtd->spsrm->GetRenameRegEx(&spRenameRegEx)); UINT itemCount = 0; - unsigned long itemEnumIndex = 1; + unsigned long itemEnumIndex = 0; winrt::check_hresult(pwtd->spsrm->GetItemCount(&itemCount)); for (UINT u = 0; u < itemCount; u++) diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.cpp b/src/modules/powerrename/lib/PowerRenameRegEx.cpp index ec374b192d..8fdeb67cf1 100644 --- a/src/modules/powerrename/lib/PowerRenameRegEx.cpp +++ b/src/modules/powerrename/lib/PowerRenameRegEx.cpp @@ -1,5 +1,6 @@ #include "pch.h" #include "PowerRenameRegEx.h" +#include "Enumerating.h" #include "Settings.h" #include #include @@ -7,15 +8,17 @@ #include #include -using namespace std; +using std::conditional_t; using std::regex_error; -IFACEMETHODIMP_(ULONG) CPowerRenameRegEx::AddRef() +IFACEMETHODIMP_(ULONG) +CPowerRenameRegEx::AddRef() { return InterlockedIncrement(&m_refCount); } -IFACEMETHODIMP_(ULONG) CPowerRenameRegEx::Release() +IFACEMETHODIMP_(ULONG) +CPowerRenameRegEx::Release() { long refCount = InterlockedDecrement(&m_refCount); @@ -127,6 +130,26 @@ IFACEMETHODIMP CPowerRenameRegEx::GetReplaceTerm(_Outptr_ PWSTR* replaceTerm) return hr; } +HRESULT CPowerRenameRegEx::_OnEnumerateItemsChanged() +{ + m_enumerators.clear(); + const auto options = parseEnumOptions(m_RawReplaceTerm); + for (const auto e : options) + m_enumerators.emplace_back(e); + + m_replaceWithEnumeratorOffsets.clear(); + std::wstring replaceWith{ m_RawReplaceTerm }; + // Remove counter expressions and calculate their offsets in replaceWith string. + int32_t offset = 0; + for (const auto& e : options) + { + replaceWith.erase(e.replaceStrSpan.offset + offset, e.replaceStrSpan.length); + m_replaceWithEnumeratorOffsets.push_back(offset); + offset -= static_cast(e.replaceStrSpan.length); + } + return SHStrDup(replaceWith.data(), &m_replaceTerm); +} + IFACEMETHODIMP CPowerRenameRegEx::PutReplaceTerm(_In_ PCWSTR replaceTerm, bool forceRenaming) { bool changed = false || forceRenaming; @@ -134,11 +157,16 @@ IFACEMETHODIMP CPowerRenameRegEx::PutReplaceTerm(_In_ PCWSTR replaceTerm, bool f if (replaceTerm) { CSRWExclusiveAutoLock lock(&m_lock); - if (m_replaceTerm == nullptr || lstrcmp(replaceTerm, m_replaceTerm) != 0) + if (m_replaceTerm == nullptr || lstrcmp(replaceTerm, m_RawReplaceTerm.c_str()) != 0) { changed = true; CoTaskMemFree(m_replaceTerm); - hr = SHStrDup(replaceTerm, &m_replaceTerm); + m_RawReplaceTerm = replaceTerm; + + if (m_flags & EnumerateItems) + hr = _OnEnumerateItemsChanged(); + else + hr = SHStrDup(replaceTerm, &m_replaceTerm); } } @@ -160,7 +188,20 @@ IFACEMETHODIMP CPowerRenameRegEx::PutFlags(_In_ DWORD flags) { if (m_flags != flags) { + const bool newEnumerate = flags & EnumerateItems; + const bool refreshReplaceTerm = !!(m_flags & EnumerateItems) != newEnumerate; m_flags = flags; + if (refreshReplaceTerm) + { + CSRWExclusiveAutoLock lock(&m_lock); + if (newEnumerate) + _OnEnumerateItemsChanged(); + else + { + CoTaskMemFree(m_replaceTerm); + SHStrDup(m_RawReplaceTerm.c_str(), &m_replaceTerm); + } + } _OnFlagsChanged(); } return S_OK; @@ -202,7 +243,7 @@ HRESULT CPowerRenameRegEx::s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameR { *renameRegEx = nullptr; - CPowerRenameRegEx *newRenameRegEx = new CPowerRenameRegEx(); + CPowerRenameRegEx* newRenameRegEx = new CPowerRenameRegEx(); HRESULT hr = E_OUTOFMEMORY; if (newRenameRegEx) { @@ -228,7 +269,20 @@ CPowerRenameRegEx::~CPowerRenameRegEx() CoTaskMemFree(m_replaceTerm); } -HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result) +template, class Options = decltype(Regex::icase)> +static std::wstring RegexReplaceEx(const std::wstring& source, const std::wstring& searchTerm, const std::wstring& replaceTerm, const bool matchAll, const bool caseInsensitive) +{ + Regex pattern(searchTerm, Options::ECMAScript | (caseInsensitive ? Options::icase : Options{})); + + using Flags = conditional_t; + const auto flags = matchAll ? Flags::match_default : Flags::format_first_only; + + return regex_replace(source, pattern, replaceTerm, flags); +} + +static constexpr std::array RegexReplaceDispatch = { RegexReplaceEx, RegexReplaceEx }; + +HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex) { *result = nullptr; @@ -238,7 +292,7 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result) { return hr; } - wstring res = source; + std::wstring res = source; try { // TODO: creating the regex could be costly. May want to cache this. @@ -250,47 +304,49 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result) fileTimeErrorOccurred = true; } - std::wstring sourceToUse(source); + std::wstring sourceToUse; + std::wstring originalSource; + sourceToUse.reserve(MAX_PATH); + originalSource.reserve(MAX_PATH); + sourceToUse = source; + originalSource = sourceToUse; + std::wstring searchTerm(m_searchTerm); - std::wstring replaceTerm(L""); + std::wstring replaceTerm; if (m_useFileTime && !fileTimeErrorOccurred) { - replaceTerm = wstring(newReplaceTerm); + replaceTerm = newReplaceTerm; } else if (m_replaceTerm) { - replaceTerm = wstring(m_replaceTerm); + replaceTerm = m_replaceTerm; } - replaceTerm = regex_replace(replaceTerm, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$[0]"), L"$1$$$0"); - replaceTerm = regex_replace(replaceTerm, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$([1-9])"), L"$1$0$4"); + static const std::wregex zeroGroupRegex(L"(([^\\$]|^)(\\$\\$)*)\\$[0]"); + static const std::wregex otherGroupsRegex(L"(([^\\$]|^)(\\$\\$)*)\\$([1-9])"); + if (m_flags & EnumerateItems) + { + std::array buffer; + int32_t offset = 0; + + for (size_t ei = 0; ei < m_enumerators.size(); ++ei) + { + const auto& e = m_enumerators[ei]; + const auto replacementLength = static_cast(e.printTo(buffer.data(), buffer.size(), enumIndex)); + replaceTerm.insert(e.replaceStrSpan.offset + offset + m_replaceWithEnumeratorOffsets[ei], buffer.data()); + offset += replacementLength; + } + } + + bool replacedSomething = false; if (m_flags & UseRegularExpressions) { - if (_useBoostLib) - { - boost::wregex pattern(m_searchTerm, (!(m_flags & CaseSensitive)) ? boost::regex::icase | boost::regex::ECMAScript : boost::regex::ECMAScript); - if (m_flags & MatchAllOccurrences) - { - res = boost::regex_replace(wstring(source), pattern, replaceTerm); - } - else - { - res = boost::regex_replace(wstring(source), pattern, replaceTerm, boost::regex_constants::format_first_only); - } - } - else - { - std::wregex pattern(m_searchTerm, (!(m_flags & CaseSensitive)) ? regex_constants::icase | regex_constants::ECMAScript : regex_constants::ECMAScript); - if (m_flags & MatchAllOccurrences) - { - res = regex_replace(wstring(source), pattern, replaceTerm); - } - else - { - res = regex_replace(wstring(source), pattern, replaceTerm, regex_constants::format_first_only); - } - } + replaceTerm = regex_replace(replaceTerm, zeroGroupRegex, L"$1$$$0"); + replaceTerm = regex_replace(replaceTerm, otherGroupsRegex, L"$1$0$4"); + + res = RegexReplaceDispatch[_useBoostLib](source, m_searchTerm, replaceTerm, m_flags & MatchAllOccurrences, !(m_flags & CaseSensitive)); + replacedSomething = originalSource != res; } else { @@ -303,16 +359,17 @@ HRESULT CPowerRenameRegEx::Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result) { res = sourceToUse.replace(pos, searchTerm.length(), replaceTerm); pos += replaceTerm.length(); + replacedSomething = true; } - if (!(m_flags & MatchAllOccurrences)) { break; } } while (pos != std::string::npos); } - hr = SHStrDup(res.c_str(), result); + if (replacedSomething) + enumIndex++; } catch (regex_error e) { diff --git a/src/modules/powerrename/lib/PowerRenameRegEx.h b/src/modules/powerrename/lib/PowerRenameRegEx.h index dae5f04380..1b4e20a713 100644 --- a/src/modules/powerrename/lib/PowerRenameRegEx.h +++ b/src/modules/powerrename/lib/PowerRenameRegEx.h @@ -1,9 +1,8 @@ #pragma once #include "pch.h" -#include -#include #include "srwlock.h" +#include "Enumerating.h" #include "PowerRenameInterfaces.h" #define DEFAULT_FLAGS 0 @@ -12,7 +11,7 @@ class CPowerRenameRegEx : public IPowerRenameRegEx { public: // IUnknown - IFACEMETHODIMP QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface); + IFACEMETHODIMP QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface); IFACEMETHODIMP_(ULONG) AddRef(); IFACEMETHODIMP_(ULONG) Release(); @@ -27,9 +26,9 @@ public: IFACEMETHODIMP PutFlags(_In_ DWORD flags); IFACEMETHODIMP PutFileTime(_In_ SYSTEMTIME fileTime); IFACEMETHODIMP ResetFileTime(); - IFACEMETHODIMP Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result); + IFACEMETHODIMP Replace(_In_ PCWSTR source, _Outptr_ PWSTR* result, unsigned long& enumIndex); - static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegEx **renameRegEx); + static HRESULT s_CreateInstance(_Outptr_ IPowerRenameRegEx** renameRegEx); protected: CPowerRenameRegEx(); @@ -39,6 +38,7 @@ protected: void _OnReplaceTermChanged(); void _OnFlagsChanged(); void _OnFileTimeChanged(); + HRESULT _OnEnumerateItemsChanged(); size_t _Find(std::wstring data, std::wstring toSearch, bool caseInsensitive, size_t pos); @@ -46,8 +46,9 @@ protected: DWORD m_flags = DEFAULT_FLAGS; PWSTR m_searchTerm = nullptr; PWSTR m_replaceTerm = nullptr; + std::wstring m_RawReplaceTerm; - SYSTEMTIME m_fileTime = {0}; + SYSTEMTIME m_fileTime = { 0 }; bool m_useFileTime = false; CSRWLock m_lock; @@ -55,6 +56,9 @@ protected: DWORD m_cookie = 0; + std::vector m_enumerators; + std::vector m_replaceWithEnumeratorOffsets; + struct RENAME_REGEX_EVENT { IPowerRenameRegExEvents* pEvents; diff --git a/src/modules/powerrename/lib/Renaming.cpp b/src/modules/powerrename/lib/Renaming.cpp index ee070bb831..bf27500529 100644 --- a/src/modules/powerrename/lib/Renaming.cpp +++ b/src/modules/powerrename/lib/Renaming.cpp @@ -86,7 +86,7 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum // Failure here means we didn't match anything or had nothing to match // Call put_newName with null in that case to reset it - winrt::check_hresult(spRenameRegEx->Replace(sourceName, &newName)); + winrt::check_hresult(spRenameRegEx->Replace(sourceName, &newName, itemEnumIndex)); if (useFileTime) { @@ -166,17 +166,6 @@ bool DoRename(CComPtr& spRenameRegEx, unsigned long& itemEnum newNameToUse = nullptr; } - wchar_t uniqueName[MAX_PATH] = { 0 }; - if (newNameToUse != nullptr && (flags & EnumerateItems)) - { - unsigned long countUsed = 0; - if (GetEnumeratedFileName(uniqueName, ARRAYSIZE(uniqueName), newNameToUse, nullptr, itemEnumIndex, &countUsed)) - { - newNameToUse = uniqueName; - } - itemEnumIndex++; - } - spItem->PutStatus(PowerRenameItemRenameStatus::ShouldRename); if (newNameToUse != nullptr) { diff --git a/src/modules/powerrename/lib/Settings.cpp b/src/modules/powerrename/lib/Settings.cpp index 655b760e35..1ebee29e5e 100644 --- a/src/modules/powerrename/lib/Settings.cpp +++ b/src/modules/powerrename/lib/Settings.cpp @@ -25,6 +25,8 @@ namespace const wchar_t c_replaceText[] = L"ReplaceText"; const wchar_t c_mruEnabled[] = L"MRUEnabled"; const wchar_t c_useBoostLib[] = L"UseBoostLib"; + const wchar_t c_lastWindowWidth[] = L"LastWindowWidth"; + const wchar_t c_lastWindowHeight[] = L"LastWindowHeight"; } @@ -49,6 +51,8 @@ void CSettings::Save() jsonData.SetNamedValue(c_searchText, json::value(settings.searchText)); jsonData.SetNamedValue(c_replaceText, json::value(settings.replaceText)); jsonData.SetNamedValue(c_useBoostLib, json::value(settings.useBoostLib)); + jsonData.SetNamedValue(c_lastWindowWidth, json::value(settings.lastWindowWidth)); + jsonData.SetNamedValue(c_lastWindowHeight, json::value(settings.lastWindowHeight)); json::to_file(jsonFilePath, jsonData); GetSystemTimeAsFileTime(&lastLoadedTime); @@ -139,6 +143,9 @@ void CSettings::ParseJson() { settings.useBoostLib = jsonSettings.GetNamedBoolean(c_useBoostLib); } + + settings.lastWindowWidth = static_cast(jsonSettings.GetNamedNumber(c_lastWindowWidth, DEFAULT_WINDOW_WIDTH)); + settings.lastWindowHeight = static_cast(jsonSettings.GetNamedNumber(c_lastWindowHeight, DEFAULT_WINDOW_HEIGHT)); } catch (const winrt::hresult_error&) { diff --git a/src/modules/powerrename/lib/Settings.h b/src/modules/powerrename/lib/Settings.h index f74201c31b..00e5f2df1e 100644 --- a/src/modules/powerrename/lib/Settings.h +++ b/src/modules/powerrename/lib/Settings.h @@ -6,6 +6,9 @@ class CSettings { public: + static constexpr inline int DEFAULT_WINDOW_WIDTH = 1400; + static constexpr inline int DEFAULT_WINDOW_HEIGHT = 800; + CSettings(); inline bool GetEnabled() @@ -25,6 +28,17 @@ public: Save(); } + inline std::tuple GetLastWindowSize() const + { + return std::make_tuple(settings.lastWindowWidth, settings.lastWindowHeight); + } + + inline void UpdateLastWindowSize(const int width, const int height) + { + settings.lastWindowWidth = std::max(width, DEFAULT_WINDOW_WIDTH); + settings.lastWindowHeight = std::max(height, DEFAULT_WINDOW_HEIGHT); + } + inline bool GetShowIconOnMenu() const { return settings.showIconOnMenu; @@ -134,6 +148,8 @@ private: unsigned int flags{ 0 }; std::wstring searchText{}; std::wstring replaceText{}; + int lastWindowWidth{ DEFAULT_WINDOW_WIDTH }; + int lastWindowHeight{ DEFAULT_WINDOW_HEIGHT }; }; void Reload(); diff --git a/src/modules/powerrename/lib/pch.h b/src/modules/powerrename/lib/pch.h index 890021fe33..af9f76bbff 100644 --- a/src/modules/powerrename/lib/pch.h +++ b/src/modules/powerrename/lib/pch.h @@ -21,6 +21,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include diff --git a/src/modules/powerrename/unittests/CommonRegExTests.h b/src/modules/powerrename/unittests/CommonRegExTests.h new file mode 100644 index 0000000000..f9cb469fb3 --- /dev/null +++ b/src/modules/powerrename/unittests/CommonRegExTests.h @@ -0,0 +1,472 @@ +//#undef TESTS_PARTIAL // Uncomment temporarily to make intellisense work in this file. +#ifndef TESTS_PARTIAL +#include "CppUnitTestInclude.h" +#include "powerrename/lib/Settings.h" +#include +#include + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +namespace PowerRenameRegExTests +{ +TEST_CLASS (SimpleTests) +{ +public: +#endif + +struct SearchReplaceExpected +{ + PCWSTR search; + PCWSTR replace; + PCWSTR test; + PCWSTR expected; +}; + +TEST_METHOD (GeneralReplaceTest) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"big") == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"bigbar", result); + CoTaskMemFree(result); +} + +TEST_METHOD (ReplaceNoMatch) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"notfound") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"big") == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar", result); + CoTaskMemFree(result); +} + +TEST_METHOD (ReplaceNoSearchOrReplaceTerm) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::IsTrue(result == nullptr); + CoTaskMemFree(result); +} + +TEST_METHOD (ReplaceNoReplaceTerm) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"bar", result); + CoTaskMemFree(result); +} + +TEST_METHOD (ReplaceEmptyStringReplaceTerm) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"") == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"bar", result); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifyDefaultFlags) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = 0; + Assert::IsTrue(renameRegEx->GetFlags(&flags) == S_OK); + Assert::IsTrue(flags == 0); +} + +TEST_METHOD (VerifyCaseSensitiveSearch) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = CaseSensitive; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = { + { L"Foo", L"Foo", L"FooBar", L"FooBar" }, + { L"Foo", L"boo", L"FooBar", L"booBar" }, + { L"Foo", L"boo", L"foobar", L"foobar" }, + { L"123", L"654", L"123456", L"654456" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } +} + +TEST_METHOD (VerifyReplaceFirstOnly) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = 0; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AABBA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } +} + +TEST_METHOD (VerifyReplaceAll) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurrences; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } +} + +TEST_METHOD (VerifyReplaceAllCaseInsensitive) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurrences | CaseSensitive; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, + { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } +} + +TEST_METHOD (VerifyReplaceFirstOnlyUseRegEx) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AABBA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } +} + +TEST_METHOD (VerifyReplaceAllUseRegEx) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurrences | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } +} + +TEST_METHOD (VerifyReplaceAllUseRegExCaseSensitive) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurrences | UseRegularExpressions | CaseSensitive; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + SearchReplaceExpected sreTable[] = { + { L"B", L"BB", L"ABA", L"ABBA" }, + { L"B", L"A", L"ABBBA", L"AAAAA" }, + { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, + }; + + for (int i = 0; i < ARRAYSIZE(sreTable); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); + CoTaskMemFree(result); + } +} + +void VerifyReplaceFirstWildcard(SearchReplaceExpected sreTable[], int tableSize, DWORD flags) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + for (int i = 0; i < tableSize; i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); + Assert::AreEqual(sreTable[i].expected, result); + CoTaskMemFree(result); + } +} + +TEST_METHOD (VerifyReplaceFirstWildCardUseRegex) +{ + SearchReplaceExpected sreTable[] = { + //search, replace, test, result + { L".*", L"Foo", L"AAAAAA", L"Foo" }, + }; + VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), UseRegularExpressions); +} + +TEST_METHOD (VerifyReplaceFirstWildCardMatchAllOccurrences) +{ + SearchReplaceExpected sreTable[] = { + //search, replace, test, result + { L".*", L"Foo", L"AAAAAA", L"AAAAAA" }, + { L".*", L"Foo", L".*", L"Foo" }, + { L".*", L"Foo", L".*Bar.*", L"FooBarFoo" }, + }; + VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), MatchAllOccurrences); +} + +TEST_METHOD (VerifyReplaceFirstWildNoFlags) +{ + SearchReplaceExpected sreTable[] = { + //search, replace, test, result + { L".*", L"Foo", L"AAAAAA", L"AAAAAA" }, + { L".*", L"Foo", L".*", L"Foo" }, + }; + VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), 0); +} + +TEST_METHOD (VerifyEventsFire) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + CMockPowerRenameRegExEvents* mockEvents = new CMockPowerRenameRegExEvents(); + CComPtr regExEvents; + Assert::IsTrue(mockEvents->QueryInterface(IID_PPV_ARGS(®ExEvents)) == S_OK); + DWORD cookie = 0; + Assert::IsTrue(renameRegEx->Advise(regExEvents, &cookie) == S_OK); + DWORD flags = MatchAllOccurrences | UseRegularExpressions | CaseSensitive; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + Assert::IsTrue(renameRegEx->PutSearchTerm(L"FOO") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"BAR") == S_OK); + Assert::IsTrue(renameRegEx->PutFileTime(SYSTEMTIME{ 0 }) == S_OK); + Assert::IsTrue(renameRegEx->ResetFileTime() == S_OK); + Assert::IsTrue(lstrcmpi(L"FOO", mockEvents->m_searchTerm) == 0); + Assert::IsTrue(lstrcmpi(L"BAR", mockEvents->m_replaceTerm) == 0); + Assert::IsTrue(flags == mockEvents->m_flags); + Assert::IsTrue(renameRegEx->UnAdvise(cookie) == S_OK); + mockEvents->Release(); +} + +TEST_METHOD (VerifySimpleCounterNoRegex) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = EnumerateItems; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${}") == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foo$1bar_0", result); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifySimpleCounterNoEnum) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"$1bar_${}") == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar_${}", result); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifySimpleCounter) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = EnumerateItems | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"bar_${}") == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar_0", result); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifyMultipleCounters) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = EnumerateItems | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"_${}_bar_${}") == S_OK); + unsigned long index = {}; + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foo_0_bar_0", result); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifyCounterIncrementCustomization) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = EnumerateItems | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"bar_${increment=10}") == S_OK); + unsigned long index = 1; + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar_10", result); + Assert::AreEqual(index, 2); + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar_20", result); + Assert::AreEqual(index, 3); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifyCounterStartCustomization) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = EnumerateItems | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"bar_${start=1000}") == S_OK); + unsigned long index = 5; + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar_1005", result); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifyCounterPaddingCustomization) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = EnumerateItems | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"bar_${padding=5}") == S_OK); + unsigned long index = 204; + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar_00204", result); + CoTaskMemFree(result); +} + +TEST_METHOD (VerifyCounterAllCustomizations) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = EnumerateItems | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + Assert::IsTrue(renameRegEx->PutSearchTerm(L"bar") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(L"bar_${increment=7,start=993,padding=5}") == S_OK); + unsigned long index = 12; + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->Replace(L"foobar", &result, index) == S_OK); + Assert::AreEqual(L"foobar_01077", result); + CoTaskMemFree(result); +} + +#ifndef TESTS_PARTIAL +}; +} +#endif diff --git a/src/modules/powerrename/unittests/CppUnitTestInclude.h b/src/modules/powerrename/unittests/CppUnitTestInclude.h new file mode 100644 index 0000000000..dce5689675 --- /dev/null +++ b/src/modules/powerrename/unittests/CppUnitTestInclude.h @@ -0,0 +1,8 @@ +#pragma once +// Headers for CppUnitTest + +// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h +#pragma warning(push) +#pragma warning(disable : 26466) +#include "CppUnitTest.h" +#pragma warning(pop) diff --git a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj index 0598fb77e6..f64a2e85a3 100644 --- a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj +++ b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj @@ -38,6 +38,7 @@ + @@ -45,6 +46,7 @@ + diff --git a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters index 8708f3397e..db5bc09af4 100644 --- a/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters +++ b/src/modules/powerrename/unittests/PowerRenameLibUnitTests.vcxproj.filters @@ -17,9 +17,11 @@ + Header Files + diff --git a/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp b/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp index bb70ebb99b..2bff1a4b9c 100644 --- a/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp +++ b/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp @@ -8,14 +8,6 @@ using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace PowerRenameRegExBoostTests { - struct SearchReplaceExpected - { - PCWSTR search; - PCWSTR replace; - PCWSTR test; - PCWSTR expected; - }; - TEST_CLASS(SimpleTests) { public: @@ -29,241 +21,8 @@ TEST_CLASS_CLEANUP(ClassCleanup) CSettingsInstance().SetUseBoostLib(false); } -TEST_METHOD(GeneralReplaceTest) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"big") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"bigbar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceNoMatch) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"notfound") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"big") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"foobar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceNoSearchOrReplaceTerm) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(result == nullptr); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceNoReplaceTerm) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"bar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceEmptyStringReplaceTerm) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"bar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(VerifyDefaultFlags) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = 0; - Assert::IsTrue(renameRegEx->GetFlags(&flags) == S_OK); - Assert::IsTrue(flags == 0); -} - -TEST_METHOD(VerifyCaseSensitiveSearch) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"Foo", L"Foo", L"FooBar", L"FooBar" }, - { L"Foo", L"boo", L"FooBar", L"booBar" }, - { L"Foo", L"boo", L"foobar", L"foobar" }, - { L"123", L"654", L"123456", L"654456" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceFirstOnly) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = 0; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AABBA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAll) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAllCaseInsensitive) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences | CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, - { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceFirstOnlyUseRegEx) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = UseRegularExpressions; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AABBA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAllUseRegEx) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences | UseRegularExpressions; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAllUseRegExCaseSensitive) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences | UseRegularExpressions | CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} +#define TESTS_PARTIAL +#include "CommonRegExTests.h" TEST_METHOD(VerifyMatchAllWildcardUseRegEx) { @@ -284,38 +43,13 @@ TEST_METHOD(VerifyMatchAllWildcardUseRegEx) PWSTR result = nullptr; Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } } -void VerifyReplaceFirstWildcard(SearchReplaceExpected sreTable[], int tableSize, DWORD flags) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - for (int i = 0; i < tableSize; i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::AreEqual(sreTable[i].expected, result); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceFirstWildCardUseRegex) -{ - SearchReplaceExpected sreTable[] = { - //search, replace, test, result - { L".*", L"Foo", L"AAAAAA", L"Foo" }, - }; - VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), UseRegularExpressions); -} - TEST_METHOD(VerifyReplaceFirstWildCardUseRegexMatchAllOccurrences) { // This differs from the Standard Library: .* has two matches (all and nothing). @@ -327,27 +61,6 @@ TEST_METHOD(VerifyReplaceFirstWildCardUseRegexMatchAllOccurrences) VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), UseRegularExpressions | MatchAllOccurrences); } -TEST_METHOD(VerifyReplaceFirstWildCardMatchAllOccurrences) -{ - SearchReplaceExpected sreTable[] = { - //search, replace, test, result - { L".*", L"Foo", L"AAAAAA", L"AAAAAA" }, - { L".*", L"Foo", L".*", L"Foo" }, - { L".*", L"Foo", L".*Bar.*", L"FooBarFoo" }, - }; - VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), MatchAllOccurrences); -} - -TEST_METHOD(VerifyReplaceFirstWildNoFlags) -{ - SearchReplaceExpected sreTable[] = { - //search, replace, test, result - { L".*", L"Foo", L"AAAAAA", L"AAAAAA" }, - { L".*", L"Foo", L".*", L"Foo" }, - }; - VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), 0); -} - TEST_METHOD(VerifyHandleCapturingGroups) { // This differs from the Standard Library: Boost does not recognize $123 as $1 and "23". @@ -380,7 +93,8 @@ TEST_METHOD(VerifyHandleCapturingGroups) PWSTR result = nullptr; Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } @@ -407,32 +121,11 @@ TEST_METHOD(VerifyLookbehind) PWSTR result = nullptr; Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } } - -TEST_METHOD(VerifyEventsFire) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - CMockPowerRenameRegExEvents* mockEvents = new CMockPowerRenameRegExEvents(); - CComPtr regExEvents; - Assert::IsTrue(mockEvents->QueryInterface(IID_PPV_ARGS(®ExEvents)) == S_OK); - DWORD cookie = 0; - Assert::IsTrue(renameRegEx->Advise(regExEvents, &cookie) == S_OK); - DWORD flags = MatchAllOccurrences | UseRegularExpressions | CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - Assert::IsTrue(renameRegEx->PutSearchTerm(L"FOO") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"BAR") == S_OK); - Assert::IsTrue(renameRegEx->PutFileTime(SYSTEMTIME{0}) == S_OK); - Assert::IsTrue(renameRegEx->ResetFileTime() == S_OK); - Assert::IsTrue(lstrcmpi(L"FOO", mockEvents->m_searchTerm) == 0); - Assert::IsTrue(lstrcmpi(L"BAR", mockEvents->m_replaceTerm) == 0); - Assert::IsTrue(flags == mockEvents->m_flags); - Assert::IsTrue(renameRegEx->UnAdvise(cookie) == S_OK); - mockEvents->Release(); -} }; } diff --git a/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp b/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp index aadfb99c1c..eff80bf4cd 100644 --- a/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp +++ b/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp @@ -8,14 +8,6 @@ using namespace Microsoft::VisualStudio::CppUnitTestFramework; namespace PowerRenameRegExTests { - struct SearchReplaceExpected - { - PCWSTR search; - PCWSTR replace; - PCWSTR test; - PCWSTR expected; - }; - TEST_CLASS(SimpleTests){ public: TEST_CLASS_INITIALIZE(ClassInitialize) @@ -23,241 +15,8 @@ TEST_CLASS_INITIALIZE(ClassInitialize) CSettingsInstance().SetUseBoostLib(false); } -TEST_METHOD(GeneralReplaceTest) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"big") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"bigbar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceNoMatch) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"notfound") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"big") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"foobar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceNoSearchOrReplaceTerm) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(result == nullptr); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceNoReplaceTerm) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"bar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(ReplaceEmptyStringReplaceTerm) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(L"foo") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"") == S_OK); - Assert::IsTrue(renameRegEx->Replace(L"foobar", &result) == S_OK); - Assert::IsTrue(wcscmp(result, L"bar") == 0); - CoTaskMemFree(result); -} - -TEST_METHOD(VerifyDefaultFlags) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = 0; - Assert::IsTrue(renameRegEx->GetFlags(&flags) == S_OK); - Assert::IsTrue(flags == 0); -} - -TEST_METHOD(VerifyCaseSensitiveSearch) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"Foo", L"Foo", L"FooBar", L"FooBar" }, - { L"Foo", L"boo", L"FooBar", L"booBar" }, - { L"Foo", L"boo", L"foobar", L"foobar" }, - { L"123", L"654", L"123456", L"654456" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceFirstOnly) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = 0; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AABBA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAll) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAllCaseInsensitive) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences | CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, - { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceFirstOnlyUseRegEx) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = UseRegularExpressions; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AABBA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABAB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAllUseRegEx) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences | UseRegularExpressions; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"B", L"BBB", L"ABABAB", L"ABBBABBBABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceAllUseRegExCaseSensitive) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - DWORD flags = MatchAllOccurrences | UseRegularExpressions | CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - SearchReplaceExpected sreTable[] = { - { L"B", L"BB", L"ABA", L"ABBA" }, - { L"B", L"A", L"ABBBA", L"AAAAA" }, - { L"b", L"BBB", L"AbABAb", L"ABBBABABBB" }, - }; - - for (int i = 0; i < ARRAYSIZE(sreTable); i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); - CoTaskMemFree(result); - } -} +#define TESTS_PARTIAL +#include "CommonRegExTests.h" TEST_METHOD(VerifyMatchAllWildcardUseRegEx) { @@ -275,38 +34,13 @@ TEST_METHOD(VerifyMatchAllWildcardUseRegEx) PWSTR result = nullptr; Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } } -void VerifyReplaceFirstWildcard(SearchReplaceExpected sreTable[], int tableSize, DWORD flags) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - - for (int i = 0; i < tableSize; i++) - { - PWSTR result = nullptr; - Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); - Assert::AreEqual(sreTable[i].expected, result); - CoTaskMemFree(result); - } -} - -TEST_METHOD(VerifyReplaceFirstWildCardUseRegex) -{ - SearchReplaceExpected sreTable[] = { - //search, replace, test, result - { L".*", L"Foo", L"AAAAAA", L"Foo" }, - }; - VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), UseRegularExpressions); -} - TEST_METHOD(VerifyReplaceFirstWildCardUseRegexMatchAllOccurrences) { SearchReplaceExpected sreTable[] = { @@ -316,27 +50,6 @@ TEST_METHOD(VerifyReplaceFirstWildCardUseRegexMatchAllOccurrences) VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), UseRegularExpressions | MatchAllOccurrences); } -TEST_METHOD(VerifyReplaceFirstWildCardMatchAllOccurrences) -{ - SearchReplaceExpected sreTable[] = { - //search, replace, test, result - { L".*", L"Foo", L"AAAAAA", L"AAAAAA" }, - { L".*", L"Foo", L".*", L"Foo" }, - { L".*", L"Foo", L".*Bar.*", L"FooBarFoo" }, - }; - VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), MatchAllOccurrences); -} - -TEST_METHOD(VerifyReplaceFirstWildNoFlags) -{ - SearchReplaceExpected sreTable[] = { - //search, replace, test, result - { L".*", L"Foo", L"AAAAAA", L"AAAAAA" }, - { L".*", L"Foo", L".*", L"Foo" }, - }; - VerifyReplaceFirstWildcard(sreTable, ARRAYSIZE(sreTable), 0); -} - TEST_METHOD(VerifyHandleCapturingGroups) { CComPtr renameRegEx; @@ -362,7 +75,8 @@ TEST_METHOD(VerifyHandleCapturingGroups) PWSTR result = nullptr; Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } @@ -387,7 +101,8 @@ TEST_METHOD (VerifyFileAttributesNoPadding) Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); Assert::IsTrue(renameRegEx->PutFileTime(fileTime) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } @@ -411,7 +126,8 @@ TEST_METHOD (VerifyFileAttributesPadding) Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); Assert::IsTrue(renameRegEx->PutFileTime(fileTime) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } @@ -459,7 +175,8 @@ TEST_METHOD (VerifyFileAttributesMonthandDayNames) Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); Assert::IsTrue(renameRegEx->PutFileTime(fileTime) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == S_OK); Assert::IsTrue(wcscmp(result, sreTable[i].expected) == 0); CoTaskMemFree(result); } @@ -483,33 +200,12 @@ TEST_METHOD(VerifyLookbehindFails) PWSTR result = nullptr; Assert::IsTrue(renameRegEx->PutSearchTerm(sreTable[i].search) == S_OK); Assert::IsTrue(renameRegEx->PutReplaceTerm(sreTable[i].replace) == S_OK); - Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result) == E_FAIL); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(sreTable[i].test, &result, index) == E_FAIL); Assert::AreEqual(sreTable[i].expected, result); CoTaskMemFree(result); } } -TEST_METHOD(VerifyEventsFire) -{ - CComPtr renameRegEx; - Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); - CMockPowerRenameRegExEvents* mockEvents = new CMockPowerRenameRegExEvents(); - CComPtr regExEvents; - Assert::IsTrue(mockEvents->QueryInterface(IID_PPV_ARGS(®ExEvents)) == S_OK); - DWORD cookie = 0; - Assert::IsTrue(renameRegEx->Advise(regExEvents, &cookie) == S_OK); - DWORD flags = MatchAllOccurrences | UseRegularExpressions | CaseSensitive; - Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); - Assert::IsTrue(renameRegEx->PutSearchTerm(L"FOO") == S_OK); - Assert::IsTrue(renameRegEx->PutReplaceTerm(L"BAR") == S_OK); - Assert::IsTrue(renameRegEx->PutFileTime(SYSTEMTIME{ 0 }) == S_OK); - Assert::IsTrue(renameRegEx->ResetFileTime() == S_OK); - Assert::IsTrue(lstrcmpi(L"FOO", mockEvents->m_searchTerm) == 0); - Assert::IsTrue(lstrcmpi(L"BAR", mockEvents->m_replaceTerm) == 0); - Assert::IsTrue(flags == mockEvents->m_flags); - Assert::IsTrue(renameRegEx->UnAdvise(cookie) == S_OK); - mockEvents->Release(); -} -} -; +}; } diff --git a/src/modules/powerrename/unittests/pch.h b/src/modules/powerrename/unittests/pch.h index 95c31bcf3f..95e69fe365 100644 --- a/src/modules/powerrename/unittests/pch.h +++ b/src/modules/powerrename/unittests/pch.h @@ -1,13 +1,7 @@ #pragma once +#define NOMINMAX #include "targetver.h" #include - -// Headers for CppUnitTest - -// Suppressing 26466 - Don't use static_cast downcasts - in CppUnitTest.h -#pragma warning(push) -#pragma warning(disable : 26466) -#include "CppUnitTest.h" -#pragma warning(pop) +#include "CppUnitTestInclude.h" \ No newline at end of file diff --git a/src/modules/previewpane/GcodeThumbnailProviderCpp/GcodeThumbnailProvider.cpp b/src/modules/previewpane/GcodeThumbnailProviderCpp/GcodeThumbnailProvider.cpp index 14b07431a2..89dcbdd986 100644 --- a/src/modules/previewpane/GcodeThumbnailProviderCpp/GcodeThumbnailProvider.cpp +++ b/src/modules/previewpane/GcodeThumbnailProviderCpp/GcodeThumbnailProvider.cpp @@ -137,6 +137,8 @@ IFACEMETHODIMP GcodeThumbnailProvider::GetThumbnail(UINT cx, HBITMAP* phbmp, WTS } file.close(); + m_pStream->Release(); + m_pStream = NULL; try { diff --git a/src/modules/previewpane/PdfThumbnailProviderCpp/PdfThumbnailProvider.cpp b/src/modules/previewpane/PdfThumbnailProviderCpp/PdfThumbnailProvider.cpp index 75a0c7a038..a636715f61 100644 --- a/src/modules/previewpane/PdfThumbnailProviderCpp/PdfThumbnailProvider.cpp +++ b/src/modules/previewpane/PdfThumbnailProviderCpp/PdfThumbnailProvider.cpp @@ -136,6 +136,9 @@ IFACEMETHODIMP PdfThumbnailProvider::GetThumbnail(UINT cx, HBITMAP* phbmp, WTS_A } file.close(); + m_pStream->Release(); + m_pStream = NULL; + try { Logger::info(L"Start PdfThumbnailProvider.exe"); diff --git a/src/modules/previewpane/StlThumbnailProviderCpp/StlThumbnailProvider.cpp b/src/modules/previewpane/StlThumbnailProviderCpp/StlThumbnailProvider.cpp index 9b1c8b8415..c06f7b9d03 100644 --- a/src/modules/previewpane/StlThumbnailProviderCpp/StlThumbnailProvider.cpp +++ b/src/modules/previewpane/StlThumbnailProviderCpp/StlThumbnailProvider.cpp @@ -136,6 +136,9 @@ IFACEMETHODIMP StlThumbnailProvider::GetThumbnail(UINT cx, HBITMAP* phbmp, WTS_A } file.close(); + m_pStream->Release(); + m_pStream = NULL; + try { Logger::info(L"Start StlThumbnailProvider.exe"); diff --git a/src/modules/previewpane/SvgThumbnailProviderCpp/SvgThumbnailProvider.cpp b/src/modules/previewpane/SvgThumbnailProviderCpp/SvgThumbnailProvider.cpp index ac749d0127..139fe99c22 100644 --- a/src/modules/previewpane/SvgThumbnailProviderCpp/SvgThumbnailProvider.cpp +++ b/src/modules/previewpane/SvgThumbnailProviderCpp/SvgThumbnailProvider.cpp @@ -136,6 +136,9 @@ IFACEMETHODIMP SvgThumbnailProvider::GetThumbnail(UINT cx, HBITMAP* phbmp, WTS_A } file.close(); + m_pStream->Release(); + m_pStream = NULL; + try { Logger::info(L"Start SvgThumbnailProvider.exe"); diff --git a/src/runner/UpdateUtils.cpp b/src/runner/UpdateUtils.cpp index 8445fdcc6b..6edde793c7 100644 --- a/src/runner/UpdateUtils.cpp +++ b/src/runner/UpdateUtils.cpp @@ -159,6 +159,10 @@ void ProcessNewVersionInfo(const github_version_info& version_info, if (download_update) { Logger::trace(L"Downloading installer for a new version"); + + // Cleanup old updates before downloading the latest + updating::cleanup_updates(); + if (download_new_version(new_version_info).get()) { state.state = UpdateState::readyToInstall; diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 9d0e1f5e44..2cda7f1363 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -156,6 +156,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"WinUI3Apps/PowerToys.HostsModuleInterface.dll", L"WinUI3Apps/PowerToys.Peek.dll", L"PowerToys.MouseWithoutBordersModuleInterface.dll", + L"PowerToys.CropAndLockModuleInterface.dll", }; const auto VCM_PATH = L"PowerToys.VideoConferenceModule.dll"; if (const auto mf = LoadLibraryA("mf.dll")) @@ -295,57 +296,6 @@ toast_notification_handler_result toast_notification_handler(const std::wstring_ } } -void cleanup_updates() -{ - auto state = UpdateState::read(); - if (state.state != UpdateState::upToDate) - { - return; - } - - auto update_dir = updating::get_pending_updates_path(); - if (std::filesystem::exists(update_dir)) - { - // Msi and exe files - for (const auto& entry : std::filesystem::directory_iterator(update_dir)) - { - auto entryPath = entry.path().wstring(); - std::transform(entryPath.begin(), entryPath.end(), entryPath.begin(), ::towlower); - - if (entryPath.ends_with(L".msi") || entryPath.ends_with(L".exe")) - { - std::error_code err; - std::filesystem::remove(entry, err); - if (err.value()) - { - Logger::warn("Failed to delete installer file {}. {}", entry.path().string(), err.message()); - } - } - } - } - - // Log files - auto rootPath{ PTSettingsHelper::get_root_save_folder_location() }; - auto currentVersion = left_trim(get_product_version(), L"v"); - if (std::filesystem::exists(rootPath)) - { - for (const auto& entry : std::filesystem::directory_iterator(rootPath)) - { - auto entryPath = entry.path().wstring(); - std::transform(entryPath.begin(), entryPath.end(), entryPath.begin(), ::towlower); - if (entry.is_regular_file() && entryPath.ends_with(L".log") && entryPath.find(currentVersion) == std::string::npos) - { - std::error_code err; - std::filesystem::remove(entry, err); - if (err.value()) - { - Logger::warn("Failed to delete log file {}. {}", entry.path().string(), err.message()); - } - } - } - } -} - int WINAPI WinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPSTR lpCmdLine, int /*nCmdShow*/) { Gdiplus::GdiplusStartupInput gpStartupInput; @@ -454,7 +404,11 @@ int WINAPI WinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPSTR l modules(); std::thread{ [] { - cleanup_updates(); + auto state = UpdateState::read(); + if (state.state == UpdateState::upToDate) + { + updating::cleanup_updates(); + } } }.detach(); auto general_settings = load_general_settings(); @@ -464,6 +418,7 @@ int WINAPI WinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPSTR l const bool elevated = is_process_elevated(); const bool with_dont_elevate_arg = cmdLine.find("--dont-elevate") != std::string::npos; const bool run_elevated_setting = general_settings.GetNamedBoolean(L"run_elevated", false); + const bool with_restartedElevated_arg = cmdLine.find("--restartedElevated") != std::string::npos; if (elevated && with_dont_elevate_arg && !run_elevated_setting) { @@ -471,8 +426,14 @@ int WINAPI WinMain(HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/, LPSTR l schedule_restart_as_non_elevated(); result = 0; } - else if (elevated || !run_elevated_setting || with_dont_elevate_arg) + else if (elevated || !run_elevated_setting || with_dont_elevate_arg || (!elevated && with_restartedElevated_arg)) { + // The condition (!elevated && with_restartedElevated_arg) solves issue #19307. Restart elevated loop detected, running non-elevated + if (!elevated && with_restartedElevated_arg) + { + Logger::info("Restart as elevated failed. Running non-elevated."); + } + result = runner(elevated, open_settings, settings_window, openOobe, openScoobe); if (result == 0) diff --git a/src/runner/restart_elevated.cpp b/src/runner/restart_elevated.cpp index 13d4b47136..c14b63d479 100644 --- a/src/runner/restart_elevated.cpp +++ b/src/runner/restart_elevated.cpp @@ -42,9 +42,9 @@ bool restart_if_scheduled() switch (state) { case RestartAsElevated: - return run_elevated(exe_path.get(), {}); + return run_elevated(exe_path.get(), L"--restartedElevated"); case RestartAsElevatedOpenSettings: - return run_elevated(exe_path.get(), L"--open-settings"); + return run_elevated(exe_path.get(), L"--restartedElevated --open-settings"); case RestartAsNonElevatedOpenSettings: return run_non_elevated(exe_path.get(), L"--open-settings", NULL); case RestartAsNonElevated: diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs b/src/settings-ui/Settings.UI.Library/CropAndLockProperties.cs new file mode 100644 index 0000000000..7a850eebf5 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/CropAndLockProperties.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.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class CropAndLockProperties + { + public static readonly HotkeySettings DefaultReparentHotkeyValue = new HotkeySettings(true, true, false, true, 0x52); // Ctrl+Win+Shift+R + public static readonly HotkeySettings DefaultThumbnailHotkeyValue = new HotkeySettings(true, true, false, true, 0x54); // Ctrl+Win+Shift+T + + public CropAndLockProperties() + { + ReparentHotkey = new KeyboardKeysProperty(DefaultReparentHotkeyValue); + ThumbnailHotkey = new KeyboardKeysProperty(DefaultThumbnailHotkeyValue); + } + + [JsonPropertyName("reparent-hotkey")] + public KeyboardKeysProperty ReparentHotkey { get; set; } + + [JsonPropertyName("thumbnail-hotkey")] + public KeyboardKeysProperty ThumbnailHotkey { get; set; } + } +} diff --git a/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs new file mode 100644 index 0000000000..2e672c3505 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/CropAndLockSettings.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class CropAndLockSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "CropAndLock"; + public const string ModuleVersion = "0.0.1"; + + public CropAndLockSettings() + { + Name = ModuleName; + Version = ModuleVersion; + Properties = new CropAndLockProperties(); + } + + [JsonPropertyName("properties")] + public CropAndLockProperties Properties { get; set; } + + public string GetModuleName() + { + return Name; + } + + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index a64da60beb..b93ad376b3 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -166,6 +166,23 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool cropAndLock = true; + + [JsonPropertyName("CropAndLock")] + public bool CropAndLock + { + get => cropAndLock; + set + { + if (cropAndLock != value) + { + LogTelemetryEvent(value); + cropAndLock = value; + NotifyChange(); + } + } + } + private bool awake; [JsonPropertyName("Awake")] diff --git a/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCropAndLock.png b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCropAndLock.png new file mode 100644 index 0000000000..767352a350 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsCropAndLock.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/CropAndLock.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CropAndLock.png new file mode 100644 index 0000000000..d374783648 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/CropAndLock.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CropAndLock.gif b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CropAndLock.gif new file mode 100644 index 0000000000..951392aaf7 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/CropAndLock.gif differ diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs index 669ee0a5e5..af54f7c679 100644 --- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs +++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs @@ -10,6 +10,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums AlwaysOnTop, Awake, ColorPicker, + CropAndLock, FancyZones, FileLocksmith, FileExplorer, diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml index 14bcb9eb6f..eef366a2ff 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml @@ -11,7 +11,6 @@ - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index 98103dc833..a96e73d674 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading.Tasks; +using Common.UI; using interop; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Helpers; @@ -208,17 +209,18 @@ namespace Microsoft.PowerToys.Settings.UI } else { +#if DEBUG // For debugging purposes // Window is also needed to show MessageDialog settingsWindow = new MainWindow(isDark); settingsWindow.ExtendsContentIntoTitleBar = true; settingsWindow.Activate(); settingsWindow.NavigateToSection(StartupPage); - -#if !DEBUG - ShowMessageDialogAndExit("The application cannot be run as a standalone process. Please start the application through the runner.", "Forbidden"); + ShowMessageDialog("The application is running in Debug mode.", "DEBUG"); #else - ShowMessageDialog("The application cannot be run as a standalone process. Please start the application through the runner.", "Forbidden"); + /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the General page. */ + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Overview, true); + Exit(); #endif } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.cs index 6c15a703a0..f7f7050f36 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.cs @@ -14,7 +14,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { public IsEnabledTextBlock() { - this.DefaultStyleKey = typeof(IsEnabledTextBlock); + this.Style = (Style)App.Current.Resources["DefaultIsEnabledTextBlockStyle"]; } protected override void OnApplyTemplate() diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml index 124f2b9167..1ea76328bc 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/IsEnabledTextBlock/IsEnabledTextBlock.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Microsoft.PowerToys.Settings.UI.Controls"> - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs index 8d1473e687..b41b99ed5b 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs @@ -97,6 +97,9 @@ namespace Microsoft.PowerToys.Settings.UI case "ColorPicker": needToUpdate = generalSettingsConfig.Enabled.ColorPicker != isEnabled; generalSettingsConfig.Enabled.ColorPicker = isEnabled; break; + case "CropAndLock": + needToUpdate = generalSettingsConfig.Enabled.CropAndLock != isEnabled; + generalSettingsConfig.Enabled.CropAndLock = isEnabled; break; case "FancyZones": needToUpdate = generalSettingsConfig.Enabled.FancyZones != isEnabled; generalSettingsConfig.Enabled.FancyZones = isEnabled; break; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml new file mode 100644 index 0000000000..9970e6eea1 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeCropAndLock.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + +