Compare commits

...

28 Commits

Author SHA1 Message Date
Shawn Yuan
8755aacd0b allow partially succeeded build
Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2025-04-24 15:58:26 +08:00
Shawn Yuan
e59ea36ccf update
Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2025-04-24 15:17:19 +08:00
Shawn Yuan
2f40aab220 Merge branch 'shawn/testAz' of https://github.com/microsoft/PowerToys into shawn/testAz 2025-04-24 15:13:58 +08:00
Shawn Yuan
33459b718b update
Signed-off-by: Shawn Yuan <shuai.yuan.zju@gmail.com>
2025-04-24 15:13:35 +08:00
Shawn Yuan
7146da92ea Update ci-using-the-latest-winappsdk.yml for Azure Pipelines 2025-04-23 17:39:58 +08:00
PesBandi
bbe248ca90 [ColorPicker]Only close on escape if focused 2025-03-12 07:06:32 +01:00
Mario Hewardt
c38faa959a Test ZoomIt PR (#37809)
* Test ZoomIt PR

* Test ZoomIt PR

* Test ZoomIt PR

* Test ZoomIt PR
2025-03-07 12:35:44 -08:00
dreamstart
fc94cd758e [FancyZones][Fix Bug] delete layout write wrong date to json file (#37763)
fix delete layout write json file bug

Co-authored-by: Zhaopeng Wang (from Dev Box) <zhaopengwang@microsoft.com>
2025-03-05 20:43:36 +08:00
Gordon Lam
61a00aa669 0.89 changelog (#37736)
* First Draft

* Update according to comments as well as re-reading

* Finished multiple wording comments

* Removed the module name that appeared at the beginning of the sentence, as it was already included in the section name.
2025-03-04 21:31:18 -05:00
Muhammad Danish
cca3f442e2 Update WinGet configuration file location & extension (#37673)
Update configuration file location & extension
2025-03-03 20:47:57 -08:00
Jerry Xu
22e29d1253 [Hosts] Converting manual release-check-list tests to UI-Test Automation and Adding more UI-Test-cases (#37657)
* Add more UI-Test, refactor UITestAutomation

* Convert manual test-case to automation

UI-Tests:

Validating Empty-view is shown if no entries in the list.
Validating Empty-view is NOT shown if 1 or more entries in the list.
Validating Add-an-entry HyperlinkButton in Empty-view works correctly.
Validating Adding-entry Button works correctly.
Validating the Add button should be Disabled if more than 9 hosts in one entry.
Validating the Add button should be Enabled if less or equal 9 hosts in one entry.
Validating error message should be shown if not run as admin.
Validating Warning-Dialog will be shown if 'Show a warning at startup' toggle is On.
Validating Warning-Dialog will NOT be shown if 'Show a warning at startup' toggle is Off.
Validating click 'Quit' button in Warning-Dialog, the Hosts File Editor window would be closed.
Validating click 'Accept' button in Warning-Dialog, the Hosts File Editor window would NOT be closed.

---------

Co-authored-by: Jerry Xu <nxu@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-02-28 19:08:08 +08:00
Laszlo Nemeth
8a2d4745fa [PTRun]Fix locking link files for MSI installers. Warning 1946 (#37654)
* [Runner] fix MSI installer issue. Warning 1946

* Fix catching exceptions

* Better error handling

* Revert "Better error handling"

This reverts commit 0f3ec2a3ef.
2025-02-27 19:17:43 +00:00
leileizhang
bf2685757a [CI] Use Download Task for X64 and Bypass ARM Testing for Forked Repositories (#37617)
* use x64

* add conditation for arm tests

* check repo

* use System.PullRequest.IsFork

* remove print

* remove condition
2025-02-27 16:39:25 +08:00
Laszlo Nemeth
b8cef42776 [Workspaces] Fix regression when capturing minimized apps (#37599) 2025-02-26 12:51:46 +01:00
Jerry Xu
959a54bcd9 Revert "[Hosts] Add UITest Cases for Hosts Module" (#37619)
* Revert "[Hosts] Add UITest Cases for Hosts Module (#37600)"

This reverts commit c656dcc9c5.

* Matching all UITest projects and UITestAutomation project

* Add back Hosts UITests

---------

Co-authored-by: Jerry Xu <nxu@microsoft.com>
2025-02-26 07:55:15 +08:00
Smeagol
491d51afaf [QuickAccent]Add final sigma ( ς ) to the Greek character set (#37611) 2025-02-25 22:35:22 +00:00
Ani
f263042aeb [AdvancedPaste]Add paste actions to allow transcoding of media files (#37188)
* [AdvancedPaste] Additional actions, including Image to text

* Spellcheck issue

* [AdvancedPaste] Paste as file and many other improvements

* Fixed typo

* Fixed typo

* [AdvancedPaste] Improved paste window menu layout

* [AdvancedPaste] Improved settings window layout

* [AdvancedPaste] Removed AudioToText for the moment

* Code cleanup

* Minor fixes

* [AdvancedPaste] Semantic Kernel support

* Changed log-line with potentially sensitive info

* Spellcheck issues

* Various improvements for Semantic Kernel

* Spellcheck issue

* Refactored Clipboard routines

* Added integration tests for KernelService

* Extra telemetry for AdvancedPaste

* Added 'Hotkey' suffix to AdvancedPaste_Settings telemetry event

* Added IsSavedQuery

* Added KernelQueryCache

* Refactoring

* Added KernelQueryCache to BugReportTool delete list

* Added opt-n for Semantic Kernel

* Fixed bug with KernelQueryCache

* Ability to view last AI chat message on error

* Improved kernel query cache

* Used System.IO.Abstractions and improved tests

* Fixed under-count of token usage

* Used Semantic Kernel icon

* Cleanup

* Add missing EndProject line

* Fix dependency version conflicts

* Fix NOTICE.md

* Correct place of SemanticKernel in NOTICE.md

* Unlinked CustomPreview toggle from AI

* Added Microsoft.Bcl.AsyncInterfaces dependency to AdvancedPaste

* Fixed NOTICE.md order

* Moved Custom Preview to behaviour section

* Made Image to Text raise error on empty output

* Added AIServiceBatchIntegrationTests

* Updated AIServiceBatchIntegrationTests

* Added prompt moderation

* [AdvancedPaste] Media Transcoding support

* Spellcheck issue

* Improved transcoding output profile and added tests

* Moved GPO Infobar to better location

* Added cancel button and minor bug fixes

* Fixed crash

* Minor cleanups

* Improved transcoding error messages

* Used software back when transcoding fails with hardware accerlation

* Added Reencode to spellcheck

* Spellcheck issue

---------

Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Dustin L. Howett <dustin@howett.net>
Co-authored-by: Jeremy Sinclair <4016293+snickler@users.noreply.github.com>
2025-02-25 21:33:39 +00:00
moooyo
c09a5337c4 [AOT] Refactor Logger function to improve performance and mark managedCommon as AOT compatible (#36327)
* Use function to init static value

* Replace GetFileName with GetFileNameWithoutExtension

* Add exception catch for GetCallerInfo

* Remove sourceLineNumber

* Add kernal to allow list

* Remove unused commit

* Add new folder to place source generation context

* update

* fix build issue

* Move line number back

* Use fileName to replace full path

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-02-26 00:12:38 +08:00
Ionuț Manța
9a658eb884 [PTRun] Disable CETCompat in Launcher (#37550)
* Disable CETCompat in Launcher

* Added comment

* Improved comment
2025-02-25 11:34:30 +00:00
Kai Tao
4eb11d6f9b [Workspaces][ARM64] Bring icon to packaged apps (#37625)
Bring icon to packaged apps
2025-02-25 19:17:41 +08:00
Dave Rayment
a5a354a70f [ImageResizer] Fix issues with blank Width and Height controls (#37373)
* Allow custom preset's dimensions to be blank in the UI while still persisted as 0.

* XAML formatting - reorder namespaces.

* Add "(auto)" text to zero-value Width/Height in Settings. Ensure Width and Height fields in flyout are formatted to empty when their value is 0.
2025-02-25 16:23:30 +08:00
Ionuț Manța
744316c400 [Settings]Fix ColorPicker dashboard shortcut (#37547)
* Fix color picker dashboard shortcut

* remove not needed code

* Remove comment
2025-02-24 23:14:45 +00:00
Jaime Bernardo
f2370912f3 [ci]Sign and fix KeyboardManagerEditorLibraryWrapper.dll (#37601)
* [ci]Sign KeyboardManagerEditorLibraryWrapper.dll

* Fix dll metadata
2025-02-24 21:12:42 +00:00
moooyo
5cc30df4db [AOT] Clean up AOT build issue in Common.UI (#36376)
* init commit

* Use path.combine

* Add useWPF and useWindowsForms back

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-02-25 02:56:04 +08:00
moooyo
f81f65db3d [AOT] clean up AOT issue in Settings.UI (#36559)
* Rename source generation context file

* fix build issue

* fix path bug

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
2025-02-25 02:48:54 +08:00
leileizhang
9f008a65d6 [Build] Revert "[Hosts] Add UITest Cases for Hosts Module (#37600)" to fix CI issue (#37606)
Revert "[Hosts] Add UITest Cases for Hosts Module (#37600)"

This reverts commit c656dcc9c5.
2025-02-24 16:30:33 +00:00
Jerry Xu
c656dcc9c5 [Hosts] Add UITest Cases for Hosts Module (#37600)
* Add UI-Tests for Host Module
2025-02-24 19:32:13 +08:00
Jerry Xu
83cea39b66 Update UI-Test Automation Framework (#37597)
* Improve UITest Automation

* Improve UITest Automation

* Exclude all UI-Test projects instead of just fancyZone UITest

* Exclude all UI-Test projects instead of just fancyZone UITest

* Fix code-style
2025-02-24 18:05:55 +08:00
154 changed files with 2746 additions and 632 deletions

View File

@@ -1283,7 +1283,7 @@ rectp
RECTSOURCE
recyclebin
Redist
reencode
Reencode
reencoded
REFCLSID
REFGUID

View File

@@ -128,6 +128,7 @@
"PowerToys.KeyboardManager.dll",
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
"PowerToys.Launcher.dll",
"PowerToys.PowerLauncher.dll",

View File

@@ -33,7 +33,7 @@ parameters:
default: true
- name: winAppSDKVersionNumber
type: string
default: 1.6
default: 1.7
- name: useExperimentalVersion
type: boolean
default: false
@@ -47,4 +47,4 @@ extends:
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}

View File

@@ -61,9 +61,18 @@ jobs:
reg add "HKLM\Software\Policies\Microsoft\Edge\WebView2\ReleaseChannels" /v PowerToys.exe /t REG_SZ /d "3"
displayName: "Enable WebView2 Canary Channel"
- template: steps-download-artifacts-with-azure-cli.yml
parameters:
artifactName: $(TestArtifactsName)
- ${{ if ne(parameters.platform, 'arm64') }}:
- download: current
displayName: Download artifacts
artifact: $(TestArtifactsName)
patterns: |-
**
!**\*.pdb
!**\*.lib
- ${{ else }}:
- template: steps-download-artifacts-with-azure-cli.yml
parameters:
artifactName: $(TestArtifactsName)
- template: steps-ensure-dotnet-version.yml
parameters:

View File

@@ -72,7 +72,7 @@ stages:
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
- ${{ if eq(parameters.runTests, true) }}:
- ${{ if and(eq(parameters.runTests, true), not(and(eq(platform, 'arm64'), eq(variables['System.PullRequest.IsFork'], true)))) }}:
- stage: Test_${{ platform }}
displayName: Test ${{ platform }}
dependsOn:

View File

@@ -27,6 +27,9 @@ steps:
buildType: 'specific'
project: '55e8140e-57ac-4e5f-8f9c-c7c15b51929d'
definition: '104083'
specificBuildWithTriggering: false
allowPartiallySucceededBuilds: true
allowFailedBuilds: true
buildVersionToDownload: 'latestFromBranch'
branchName: 'refs/heads/release/${{ parameters.versionNumber }}-stable'
artifactName: 'WindowsAppSDK_Nuget_And_MSIX'

View File

@@ -15,8 +15,8 @@ Param(
$referencedFileVersionsPerDll = @{}
$totalFailures = 0
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude UITests-FancyZones*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object {
# Temporarily exclude FancyZones UI tests because of Appium.WebDriver dependencies
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITest*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object {
# Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies
$depsJsonFullFileName = $_.FullName
$depsJsonFileName = $_.Name
$depsJson = Get-Content $depsJsonFullFileName | ConvertFrom-Json

View File

@@ -644,6 +644,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "KeyboardManagerEditorLibrar
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.FuzzTests", "src\modules\Hosts\Hosts.FuzzTests\Hosts.FuzzTests.csproj", "{EBED240C-8702-452D-B764-6DB9DA9179AF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.UITests", "src\modules\Hosts\Hosts.UITests\Hosts.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2252,20 +2254,14 @@ Global
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|ARM64.Build.0 = Release|ARM64
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.ActiveCfg = Release|x64
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x64.Build.0 = Release|x64
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x86.ActiveCfg = Release|x64
{CA7D8106-30B9-4AEC-9D05-B69B31B8C461}.Release|x86.Build.0 = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|ARM64.Build.0 = Debug|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.ActiveCfg = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x64.Build.0 = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x86.ActiveCfg = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Debug|x86.Build.0 = Debug|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.ActiveCfg = Release|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|ARM64.Build.0 = Release|ARM64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.ActiveCfg = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x64.Build.0 = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x86.ActiveCfg = Release|x64
{A558C25D-2007-498E-8B6F-43405AFAE9E2}.Release|x86.Build.0 = Release|x64
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|ARM64.Build.0 = Debug|ARM64
{08F9155D-B6DC-46E5-9C83-AF60B655898B}.Debug|x64.ActiveCfg = Debug|x64
@@ -2290,6 +2286,14 @@ Global
{EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|ARM64.Build.0 = Release|ARM64
{EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|x64.ActiveCfg = Release|x64
{EBED240C-8702-452D-B764-6DB9DA9179AF}.Release|x64.Build.0 = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|ARM64.ActiveCfg = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|ARM64.Build.0 = Debug|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|x64.ActiveCfg = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Debug|x64.Build.0 = Debug|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|ARM64.ActiveCfg = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|ARM64.Build.0 = Release|ARM64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.ActiveCfg = Release|x64
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2529,6 +2533,7 @@ Global
{08F9155D-B6DC-46E5-9C83-AF60B655898B} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
{4382A954-179A-4078-92AF-715187DFFF50} = {38BDB927-829B-4C65-9CD9-93FB05D66D65}
{EBED240C-8702-452D-B764-6DB9DA9179AF} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0} = {F05E590D-AD46-42BE-9C25-6A63ADD2E3EA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

170
README.md
View File

@@ -34,19 +34,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
<!-- items that need to be updated release to release -->
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.89%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.88%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.88.0/PowerToysUserSetup-0.88.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.88.0/PowerToysUserSetup-0.88.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.88.0/PowerToysSetup-0.88.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.88.0/PowerToysSetup-0.88.0-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.90%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.89%22
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysUserSetup-0.89.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysUserSetup-0.89.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysSetup-0.89.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.89.0/PowerToysSetup-0.89.0-arm64.exe
| Description | Filename | sha256 hash |
|----------------|----------|-------------|
| Per user - x64 | [PowerToysUserSetup-0.88.0-x64.exe][ptUserX64] | 5BBA2E06603CAAE0269DFBC991095C6664FD934130335197C1BA3120E19B7CA3 |
| Per user - ARM64 | [PowerToysUserSetup-0.88.0-arm64.exe][ptUserArm64] | E79723F9F94068C699E01334C8CC0C85F37818EB4664FC772D2B545A1C37C3FA |
| Machine wide - x64 | [PowerToysSetup-0.88.0-x64.exe][ptMachineX64] | C43742DB7AA3F8B01FE7AE1DA591F0342767AFE5BBACB72F2968CE5E8EE1E3AC |
| Machine wide - ARM64 | [PowerToysSetup-0.88.0-arm64.exe][ptMachineArm64] | AEE4A67643C886336F31F86C4117BA5F01BCA5E0E99FF34524217DC91AFA7132 |
| Per user - x64 | [PowerToysUserSetup-0.89.0-x64.exe][ptUserX64] | B4F130CC96F321024A257499247F6FF6DA56612215ED3882E868AAE26C689E33 |
| Per user - ARM64 | [PowerToysUserSetup-0.89.0-arm64.exe][ptUserArm64] | F69B00F4E520EB09FA0D1D1669E21910C5225FE7A2EEDC0FA7C283B201A5F9C6 |
| Machine wide - x64 | [PowerToysSetup-0.89.0-x64.exe][ptMachineX64] | E18AC8F9023E341CF7DAD35367FB9DDDB6565D83D8155DBCDDB40AE8A24AE731 |
| Machine wide - ARM64 | [PowerToysSetup-0.89.0-arm64.exe][ptMachineArm64] | 17DEADEC601D6061D7AF4F487595CC36D9191813003CC2ECE381017F0EC71FBB |
This is our preferred method.
@@ -92,141 +92,103 @@ For guidance on developing for PowerToys, please read the [developer docs](/doc/
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
### 0.88 - January 2025 Update
### 0.89 - February 2025 Update
In this release, we focused on new features, stability, and improvements.
In this release, we focused on new features, stability, accessibility and automation.
**Highlights**
**Highlights**
- New utility: ZoomIt - a screen zoom, annotation, and recording tool for technical presentations and demos. This utility from Sysinternals has had its source code released and included in PowerToys. ZoomIt will still continue to be updated and shipped by Sysinternals for users who prefer to have it as a standalone utility outside of PowerToys. Thanks [@markrussinovich](https://github.com/markrussinovich), [@foxmsft](https://github.com/foxmsft) and [@johnstep](https://github.com/johnstep) for contributing the original code and reviewing the PowerToys integration!
- Video Conference Mute has been deprecated and was removed from PowerToys.
- .Net 9.0.1 fixed many issue in WPF, improving stability for PowerToys Run.
- Enhanced Advanced Paste by adding media transcoding support to convert different video and audio file formats! Thanks [@snickler](https://github.com/snickler) for your help!
- Fixed crashes when loading thumbnails after the .NET 9 update and resolved PowerLauncher.exe blocking other MSI installers from creating shortcuts!
- Fixed accessibility issues across FancyZones, Image Resizer, and Settings to improve screen reader support and clarity!
- Enhanced UI automation framework across modules and added new tests to cover manual checks, with more improvements coming!
### General
- Applied a workaround for the Windows App SDK applications title bar override that was causing accent color to not be shown on the top bar of applications on Windows 10. Thanks [@pingzing](https://github.com/pingzing)!
- Improved the "admin application running" notification checking logic to be less demanding on resources. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed an issue causing many utilities to crash when the GPO to disable data diagnostics was applied.
- Fixed an issue where updating PowerToys on Windows 11 did not properly update context menu entries, impacting New+, PowerRename, Image Resizer, and File Locksmith.
- Updated .NET Packages from 9.0.1 to 9.0.2. Thanks [@snickler](https://github.com/snickler) for this.
- Enabled compatibility with VS17.3 and later, for C++23. Thanks [@LNKLEO](https://github.com/LNKLEO) for this.
### Advanced Paste
- Fixed a crash when the application was exiting. (This was a hotfix for 0.87)
- Added a Json format validation step to verify if a conversion to Json should be applied.
- Fixed accessibility issues when using a screen reader.
- Added support for all BitmapDecoder supported image file types to the Image to Text functionality. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed an issue causing Advanced Paste initialization errors to hang the PowerToys main process.
- Added media transcoding support to convert different video and audio file formats, improved UI layouts, refined clipboard handling, and integrated Semantic Kernel for smarter pasting. Thanks [@snickler](https://github.com/snickler) for your help!
### FancyZones
- Removed Workspaces Editor from the exclusions list so it can be snapped by FancyZones.
- Fixed accessibility by improving the text for monitors, ensuring clearer naming and help text for screen readers.
### Keyboard Manager
- Added an option to make a shortcut remapping only trigger with exact modifiers.
### Image Resizer
- Fixed issues with Width and Height fields in Image Resizer's Custom preset, ensuring empty values no longer cause errors, settings save correctly, and auto-scaling behaves as expected. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed accessibility by ensuring screen readers announce selected image dimensions in the combo-box for better navigation.
### Monaco Preview
- Added support for .resx and .resw files in Peek and File Explorer add-ons. Thanks [@asif4318](https://github.com/asif4318)!
- Added a setting to make the code minimap toggle-able in Peek and File Explorer add-ons. Thanks [@PesBandi](https://github.com/PesBandi)!
- Fixed an issue causing Json format preview setting to not be applied correctly.
- Fixed an issue causing the wrong Monaco assets to be used at runtime.
- Fixed open link in default browser rather than Microsoft Edge. Thanks [@OldUser101](https://github.com/OldUser101)!
### Mouse Highlighter
- Fixed a highlight released on an Administrator window will start fading, instead of staying on the screen indefinitely until the mouse button is pressed again on an unelevated window.
### Mouse Without Borders
- Fixed an issue in service mode where copy-paste and drag-drop file transfers didnt work, ensuring seamless file operations.
- Enabled GPO for enable/disable for Mouse Without Borders in Service Mode. Thanks [@htcfreek](https://github.com/htcfreek) for review and comments!
- Fixed code maintainability by refactoring the oversized 'Common' class in Mouse Without Borders into smaller, focused classes for better structure and clarity. Thanks [@mikeclayton](https://github.com/mikeclayton) and thanks [@htcfreek](https://github.com/htcfreek) for review!
- Fixed an issue causing clipboard to stop working after going through a UAC screen when using the Service mode. Thanks [@YDKK](https://github.com/YDKK)!
### New+
- Fixed an issue causing New+ to override the New file or folder creation from the File Explorer Ribbon buttons or keyboard shortcuts on Windows 10.
- When creating file or folders through a template, they should now have the current time as the last modified date. Thanks [@cgaarden](https://github.com/cgaarden)!
### Peek
- Fixed an issue causing Peek to not appear if it was previously minimized. Thanks [@asif4318](https://github.com/asif4318)!
### PowerRename
- Supported negative value as Start value in regular expression, e.g. ${start=-1314}
- Enhanced RegEx help by adding $, ^, quantifiers, and common patterns for better usability. Thanks [@PesBandi](https://github.com/PesBandi) and thanks [@htcfreek](https://github.com/htcfreek) for review.
### PowerToys Run
- Fixed a transparent border issue on Windows 10. (This was a hotfix for 0.87)
- Fixed a crash in the OneNote plugin after the .Net 9 update. (This was a hotfix for 0.87)
- Fixed an issue causing the Calculator plugin to return division by zero errors when dividing by hexadecimal numbers. Thanks [@plante-msft](https://github.com/plante-msft)!
- Updated the Calculator plugin Mages library to 3.0.0 and added support for the random integer function. Thanks [@htcfreek](https://github.com/htcfreek)!
- Improved handling of non-base 10 numbers to add support for binary and octal numbers in the Calculator plugin. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added a setting to enable selection of which units to use for trigonometric functions. Thanks [@OldUser101](https://github.com/OldUser101)!
- Fixed a .NET 9 regression causing the PowerToys Run dialog to not be draggable. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added context menu buttons for the VS Code Workspaces plugin, for copying the path, opening in File Explorer or in Console. Thanks [@programming-with-ia](https://github.com/programming-with-ia)!
- Added some telemetry to gather data on which hotkey is used to trigger PowerToys Run.
- Removed the workarounds that were in place to fix some WPF issues that were fixed in .NET 9.0.1.
- Fixed a typo in the Value Generator plugin messages. Thanks [@OldUser101](https://github.com/OldUser101)!
- Fixed crashes when loading thumbnails after the .NET 9 update by disabling CETCompat.
- Fixed PowerLauncher.exe blocking other MSI installers creating shortcuts. Thanks [@OneBlue](https://github.com/OneBlue)!
- Fixed Runs dark mode detection to work reliably, preventing issues with incorrect theme detection and ensuring a smoother user experience. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed list separator handling in Calculator, allowing functions with multiple arguments to work correctly across different locales. For example pow(2;3) would be replaced with pow(2,3). Thanks [@PesBandi](https://github.com/PesBandi) and thanks [@htcfreek](https://github.com/htcfreek) for review!
- Fixed angle unit conversions in the PowerToys Run calculator, allowing quick conversions between radians, degrees, and gradians. Thanks [@OldUser101](https://github.com/OldUser101)!
### Quick Accent
- Added the ć character to the Slovenian character set. Thanks [@dsoklic](https://github.com/dsoklic)!
- Added the Proto-Indo-European character set.
### Registry Preview
- Fixed an issue causing line breaks to not be parsed correctly for REG_MULTI_SZ values. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added a tooltip to values to show multiple lines of data. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added a context menu to enable copying type, value and key paths. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added ǎ, ǒ and ǔ to the IPA character set. Thanks [@PesBandi](https://github.com/PesBandi)!
- Added ` (backtick) and ~ (tilde) to the VK_OEM_5 character set. Thanks [@xanatos](https://github.com/xanatos)!
- Added ς (final sigma) to the Greek character set. Thanks [@IamSmeagol](https://github.com/IamSmeagol)!
### Settings
- Made the Advanced Paste paste OpenAI configuration modal scrollable.
- Fixed the text on the Quick Accent page to refer to "character sets" instead of "character set". Thanks [@PesBandi](https://github.com/PesBandi)!
- Added the plugin's dll file version and website to the PowerToys Run plugin settings. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added the Workspaces file to the list of files that gets backed up by the Back up / Restore functionality.
- Fixed an issue causing some of the selected character sets to be unselected when opening the character set expander in the Quick Accent page.
- Improved GPO logic, icons, info bar layout and enabled state of all modules settings pages. Thanks [@htcfreek](https://github.com/htcfreek)!
- Fixed some accessibility issues and refactored and improved quality of the code related to image sizes in the Image Resizer page. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed mentions of "Backup" to "Back up" when it should be used as a verb. Thanks [@JackStuart](https://github.com/JackStuart)!
- Added a "New" label to Settings to better highlight new utilities that get released. Thanks [@niels9001](https://github.com/niels9001) for the UI tweaks!
### Text Extractor
- Fixed many accessibility and UI issues on the overlay UI. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Enabled GPO for the "run at startup" setting. Thanks [@htcfreek](https://github.com/htcfreek) for review and comments!
- Fixed accessibility issue by allowing screen readers to announce the group name for secondary links in Settings pages, instead of reading link descriptions without context.
- Fixed an issue where the Color Picker shortcut was not displaying correctly in the Dashboard.
### Workspaces
- Fixed an issue causing the Workspaces Editor to start outside of visible desktop area.
- Fixed an issue to maintain command line arguments for applications when trying using the "Launch and Edit" feature.
### Video Conference Mute
- The module has been deprecated in 0.88.0, being removed from PowerToys.
- Fixed if a window was last placed on a disconnected monitor, it launches minimized and repositions within the main monitor's visible area when restored, instead of remaining off-screen and invisible.
- Fixed on ARM64 to correctly display icons for packaged apps by resolving path mismatches.
### ZoomIt
- New utility: Zoom It - a screen zoom, annotation, and recording tool for technical presentations and demos. This utility from Sysinternals has had its source code released and included in PowerToys. ZoomIt will still continue to be updated and shipped by Sysinternals for users who prefer to have it as a standalone utility outside of PowerToys. Thanks [@markrussinovich](https://github.com/markrussinovich), [@foxmsft](https://github.com/foxmsft) and [@johnstep](https://github.com/johnstep) for contributing the original code and reviewing the PowerToys integration!
- Fixed warning C4706 and related error C2220 during build. Thanks [@xanatos](https://github.com/xanatos)!
### Documentation
- Updated the PowerToys Run documentation to reflect documentation pages for new plugins.
- Added YubicoOauthOTP plugin mention to thirdPartyRunPlugins.md. Thanks [@dlnilsson](https://github.com/dlnilsson)!
- Fixed runner-ipc.md doc on the broken link. Thanks [@daverayment](https://github.com/daverayment)!
- Fixed the new plugin checklist by updating the target framework, removing duplicates, and improving statement organization. Thanks [@hlaueriksson](https://github.com/hlaueriksson)!
- Updated runner documentation to align with the latest code structure.
### Development
- Added fuzz testing for AdvancedPaste, with a new pipeline for OneFuzz.
- Added a new CI pipeline to build with the latest WindowsAppSDK.
- Added a new CI pipeline to build with the latest webview2 from Edge Canary.
- Made the HostsUILib project AOT compatible. Thanks [@snickler](https://github.com/snickler) for your help reviewing this!
- Made FilePreviewCommon and MarkdownPreviewHandler AOT compatible. Thanks [@snickler](https://github.com/snickler) for your help reviewing this!
- Made the PowerAccent.Core project AOT compatible. Thanks [@snickler](https://github.com/snickler) for your help reviewing this!
- Cleaned up some code for AOT compatibility in the Advanced Paste module. Thanks [@snickler](https://github.com/snickler) for your help reviewing this!
- Removed the prerelease flag from the PowerToys development DSC configurations. Thanks [@denelon](https://github.com/denelon)!
- Improved Dart CI reliability by improving error messages and retrying to the step that installs the correct dotnet version.
- Improved Dart CI reliability by fixing retries when downloading the localization files.
- Improved Dart CI build times by removing the steps to build the no longer needed abstracted utility nuget packages.
- Removed the solution.props file from the solution root.
- Fixed PowerToys Run Calculator plugin tests when running in systems with different number formats. Thanks [@htcfreek](https://github.com/htcfreek)!
- Updated many .NET packages from .NET 9.0.0 to 9.0.1 for security fixes. Thanks [@snickler](https://github.com/snickler)!
- Refactored the Mouse Without Borders Common.Log.cs and Common.Receiver.cs files. Thanks [@mikeclayton](https://github.com/mikeclayton)!
- Stabilized pipeline on ARM64 and forked build.
- Added fuzz testing for HostUILib, added as part of pipeline for OneFuzz.
- Fixed and improved UI-Test automation framework, and added new test cases for the FancyZones and Hosts module.
- Optimized Logger function as AOT compatible, improving performance by 18%.
- Made Common.UI and Setting.UI to be AOT compatible.
### What is being planned for version 0.90
#### What is being planned for version 0.89
For [v0.90][github-next-release-work], we'll work on the items below:
For [v0.89][github-next-release-work], we'll work on the items below:
- Stability / bug fixes
- New module: PowerToys Run v2
- New module: File Actions Menu
- PowerToys Run v2 development work
- Working on installer upgrades
- Upgrading keyboard manager's editor UI
- Stability / bug fixes
## PowerToys Community

View File

@@ -94,5 +94,5 @@ Note that we've supplied `Debug` option, so a `%TEMP\PowerToys.DSC.TestConfigure
Finally, you can test it with winget by invoking it as such:
```ps
winget configure .\configuration.dsc.yaml --accept-configuration-agreements --disable-interactivity
winget configure .\configuration.winget --accept-configuration-agreements --disable-interactivity
```

View File

@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<UseWPF>true</UseWPF>

View File

@@ -91,20 +91,20 @@ namespace Common.UI
{
try
{
var assemblyPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
var fullPath = new DirectoryInfo(assemblyPath).FullName;
var directoryPath = System.AppContext.BaseDirectory;
if (mainExecutableIsOnTheParentFolder)
{
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
fullPath = fullPath + "\\..\\PowerToys.exe";
directoryPath = Path.Combine(directoryPath, "..");
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
else
{
// PowerToys.exe is in the same path as the application.
fullPath = fullPath + "\\PowerToys.exe";
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
}
Process.Start(new ProcessStartInfo(fullPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
}
catch
{

View File

@@ -11,7 +11,7 @@ using Microsoft.Win32;
namespace Common.UI
{
public class ThemeManager : IDisposable
public partial class ThemeManager : IDisposable
{
private readonly Application _app;
private const string LightTheme = "Light.Accent1";

View File

@@ -6,6 +6,7 @@ using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using ManagedCommon.Serialization;
namespace ManagedCommon
{
@@ -35,7 +36,7 @@ namespace ManagedCommon
inputStream.Close();
reader.Dispose();
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data).LanguageTag;
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data, SourceGenerationContext.Default.OutGoingLanguageSettings).LanguageTag;
}
catch (Exception)
{

View File

@@ -15,15 +15,23 @@ namespace ManagedCommon
{
public static class Logger
{
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
private static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location).ProductVersion;
private static readonly string Error = "Error";
private static readonly string Warning = "Warning";
private static readonly string Info = "Info";
private static readonly string Debug = "Debug";
private static readonly string TraceFlag = "Trace";
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
/*
* Please pay more attention!
* If you want to publish it with Native AOT enabled (or publish as a single file).
* You need to find another way to remove Assembly.Location usage.
*/
#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file
private static readonly string Version = FileVersionInfo.GetVersionInfo(Assembly.Location).ProductVersion;
#pragma warning restore IL3000 // Avoid accessing Assembly file path when publishing as a single file
/// <summary>
/// Initializes the logger and sets the path for logging.
/// </summary>
@@ -53,18 +61,16 @@ namespace ManagedCommon
Trace.AutoFlush = true;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogError(string message)
public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Error);
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogError(string message, Exception ex)
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
if (ex == null)
{
Log(message, Error);
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
}
else
{
@@ -83,38 +89,33 @@ namespace ManagedCommon
"Stack trace: " + Environment.NewLine +
ex.StackTrace;
Log(exMessage, Error);
Log(exMessage, Error, memberName, sourceFilePath, sourceLineNumber);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogWarning(string message)
public static void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Warning);
Log(message, Warning, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogInfo(string message)
public static void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Info);
Log(message, Info, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogDebug(string message)
public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(message, Debug);
Log(message, Debug, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void LogTrace()
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
Log(string.Empty, TraceFlag);
Log(string.Empty, TraceFlag, memberName, sourceFilePath, sourceLineNumber);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void Log(string message, string type)
private static void Log(string message, string type, string memberName, string sourceFilePath, int sourceLineNumber)
{
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo());
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo(memberName, sourceFilePath, sourceLineNumber));
Trace.Indent();
if (message != string.Empty)
{
@@ -124,49 +125,27 @@ namespace ManagedCommon
Trace.Unindent();
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static string GetCallerInfo()
private static string GetCallerInfo(string memberName, string sourceFilePath, int sourceLineNumber)
{
StackTrace stackTrace = new();
var callerMethod = GetCallerMethod(stackTrace);
return $"{callerMethod?.DeclaringType?.Name}::{callerMethod.Name}";
}
private static MethodBase GetCallerMethod(StackTrace stackTrace)
{
const int topFrame = 3;
var topMethod = stackTrace.GetFrame(topFrame)?.GetMethod();
string callerFileName = "Unknown";
try
{
if (topMethod?.Name == nameof(IAsyncStateMachine.MoveNext) && typeof(IAsyncStateMachine).IsAssignableFrom(topMethod?.DeclaringType))
string fileName = Path.GetFileName(sourceFilePath);
if (!string.IsNullOrEmpty(fileName))
{
// Async method; return actual method as determined by heuristic:
// "Nearest method on stack to async state-machine's MoveNext() in same namespace but in a different type".
// There are tighter ways of determining the actual method, but this is good enough and probably faster.
for (int deepFrame = topFrame + 1; deepFrame < stackTrace.FrameCount; deepFrame++)
{
var deepMethod = stackTrace.GetFrame(deepFrame)?.GetMethod();
if (deepMethod?.DeclaringType != topMethod?.DeclaringType && deepMethod?.DeclaringType?.Namespace == topMethod?.DeclaringType?.Namespace)
{
return deepMethod;
}
}
callerFileName = fileName;
}
}
catch (Exception)
{
// Ignore exceptions in Release. The code above won't throw, but if it does, we don't want to crash the app.
callerFileName = "Unknown";
#if DEBUG
throw;
#endif
}
return topMethod;
return $"{callerFileName}::{memberName}::{sourceLineNumber}";
}
}
}

View File

@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\Common.Dotnet.AotCompatibility.props" />
<PropertyGroup>
<Description>PowerToys ManagedCommon</Description>

View File

@@ -53,7 +53,7 @@ namespace ManagedCommon
internal static int Size
{
get { return Marshal.SizeOf(typeof(INPUT)); }
get { return Marshal.SizeOf<INPUT>(); }
}
}

View File

@@ -14,12 +14,11 @@ namespace ManagedCommon
{
public static class RunnerHelper
{
public static void WaitForPowerToysRunner(int powerToysPID, Action act)
public static void WaitForPowerToysRunner(int powerToysPID, Action act, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
{
var stackTrace = new StackTrace();
var assembly = Assembly.GetCallingAssembly().GetName();
var callingMethod = stackTrace.GetFrame(1).GetMethod().Name;
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{memberName}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
Task.Run(() =>
{
const uint INFINITE = 0xFFFFFFFF;
@@ -29,7 +28,7 @@ namespace ManagedCommon
IntPtr powerToysProcHandle = NativeMethods.OpenProcess(SYNCHRONIZE, false, powerToysPID);
if (NativeMethods.WaitForSingleObject(powerToysProcHandle, INFINITE) == WAIT_OBJECT_0)
{
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{memberName}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
act.Invoke();
}
});

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
using static ManagedCommon.LanguageHelper;
namespace ManagedCommon.Serialization;
[JsonSerializable(typeof(OutGoingLanguageSettings))]
internal sealed partial class SourceGenerationContext : JsonSerializerContext
{
}

View File

@@ -14,7 +14,7 @@ namespace ManagedCommon
/// <param name="sender">Sender ThemeListener</param>
public delegate void ThemeChangedEvent(ThemeListener sender);
public class ThemeListener : IDisposable
public partial class ThemeListener : IDisposable
{
/// <summary>
/// Gets the App Theme.

View File

@@ -2,13 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.Events;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
@@ -16,5 +9,14 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public class Button : Element
{
private static readonly string ExpectedControlType = "ControlType.Button";
/// <summary>
/// Initializes a new instance of the <see cref="Button"/> class.
/// </summary>
public Button()
{
this.TargetControlType = Button.ExpectedControlType;
}
}
}

View File

@@ -18,6 +18,12 @@ namespace Microsoft.PowerToys.UITest
this.by = by;
}
public override string ToString()
{
// override ToString to return detailed debugging content provided by OpenQA.Selenium.By
return this.by.ToString();
}
/// <summary>
/// Creates a By object using the name attribute.
/// </summary>

View File

@@ -3,16 +3,11 @@
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.Events;
using static Microsoft.PowerToys.UITest.UITestBase;
[assembly: InternalsVisibleTo("Session")]
@@ -24,8 +19,17 @@ namespace Microsoft.PowerToys.UITest
public class Element
{
private WindowsElement? windowsElement;
private WindowsDriver<WindowsElement>? driver;
protected string? TargetControlType { get; set; }
internal bool IsMatchingTarget()
{
var ct = this.ControlType;
return string.IsNullOrEmpty(this.TargetControlType) || this.TargetControlType == this.ControlType;
}
internal void SetWindowsElement(WindowsElement windowsElement) => this.windowsElement = windowsElement;
internal void SetSession(WindowsDriver<WindowsElement> driver) => this.driver = driver;
@@ -43,7 +47,20 @@ namespace Microsoft.PowerToys.UITest
/// </summary>
public string Text
{
get { return GetAttribute("Value"); }
get { return this.windowsElement?.Text ?? string.Empty; }
}
/// <summary>
/// Gets a value indicating whether the UI element is Enabled or not.
/// </summary>
public bool Enabled
{
get { return this.windowsElement?.Enabled ?? false; }
}
public bool Selected
{
get { return this.windowsElement?.Selected ?? false; }
}
/// <summary>
@@ -78,26 +95,19 @@ namespace Microsoft.PowerToys.UITest
get { return GetAttribute("ControlType"); }
}
/// <summary>
/// Checks if the UI element is enabled.
/// </summary>
/// <returns>True if the element is enabled; otherwise, false.</returns>
public bool IsEnabled() => GetAttribute("IsEnabled") == "True";
/// <summary>
/// Checks if the UI element is selected.
/// </summary>
/// <returns>True if the element is selected; otherwise, false.</returns>
public bool IsSelected() => GetAttribute("IsSelected") == "True";
/// <summary>
/// Click the UI element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click.</param>
public void Click(bool rightClick = false)
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
public virtual void Click(bool rightClick = false)
{
PerformAction(actions =>
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement);
// Move 2by2 offset to make click more stable instead of click on the border of the element
actions.MoveByOffset(2, 2);
if (rightClick)
{
actions.ContextClick();
@@ -106,6 +116,25 @@ namespace Microsoft.PowerToys.UITest
{
actions.Click();
}
actions.Build().Perform();
});
}
/// <summary>
/// Double Click the UI element.
/// </summary>
public virtual void DoubleClick()
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement);
// Move 2by2 offset to make click more stable instead of click on the border of the element
actions.MoveByOffset(2, 2);
actions.DoubleClick();
actions.Build().Perform();
});
}
@@ -118,7 +147,6 @@ namespace Microsoft.PowerToys.UITest
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method GetAttribute with parameter: attributeName = {attributeName}");
var attributeValue = this.windowsElement.GetAttribute(attributeName);
Assert.IsNotNull(attributeValue, $"Attribute '{attributeName}' is null.");
return attributeValue;
}
@@ -133,17 +161,51 @@ namespace Microsoft.PowerToys.UITest
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
var foundElement = FindElementHelper.Find<T, AppiumWebElement>(
() =>
{
var element = this.windowsElement.FindElement(by.ToSeleniumBy());
Assert.IsNotNull(element, $"Element not found using selector: {by}");
return element;
},
this.driver,
timeoutMS);
return foundElement;
// leverage findAll to filter out mismatched elements
var collection = this.FindAll<T>(by, timeoutMS);
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
return collection[0];
}
/// <summary>
/// Finds an element by the selector.
/// Shortcut for this.Find<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class type of the element to find.</typeparam>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public T Find<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Find<T>(By.Name(name), timeoutMS);
}
/// <summary>
/// Finds an element by the selector.
/// Shortcut for this.Find<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to use for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public Element Find(By by, int timeoutMS = 3000)
{
return this.Find<Element>(by, timeoutMS);
}
/// <summary>
/// Finds an element by the selector.
/// Shortcut for this.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>The found element.</returns>
public Element Find(string name, int timeoutMS = 3000)
{
return this.Find<Element>(By.Name(name), timeoutMS);
}
/// <summary>
@@ -153,33 +215,81 @@ namespace Microsoft.PowerToys.UITest
/// <param name="by">The selector to use for finding the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T>? FindAll<T>(By by, int timeoutMS = 3000)
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.windowsElement, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
var foundElements = FindElementHelper.FindAll<T, AppiumWebElement>(
var foundElements = FindHelper.FindAll<T, AppiumWebElement>(
() =>
{
var elements = this.windowsElement.FindElements(by.ToSeleniumBy());
Assert.IsTrue(elements.Count > 0, $"Elements not found using selector: {by}");
return elements;
},
this.driver,
timeoutMS);
return foundElements;
return foundElements ?? new ReadOnlyCollection<T>([]);
}
/// <summary>
/// Finds all elements by the selector.
/// Shortcut for this.FindAll<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class type of the elements to find.</typeparam>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.FindAll<T>(By.Name(name), timeoutMS);
}
/// <summary>
/// Finds all elements by the selector.
/// Shortcut for this.FindAll<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to use for finding the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
{
return this.FindAll<Element>(by, timeoutMS);
}
/// <summary>
/// Finds all elements by the selector.
/// Shortcut for this.FindAll<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name for finding the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds.</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
{
return this.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
/// Simulates a manual operation on the element.
/// </summary>
private void PerformAction(Action<Actions> action)
/// <param name="action">The action to perform on the element.</param>
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 500 ms</param>
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 500 ms</param>
protected void PerformAction(Action<Actions, WindowsElement> action, int msPreAction = 500, int msPostAction = 500)
{
var element = this.windowsElement;
if (msPreAction > 0)
{
Task.Delay(msPreAction).Wait();
}
var windowElement = this.windowsElement!;
Actions actions = new Actions(this.driver);
actions.MoveToElement(element);
action(actions);
actions.Build().Perform();
action(actions, windowElement);
if (msPostAction > 0)
{
Task.Delay(msPostAction).Wait();
}
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Represents a HyperLinkButton in the UI test environment.
/// HyperLinkButton represents a button control that functions as a hyperlink.
/// </summary>
public class HyperlinkButton : Button
{
private static readonly string ExpectedControlType = "ControlType.HyperLink";
/// <summary>
/// Initializes a new instance of the <see cref="HyperlinkButton"/> class.
/// </summary>
public HyperlinkButton()
{
this.TargetControlType = HyperlinkButton.ExpectedControlType;
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Represents a NavigationViewItem in the UI test environment.
/// NavigationViewItem represents the container for an item in a NavigationView control.
/// </summary>
public class NavigationViewItem : Element
{
private static readonly string ExpectedControlType = "ControlType.ListItem";
/// <summary>
/// Initializes a new instance of the <see cref="NavigationViewItem"/> class.
/// </summary>
public NavigationViewItem()
{
this.TargetControlType = NavigationViewItem.ExpectedControlType;
}
/// <summary>
/// Click the ListItem element.
/// </summary>
/// <param name="rightClick">If true, performs a right-click; otherwise, performs a left-click. Default value is false</param>
public override void Click(bool rightClick = false)
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement, 10, 10);
if (rightClick)
{
actions.ContextClick();
}
else
{
actions.Click();
}
actions.Build().Perform();
});
}
/// <summary>
/// Double Click the ListItem element.
/// </summary>
public override void DoubleClick()
{
PerformAction((actions, windowElement) =>
{
actions.MoveToElement(windowElement, 10, 10);
actions.DoubleClick();
actions.Build().Perform();
});
}
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Represents a TextBlock in the UI test environment.
/// TextBlock provides a lightweight control for displaying small amounts of flow content.
/// </summary>
public class TextBlock : Element
{
private static readonly string ExpectedControlType = "ControlType.Text";
/// <summary>
/// Initializes a new instance of the <see cref="TextBlock"/> class.
/// </summary>
public TextBlock()
{
this.TargetControlType = TextBlock.ExpectedControlType;
}
}
}

View File

@@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using OpenQA.Selenium;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Represents a TextBox in the UI test environment.
/// TextBox represents a control that can be used to display and edit plain text (single or multi-line).
/// </summary>
public class TextBox : Element
{
private static readonly string ExpectedControlType = "ControlType.Edit";
/// <summary>
/// Initializes a new instance of the <see cref="TextBox"/> class.
/// </summary>
public TextBox()
{
this.TargetControlType = TextBox.ExpectedControlType;
}
/// <summary>
/// Sets the text of the textbox.
/// </summary>
/// <param name="value">The text to set.</param>
/// <param name="clearText">A value indicating whether to clear the text before setting it. Default value is true</param>
/// <returns>The current TextBox instance.</returns>
public TextBox SetText(string value, bool clearText = true)
{
if (clearText)
{
PerformAction((actions, windowElement) =>
{
// select all text and delete it
windowElement.SendKeys(Keys.Control + "a");
windowElement.SendKeys(Keys.Delete);
});
}
PerformAction((actions, windowElement) =>
{
windowElement.SendKeys(value);
});
return this;
}
}
}

View File

@@ -0,0 +1,41 @@
// 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 Newtonsoft.Json.Linq;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Represents a ToggleSwitch in the UI test environment.
/// </summary>
public class ToggleSwitch : Button
{
/// <summary>
/// Gets a value indicating whether the ToggleSwitch is on.
/// </summary>
public bool IsOn
{
get
{
return this.Selected;
}
}
/// <summary>
/// Sets the ToggleSwitch to the specified value.
/// </summary>
/// <param name="value">A value indicating whether the ToggleSwitch should be active. Default is true</param>
/// <returns>The current ToggleSwitch instance.</returns>
public ToggleSwitch Toggle(bool value = true)
{
if (this.IsOn != value)
{
// Toggle the switch
this.Click();
}
return this;
}
}
}

View File

@@ -2,13 +2,6 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Support.Events;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
@@ -25,7 +18,7 @@ namespace Microsoft.PowerToys.UITest
{
if (byClickButton)
{
Find<Button>(By.Name("Maximize")).Click();
Find<Button>("Maximize").Click();
}
else
{
@@ -44,7 +37,7 @@ namespace Microsoft.PowerToys.UITest
{
if (byClickButton)
{
Find<Button>(By.Name("Restore")).Click();
Find<Button>("Restore").Click();
}
else
{
@@ -63,7 +56,7 @@ namespace Microsoft.PowerToys.UITest
{
if (byClickButton)
{
Find<Button>(By.Name("Minimize")).Click();
Find<Button>("Minimize").Click();
}
else
{
@@ -81,7 +74,7 @@ namespace Microsoft.PowerToys.UITest
{
if (byClickButton)
{
Find<Button>(By.Name("Close")).Click();
Find<Button>("Close").Click();
}
else
{

View File

@@ -2,16 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
[assembly: InternalsVisibleTo("Element")]
@@ -22,15 +15,8 @@ namespace Microsoft.PowerToys.UITest
/// <summary>
/// Helper class for finding elements.
/// </summary>
internal static class FindElementHelper
internal static class FindHelper
{
public static T Find<T, TW>(Func<TW> findElementFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
var item = findElementFunc() as WindowsElement;
return NewElement<T>(item, driver, timeoutMS);
}
public static ReadOnlyCollection<T>? FindAll<T, TW>(Func<ReadOnlyCollection<TW>> findElementsFunc, WindowsDriver<WindowsElement>? driver, int timeoutMS)
where T : Element, new()
{
@@ -39,7 +25,7 @@ namespace Microsoft.PowerToys.UITest
{
var element = item as WindowsElement;
return NewElement<T>(element, driver, timeoutMS);
}).ToList();
}).Where(item => item.IsMatchingTarget()).ToList();
return new ReadOnlyCollection<T>(res);
}
@@ -51,7 +37,12 @@ namespace Microsoft.PowerToys.UITest
Assert.IsNotNull(element, $"New Element {typeof(T).Name} error: element is null.");
T newElement = new T();
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
if (timeoutMS > 0)
{
// Only set timeout if it is positive value
driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromMilliseconds(timeoutMS);
}
newElement.SetSession(driver);
newElement.SetWindowsElement(element);
return newElement;

View File

@@ -2,17 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
namespace Microsoft.PowerToys.UITest
{
@@ -45,17 +40,48 @@ namespace Microsoft.PowerToys.UITest
where T : Element, new()
{
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method Find<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
var foundElement = FindElementHelper.Find<T, WindowsElement>(
() =>
{
var element = this.WindowsDriver.FindElement(by.ToSeleniumBy());
Assert.IsNotNull(element, $"Element not found using selector: {by}");
return element;
},
this.WindowsDriver,
timeoutMS);
return foundElement;
// leverage findAll to filter out mismatched elements
var collection = this.FindAll<T>(by, timeoutMS);
Assert.IsTrue(collection.Count > 0, $"Element not found using selector: {by}");
return collection[0];
}
/// <summary>
/// Shortcut for this.Find<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
public T Find<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Find<T>(By.Name(name), timeoutMS);
}
/// <summary>
/// Shortcut for this.Find<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
public Element Find(By by, int timeoutMS = 3000)
{
return this.Find<Element>(by, timeoutMS);
}
/// <summary>
/// Shortcut for this.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
public Element Find(string name, int timeoutMS = 3000)
{
return this.Find<Element>(By.Name(name), timeoutMS);
}
/// <summary>
@@ -65,21 +91,58 @@ namespace Microsoft.PowerToys.UITest
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T>? FindAll<T>(By by, int timeoutMS = 3000)
public ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
Assert.IsNotNull(this.WindowsDriver, $"WindowsElement is null in method FindAll<{typeof(T).Name}> with parameters: by = {by}, timeoutMS = {timeoutMS}");
var foundElements = FindElementHelper.FindAll<T, WindowsElement>(
var foundElements = FindHelper.FindAll<T, WindowsElement>(
() =>
{
var elements = this.WindowsDriver.FindElements(by.ToSeleniumBy());
Assert.IsTrue(elements.Count > 0, $"Elements not found using selector: {by}");
return elements;
},
this.WindowsDriver,
timeoutMS);
return foundElements;
return foundElements ?? new ReadOnlyCollection<T>([]);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.FindAll<T>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="name">The name to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.FindAll<T>(By.Name(name), timeoutMS);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.FindAll<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
{
return this.FindAll<Element>(by, timeoutMS);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.FindAll<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
public ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
{
return this.FindAll<Element>(By.Name(name), timeoutMS);
}
/// <summary>
@@ -110,6 +173,7 @@ namespace Microsoft.PowerToys.UITest
SetForegroundWindow(windowHandle);
var hexWindowHandle = windowHandle.ToString("x");
var appCapabilities = new AppiumOptions();
appCapabilities.AddAdditionalCapability("appTopLevelWindow", hexWindowHandle);
appCapabilities.AddAdditionalCapability("deviceName", "WindowsPC");
this.WindowsDriver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), appCapabilities);

View File

@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Nested class for test initialization.
/// </summary>
internal class SessionHelper
{
// Default session path is PowerToys settings dashboard
private readonly string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
private WindowsDriver<WindowsElement> Root { get; set; }
private WindowsDriver<WindowsElement>? Driver { get; set; }
private Process? appDriver;
public SessionHelper(PowerToysModule scope)
{
this.sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
var winAppDriverProcessInfo = new ProcessStartInfo
{
FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
Verb = "runas",
};
this.appDriver = Process.Start(winAppDriverProcessInfo);
var desktopCapabilities = new AppiumOptions();
desktopCapabilities.AddAdditionalCapability("app", "Root");
this.Root = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
// Set default timeout to 5 seconds
this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Initializes the test environment.
/// </summary>
/// <param name="scope">The PowerToys module to start.</param>
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
public SessionHelper Init()
{
string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
this.StartExe(path + this.sessionPath);
Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
// Set default timeout to 5 seconds
this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
return this;
}
/// <summary>
/// Cleans up the test environment.
/// </summary>
public void Cleanup()
{
try
{
appDriver?.Kill();
}
catch (Exception ex)
{
// Handle exceptions if needed
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
}
}
/// <summary>
/// Starts a new exe and takes control of it.
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
public void StartExe(string appPath)
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", appPath);
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
}
public WindowsDriver<WindowsElement> GetRoot() => this.Root;
public WindowsDriver<WindowsElement> GetDriver()
{
Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
return this.Driver;
}
}
}

View File

@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
@@ -15,7 +16,4 @@
<PackageReference Include="System.IO.Abstractions" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
</Project>

View File

@@ -2,129 +2,156 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Windows;
using OpenQA.Selenium.Interactions;
namespace Microsoft.PowerToys.UITest
{
/// <summary>
/// Base class that should be inherited by all Test Classes.
/// </summary>
[TestClass]
public class UITestBase
{
public Session Session { get; set; }
private readonly TestInit testInit = new TestInit();
private readonly SessionHelper sessionHelper;
private readonly PowerToysModule scope;
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
{
this.testInit.SetScope(scope);
this.testInit.Init();
this.Session = new Session(this.testInit.GetRoot(), this.testInit.GetDriver());
this.scope = scope;
this.sessionHelper = new SessionHelper(scope).Init();
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
}
~UITestBase()
{
this.testInit.Cleanup();
this.sessionHelper.Cleanup();
}
/// <summary>
/// Nested class for test initialization.
/// Initializes the test.
/// </summary>
private sealed class TestInit
[TestInitialize]
public void TestInit()
{
private WindowsDriver<WindowsElement> Root { get; set; }
private WindowsDriver<WindowsElement>? Driver { get; set; }
private static Process? appDriver;
// Default session path is PowerToys settings dashboard
private static string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
public TestInit()
if (this.scope == PowerToysModule.PowerToysSettings)
{
appDriver = Process.Start(new ProcessStartInfo
// close Debug warning dialog if any
// Such debug warning dialog seems only appear in PowerToys Settings
if (this.FindAll("DEBUG").Count > 0)
{
FileName = "C:\\Program Files (x86)\\Windows Application Driver\\WinAppDriver.exe",
Verb = "runas",
});
var desktopCapabilities = new AppiumOptions();
desktopCapabilities.AddAdditionalCapability("app", "Root");
this.Root = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), desktopCapabilities);
// Set default timeout to 5 seconds
this.Root.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Initializes the test environment.
/// </summary>
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "<Pending>")]
public void Init()
{
string? path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
this.StartExe(path + sessionPath);
Assert.IsNotNull(this.Driver, $"Failed to initialize the test environment. Driver is null.");
// Set default timeout to 5 seconds
this.Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(5);
}
/// <summary>
/// Cleans up the test environment.
/// </summary>
public void Cleanup()
{
try
{
appDriver?.Kill();
}
catch (Exception ex)
{
// Handle exceptions if needed
Debug.WriteLine($"Exception during Cleanup: {ex.Message}");
this.Find("DEBUG").Find<Button>("Close").Click();
}
}
}
/// <summary>
/// Starts a new exe and takes control of it.
/// </summary>
/// <param name="appPath">The path to the application executable.</param>
public void StartExe(string appPath)
{
var opts = new AppiumOptions();
opts.AddAdditionalCapability("app", appPath);
this.Driver = new WindowsDriver<WindowsElement>(new Uri(ModuleConfigData.Instance.GetWindowsApplicationDriverUrl()), opts);
}
/// <summary>
/// Finds an element by selector.
/// Shortcut for this.Session.Find<T>(by, timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected T Find<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.Find<T>(by, timeoutMS);
}
/// <summary>
/// Sets scope to the Test Class.
/// </summary>
/// <param name="scope">The PowerToys module to start.</param>
public void SetScope(PowerToysModule scope)
{
sessionPath = ModuleConfigData.Instance.GetModulePath(scope);
}
/// <summary>
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the element, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected T Find<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.Find<T>(By.Name(name), timeoutMS);
}
public WindowsDriver<WindowsElement> GetRoot() => this.Root;
/// <summary>
/// Shortcut for this.Session.Find<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected Element Find(By by, int timeoutMS = 3000)
{
return this.Session.Find(by, timeoutMS);
}
public WindowsDriver<WindowsElement> GetDriver()
{
Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
return this.Driver;
}
/// <summary>
/// Shortcut for this.Session.Find<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name of the element.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>The found element.</returns>
protected Element Find(string name, int timeoutMS = 3000)
{
return this.Session.Find(name, timeoutMS);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll<T>(by, timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<T> FindAll<T>(By by, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.FindAll<T>(by, timeoutMS);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <typeparam name="T">The class of the elements, should be Element or its derived class.</typeparam>
/// <param name="name">The name of the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<T> FindAll<T>(string name, int timeoutMS = 3000)
where T : Element, new()
{
return this.Session.FindAll<T>(By.Name(name), timeoutMS);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll<Element>(by, timeoutMS)
/// </summary>
/// <param name="by">The selector to find the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<Element> FindAll(By by, int timeoutMS = 3000)
{
return this.Session.FindAll<Element>(by, timeoutMS);
}
/// <summary>
/// Finds all elements by selector.
/// Shortcut for this.Session.FindAll<Element>(By.Name(name), timeoutMS)
/// </summary>
/// <param name="name">The name of the elements.</param>
/// <param name="timeoutMS">The timeout in milliseconds (default is 3000).</param>
/// <returns>A read-only collection of the found elements.</returns>
protected ReadOnlyCollection<Element> FindAll(string name, int timeoutMS = 3000)
{
return this.Session.FindAll<Element>(By.Name(name), timeoutMS);
}
}
}

View File

@@ -5,22 +5,22 @@ This folder contains samples of DSC files that can be used to configure PowerToy
> [!NOTE]
> PowerToys DSC user documentation can be found on [Learn Microsoft PowerToys documentation](https://aka.ms/powertoys-docs-dsc-configure).
### [configuration.dsc.yaml](./configuration.dsc.yaml)
### [configuration.winget](./configuration.winget)
Sample configuration file that changes the enabled state of some modules, changes an integer setting and a hotkey setting.
### [installAndConfiguration.dsc.yaml](./installAndConfiguration.dsc.yaml)
### [installAndConfiguration.winget](./installAndConfiguration.winget)
Installs PowerToys and applies configuration.dsc.yaml to it.
Installs PowerToys and applies configuration.winget to it.
### [enableAllModules.dsc.yaml](./enableAllModules.dsc.yaml)
### [enableAllModules.winget](./enableAllModules.winget)
Enables all PowerToys utilities.
### [disableAllModules.dsc.yaml](./disableAllModules.dsc.yaml)
### [disableAllModules.winget](./disableAllModules.winget)
Disables all PowerToys utilities.
### [configureLauncherPlugins.dsc.yaml](./configureLauncherPlugins.dsc.yaml)
### [configureLauncherPlugins.winget](./configureLauncherPlugins.winget)
Enables PowerToys Run and all its plugins and changes action keyword and global state for the Program plugin.

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using AdvancedPaste.Models.KernelQueryCache;

View File

@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace AdvancedPaste.UnitTests.Mocks;
internal sealed class NoOpProgress : IProgress<double>
{
public void Report(double value)
{
}
}

View File

@@ -8,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -131,17 +132,18 @@ public sealed class AIServiceBatchIntegrationTests
{
VaultCredentialsProvider credentialsProvider = new();
PromptModerationService promptModerationService = new(credentialsProvider);
NoOpProgress progress = new();
CustomTextTransformService customTextTransformService = new(credentialsProvider, promptModerationService);
switch (format)
{
case PasteFormats.CustomTextTransformation:
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard));
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard, CancellationToken.None, progress));
case PasteFormats.KernelQuery:
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false);
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
default:
throw new InvalidOperationException($"Unexpected format {format}");

View File

@@ -6,6 +6,7 @@ using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -130,7 +131,7 @@ public sealed class KernelServiceIntegrationTests : IDisposable
private async Task<DataPackageView> GetKernelOutputAsync(string prompt, DataPackage input)
{
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false);
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false, CancellationToken.None, new NoOpProgress());
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);

View File

@@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
using AdvancedPaste.Models;
using AdvancedPaste.UnitTests.Mocks;
using ManagedCommon;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Storage;
using Windows.Storage.FileProperties;
namespace AdvancedPaste.UnitTests.ServicesTests;
[TestClass]
public sealed class TranscodeHelperIntegrationTests
{
private sealed record class MediaProperties(BasicProperties Basic, MusicProperties Music, VideoProperties Video);
private const string InputRootFolder = @"%USERPROFILE%\AdvancedPasteTranscodeMediaTestData";
/// <summary> Tests transforming a folder of media files.
/// - Verifies that the output file has the same basic properties (e.g. duration) as the input file.
/// - Copies the output file to a subfolder of the input folder for manual inspection.
/// </summary>
[TestMethod]
[DataRow(@"audio", PasteFormats.TranscodeToMp3)]
[DataRow(@"video", PasteFormats.TranscodeToMp4)]
public async Task TestTransformFolder(string inputSubfolder, PasteFormats format)
{
var inputFolder = Environment.ExpandEnvironmentVariables(Path.Combine(InputRootFolder, inputSubfolder));
if (!Directory.Exists(inputFolder))
{
Assert.Inconclusive($"Skipping tests for {inputFolder} as it does not exist");
}
var outputPath = Path.Combine(inputFolder, $"test_output_{format}");
foreach (var inputPath in Directory.EnumerateFiles(inputFolder))
{
await RunTestTransformFileAsync(inputPath, outputPath, format);
}
}
private async Task RunTestTransformFileAsync(string inputPath, string finalOutputPath, PasteFormats format)
{
Logger.LogDebug($"Running {nameof(RunTestTransformFileAsync)} for {inputPath}/{format}");
Directory.CreateDirectory(finalOutputPath);
var inputPackage = await DataPackageHelpers.CreateFromFileAsync(inputPath);
var inputProperties = await GetPropertiesAsync(await StorageFile.GetFileFromPathAsync(inputPath));
var outputPackage = await TransformHelpers.TransformAsync(format, inputPackage.GetView(), CancellationToken.None, new NoOpProgress());
var outputItems = await outputPackage.GetView().GetStorageItemsAsync();
Assert.AreEqual(1, outputItems.Count);
var outputFile = outputItems.Single() as StorageFile;
Assert.IsNotNull(outputFile);
var outputProperties = await GetPropertiesAsync(outputFile);
AssertPropertiesMatch(format, inputProperties, outputProperties);
await outputFile.CopyAsync(await StorageFolder.GetFolderFromPathAsync(finalOutputPath), outputFile.Name, NameCollisionOption.ReplaceExisting);
await outputPackage.GetView().TryCleanupAfterDelayAsync(TimeSpan.Zero);
}
private static void AssertPropertiesMatch(PasteFormats format, MediaProperties inputProperties, MediaProperties outputProperties)
{
Assert.IsTrue(outputProperties.Basic.Size > 0);
Assert.AreEqual(inputProperties.Music.Title, outputProperties.Music.Title);
Assert.AreEqual(inputProperties.Music.Album, outputProperties.Music.Album);
Assert.AreEqual(inputProperties.Music.Artist, outputProperties.Music.Artist);
AssertDurationsApproxEqual(inputProperties.Music.Duration, outputProperties.Music.Duration);
if (format == PasteFormats.TranscodeToMp4)
{
Assert.AreEqual(inputProperties.Video.Title, outputProperties.Video.Title);
AssertDurationsApproxEqual(inputProperties.Video.Duration, outputProperties.Video.Duration);
var inputVideoDimensions = GetNormalizedDimensions(inputProperties.Video);
if (inputVideoDimensions != null)
{
Assert.AreEqual(inputVideoDimensions, GetNormalizedDimensions(outputProperties.Video));
}
}
}
private static async Task<MediaProperties> GetPropertiesAsync(StorageFile file) =>
new(await file.GetBasicPropertiesAsync(), await file.Properties.GetMusicPropertiesAsync(), await file.Properties.GetVideoPropertiesAsync());
private static void AssertDurationsApproxEqual(TimeSpan expected, TimeSpan actual) =>
Assert.AreEqual(expected.Ticks, actual.Ticks, delta: TimeSpan.FromSeconds(1).Ticks);
/// <summary>
/// Gets the dimensions of a video, if available. Accounts for the fact that the dimensions may sometimes be swapped.
/// </summary>
private static (uint Width, uint Height)? GetNormalizedDimensions(VideoProperties properties) =>
properties.Width == 0 || properties.Height == 0
? null
: (Math.Max(properties.Width, properties.Height), Math.Min(properties.Width, properties.Height));
}

View File

@@ -28,6 +28,7 @@
Background="Transparent"
BorderThickness="4"
CornerRadius="{TemplateBinding CornerRadius}"
IsHitTestVisible="False"
Visibility="Collapsed">
<!-- CornerRadius needs to be > 0 -->
<Grid.BorderBrush>

View File

@@ -178,17 +178,36 @@
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Image
x:Name="AIGlyphImage"
AutomationProperties.AccessibilityView="Raw"
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
<PathIcon
x:Name="AIGlyph"
AutomationProperties.AccessibilityView="Raw"
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
<ProgressRing
Width="30"
Height="30"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsActive="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
IsIndeterminate="{Binding DataContext.HasIndeterminateTransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}"
Maximum="100"
Minimum="0"
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}"
Value="{Binding DataContext.TransformProgress, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
<StackPanel
Margin="0"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Visibility="{Binding DataContext.IsBusy, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
<Image
x:Name="AIGlyphImage"
AutomationProperties.AccessibilityView="Raw"
Source="/Assets/AdvancedPaste/SemanticKernel.svg"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource Mode=TemplatedParent}, Converter={StaticResource BoolToVisibilityConverter}}" />
<PathIcon
x:Name="AIGlyph"
AutomationProperties.AccessibilityView="Raw"
Data="M128 766q0-42 24-77t65-48l178-57q32-11 61-30t52-42q50-50 71-114l58-179q13-40 48-65t78-26q42 0 77 24t50 65l58 177q21 66 72 117 49 50 117 72l176 58q43 14 69 48t26 80q0 41-25 76t-64 49l-178 58q-66 21-117 72-32 32-51 73t-33 84-26 83-30 73-45 51-71 20q-42 0-77-24t-49-65l-58-178q-8-25-19-47t-28-43q-34-43-77-68t-89-41-89-27-78-29-55-45-21-75zm1149 7q-76-29-145-53t-129-60-104-88-73-138l-57-176-67 176q-18 48-42 89t-60 78q-34 34-76 61t-89 43l-177 57q75 29 144 53t127 60 103 89 73 137l57 176 67-176q37-97 103-168t168-103l177-57zm-125 759q0-31 20-57t49-36l99-32q34-11 53-34t30-51 20-59 20-54 33-41 58-16q32 0 59 19t38 50q6 20 11 40t13 40 17 38 25 34q16 17 39 26t48 18 49 16 44 20 31 32 12 50q0 33-18 60t-51 38q-19 6-39 11t-41 13-39 17-34 25q-24 25-35 62t-24 73-35 61-68 25q-32 0-59-19t-38-50q-6-18-11-39t-13-41-17-40-24-33q-18-17-41-27t-47-17-49-15-43-20-30-33-12-54zm583 4q-43-13-74-30t-55-41-40-55-32-74q-12 41-29 72t-42 55-55 42-71 31q81 23 128 71t71 129q15-43 31-74t40-54 53-40 75-32z"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Visibility="{Binding DataContext.IsAdvancedAIEnabled, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
</StackPanel>
</StackPanel>
</Viewbox>
<ScrollViewer
@@ -572,6 +591,24 @@
Duration="0:0:0.167" />
</animations:Implicit.HideAnimations>
</Button>
<Button
x:Name="CancelBtn"
x:Uid="CancelBtnAutomation"
Padding="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
Command="{x:Bind CancelPasteActionCommand}"
Content="{ui:FontIcon Glyph=&#xE711;,
FontSize=16}"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
IsEnabled="False"
Style="{StaticResource SubtleButtonStyle}"
Visibility="Collapsed">
<ToolTipService.ToolTip>
<TextBlock x:Uid="CancelBtnToolTip" TextWrapping="WrapWholeWords" />
</ToolTipService.ToolTip>
</Button>
<!-- Transparent overlay to show tooltip -->
<Grid
x:Name="SendBtnOverlay"
@@ -679,6 +716,10 @@
<Setter Target="Loader.IsLoading" Value="True" />
<Setter Target="InputTxtBox.IsEnabled" Value="False" />
<Setter Target="SendBtn.IsEnabled" Value="False" />
<Setter Target="SendBtn.Visibility" Value="Collapsed" />
<Setter Target="SendBtnOverlay.Visibility" Value="Collapsed" />
<Setter Target="CancelBtn.IsEnabled" Value="True" />
<Setter Target="CancelBtn.Visibility" Value="Visible" />
<Setter Target="DisclaimerPresenter.Visibility" Value="Collapsed" />
<Setter Target="LoadingText.Visibility" Value="Visible" />
</VisualState.Setters>

View File

@@ -55,9 +55,9 @@ namespace AdvancedPaste.Controls
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteActionError))
if (e.PropertyName is nameof(ViewModel.IsBusy) or nameof(ViewModel.PasteActionError))
{
var state = ViewModel.Busy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
VisualStateManager.GoToState(this, state, true);
}
}
@@ -78,6 +78,9 @@ namespace AdvancedPaste.Controls
[RelayCommand]
private async Task GenerateCustomAIAsync() => await ViewModel.ExecuteCustomAIFormatFromCurrentQueryAsync(PasteActionSource.PromptBox);
[RelayCommand]
private async Task CancelPasteActionAsync() => await ViewModel.CancelPasteActionAsync();
private async void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
{
if (e.Key == Windows.System.VirtualKey.Enter && InputTxtBox.Text.Length > 0 && ViewModel.IsCustomAIAvailable)

View File

@@ -24,6 +24,7 @@ namespace AdvancedPaste
{
private readonly WindowMessageMonitor _msgMonitor;
private readonly IUserSettings _userSettings;
private readonly OptionsViewModel _optionsViewModel;
private bool _disposedValue;
@@ -32,8 +33,7 @@ namespace AdvancedPaste
InitializeComponent();
_userSettings = App.GetService<IUserSettings>();
var optionsViewModel = App.GetService<OptionsViewModel>();
_optionsViewModel = App.GetService<OptionsViewModel>();
var baseHeight = MinHeight;
var coreActionCount = PasteFormat.MetadataDict.Values.Count(metadata => metadata.IsCoreAction);
@@ -43,7 +43,7 @@ namespace AdvancedPaste
double GetHeight(int maxCustomActionCount) =>
baseHeight +
new PasteFormatsToHeightConverter().GetHeight(coreActionCount + _userSettings.AdditionalActions.Count) +
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
new PasteFormatsToHeightConverter() { MaxItems = maxCustomActionCount }.GetHeight(_optionsViewModel.IsCustomAIServiceEnabled ? _userSettings.CustomActions.Count : 0);
MinHeight = GetHeight(1);
Height = GetHeight(5);
@@ -52,9 +52,9 @@ namespace AdvancedPaste
UpdateHeight();
_userSettings.Changed += (_, _) => UpdateHeight();
optionsViewModel.PropertyChanged += (_, e) =>
_optionsViewModel.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(optionsViewModel.IsCustomAIServiceEnabled))
if (e.PropertyName == nameof(_optionsViewModel.IsCustomAIServiceEnabled))
{
UpdateHeight();
}
@@ -111,8 +111,9 @@ namespace AdvancedPaste
GC.SuppressFinalize(this);
}
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
private async void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
{
await _optionsViewModel.CancelPasteActionAsync();
Hide();
args.Handled = true;
}

View File

@@ -4,10 +4,14 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AdvancedPaste.Models;
using ManagedCommon;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.Data.Html;
using Windows.Graphics.Imaging;
@@ -18,8 +22,6 @@ namespace AdvancedPaste.Helpers;
internal static class DataPackageHelpers
{
private static readonly Lazy<HashSet<string>> ImageFileTypes = new(GetImageFileTypes());
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
[
(StandardDataFormats.Text, ClipboardFormat.Text),
@@ -27,6 +29,14 @@ internal static class DataPackageHelpers
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
];
private static readonly Lazy<(ClipboardFormat Format, HashSet<string> FileTypes)[]> SupportedFileTypes =
new(() =>
[
(ClipboardFormat.Image, GetImageFileTypes()),
(ClipboardFormat.Audio, GetMediaFileTypes("audio")),
(ClipboardFormat.Video, GetMediaFileTypes("video")),
]);
internal static DataPackage CreateFromText(string text)
{
DataPackage dataPackage = new();
@@ -57,9 +67,12 @@ internal static class DataPackageHelpers
{
availableFormats |= ClipboardFormat.File;
if (ImageFileTypes.Value.Contains(file.FileType))
foreach (var (format, fileTypes) in SupportedFileTypes.Value)
{
availableFormats |= ClipboardFormat.Image;
if (fileTypes.Contains(file.FileType))
{
availableFormats |= format;
}
}
}
}
@@ -93,6 +106,60 @@ internal static class DataPackageHelpers
return availableFormats == ClipboardFormat.Text ? !string.IsNullOrEmpty(await dataPackageView.GetTextAsync()) : availableFormats != ClipboardFormat.None;
}
internal static async Task TryCleanupAfterDelayAsync(this DataPackageView dataPackageView, TimeSpan delay)
{
try
{
var tempFile = await GetSingleTempFileOrNullAsync(dataPackageView);
if (tempFile != null)
{
await Task.Delay(delay);
Logger.LogDebug($"Cleaning up temporary file with extension [{tempFile.Extension}] from data package after delay");
tempFile.Delete();
if (NormalizeDirectoryPath(tempFile.Directory?.Parent?.FullName) == NormalizeDirectoryPath(Path.GetTempPath()))
{
tempFile.Directory?.Delete();
}
}
}
catch (Exception ex)
{
Logger.LogError("Failed to clean up temporary files", ex);
}
}
private static async Task<FileInfo> GetSingleTempFileOrNullAsync(this DataPackageView dataPackageView)
{
if (!dataPackageView.Contains(StandardDataFormats.StorageItems))
{
return null;
}
var storageItems = await dataPackageView.GetStorageItemsAsync();
if (storageItems.Count != 1 || storageItems.Single() is not StorageFile file)
{
return null;
}
FileInfo fileInfo = new(file.Path);
var tempPathDirectory = NormalizeDirectoryPath(Path.GetTempPath());
var directoryPaths = new[] { fileInfo.Directory, fileInfo.Directory?.Parent }
.Where(directory => directory != null)
.Select(directory => NormalizeDirectoryPath(directory.FullName));
return directoryPaths.Contains(NormalizeDirectoryPath(Path.GetTempPath())) ? fileInfo : null;
}
private static string NormalizeDirectoryPath(string path) =>
Path.GetFullPath(new Uri(path).LocalPath)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.ToUpperInvariant();
internal static async Task<string> GetTextOrEmptyAsync(this DataPackageView dataPackageView) =>
dataPackageView.Contains(StandardDataFormats.Text) ? await dataPackageView.GetTextAsync() : string.Empty;
@@ -153,4 +220,27 @@ internal static class DataPackageHelpers
BitmapDecoder.GetDecoderInformationEnumerator()
.SelectMany(di => di.FileExtensions)
.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
private static HashSet<string> GetMediaFileTypes(string mediaKind)
{
static string AssocQueryString(NativeMethods.AssocStr assocStr, string extension)
{
uint pcchOut = 0;
NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, null, ref pcchOut);
StringBuilder pszOut = new((int)pcchOut);
var hResult = NativeMethods.AssocQueryString(NativeMethods.AssocF.None, assocStr, extension, null, pszOut, ref pcchOut);
return hResult == NativeMethods.HResult.Ok ? pszOut.ToString() : string.Empty;
}
var comparison = StringComparison.OrdinalIgnoreCase;
var extensions = from extension in Registry.ClassesRoot.GetSubKeyNames()
where extension.StartsWith('.')
where AssocQueryString(NativeMethods.AssocStr.PerceivedType, extension).Equals(mediaKind, comparison) ||
AssocQueryString(NativeMethods.AssocStr.ContentType, extension).StartsWith($"{mediaKind}/", comparison)
select extension;
return extensions.ToHashSet(StringComparer.InvariantCultureIgnoreCase);
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
@@ -17,6 +18,8 @@ internal static class KernelExtensions
private const string DataPackageKey = "DataPackage";
private const string LastErrorKey = "LastError";
private const string ActionChainKey = "ActionChain";
private const string CancellationTokenKey = "CancellationToken";
private const string ProgressKey = "Progress";
internal static DataPackageView GetDataPackageView(this Kernel kernel)
{
@@ -40,6 +43,14 @@ internal static class KernelExtensions
internal static void SetDataPackageView(this Kernel kernel, DataPackageView dataPackageView) => kernel.Data[DataPackageKey] = dataPackageView;
internal static CancellationToken GetCancellationToken(this Kernel kernel) => kernel.Data.TryGetValue(CancellationTokenKey, out object value) ? (CancellationToken)value : CancellationToken.None;
internal static void SetCancellationToken(this Kernel kernel, CancellationToken cancellationToken) => kernel.Data[CancellationTokenKey] = cancellationToken;
internal static IProgress<double> GetProgress(this Kernel kernel) => kernel.Data.TryGetValue(ProgressKey, out object obj) ? obj as IProgress<double> : null;
internal static void SetProgress(this Kernel kernel, IProgress<double> progress) => kernel.Data[ProgressKey] = progress;
internal static Exception GetLastError(this Kernel kernel) => kernel.Data.TryGetValue(LastErrorKey, out object obj) ? obj as Exception : null;
internal static void SetLastError(this Kernel kernel, Exception error) => kernel.Data[LastErrorKey] = error;

View File

@@ -4,6 +4,7 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace AdvancedPaste.Helpers
{
@@ -83,6 +84,68 @@ namespace AdvancedPaste.Helpers
Scancode = 0x0008,
}
public enum HResult
{
Ok = 0x0000,
False = 0x0001,
InvalidArguments = unchecked((int)0x80070057),
OutOfMemory = unchecked((int)0x8007000E),
NoInterface = unchecked((int)0x80004002),
Fail = unchecked((int)0x80004005),
ExtractionFailed = unchecked((int)0x8004B200),
ElementNotFound = unchecked((int)0x80070490),
TypeElementNotFound = unchecked((int)0x8002802B),
NoObject = unchecked((int)0x800401E5),
Win32ErrorCanceled = 1223,
Canceled = unchecked((int)0x800704C7),
ResourceInUse = unchecked((int)0x800700AA),
AccessDenied = unchecked((int)0x80030005),
}
[Flags]
public enum AssocF
{
None = 0,
Init_NoRemapCLSID = 0x1,
Init_ByExeName = 0x2,
Open_ByExeName = 0x3,
Init_DefaultToStar = 0x4,
Init_DefaultToFolder = 0x8,
NoUserSettings = 0x10,
NoTruncate = 0x20,
Verify = 0x40,
RemapRunDll = 0x80,
NoFixUps = 0x100,
IgnoreBaseClass = 0x200,
}
public enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDETopic,
InfoTip,
QuickTip,
TileInfo,
ContentType,
DefaultIcon,
ShellExtension,
PerceivedType,
DelegateExecute,
SupportedUriProtocols,
ProgId,
AppId,
AppPublisher,
AppIconReference,
}
[DllImport("user32.dll")]
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
@@ -100,5 +163,8 @@ namespace AdvancedPaste.Helpers
[DllImport("user32.dll")]
internal static extern bool GetCursorPos(out PointInter lpPoint);
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern HResult AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string pszExtra, [Out] StringBuilder pszOut, [In][Out] ref uint pcchOut);
}
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.Globalization;
@@ -15,11 +16,14 @@ namespace AdvancedPaste.Helpers;
public static class OcrHelpers
{
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap)
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
{
var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
cancellationToken.ThrowIfCancellationRequested();
var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
cancellationToken.ThrowIfCancellationRequested();
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
return string.IsNullOrWhiteSpace(ocrResult.Text)

View File

@@ -0,0 +1,167 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
using ManagedCommon;
using Windows.ApplicationModel.DataTransfer;
using Windows.Media.MediaProperties;
using Windows.Media.Transcoding;
using Windows.Storage;
namespace AdvancedPaste.Helpers;
internal static class TranscodeHelpers
{
public static async Task<DataPackage> TranscodeToMp3Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp3(AudioEncodingQuality.High), ".mp3", cancellationToken, progress);
public static async Task<DataPackage> TranscodeToMp4Async(DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress) =>
await TranscodeMediaAsync(clipboardData, MediaEncodingProfile.CreateMp4(VideoEncodingQuality.HD1080p), ".mp4", cancellationToken, progress);
private static async Task<DataPackage> TranscodeMediaAsync(DataPackageView clipboardData, MediaEncodingProfile baseOutputProfile, string extension, CancellationToken cancellationToken, IProgress<double> progress)
{
Logger.LogTrace();
var inputFiles = await clipboardData.GetStorageItemsAsync();
if (inputFiles.Count != 1)
{
throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} does not support multiple files");
}
var inputFile = inputFiles.Single() as StorageFile ?? throw new InvalidOperationException($"{nameof(TranscodeMediaAsync)} only supports files");
var inputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(inputFile.Path);
var inputProfile = await MediaEncodingProfile.CreateFromFileAsync(inputFile);
var outputProfile = CreateOutputProfile(inputProfile, baseOutputProfile);
#if DEBUG
static string ProfileToString(MediaEncodingProfile profile) => System.Text.Json.JsonSerializer.Serialize(profile, options: new() { WriteIndented = true });
Logger.LogDebug($"{nameof(inputProfile)}: {ProfileToString(inputProfile)}");
Logger.LogDebug($"{nameof(outputProfile)}: {ProfileToString(outputProfile)}");
#endif
var outputFolder = await Task.Run(() => Directory.CreateTempSubdirectory("PowerToys_AdvancedPaste_"), cancellationToken);
var outputFileName = StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(inputFile.Path), extension) ? inputFileNameWithoutExtension + "_1" : inputFileNameWithoutExtension;
var outputFilePath = Path.Combine(outputFolder.FullName, Path.ChangeExtension(outputFileName, extension));
await File.WriteAllBytesAsync(outputFilePath, [], cancellationToken); // TranscodeAsync seems to require the output file to exist
await TranscodeMediaAsync(inputFile, await StorageFile.GetFileFromPathAsync(outputFilePath), outputProfile, cancellationToken, progress);
return await DataPackageHelpers.CreateFromFileAsync(outputFilePath);
}
private static MediaEncodingProfile CreateOutputProfile(MediaEncodingProfile inputProfile, MediaEncodingProfile baseOutputProfile)
{
MediaEncodingProfile outputProfile = new()
{
Video = null,
Audio = null,
};
outputProfile.Container = baseOutputProfile.Container.Copy();
if (inputProfile.Video != null && baseOutputProfile.Video != null)
{
outputProfile.Video = baseOutputProfile.Video.Copy();
if (inputProfile.Video.Bitrate != 0)
{
outputProfile.Video.Bitrate = inputProfile.Video.Bitrate;
}
if (inputProfile.Video.FrameRate.Numerator != 0)
{
outputProfile.Video.FrameRate.Numerator = inputProfile.Video.FrameRate.Numerator;
}
if (inputProfile.Video.FrameRate.Denominator != 0)
{
outputProfile.Video.FrameRate.Denominator = inputProfile.Video.FrameRate.Denominator;
}
if (inputProfile.Video.PixelAspectRatio.Numerator != 0)
{
outputProfile.Video.PixelAspectRatio.Numerator = inputProfile.Video.PixelAspectRatio.Numerator;
}
if (inputProfile.Video.PixelAspectRatio.Denominator != 0)
{
outputProfile.Video.PixelAspectRatio.Denominator = inputProfile.Video.PixelAspectRatio.Denominator;
}
outputProfile.Video.Width = inputProfile.Video.Width;
outputProfile.Video.Height = inputProfile.Video.Height;
}
if (inputProfile.Audio != null && baseOutputProfile.Audio != null)
{
outputProfile.Audio = baseOutputProfile.Audio.Copy();
if (inputProfile.Audio.Bitrate != 0)
{
outputProfile.Audio.Bitrate = inputProfile.Audio.Bitrate;
}
if (inputProfile.Audio.BitsPerSample != 0)
{
outputProfile.Audio.BitsPerSample = inputProfile.Audio.BitsPerSample;
}
if (inputProfile.Audio.ChannelCount != 0)
{
outputProfile.Audio.ChannelCount = inputProfile.Audio.ChannelCount;
}
if (inputProfile.Audio.SampleRate != 0)
{
outputProfile.Audio.SampleRate = inputProfile.Audio.SampleRate;
}
}
return outputProfile;
}
private static async Task TranscodeMediaAsync(StorageFile inputFile, StorageFile outputFile, MediaEncodingProfile outputProfile, CancellationToken cancellationToken, IProgress<double> progress)
{
if (outputProfile.Video == null && outputProfile.Audio == null)
{
throw new InvalidOperationException("Target profile does not contain media");
}
async Task<PrepareTranscodeResult> GetPrepareResult(bool hardwareAccelerationEnabled)
{
MediaTranscoder transcoder = new()
{
AlwaysReencode = false,
HardwareAccelerationEnabled = hardwareAccelerationEnabled,
};
return await transcoder.PrepareFileTranscodeAsync(inputFile, outputFile, outputProfile);
}
var prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: true);
if (!prepareResult.CanTranscode)
{
Logger.LogWarning($"Unable to transcode with hardware acceleration enabled, falling back to software; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}");
prepareResult = await GetPrepareResult(hardwareAccelerationEnabled: false);
}
if (!prepareResult.CanTranscode)
{
var message = ResourceLoaderInstance.ResourceLoader.GetString(prepareResult.FailureReason == TranscodeFailureReason.CodecNotFound ? "TranscodeErrorUnsupportedCodec" : "TranscodeErrorGeneral");
throw new PasteActionException(message, new InvalidOperationException($"Error transcoding; {nameof(prepareResult.FailureReason)}={prepareResult.FailureReason}"));
}
await prepareResult.TranscodeAsync().AsTask(cancellationToken, progress);
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
@@ -17,17 +18,19 @@ namespace AdvancedPaste.Helpers;
public static class TransformHelpers
{
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData)
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress)
{
return format switch
{
PasteFormats.PlainText => await ToPlainTextAsync(clipboardData),
PasteFormats.Markdown => await ToMarkdownAsync(clipboardData),
PasteFormats.Json => await ToJsonAsync(clipboardData),
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData),
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData),
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData),
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData),
PasteFormats.ImageToText => await ImageToTextAsync(clipboardData, cancellationToken),
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData, cancellationToken),
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData, cancellationToken),
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData, cancellationToken),
PasteFormats.TranscodeToMp3 => await TranscodeHelpers.TranscodeToMp3Async(clipboardData, cancellationToken, progress),
PasteFormats.TranscodeToMp4 => await TranscodeHelpers.TranscodeToMp4Async(clipboardData, cancellationToken, progress),
PasteFormats.KernelQuery => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
PasteFormats.CustomTextTransformation => throw new ArgumentException($"Unsupported format {format}", nameof(format)),
_ => throw new ArgumentException($"Unknown value {format}", nameof(format)),
@@ -52,16 +55,16 @@ public static class TransformHelpers
return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData));
}
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
var bitmap = await clipboardData.GetImageContentAsync() ?? throw new ArgumentException("No image content found in clipboard", nameof(clipboardData));
var text = await OcrHelpers.ExtractTextAsync(bitmap);
var text = await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
return CreateDataPackageFromText(text);
}
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
@@ -72,25 +75,25 @@ public static class TransformHelpers
encoder.SetSoftwareBitmap(clipboardBitmap);
await encoder.FlushAsync();
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png");
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png", cancellationToken);
}
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
var text = await clipboardData.GetTextOrHtmlTextAsync();
return await CreateDataPackageFromFileContentAsync(text, "txt");
return await CreateDataPackageFromFileContentAsync(text, "txt", cancellationToken);
}
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData)
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
{
Logger.LogTrace();
var cfHtml = await clipboardData.GetHtmlContentAsync();
var html = RemoveHtmlMetadata(cfHtml);
return await CreateDataPackageFromFileContentAsync(html, "html");
return await CreateDataPackageFromFileContentAsync(html, "html", cancellationToken);
}
/// <summary>
@@ -114,7 +117,7 @@ public static class TransformHelpers
return (startFragmentIndex == null || endFragmentIndex == null) ? cfHtml : cfHtml[startFragmentIndex.Value..endFragmentIndex.Value];
}
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension)
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(data))
{
@@ -123,16 +126,16 @@ public static class TransformHelpers
var path = GetPasteAsFileTempFilePath(fileExtension);
await File.WriteAllTextAsync(path, data);
await File.WriteAllTextAsync(path, data, cancellationToken);
return await DataPackageHelpers.CreateFromFileAsync(path);
}
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension)
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension, CancellationToken cancellationToken)
{
var path = GetPasteAsFileTempFilePath(fileExtension);
using var fileStream = File.Create(path);
await stream.CopyToAsync(fileStream);
await stream.CopyToAsync(fileStream, cancellationToken);
return await DataPackageHelpers.CreateFromFileAsync(path);
}

View File

@@ -108,7 +108,9 @@ namespace AdvancedPaste.Settings
(PasteFormats.ImageToText, [sourceAdditionalActions.ImageToText]),
(PasteFormats.PasteAsTxtFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsTxtFile]),
(PasteFormats.PasteAsPngFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsPngFile]),
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile])
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile]),
(PasteFormats.TranscodeToMp3, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp3]),
(PasteFormats.TranscodeToMp4, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp4]),
];
_additionalActions.Clear();

View File

@@ -13,6 +13,7 @@ public enum ClipboardFormat
Text = 1 << 0,
Html = 1 << 1,
Audio = 1 << 2,
Image = 1 << 3,
File = 1 << 4, // output only for now
Video = 1 << 3,
Image = 1 << 4,
File = 1 << 5, // output only for now
}

View File

@@ -30,7 +30,7 @@ public sealed class PasteActionError
public static PasteActionError FromException(Exception ex) =>
new()
{
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString(ex is OperationCanceledException ? "PasteActionCanceled" : "PasteError"),
Details = (ex as PasteActionException)?.AIServiceMessage ?? string.Empty,
};
}

View File

@@ -82,12 +82,34 @@ public enum PasteFormats
KernelFunctionDescription = "Takes HTML data in the clipboard and transforms it to an HTML file.")]
PasteAsHtmlFile,
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "TranscodeToMp3",
IconGlyph = "\uE8D6",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Audio | ClipboardFormat.Video,
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp3,
KernelFunctionDescription = "Takes an audio or video file in the clipboard and transcodes it to MP3.")]
TranscodeToMp3,
[PasteFormatMetadata(
IsCoreAction = false,
ResourceId = "TranscodeToMp4",
IconGlyph = "\uE714",
RequiresAIService = false,
CanPreview = false,
SupportedClipboardFormats = ClipboardFormat.Video,
IPCKey = AdvancedPasteTranscodeAction.PropertyNames.TranscodeToMp4,
KernelFunctionDescription = "Takes a video file in the clipboard and transcodes it to MP4 (H.264/AAC).")]
TranscodeToMp4,
[PasteFormatMetadata(
IsCoreAction = false,
IconGlyph = "\uE945",
RequiresAIService = true,
CanPreview = true,
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Image,
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Video | ClipboardFormat.Image,
RequiresPrompt = true)]
KernelQuery,

View File

@@ -2,11 +2,13 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace AdvancedPaste.Services;
public interface ICustomTextTransformService
{
Task<string> TransformTextAsync(string prompt, string inputText);
Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress);
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
@@ -10,5 +12,5 @@ namespace AdvancedPaste.Services;
public interface IKernelService
{
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery);
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress);
}

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Models;
@@ -11,5 +13,5 @@ namespace AdvancedPaste.Services;
public interface IPasteFormatExecutor
{
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress);
}

View File

@@ -2,11 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Threading;
using System.Threading.Tasks;
namespace AdvancedPaste.Services;
public interface IPromptModerationService
{
Task ValidateAsync(string fullPrompt);
Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken);
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -36,12 +37,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery)
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress)
{
Logger.LogTrace();
var kernel = CreateKernel();
kernel.SetDataPackageView(clipboardData);
kernel.SetCancellationToken(cancellationToken);
kernel.SetProgress(progress);
CacheKey cacheKey = new() { Prompt = prompt, AvailableFormats = await clipboardData.GetAvailableFormatsAsync() };
var maybeCacheValue = _queryCacheService.ReadOrNull(cacheKey);
@@ -51,7 +54,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
try
{
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt);
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt, cancellationToken);
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
@@ -84,7 +87,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
AdvancedPasteSemanticKernelErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
PowerToysTelemetry.Log.WriteEvent(errorEvent);
if (ex is PasteActionException)
if (ex is PasteActionException or OperationCanceledException)
{
throw;
}
@@ -127,7 +130,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
return $"{combinedSystemMessage}{newLine}{newLine}User instructions:{newLine}{userPromptMessage.Content}";
}
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt)
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt, CancellationToken cancellationToken)
{
ChatHistory chatHistory = [];
@@ -141,10 +144,10 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
chatHistory.AddUserMessage(prompt);
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory));
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel);
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
chatHistory.Add(chatResult);
var totalUsage = chatHistory.Select(GetAIServiceUsage)
@@ -157,6 +160,8 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
{
foreach (var item in actionChain)
{
kernel.GetCancellationToken().ThrowIfCancellationRequested();
if (item.Arguments.Count > 0)
{
await ExecutePromptTransformAsync(kernel, item.Format, item.Arguments[PromptParameterName]);
@@ -208,14 +213,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
async dataPackageView =>
{
var input = await dataPackageView.GetTextAsync();
string output = await GetPromptBasedOutput(format, prompt, input);
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
return DataPackageHelpers.CreateFromText(output);
});
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input) =>
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
format switch
{
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input),
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
};
@@ -223,7 +228,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
ExecuteTransformAsync(
kernel,
new ActionChainItem(format, Arguments: []),
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView));
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView, kernel.GetCancellationToken(), kernel.GetProgress()));
private static async Task<string> ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func<DataPackageView, Task<DataPackage>> transformFunc)
{

View File

@@ -4,6 +4,7 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -23,11 +24,11 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
private readonly IPromptModerationService _promptModerationService = promptModerationService;
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage)
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage, CancellationToken cancellationToken)
{
var fullPrompt = systemInstructions + "\n\n" + userMessage;
await _promptModerationService.ValidateAsync(fullPrompt);
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
@@ -41,7 +42,8 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
},
Temperature = 0.01F,
MaxTokens = 2000,
});
},
cancellationToken);
if (response.Value.Choices[0].FinishReason == "length")
{
@@ -51,7 +53,7 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
return response;
}
public async Task<string> TransformTextAsync(string prompt, string inputText)
public async Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
{
if (string.IsNullOrWhiteSpace(prompt))
{
@@ -80,7 +82,7 @@ Output:
try
{
var response = await GetAICompletionAsync(systemInstructions, userMessage);
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
var usage = response.Usage;
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
@@ -98,7 +100,7 @@ Output:
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
PowerToysTelemetry.Log.WriteEvent(errorEvent);
if (ex is PasteActionException)
if (ex is PasteActionException or OperationCanceledException)
{
throw;
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ClientModel;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -18,12 +19,12 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
public async Task ValidateAsync(string fullPrompt)
public async Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken)
{
try
{
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt);
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
var moderationResult = moderationClientResult.Value;
Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} complete; {nameof(moderationResult.Flagged)}={moderationResult.Flagged}");

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -17,7 +18,7 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
private readonly IKernelService _kernelService = kernelService;
private readonly ICustomTextTransformService _customTextTransformService = customTextTransformService;
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress)
{
if (!pasteFormat.IsEnabled)
{
@@ -34,9 +35,9 @@ public sealed class PasteFormatExecutor(IKernelService kernelService, ICustomTex
return await Task.Run(async () =>
pasteFormat.Format switch
{
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync())),
_ => await TransformHelpers.TransformAsync(format, clipboardData),
PasteFormats.KernelQuery => await _kernelService.TransformClipboardAsync(pasteFormat.Prompt, clipboardData, pasteFormat.IsSavedQuery, cancellationToken, progress),
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
});
}

View File

@@ -135,6 +135,9 @@
<data name="OpenAIApiKeyError" xml:space="preserve">
<value>OpenAI request failed with status code: </value>
</data>
<data name="PasteActionCanceled" xml:space="preserve">
<value>The paste operation was canceled</value>
</data>
<data name="PasteError" xml:space="preserve">
<value>An error occurred during the paste operation</value>
</data>
@@ -188,7 +191,19 @@
</data>
<data name="PasteAsHtmlFile" xml:space="preserve">
<value>Paste as .html file</value>
</data>
<data name="TranscodeToMp3" xml:space="preserve">
<value>Transcode to .mp3</value>
</data>
<data name="TranscodeToMp4" xml:space="preserve">
<value>Transcode to .mp4 (H.264/AAC)</value>
</data>
<data name="TranscodeErrorGeneral" xml:space="preserve">
<value>An error occurred while transcoding media file</value>
</data>
<data name="TranscodeErrorUnsupportedCodec" xml:space="preserve">
<value>The media file contains an unsupported codec</value>
</data>
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Paste</value>
</data>
@@ -207,6 +222,9 @@
<data name="SendBtnToolTip.Text" xml:space="preserve">
<value>Generate and paste data</value>
</data>
<data name="CancelBtnToolTip.Text" xml:space="preserve">
<value>Cancel paste operation</value>
</data>
<data name="RegenerateBtnToolTip.Text" xml:space="preserve">
<value>Regenerate</value>
</data>
@@ -216,6 +234,9 @@
<data name="SendButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Generate and paste data</value>
</data>
<data name="CancelBtnAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Cancel paste operation</value>
</data>
<data name="SettingsBtn.Content" xml:space="preserve">
<value>Open settings</value>
</data>

View File

@@ -8,6 +8,8 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO.Abstractions;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using AdvancedPaste.Helpers;
@@ -29,7 +31,7 @@ using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace AdvancedPaste.ViewModels
{
public sealed partial class OptionsViewModel : ObservableObject, IDisposable
public sealed partial class OptionsViewModel : ObservableObject, IProgress<double>, IDisposable
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly DispatcherTimer _clipboardTimer;
@@ -37,6 +39,8 @@ namespace AdvancedPaste.ViewModels
private readonly IPasteFormatExecutor _pasteFormatExecutor;
private readonly IAICredentialsProvider _aiCredentialsProvider;
private CancellationTokenSource _pasteActionCancellationTokenSource;
public DataPackageView ClipboardData { get; set; }
[ObservableProperty]
@@ -65,7 +69,11 @@ namespace AdvancedPaste.ViewModels
private bool _pasteFormatsDirty;
[ObservableProperty]
private bool _busy;
private bool _isBusy;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasIndeterminateTransformProgress))]
private double _transformProgress = double.NaN;
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
@@ -81,9 +89,24 @@ namespace AdvancedPaste.ViewModels
public bool ClipboardHasDataForCustomAI => PasteFormat.SupportsClipboardFormats(CustomAIFormat, AvailableClipboardFormats);
public bool HasIndeterminateTransformProgress => double.IsNaN(TransformProgress);
private PasteFormats CustomAIFormat => _userSettings.IsAdvancedAIEnabled ? PasteFormats.KernelQuery : PasteFormats.CustomTextTransformation;
private bool Visible => GetMainWindow()?.Visible is true;
private bool Visible
{
get
{
try
{
return GetMainWindow()?.Visible is true;
}
catch (COMException)
{
return false; // window is closed
}
}
}
public event EventHandler PreviewRequested;
@@ -189,7 +212,12 @@ namespace AdvancedPaste.ViewModels
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
{
collection.Clear();
// Hack: Clear collection via repeated RemoveAt to avoid this crash, which seems to occasionally occur when using Clear:
// https://github.com/microsoft/microsoft-ui-xaml/issues/8684
while (collection.Count > 0)
{
collection.RemoveAt(collection.Count - 1);
}
foreach (var format in FilterAndSort(pasteFormats))
{
@@ -214,12 +242,13 @@ namespace AdvancedPaste.ViewModels
public void Dispose()
{
_clipboardTimer.Stop();
_pasteActionCancellationTokenSource?.Dispose();
GC.SuppressFinalize(this);
}
public async Task ReadClipboardAsync()
{
if (Busy)
if (IsBusy)
{
return;
}
@@ -324,6 +353,10 @@ namespace AdvancedPaste.ViewModels
{
await ClipboardHelper.TryCopyPasteAsync(package, HideWindow);
Query = string.Empty;
// Delete any temp files created. A delay is needed to ensure the file is not in use by the target application -
// for example, when pasting onto File Explorer, the paste operation will trigger a file copy.
_ = Task.Run(() => package.GetView().TryCleanupAfterDelayAsync(TimeSpan.FromSeconds(30)));
}
// Command to select the previous custom format
@@ -362,7 +395,7 @@ namespace AdvancedPaste.ViewModels
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
{
if (Busy)
if (IsBusy)
{
Logger.LogWarning($"Execution of {pasteFormat.Format} from {source} suppressed as busy");
return;
@@ -377,16 +410,18 @@ namespace AdvancedPaste.ViewModels
var elapsedWatch = Stopwatch.StartNew();
Logger.LogDebug($"Started executing {pasteFormat.Format} from source {source}");
Busy = true;
IsBusy = true;
_pasteActionCancellationTokenSource = new();
TransformProgress = double.NaN;
PasteActionError = PasteActionError.None;
Query = pasteFormat.Query;
try
{
// Minimum time to show busy spinner for AI actions when triggered by global keyboard shortcut.
var aiActionMinTaskTime = TimeSpan.FromSeconds(2);
var aiActionMinTaskTime = TimeSpan.FromSeconds(1.5);
var delayTask = (Visible && source == PasteActionSource.GlobalKeyboardShortcut) ? Task.Delay(aiActionMinTaskTime) : Task.CompletedTask;
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source);
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source, _pasteActionCancellationTokenSource.Token, this);
await delayTask;
@@ -410,7 +445,9 @@ namespace AdvancedPaste.ViewModels
PasteActionError = PasteActionError.FromException(ex);
}
Busy = false;
IsBusy = false;
_pasteActionCancellationTokenSource?.Dispose();
_pasteActionCancellationTokenSource = null;
elapsedWatch.Stop();
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
}
@@ -484,5 +521,26 @@ namespace AdvancedPaste.ViewModels
return IsAllowedByGPO && _aiCredentialsProvider.Refresh();
}
public async Task CancelPasteActionAsync()
{
if (_pasteActionCancellationTokenSource != null)
{
await _pasteActionCancellationTokenSource.CancelAsync();
}
}
void IProgress<double>.Report(double value)
{
ReportProgress(value);
}
private void ReportProgress(double value)
{
_dispatcherQueue.TryEnqueue(() =>
{
TransformProgress = value;
});
}
}
}

View File

@@ -88,6 +88,8 @@ void Trace::AdvancedPaste_SettingsTelemetry(const PowertoyModuleIface::Hotkey& p
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"ImageToText"), "ImageToTextHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsTxtFile"), "PasteAsTxtFileHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsPngFile"), "PasteAsPngFileHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey")
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp3"), "TranscodeToMp3Hotkey"),
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp4"), "TranscodeToMp4Hotkey")
);
}

View File

@@ -0,0 +1,267 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Hosts.UITests
{
[TestClass]
public class HostModuleTests : UITestBase
{
public HostModuleTests()
: base(PowerToysModule.Hosts)
{
}
/// <summary>
/// Test Empty-view in the Hosts-File-Editor
/// <list type="bullet">
/// <item>
/// <description>Validating Empty-view is shown if no entries in the list.</description>
/// </item>
/// <item>
/// <description>Validating Empty-view is NOT shown if 1 or more entries in the list.</description>
/// </item>
/// <item>
/// <description>Validating Add-an-entry HyperlinkButton in Empty-view works correctly.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod]
public void TestEmptyView()
{
this.CloseWarningDialog();
this.RemoveAllEntries();
// 'Add an entry' button (only show-up when list is empty) should be visible
Assert.IsTrue(this.FindAll<HyperlinkButton>("Add an entry").Count == 1, "'Add an entry' button should be visible in the empty view");
// Click 'Add an entry' from empty-view for adding Host override rule
this.Find<HyperlinkButton>("Add an entry").Click();
this.AddEntry("192.168.0.1", "localhost", false, false);
// Should have one row now and not more empty view
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 1, "Should have one row now");
Assert.IsTrue(this.FindAll<HyperlinkButton>("Add an entry").Count == 0, "'Add an entry' button should be invisible if not empty view");
}
/// <summary>
/// Test Adding-entry Button in the Hosts-File-Editor
/// <list type="bullet">
/// <item>
/// <description>Validating Adding-entry Button works correctly.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod]
public void TestAddingEntry()
{
this.CloseWarningDialog();
this.RemoveAllEntries();
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0, "Should have no row after removing all");
this.AddEntry("192.168.0.1", "localhost", true);
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 1, "Should have one row now");
}
/// <summary>
/// Test Multiple-hosts validation logic
/// <list type="bullet">
/// <item>
/// <description>Validating the Add button should be Disabled if more than 9 hosts in one entry.</description>
/// </item>
/// <item>
/// <description>Validating the Add button should be Enabled if less or equal 9 hosts in one entry.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod]
public void TestTooManyHosts()
{
this.CloseWarningDialog();
// only at most 9 hosts allowed in one entry
string validHosts = string.Join(" ", "host_1", "host_2", "host_3", "host_4", "host_5", "host_6", "host_7", "host_8", "host_9");
// should not allow to add more than 9 hosts in one entry, hosts are separated by space
string inValidHosts = validHosts + " more_host";
this.Find<Button>("New entry").Click();
Assert.IsFalse(this.Find<Button>("Add").Enabled, "Add button should be Disabled by default");
this.Find<TextBox>("Address").SetText("127.0.0.1");
this.Find<TextBox>("Hosts").SetText(validHosts);
Assert.IsTrue(this.Find<Button>("Add").Enabled, "Add button should be Enabled with validHosts");
this.Find<TextBox>("Hosts").SetText(inValidHosts);
Assert.IsFalse(this.Find<Button>("Add").Enabled, "Add button should be Disabled with inValidHosts");
this.Find<Button>("Cancel").Click();
}
/// <summary>
/// Test Error-message in the Hosts-File-Editor
/// <list type="bullet">
/// <item>
/// <description>Validating error message should be shown if not run as admin.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod]
public void TestErrorMessageWithNonAdminPermission()
{
this.CloseWarningDialog();
this.RemoveAllEntries();
// Add new URL override and a warning tip should be shown
this.AddEntry("192.168.0.1", "localhost", true);
Assert.IsTrue(
this.FindAll<TextBlock>("The hosts file cannot be saved because the program isn't running as administrator.").Count == 1,
"Should display host-file saving error if not run as administrator");
}
/// <summary>
/// Test Filter-panel function in the Hosts-File-Editor
/// <list type="bullet">
/// <item>
/// <description>Validating Address filter matching pattern: contains, endsWith, startsWith, exactly-match.</description>
/// </item>
/// <item>
/// <description>Validating Hosts filter matching pattern: contains, endsWith, startsWith, exactly-match.</description>
/// </item>
/// <item>
/// <description>Validating click Filters Button to open filter-panel, and click Filter Button again to close filter-panel.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod]
public void TestFilterControl()
{
this.CloseWarningDialog();
this.RemoveAllEntries();
for (int i = 0; i < 10; i++)
{
this.AddEntry("192.168.0." + i, "localhost_" + i, true);
}
// Open-filter-panel
this.Find<Button>("Filters").Click();
Assert.IsTrue(this.FindAll<Button>("Clear filters").Count == 1, "Filter panel should be opened afer click Filter Button");
var addressFilterCases = new KeyValuePair<string, int>[]
{
// contains text, expected matched more rows
new("168.0", 10),
// ends with text, expected matched 1 row
new("168.0.1", 1),
// starts with text, expected matched more rows
new("192.168.", 10),
// full text, expected matched 1 row
new("192.168.0.1", 1),
// no-matching text, expected matched no row
new("127.0.0", 0),
// empty filter, should display all rows
new(string.Empty, 10),
};
foreach (var (addressFilter, expectedCount) in addressFilterCases)
{
this.Find<TextBox>("Address").SetText(addressFilter);
Assert.IsTrue(this.Find("Entries").FindAll<Button>("Delete").Count == expectedCount);
}
var hostFilterCases = new KeyValuePair<string, int>[]
{
// contains text, expected matched more rows
new("host_", 10),
// ends with text, expected matched 1 row
new("host_4", 1),
// starts with text, expected matched more rows
new("localhost", 10),
// full text, expected matched 1 row
new("localhost_5", 1),
// empty filter, should display all rows
new(string.Empty, 10),
};
foreach (var (hostFilterCase, expectedCount) in hostFilterCases)
{
this.Find<TextBox>("Hosts").SetText(hostFilterCase);
Assert.IsTrue(this.Find("Entries").FindAll<Button>("Delete").Count == expectedCount);
}
// Close-filter-panel
this.Find<Button>("Filters").Click();
Assert.IsFalse(this.FindAll<Button>("Clear filters").Count == 1, "Filter panel should be closed afer click Filter Button");
}
private void AddEntry(string ip, string host, bool active = true, bool clickAddEntryButton = true)
{
if (clickAddEntryButton)
{
// Click 'Add an entry' for adding Host override rule
this.Find<Button>("New entry").Click();
}
// Adding a new host override localhost -> 192.168.0.1
Assert.IsFalse(this.Find<Button>("Add").Enabled, "Add button should be Disabled by default");
Assert.IsTrue(this.Find<TextBox>("Address").SetText(ip).Text == ip);
Assert.IsTrue(this.Find<TextBox>("Hosts").SetText(host).Text == host);
this.Find<ToggleSwitch>("Active").Toggle(active);
Assert.IsTrue(this.Find<Button>("Add").Enabled, "Add button should be Enabled after providing valid inputs");
// Add the entry
this.Find<Button>("Add").Click();
// 0.5 second delay after adding an entry
Task.Delay(500).Wait();
}
private void CloseWarningDialog()
{
// Find 'Accept' button which come in 'Warning' dialog
if (this.FindAll("Warning").Count > 0 &&
this.FindAll<Button>("Accept").Count > 0)
{
// Hide Warning dialog if any
this.Find<Button>("Accept").Click();
}
}
private void RemoveAllEntries()
{
// Delete all existing host-override rules
foreach (var deleteBtn in this.FindAll<Button>("Delete"))
{
deleteBtn.Click();
this.Find<Button>("Yes").Click();
}
// Should have no row left, and no more delete button
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0);
}
}
}

View File

@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<ProjectGuid>{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}</ProjectGuid>
<RootNamespace>PowerToys.Hosts.UITests</RootNamespace>
<AssemblyName>PowerToys.Hosts.UITests</AssemblyName>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<!-- This is a UI test, so don't run as part of MSBuild -->
<RunVSTest>false</RunVSTest>
</PropertyGroup>
<PropertyGroup>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\Hosts.UITests\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest" />
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Threading.Tasks;
using Microsoft.PowerToys.UITest;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Hosts.UITests
{
[TestClass]
public class HostsSettingTests : UITestBase
{
/// <summary>
/// Test Warning Dialog at startup
/// <list type="bullet">
/// <item>
/// <description>Validating Warning-Dialog will be shown if 'Show a warning at startup' toggle is On.</description>
/// </item>
/// <item>
/// <description>Validating Warning-Dialog will NOT be shown if 'Show a warning at startup' toggle is Off.</description>
/// </item>
/// <item>
/// <description>Validating click 'Quit' button in Warning-Dialog, the Hosts File Editor window would be closed.</description>
/// </item>
/// <item>
/// <description>Validating click 'Accept' button in Warning-Dialog, the Hosts File Editor window would NOT be closed.</description>
/// </item>
/// </list>
/// </summary>
[TestMethod]
public void TestWarningDialog()
{
this.LaunchFromSetting(showWarning: true);
// Validating Warning-Dialog will be shown if 'Show a warning at startup' toggle is on
Assert.IsTrue(this.FindAll("Warning").Count > 0, "Should show warning dialog");
// Quit Hosts File Editor
this.Find<Button>("Quit").Click();
// Wait for 500 ms to make sure Hosts File Editor is closed
Task.Delay(500).Wait();
// Validating click 'Quit' button in Warning-Dialog, the Hosts File Editor window would be closed
Assert.IsTrue(this.IsHostsFileEditorClosed(), "Hosts File Editor should be closed after click Quit button in Warning Dialog");
// Re-attaching to Setting Windows
this.Session.Attach(PowerToysModule.PowerToysSettings);
this.Find<Button>("Launch Hosts File Editor").Click();
// wait for 500 ms to make sure Hosts File Editor is launched
Task.Delay(500).Wait();
this.Session.Attach(PowerToysModule.Hosts);
// Should show warning dialog
Assert.IsTrue(this.FindAll("Warning").Count > 0, "Should show warning dialog");
// Quit Hosts File Editor
this.Find<Button>("Accept").Click();
Task.Delay(500).Wait();
// Validating click 'Accept' button in Warning-Dialog, the Hosts File Editor window would NOT be closed
Assert.IsFalse(this.IsHostsFileEditorClosed(), "Hosts File Editor should NOT be closed after click Accept button in Warning Dialog");
// Close Hosts File Editor window
this.Session.Find<Window>("Hosts File Editor").Close();
// Restore back to PowerToysSettings Session
this.Session.Attach(PowerToysModule.PowerToysSettings);
this.LaunchFromSetting(showWarning: false);
// Should NOT show warning dialog
Assert.IsTrue(this.FindAll("Warning").Count == 0, "Should NOT show warning dialog");
// Host Editor Window should not be closed
Assert.IsFalse(this.IsHostsFileEditorClosed(), "Hosts File Editor should NOT be closed");
// Close Hosts File Editor window
this.Session.Find<Window>("Hosts File Editor").Close();
// Restore back to PowerToysSettings Session
this.Session.Attach(PowerToysModule.PowerToysSettings);
}
private bool IsHostsFileEditorClosed()
{
try
{
this.Session.FindAll<Window>("Hosts File Editor");
}
catch (Exception ex)
{
// Validate if editor window closed by checking exception.Message
return ex.Message.Contains("Currently selected window has been closed");
}
return false;
}
private void LaunchFromSetting(bool showWarning = false, bool launchAsAdmin = false)
{
// Goto Hosts File Editor setting page
if (this.FindAll<NavigationViewItem>("Hosts File Editor").Count == 0)
{
// Expand Advanced list-group if needed
this.Find<NavigationViewItem>("Advanced").Click();
}
this.Find<NavigationViewItem>("Hosts File Editor").Click();
this.Find<ToggleSwitch>("Enable Hosts File Editor").Toggle(true);
this.Find<ToggleSwitch>("Launch as administrator").Toggle(launchAsAdmin);
this.Find<ToggleSwitch>("Show a warning at startup").Toggle(showWarning);
// launch Hosts File Editor
this.Find<Button>("Launch Hosts File Editor").Click();
Task.Delay(500).Wait();
this.Session.Attach(PowerToysModule.Hosts);
}
}
}

View File

@@ -0,0 +1,26 @@
## This is for tracking UI-Tests migration progress for Hosts File Editor Module
Refer to [release check list] (https://github.com/microsoft/PowerToys/blob/releaseChecklist/doc/releases/tests-checklist-template.md#hosts-file-editor) for all manual tests.
### Existing Manual Test-cases run by previous PowerToys owner
For existing manual test-cases, we will convert them to UI-Tests and run them in CI and Release pipeline
* Launch Host File Editor:
- [x] Verify the application exits if "Quit" is clicked on the initial warning. (**HostsSettingTests.TestWarningDialog**)
- [x] Launch Host File Editor again and click "Accept". The module should not close. (**HostModuleTests.TestEmptyView**)
- [ ] Launch Host File Editor again and click "Accept". The module should not close. Open the hosts file (`%WinDir%\System32\Drivers\Etc`) in a text editor that auto-refreshes so you can see the changes applied by the editor in real time. (VSCode is an editor like this, for example)
- [ ] Enable and disable lines and verify they are applied to the file.
- [ ] Add a new entry and verify it's applied.
- [ ] Add manually an entry with more than 9 hosts in hosts file (Windows limitation) and verify it is split correctly on loading and the info bar is shown.
- [x] Try to filter for lines and verify you can find them. (**HostModuleTests.TestFilterControl**)
- [ ] Click the "Open hosts file" button and verify it opens in your default editor. (likely Notepad)
* Test the different settings and verify they are applied:
- [ ] Launch as Administrator.
- [x] Show a warning at startup. (**HostsSettingTests.TestWarningDialog**)
- [ ] Additional lines position.
### Additional UI-Tests cases
- [x] Add manually an entry with more than 9 hosts and Add button should be disabled. (**HostModuleTests.TestTooManyHosts**)
- [x] Add manually an entry with less or equal 9 hosts and Add button should be enabled. (**HostModuleTests.TestTooManyHosts**)
- [x] Should show empty view if no entries. (**HostModuleTests.TestEmptyView**)
- [x] Add a new entry with valid or invalid input (**HostModuleTests.TestAddHost**)
- [x] Show save host file error if not run as Administrator. (**HostModuleTests.TestErrorMessageWithNonAdminPermission**)

View File

@@ -192,7 +192,7 @@ namespace WorkspacesCsharpLibrary.Models
else
{
string appPath = AppPath.Replace("C:\\Program Files\\WindowsApps\\", string.Empty);
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_(:?x64|arm64)__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
Match match = packagedAppPathRegex.Match(appPath);
_isPackagedApp = match.Success;
if (match.Success)

View File

@@ -62,10 +62,10 @@ namespace PlacementHelper
else
{
placement.showCmd = SW_RESTORE;
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
}
ScreenToWorkAreaCoords(window, monitor, rect);
placement.rcNormalPosition = rect;
placement.flags |= WPF_ASYNCWINDOWPLACEMENT;
auto result = ::SetWindowPlacement(window, &placement);

View File

@@ -49,7 +49,7 @@ namespace util
}
// This workaround keeps live zoom enabled after zooming out at level 1 (not zoomed) and disables
// live zoom when recording is stopped.
// live zoom when recording is stopped
#define WINDOWS_CURSOR_RECORDING_WORKAROUND 1
HINSTANCE g_hInstance;
@@ -320,10 +320,10 @@ void EnsureForeground()
//----------------------------------------------------------------------------
void RestoreForeground()
{
// If the main window is not visible, move foreground to the next window
// If the main window is not visible, move foreground to the next window.
if( !IsWindowVisible( g_hWndMain ) ) {
// Activate the next window by unhiding and hiding the main window
// Activate the next window by unhiding and hiding the main window.
MoveWindow( g_hWndMain, 0, 0, 0, 0, FALSE );
ShowWindow( g_hWndMain, SW_SHOWNA );
ShowWindow( g_hWndMain, SW_HIDE );
@@ -493,7 +493,7 @@ bool IsAutostartConfigured()
// RunningOnWin64
//
// Returns true if this is the 32-bit version of the executable
// and we're on 64-bit Windows
// and we're on 64-bit Windows.
//
//--------------------------------------------------------------------
typedef BOOL (__stdcall *P_IS_WOW64PROCESS)(
@@ -559,7 +559,7 @@ BOOLEAN ExtractImageResource( PTCHAR ResourceName, PTCHAR TargetFile )
// Run64bitVersion
//
// Returns true if this is the 32-bit version of the executable
// and we're on 64-bit Windows
// and we're on 64-bit Windows.
//
//--------------------------------------------------------------------
DWORD

View File

@@ -230,7 +230,7 @@ namespace ColorPicker.Helpers
public bool HandleEnterPressed()
{
if (!IsColorPickerVisible())
if (!_colorPickerShown)
{
return false;
}
@@ -241,14 +241,13 @@ namespace ColorPicker.Helpers
public bool HandleEscPressed()
{
if (!BlockEscapeKeyClosingColorPickerEditor)
if (!BlockEscapeKeyClosingColorPickerEditor
&& (_colorPickerShown || (_colorEditorWindow != null && _colorEditorWindow.IsActive)))
{
return EndUserSession();
}
else
{
return false;
}
return false;
}
internal void MoveCursor(int xOffset, int yOffset)

View File

@@ -70,17 +70,10 @@ namespace ColorPicker.Keyboard
var virtualCode = e.KeyboardData.VirtualCode;
// ESC pressed
if (virtualCode == KeyInterop.VirtualKeyFromKey(Key.Escape)
&& e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown
)
if (virtualCode == KeyInterop.VirtualKeyFromKey(Key.Escape) && e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown)
{
if (_appStateHandler.IsColorPickerVisible()
|| !AppStateHandler.BlockEscapeKeyClosingColorPickerEditor
)
{
e.Handled = _appStateHandler.EndUserSession();
return;
}
e.Handled = _appStateHandler.HandleEscPressed();
return;
}
if ((virtualCode == KeyInterop.VirtualKeyFromKey(Key.Space) || virtualCode == KeyInterop.VirtualKeyFromKey(Key.Enter)) && (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown))

View File

@@ -487,10 +487,10 @@ namespace FancyZonesEditor
}
}
model.Delete();
App.FancyZonesEditorIO.SerializeAppliedLayouts();
App.FancyZonesEditorIO.SerializeCustomLayouts();
App.FancyZonesEditorIO.SerializeDefaultLayouts();
model.Delete();
}
}

View File

@@ -21,6 +21,8 @@
<v:VisibilityBoolConverter x:Key="VisibilityBoolConverter" />
<v:EnumToIntConverter x:Key="EnumToIntConverter" />
<v:AccessTextToTextConverter x:Key="AccessTextToTextConverter" />
<v:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
<v:ZeroToEmptyStringNumberFormatter x:Key="ZeroToEmptyStringNumberFormatter" />
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -10,29 +10,55 @@ using System.Windows.Data;
using ImageResizer.Properties;
namespace ImageResizer.Views
namespace ImageResizer.Views;
/// <summary>
/// Converts between double and string for text-based controls bound to Width or Height fields.
/// Optionally returns localized "Auto" text when the underlying value is 0, letting the UI show,
/// for example "(auto) x 1024 pixels".
/// </summary>
[ValueConversion(typeof(double), typeof(string))]
internal class AutoDoubleConverter : IValueConverter
{
[ValueConversion(typeof(double), typeof(string))]
internal class AutoDoubleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
/// <summary>
/// Converts a double to a string, optionally showing "Auto" for 0 values. NaN values are
/// converted to empty strings.
/// </summary>
/// <param name="value">The value to convert from <see cref="double"/> to
/// <see cref="string"/>.</param>
/// <param name="targetType">The conversion target type. <see cref="string"/> here.</param>
/// <param name="parameter">Set to "Auto" to return the localized "Auto" string if the
/// value is 0.</param>
/// <param name="culture">The <see cref="CultureInfo"/> to use for the number formatting.
/// </param>
/// <returns>The string representation of the passed-in value.</returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
var d = (double)value;
double d => d switch
{
double.NaN => "0",
0 => (string)parameter == "Auto" ? Resources.Input_Auto : "0",
_ => d.ToString(culture),
},
return d != 0
? d.ToString(culture)
: (string)parameter == "Auto"
? Resources.Input_Auto
: string.Empty;
}
_ => "0",
};
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
/// <summary>
/// Converts the string representation back to a double, returning 0 if the string is empty,
/// null or not a valid number in the specified culture.
/// </summary>
/// <param name="value">The string value to convert.</param>
/// <param name="targetType">The conversion target type. <see cref="double"/> here.</param>
/// <param name="parameter">Converter parameter. Unused.</param>
/// <param name="culture">The <see cref="CultureInfo"/> to use for the text parsing.</param>
/// <returns>The corresponding double value.</returns>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
var text = (string)value;
return !string.IsNullOrEmpty(text)
? double.Parse(text, culture)
: 0;
}
}
null or "" => 0,
string text when double.TryParse(text, NumberStyles.Any, culture, out double result) => result,
_ => 0,
};
}

View File

@@ -4,7 +4,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:m="clr-namespace:ImageResizer.Models"
xmlns:p="clr-namespace:ImageResizer.Properties"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml">
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:v="clr-namespace:ImageResizer.Views">
<Grid>
<Grid.RowDefinitions>
@@ -114,8 +115,12 @@
KeyDown="Button_KeyDown"
Minimum="0"
SpinButtonPlacementMode="Inline">
<ui:NumberBox.NumberFormatter>
<v:ZeroToEmptyStringNumberFormatter />
</ui:NumberBox.NumberFormatter>
<ui:NumberBox.Value>
<Binding
Converter="{StaticResource NumberBoxValueConverter}"
ElementName="SizeComboBox"
Mode="TwoWay"
Path="SelectedValue.Width"
@@ -143,8 +148,12 @@
Minimum="0"
SpinButtonPlacementMode="Inline"
Visibility="{Binding ElementName=SizeComboBox, Path=SelectedValue.ShowHeight, Converter={StaticResource BoolValueConverter}}">
<ui:NumberBox.NumberFormatter>
<v:ZeroToEmptyStringNumberFormatter />
</ui:NumberBox.NumberFormatter>
<ui:NumberBox.Value>
<Binding
Converter="{StaticResource NumberBoxValueConverter}"
ElementName="SizeComboBox"
Mode="TwoWay"
Path="SelectedValue.Height"

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Windows.Data;
namespace ImageResizer.Views;
public class NumberBoxValueConverter : IValueConverter
{
/// <summary>
/// Converts the underlying double value to a display-friendly format. Ensures that NaN values
/// are not propagated to the UI.
/// </summary>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value is double d && double.IsNaN(d) ? 0 : value;
/// <summary>
/// Converts the user input back to the underlying double value. If the input is not a valid
/// number, 0 is returned.
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
value switch
{
null => 0,
double d when double.IsNaN(d) => 0,
string str when !double.TryParse(str, out _) => 0,
_ => value,
};
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Wpf.Ui.Controls;
namespace ImageResizer.Views;
public class ZeroToEmptyStringNumberFormatter : INumberFormatter, INumberParser
{
public string FormatDouble(double? value) => value switch
{
null => string.Empty,
0 => string.Empty,
_ => value.Value.ToString(CultureInfo.CurrentCulture),
};
public double? ParseDouble(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return 0;
}
return double.TryParse(value, NumberStyles.Any, CultureInfo.CurrentCulture, out double result) ? result : 0;
}
public string FormatInt(int? value) => throw new NotImplementedException();
public string FormatUInt(uint? value) => throw new NotImplementedException();
public int? ParseInt(string value) => throw new NotImplementedException();
public uint? ParseUInt(string value) => throw new NotImplementedException();
}

View File

@@ -0,0 +1,40 @@
#include <windows.h>
#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

View File

@@ -34,6 +34,9 @@
<RootNamespace>KeyboardManagerEditorLibraryWrapper</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<PropertyGroup>
<TargetName>PowerToys.KeyboardManagerEditorLibraryWrapper</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
@@ -218,6 +221,7 @@
<ClInclude Include="framework.h" />
<ClInclude Include="KeyboardManagerEditorLibraryWrapper.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
@@ -231,6 +235,9 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="KeyboardManagerEditorLibraryWrapper.rc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>

View File

@@ -24,6 +24,9 @@
<ClInclude Include="KeyboardManagerEditorLibraryWrapper.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Resource Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
@@ -39,4 +42,9 @@
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="KeyboardManagerEditorLibraryWrapper.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by AlwaysOnTopModuleInterface.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys Keyboard Manager Editor Library Wrapper"
#define INTERNAL_NAME "PowerToys.KeyboardManagerEditorLibraryWrapper"
#define ORIGINAL_FILENAME "PowerToys.KeyboardManagerEditorLibraryWrapper.dll"
// Non-localizable
//////////////////////////////

View File

@@ -7,6 +7,8 @@
<AssemblyTitle>PowerToys.Run</AssemblyTitle>
<OutputType>WinExe</OutputType>
<UseWPF>true</UseWPF>
<!-- For some users, it crashes in ExtractAssociatedIcon https://github.com/microsoft/PowerToys/issues/37254. Workaround suggested here https://github.com/dotnet/wpf/issues/10483 was to disable CETCompat -->
<CETCompat>false</CETCompat>
<UseWindowsForms>False</UseWindowsForms>
<StartupObject>PowerLauncher.App</StartupObject>
<ApplicationIcon>Assets\PowerLauncher\RunResource.ico</ApplicationIcon>

View File

@@ -136,13 +136,25 @@ namespace Wox.Infrastructure
var link = new ShellLink();
const int STGM_READ = 0;
// Make sure not to open exclusive handles.
// See: https://github.com/microsoft/WSL/issues/11276
const int STGM_SHARE_DENY_NONE = 0x00000040;
const int STGM_TRANSACTED = 0x00010000;
try
{
((IPersistFile)link).Load(path, STGM_READ);
((IPersistFile)link).Load(path, STGM_READ | STGM_SHARE_DENY_NONE | STGM_TRANSACTED);
}
catch (System.IO.FileNotFoundException ex)
{
Log.Exception("Path could not be retrieved", ex, GetType(), path);
Log.Exception("Path could not be retrieved " + path, ex, GetType(), path);
Marshal.ReleaseComObject(link);
return string.Empty;
}
catch (System.Exception ex)
{
Log.Exception("Exception loading path " + path, ex, GetType(), path);
Marshal.ReleaseComObject(link);
return string.Empty;
}

View File

@@ -677,7 +677,7 @@ namespace PowerAccent.Core
LetterKey.VK_O => new string[] { "ο", "ό", "ω", "ώ" },
LetterKey.VK_P => new string[] { "π", "φ", "ψ" },
LetterKey.VK_R => new string[] { "ρ" },
LetterKey.VK_S => new string[] { "σ" },
LetterKey.VK_S => new string[] { "σ", "ς" },
LetterKey.VK_T => new string[] { "τ", "θ", "ϑ" },
LetterKey.VK_U => new string[] { "υ", "ύ" },
LetterKey.VK_X => new string[] { "ξ" },

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -36,4 +37,7 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
get => _isShown;
set => Set(ref _isShown, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [];
}

View File

@@ -14,6 +14,7 @@ public sealed class AdvancedPasteAdditionalActions
{
public const string ImageToText = "image-to-text";
public const string PasteAsFile = "paste-as-file";
public const string Transcode = "transcode";
}
[JsonPropertyName(PropertyNames.ImageToText)]
@@ -22,6 +23,22 @@ public sealed class AdvancedPasteAdditionalActions
[JsonPropertyName(PropertyNames.PasteAsFile)]
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> AllActions => new IAdvancedPasteAction[] { ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions);
[JsonPropertyName(PropertyNames.Transcode)]
public AdvancedPasteTranscodeAction Transcode { get; init; } = new();
public IEnumerable<IAdvancedPasteAction> GetAllActions()
{
Queue<IAdvancedPasteAction> queue = new([ImageToText, PasteAsFile, Transcode]);
while (queue.Count != 0)
{
var action = queue.Dequeue();
yield return action;
foreach (var subAction in action.SubActions)
{
queue.Enqueue(subAction);
}
}
}
}

View File

@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
@@ -98,6 +99,9 @@ public sealed class AdvancedPasteCustomAction : Observable, IAdvancedPasteAction
private set => Set(ref _isValid, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [];
public object Clone()
{
AdvancedPasteCustomAction clone = new();

View File

@@ -52,5 +52,5 @@ public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteA
}
[JsonIgnore]
public IEnumerable<AdvancedPasteAdditionalAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
public IEnumerable<IAdvancedPasteAction> SubActions => [PasteAsTxtFile, PasteAsPngFile, PasteAsHtmlFile];
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
namespace Microsoft.PowerToys.Settings.UI.Library;
public sealed class AdvancedPasteTranscodeAction : Observable, IAdvancedPasteAction
{
public static class PropertyNames
{
public const string TranscodeToMp3 = "transcode-to-mp3";
public const string TranscodeToMp4 = "transcode-to-mp4";
}
private AdvancedPasteAdditionalAction _transcodeToMp3 = new();
private AdvancedPasteAdditionalAction _transcodeToMp4 = new();
private bool _isShown = true;
[JsonPropertyName("isShown")]
public bool IsShown
{
get => _isShown;
set => Set(ref _isShown, value);
}
[JsonPropertyName(PropertyNames.TranscodeToMp3)]
public AdvancedPasteAdditionalAction TranscodeToMp3
{
get => _transcodeToMp3;
init => Set(ref _transcodeToMp3, value);
}
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
public AdvancedPasteAdditionalAction TranscodeToMp4
{
get => _transcodeToMp4;
init => Set(ref _transcodeToMp4, value);
}
[JsonIgnore]
public IEnumerable<IAdvancedPasteAction> SubActions => [TranscodeToMp3, TranscodeToMp4];
}

Some files were not shown because too many files have changed in this diff Show More