mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-30 00:46:20 +01:00
Compare commits
7 Commits
shawn/test
...
regist
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1b4fee0b6 | ||
|
|
6ee7dba97e | ||
|
|
330e292037 | ||
|
|
5e28035d9f | ||
|
|
88f5e65ee8 | ||
|
|
8aefd45805 | ||
|
|
b205794d6a |
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@@ -1283,7 +1283,7 @@ rectp
|
||||
RECTSOURCE
|
||||
recyclebin
|
||||
Redist
|
||||
Reencode
|
||||
reencode
|
||||
reencoded
|
||||
REFCLSID
|
||||
REFGUID
|
||||
|
||||
@@ -128,7 +128,6 @@
|
||||
"PowerToys.KeyboardManager.dll",
|
||||
"KeyboardManagerEditor\\PowerToys.KeyboardManagerEditor.exe",
|
||||
"KeyboardManagerEngine\\PowerToys.KeyboardManagerEngine.exe",
|
||||
"PowerToys.KeyboardManagerEditorLibraryWrapper.dll",
|
||||
|
||||
"PowerToys.Launcher.dll",
|
||||
"PowerToys.PowerLauncher.dll",
|
||||
|
||||
@@ -33,7 +33,7 @@ parameters:
|
||||
default: true
|
||||
- name: winAppSDKVersionNumber
|
||||
type: string
|
||||
default: 1.7
|
||||
default: 1.6
|
||||
- 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 }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
displayName: Download artifacts
|
||||
artifact: $(ArtifactName)
|
||||
patterns: |-
|
||||
**/tests/*.FuzzTests/**
|
||||
**/tests/RegistryPreview.FuzzTests/**
|
||||
|
||||
- task: onefuzz-task@0
|
||||
inputs:
|
||||
|
||||
@@ -61,18 +61,9 @@ jobs:
|
||||
reg add "HKLM\Software\Policies\Microsoft\Edge\WebView2\ReleaseChannels" /v PowerToys.exe /t REG_SZ /d "3"
|
||||
displayName: "Enable WebView2 Canary Channel"
|
||||
|
||||
- ${{ 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-download-artifacts-with-azure-cli.yml
|
||||
parameters:
|
||||
artifactName: $(TestArtifactsName)
|
||||
|
||||
- template: steps-ensure-dotnet-version.yml
|
||||
parameters:
|
||||
|
||||
@@ -72,7 +72,7 @@ stages:
|
||||
winAppSDKVersionNumber: ${{ parameters.winAppSDKVersionNumber }}
|
||||
useExperimentalVersion: ${{ parameters.useExperimentalVersion }}
|
||||
|
||||
- ${{ if and(eq(parameters.runTests, true), not(and(eq(platform, 'arm64'), eq(variables['System.PullRequest.IsFork'], true)))) }}:
|
||||
- ${{ if eq(parameters.runTests, true) }}:
|
||||
- stage: Test_${{ platform }}
|
||||
displayName: Test ${{ platform }}
|
||||
dependsOn:
|
||||
|
||||
@@ -27,9 +27,6 @@ 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'
|
||||
|
||||
@@ -15,7 +15,7 @@ Param(
|
||||
$referencedFileVersionsPerDll = @{}
|
||||
$totalFailures = 0
|
||||
|
||||
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITest*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object {
|
||||
Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITests*,MouseJump.Common.UnitTests*,*.FuzzTests* | ForEach-Object {
|
||||
# Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies
|
||||
$depsJsonFullFileName = $_.FullName
|
||||
$depsJsonFileName = $_.Name
|
||||
|
||||
@@ -646,6 +646,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.FuzzTests", "src\modu
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hosts.UITests", "src\modules\Hosts\Hosts.UITests\Hosts.UITests.csproj", "{4E0AE3A4-2EE0-44D7-A2D0-8769977254A0}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RegistryPreview.FuzzTests", "src\modules\registrypreview\RegistryPreview.FuzzTests\RegistryPreview.FuzzTests.csproj", "{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
@@ -2294,6 +2296,14 @@ Global
|
||||
{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
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Debug|x64.Build.0 = Debug|x64
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|x64.ActiveCfg = Release|x64
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -2534,6 +2544,7 @@ Global
|
||||
{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}
|
||||
{50A9F3DE-CF0B-4CF0-AFDE-3A3E245D7734} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
170
README.md
170
README.md
@@ -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.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
|
||||
[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
|
||||
|
||||
| Description | Filename | sha256 hash |
|
||||
|----------------|----------|-------------|
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
This is our preferred method.
|
||||
|
||||
@@ -92,103 +92,141 @@ 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.89 - February 2025 Update
|
||||
### 0.88 - January 2025 Update
|
||||
|
||||
In this release, we focused on new features, stability, accessibility and automation.
|
||||
In this release, we focused on new features, stability, and improvements.
|
||||
|
||||
**✨Highlights**
|
||||
**Highlights**
|
||||
|
||||
- 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!
|
||||
- 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.
|
||||
|
||||
### General
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### Advanced Paste
|
||||
|
||||
- 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!
|
||||
- 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.
|
||||
|
||||
### FancyZones
|
||||
|
||||
- Fixed accessibility by improving the text for monitors, ensuring clearer naming and help text for screen readers.
|
||||
- Removed Workspaces Editor from the exclusions list so it can be snapped by FancyZones.
|
||||
|
||||
### 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.
|
||||
### Keyboard Manager
|
||||
|
||||
- Added an option to make a shortcut remapping only trigger with exact modifiers.
|
||||
|
||||
### Monaco Preview
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### Mouse Without Borders
|
||||
- Fixed an issue in service mode where copy-paste and drag-drop file transfers didn’t 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!
|
||||
|
||||
### 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.
|
||||
- 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)!
|
||||
|
||||
### PowerToys Run
|
||||
- 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 Run’s 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)!
|
||||
|
||||
- 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)!
|
||||
|
||||
### Quick Accent
|
||||
|
||||
- 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)!
|
||||
- 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)!
|
||||
|
||||
### Settings
|
||||
|
||||
- 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.
|
||||
- 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)!
|
||||
|
||||
### Workspaces
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### ZoomIt
|
||||
|
||||
- Fixed warning C4706 and related error C2220 during build. Thanks [@xanatos](https://github.com/xanatos)!
|
||||
- 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!
|
||||
|
||||
### Documentation
|
||||
|
||||
- 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.
|
||||
- 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)!
|
||||
|
||||
### Development
|
||||
|
||||
- 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
|
||||
- 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)!
|
||||
|
||||
For [v0.90][github-next-release-work], we'll work on the items below:
|
||||
#### What is being planned for version 0.89
|
||||
|
||||
For [v0.89][github-next-release-work], we'll work on the items below:
|
||||
|
||||
- New module: PowerToys Run v2
|
||||
- New module: File Actions Menu
|
||||
- Working on installer upgrades
|
||||
- Upgrading keyboard manager's editor UI
|
||||
- Stability / bug fixes
|
||||
- New module: File Actions Menu
|
||||
- PowerToys Run v2 development work
|
||||
|
||||
## PowerToys Community
|
||||
|
||||
|
||||
@@ -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.winget --accept-configuration-agreements --disable-interactivity
|
||||
winget configure .\configuration.dsc.yaml --accept-configuration-agreements --disable-interactivity
|
||||
```
|
||||
@@ -1,7 +1,6 @@
|
||||
<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>
|
||||
|
||||
@@ -91,20 +91,20 @@ namespace Common.UI
|
||||
{
|
||||
try
|
||||
{
|
||||
var directoryPath = System.AppContext.BaseDirectory;
|
||||
var assemblyPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
var fullPath = new DirectoryInfo(assemblyPath).FullName;
|
||||
if (mainExecutableIsOnTheParentFolder)
|
||||
{
|
||||
// Need to go into parent folder for PowerToys.exe. Likely a WinUI3 App SDK application.
|
||||
directoryPath = Path.Combine(directoryPath, "..");
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
fullPath = fullPath + "\\..\\PowerToys.exe";
|
||||
}
|
||||
else
|
||||
{
|
||||
// PowerToys.exe is in the same path as the application.
|
||||
directoryPath = Path.Combine(directoryPath, "PowerToys.exe");
|
||||
fullPath = fullPath + "\\PowerToys.exe";
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo(directoryPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
|
||||
Process.Start(new ProcessStartInfo(fullPath) { Arguments = "--open-settings=" + SettingsWindowNameToString(window) });
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ using Microsoft.Win32;
|
||||
|
||||
namespace Common.UI
|
||||
{
|
||||
public partial class ThemeManager : IDisposable
|
||||
public class ThemeManager : IDisposable
|
||||
{
|
||||
private readonly Application _app;
|
||||
private const string LightTheme = "Light.Accent1";
|
||||
|
||||
@@ -6,7 +6,6 @@ using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ManagedCommon.Serialization;
|
||||
|
||||
namespace ManagedCommon
|
||||
{
|
||||
@@ -36,7 +35,7 @@ namespace ManagedCommon
|
||||
inputStream.Close();
|
||||
reader.Dispose();
|
||||
|
||||
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data, SourceGenerationContext.Default.OutGoingLanguageSettings).LanguageTag;
|
||||
return JsonSerializer.Deserialize<OutGoingLanguageSettings>(data).LanguageTag;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
||||
@@ -15,23 +15,15 @@ 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>
|
||||
@@ -61,16 +53,18 @@ namespace ManagedCommon
|
||||
Trace.AutoFlush = true;
|
||||
}
|
||||
|
||||
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)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogError(string message)
|
||||
{
|
||||
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
|
||||
Log(message, Error);
|
||||
}
|
||||
|
||||
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)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogError(string message, Exception ex)
|
||||
{
|
||||
if (ex == null)
|
||||
{
|
||||
Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
|
||||
Log(message, Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -89,33 +83,38 @@ namespace ManagedCommon
|
||||
"Stack trace: " + Environment.NewLine +
|
||||
ex.StackTrace;
|
||||
|
||||
Log(exMessage, Error, memberName, sourceFilePath, sourceLineNumber);
|
||||
Log(exMessage, Error);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogWarning(string message)
|
||||
{
|
||||
Log(message, Warning, memberName, sourceFilePath, sourceLineNumber);
|
||||
Log(message, Warning);
|
||||
}
|
||||
|
||||
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)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogInfo(string message)
|
||||
{
|
||||
Log(message, Info, memberName, sourceFilePath, sourceLineNumber);
|
||||
Log(message, Info);
|
||||
}
|
||||
|
||||
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)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogDebug(string message)
|
||||
{
|
||||
Log(message, Debug, memberName, sourceFilePath, sourceLineNumber);
|
||||
Log(message, Debug);
|
||||
}
|
||||
|
||||
public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static void LogTrace()
|
||||
{
|
||||
Log(string.Empty, TraceFlag, memberName, sourceFilePath, sourceLineNumber);
|
||||
Log(string.Empty, TraceFlag);
|
||||
}
|
||||
|
||||
private static void Log(string message, string type, string memberName, string sourceFilePath, int sourceLineNumber)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void Log(string message, string type)
|
||||
{
|
||||
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo(memberName, sourceFilePath, sourceLineNumber));
|
||||
Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo());
|
||||
Trace.Indent();
|
||||
if (message != string.Empty)
|
||||
{
|
||||
@@ -125,27 +124,49 @@ namespace ManagedCommon
|
||||
Trace.Unindent();
|
||||
}
|
||||
|
||||
private static string GetCallerInfo(string memberName, string sourceFilePath, int sourceLineNumber)
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static string GetCallerInfo()
|
||||
{
|
||||
string callerFileName = "Unknown";
|
||||
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();
|
||||
|
||||
try
|
||||
{
|
||||
string fileName = Path.GetFileName(sourceFilePath);
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
if (topMethod?.Name == nameof(IAsyncStateMachine.MoveNext) && typeof(IAsyncStateMachine).IsAssignableFrom(topMethod?.DeclaringType))
|
||||
{
|
||||
callerFileName = 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
callerFileName = "Unknown";
|
||||
// Ignore exceptions in Release. The code above won't throw, but if it does, we don't want to crash the app.
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
|
||||
return $"{callerFileName}::{memberName}::{sourceLineNumber}";
|
||||
return topMethod;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<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>
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace ManagedCommon
|
||||
|
||||
internal static int Size
|
||||
{
|
||||
get { return Marshal.SizeOf<INPUT>(); }
|
||||
get { return Marshal.SizeOf(typeof(INPUT)); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,12 @@ namespace ManagedCommon
|
||||
{
|
||||
public static class RunnerHelper
|
||||
{
|
||||
public static void WaitForPowerToysRunner(int powerToysPID, Action act, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
|
||||
public static void WaitForPowerToysRunner(int powerToysPID, Action act)
|
||||
{
|
||||
var stackTrace = new StackTrace();
|
||||
var assembly = Assembly.GetCallingAssembly().GetName();
|
||||
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{memberName}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
|
||||
var callingMethod = stackTrace.GetFrame(1).GetMethod().Name;
|
||||
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner waiting for Event powerToysPID={powerToysPID}" });
|
||||
Task.Run(() =>
|
||||
{
|
||||
const uint INFINITE = 0xFFFFFFFF;
|
||||
@@ -28,7 +29,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}][{memberName}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
|
||||
PowerToysTelemetry.Log.WriteEvent(new DebugEvent() { Message = $"[{assembly}][{callingMethod}]WaitForPowerToysRunner Event Notified powerToysPID={powerToysPID}" });
|
||||
act.Invoke();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using static ManagedCommon.LanguageHelper;
|
||||
|
||||
namespace ManagedCommon.Serialization;
|
||||
|
||||
[JsonSerializable(typeof(OutGoingLanguageSettings))]
|
||||
internal sealed partial class SourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace ManagedCommon
|
||||
/// <param name="sender">Sender ThemeListener</param>
|
||||
public delegate void ThemeChangedEvent(ThemeListener sender);
|
||||
|
||||
public partial class ThemeListener : IDisposable
|
||||
public class ThemeListener : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the App Theme.
|
||||
|
||||
@@ -9,14 +9,5 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,20 +16,12 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Represents a basic UI element in the application.
|
||||
/// </summary>
|
||||
public class Element
|
||||
public abstract 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;
|
||||
@@ -99,7 +91,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// Click the UI element.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
public void Click(bool rightClick = false)
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
@@ -124,7 +116,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// <summary>
|
||||
/// Double Click the UI element.
|
||||
/// </summary>
|
||||
public virtual void DoubleClick()
|
||||
public void DoubleClick()
|
||||
{
|
||||
PerformAction((actions, windowElement) =>
|
||||
{
|
||||
@@ -147,6 +139,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -161,51 +154,17 @@ 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 = FindHelper.Find<T, AppiumWebElement>(
|
||||
() =>
|
||||
{
|
||||
var element = this.windowsElement.FindElement(by.ToSeleniumBy());
|
||||
Assert.IsNotNull(element, $"Element not found using selector: {by}");
|
||||
return element;
|
||||
},
|
||||
this.driver,
|
||||
timeoutMS);
|
||||
|
||||
// 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);
|
||||
return foundElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -215,7 +174,7 @@ 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}");
|
||||
@@ -223,59 +182,22 @@ namespace Microsoft.PowerToys.UITest
|
||||
() =>
|
||||
{
|
||||
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 ?? 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);
|
||||
return foundElements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates a manual operation on the element.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
/// <param name="msPreAction">The number of milliseconds to wait before the action. Default value is 100 ms</param>
|
||||
/// <param name="msPostAction">The number of milliseconds to wait after the action. Default value is 100 ms</param>
|
||||
protected void PerformAction(Action<Actions, WindowsElement> action, int msPreAction = 100, int msPostAction = 100)
|
||||
{
|
||||
if (msPreAction > 0)
|
||||
{
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,21 +7,10 @@ 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).
|
||||
/// Represents a textbox in the UI test environment.
|
||||
/// </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>
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Maximize").Click();
|
||||
Find<Button>(By.Name("Maximize")).Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -37,7 +37,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Restore").Click();
|
||||
Find<Button>(By.Name("Restore")).Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -56,7 +56,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Minimize").Click();
|
||||
Find<Button>(By.Name("Minimize")).Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -74,7 +74,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
if (byClickButton)
|
||||
{
|
||||
Find<Button>("Close").Click();
|
||||
Find<Button>(By.Name("Close")).Click();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -17,6 +17,13 @@ namespace Microsoft.PowerToys.UITest
|
||||
/// </summary>
|
||||
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()
|
||||
{
|
||||
@@ -25,7 +32,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
var element = item as WindowsElement;
|
||||
return NewElement<T>(element, driver, timeoutMS);
|
||||
}).Where(item => item.IsMatchingTarget()).ToList();
|
||||
}).ToList();
|
||||
|
||||
return new ReadOnlyCollection<T>(res);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using OpenQA.Selenium.Appium;
|
||||
using OpenQA.Selenium.Appium.Windows;
|
||||
@@ -40,48 +39,17 @@ 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 = FindHelper.Find<T, WindowsElement>(
|
||||
() =>
|
||||
{
|
||||
var element = this.WindowsDriver.FindElement(by.ToSeleniumBy());
|
||||
Assert.IsNotNull(element, $"Element not found using selector: {by}");
|
||||
return element;
|
||||
},
|
||||
this.WindowsDriver,
|
||||
timeoutMS);
|
||||
|
||||
// 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);
|
||||
return foundElement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -104,45 +72,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
this.WindowsDriver,
|
||||
timeoutMS);
|
||||
|
||||
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);
|
||||
return foundElements ?? new ReadOnlyCollection<T>(new List<T>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,42 +15,22 @@ 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 SessionHelper sessionHelper;
|
||||
|
||||
private readonly PowerToysModule scope;
|
||||
private readonly TestInit testInit = new TestInit();
|
||||
|
||||
public UITestBase(PowerToysModule scope = PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
this.scope = scope;
|
||||
this.sessionHelper = new SessionHelper(scope).Init();
|
||||
this.Session = new Session(this.sessionHelper.GetRoot(), this.sessionHelper.GetDriver());
|
||||
this.testInit.SetScope(scope);
|
||||
this.testInit.Init();
|
||||
this.Session = new Session(this.testInit.GetRoot(), this.testInit.GetDriver());
|
||||
}
|
||||
|
||||
~UITestBase()
|
||||
{
|
||||
this.sessionHelper.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the test.
|
||||
/// </summary>
|
||||
[TestInitialize]
|
||||
public void TestInit()
|
||||
{
|
||||
if (this.scope == PowerToysModule.PowerToysSettings)
|
||||
{
|
||||
// close Debug warning dialog if any
|
||||
// Such debug warning dialog seems only appear in PowerToys Settings
|
||||
if (this.FindAll("DEBUG").Count > 0)
|
||||
{
|
||||
this.Find("DEBUG").Find<Button>("Close").Click();
|
||||
}
|
||||
}
|
||||
this.testInit.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -67,41 +47,6 @@ namespace Microsoft.PowerToys.UITest
|
||||
return this.Session.Find<T>(by, timeoutMS);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
@@ -117,41 +62,93 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds all elements by selector.
|
||||
/// Shortcut for this.Session.FindAll<Element>(By.Name(name), timeoutMS)
|
||||
/// Nested class for test initialization.
|
||||
/// </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()
|
||||
private sealed class TestInit
|
||||
{
|
||||
return this.Session.FindAll<T>(By.Name(name), timeoutMS);
|
||||
}
|
||||
private WindowsDriver<WindowsElement> Root { get; set; }
|
||||
|
||||
/// <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);
|
||||
}
|
||||
private WindowsDriver<WindowsElement>? Driver { get; set; }
|
||||
|
||||
/// <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);
|
||||
private static Process? appDriver;
|
||||
|
||||
// Default session path is PowerToys settings dashboard
|
||||
private static string sessionPath = ModuleConfigData.Instance.GetModulePath(PowerToysModule.PowerToysSettings);
|
||||
|
||||
public TestInit()
|
||||
{
|
||||
appDriver = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
public WindowsDriver<WindowsElement> GetRoot() => this.Root;
|
||||
|
||||
public WindowsDriver<WindowsElement> GetDriver()
|
||||
{
|
||||
Assert.IsNotNull(this.Driver, $"Failed to get driver. Driver is null.");
|
||||
return this.Driver;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.winget](./configuration.winget)
|
||||
### [configuration.dsc.yaml](./configuration.dsc.yaml)
|
||||
|
||||
Sample configuration file that changes the enabled state of some modules, changes an integer setting and a hotkey setting.
|
||||
|
||||
### [installAndConfiguration.winget](./installAndConfiguration.winget)
|
||||
### [installAndConfiguration.dsc.yaml](./installAndConfiguration.dsc.yaml)
|
||||
|
||||
Installs PowerToys and applies configuration.winget to it.
|
||||
Installs PowerToys and applies configuration.dsc.yaml to it.
|
||||
|
||||
### [enableAllModules.winget](./enableAllModules.winget)
|
||||
### [enableAllModules.dsc.yaml](./enableAllModules.dsc.yaml)
|
||||
|
||||
Enables all PowerToys utilities.
|
||||
|
||||
### [disableAllModules.winget](./disableAllModules.winget)
|
||||
### [disableAllModules.dsc.yaml](./disableAllModules.dsc.yaml)
|
||||
|
||||
Disables all PowerToys utilities.
|
||||
|
||||
### [configureLauncherPlugins.winget](./configureLauncherPlugins.winget)
|
||||
### [configureLauncherPlugins.dsc.yaml](./configureLauncherPlugins.dsc.yaml)
|
||||
|
||||
Enables PowerToys Run and all its plugins and changes action keyword and global state for the Program plugin.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models.KernelQueryCache;
|
||||
|
||||
@@ -8,7 +8,6 @@ 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;
|
||||
@@ -132,18 +131,17 @@ 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, CancellationToken.None, progress));
|
||||
return DataPackageHelpers.CreateFromText(await customTextTransformService.TransformTextAsync(batchTestInput.Prompt, batchTestInput.Clipboard));
|
||||
|
||||
case PasteFormats.KernelQuery:
|
||||
var clipboardData = DataPackageHelpers.CreateFromText(batchTestInput.Clipboard).GetView();
|
||||
KernelService kernelService = new(new NoOpKernelQueryCacheService(), credentialsProvider, promptModerationService, customTextTransformService);
|
||||
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false, CancellationToken.None, progress);
|
||||
return await kernelService.TransformClipboardAsync(batchTestInput.Prompt, clipboardData, isSavedQuery: false);
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected format {format}");
|
||||
|
||||
@@ -6,7 +6,6 @@ using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -131,7 +130,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, CancellationToken.None, new NoOpProgress());
|
||||
var output = await _kernelService.TransformClipboardAsync(prompt, input.GetView(), isSavedQuery: false);
|
||||
|
||||
Assert.AreEqual(1, _eventListener.SemanticKernelEvents.Count);
|
||||
Assert.IsTrue(_eventListener.SemanticKernelTokens > 0);
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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));
|
||||
}
|
||||
@@ -28,7 +28,6 @@
|
||||
Background="Transparent"
|
||||
BorderThickness="4"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed">
|
||||
<!-- CornerRadius needs to be > 0 -->
|
||||
<Grid.BorderBrush>
|
||||
|
||||
@@ -178,36 +178,17 @@
|
||||
Padding="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<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>
|
||||
<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>
|
||||
</Viewbox>
|
||||
<ScrollViewer
|
||||
@@ -591,24 +572,6 @@
|
||||
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=,
|
||||
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"
|
||||
@@ -716,10 +679,6 @@
|
||||
<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>
|
||||
|
||||
@@ -55,9 +55,9 @@ namespace AdvancedPaste.Controls
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName is nameof(ViewModel.IsBusy) or nameof(ViewModel.PasteActionError))
|
||||
if (e.PropertyName == nameof(ViewModel.Busy) || e.PropertyName == nameof(ViewModel.PasteActionError))
|
||||
{
|
||||
var state = ViewModel.IsBusy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
|
||||
var state = ViewModel.Busy ? "LoadingState" : ViewModel.PasteActionError.HasText ? "ErrorState" : "DefaultState";
|
||||
VisualStateManager.GoToState(this, state, true);
|
||||
}
|
||||
}
|
||||
@@ -78,9 +78,6 @@ 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)
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace AdvancedPaste
|
||||
{
|
||||
private readonly WindowMessageMonitor _msgMonitor;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private readonly OptionsViewModel _optionsViewModel;
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
@@ -33,7 +32,8 @@ namespace AdvancedPaste
|
||||
InitializeComponent();
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
_optionsViewModel = App.GetService<OptionsViewModel>();
|
||||
|
||||
var 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,9 +111,8 @@ namespace AdvancedPaste
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private async void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
{
|
||||
await _optionsViewModel.CancelPasteActionAsync();
|
||||
Hide();
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
@@ -4,14 +4,10 @@
|
||||
|
||||
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;
|
||||
@@ -22,6 +18,8 @@ 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),
|
||||
@@ -29,14 +27,6 @@ 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();
|
||||
@@ -67,12 +57,9 @@ internal static class DataPackageHelpers
|
||||
{
|
||||
availableFormats |= ClipboardFormat.File;
|
||||
|
||||
foreach (var (format, fileTypes) in SupportedFileTypes.Value)
|
||||
if (ImageFileTypes.Value.Contains(file.FileType))
|
||||
{
|
||||
if (fileTypes.Contains(file.FileType))
|
||||
{
|
||||
availableFormats |= format;
|
||||
}
|
||||
availableFormats |= ClipboardFormat.Image;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,60 +93,6 @@ 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;
|
||||
|
||||
@@ -220,27 +153,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
@@ -18,8 +17,6 @@ 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)
|
||||
{
|
||||
@@ -43,14 +40,6 @@ 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;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace AdvancedPaste.Helpers
|
||||
{
|
||||
@@ -84,68 +83,6 @@ 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);
|
||||
|
||||
@@ -163,8 +100,5 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Windows.Globalization;
|
||||
@@ -16,14 +15,11 @@ namespace AdvancedPaste.Helpers;
|
||||
|
||||
public static class OcrHelpers
|
||||
{
|
||||
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
|
||||
public static async Task<string> ExtractTextAsync(SoftwareBitmap bitmap)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
@@ -18,19 +17,17 @@ namespace AdvancedPaste.Helpers;
|
||||
|
||||
public static class TransformHelpers
|
||||
{
|
||||
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public static async Task<DataPackage> TransformAsync(PasteFormats format, DataPackageView clipboardData)
|
||||
{
|
||||
return format switch
|
||||
{
|
||||
PasteFormats.PlainText => await ToPlainTextAsync(clipboardData),
|
||||
PasteFormats.Markdown => await ToMarkdownAsync(clipboardData),
|
||||
PasteFormats.Json => await ToJsonAsync(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.ImageToText => await ImageToTextAsync(clipboardData),
|
||||
PasteFormats.PasteAsTxtFile => await ToTxtFileAsync(clipboardData),
|
||||
PasteFormats.PasteAsPngFile => await ToPngFileAsync(clipboardData),
|
||||
PasteFormats.PasteAsHtmlFile => await ToHtmlFileAsync(clipboardData),
|
||||
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)),
|
||||
@@ -55,16 +52,16 @@ public static class TransformHelpers
|
||||
return CreateDataPackageFromText(await JsonHelper.ToJsonFromXmlOrCsvAsync(clipboardData));
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
private static async Task<DataPackage> ImageToTextAsync(DataPackageView clipboardData)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var bitmap = await clipboardData.GetImageContentAsync() ?? throw new ArgumentException("No image content found in clipboard", nameof(clipboardData));
|
||||
var text = await OcrHelpers.ExtractTextAsync(bitmap, cancellationToken);
|
||||
var text = await OcrHelpers.ExtractTextAsync(bitmap);
|
||||
return CreateDataPackageFromText(text);
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
private static async Task<DataPackage> ToPngFileAsync(DataPackageView clipboardData)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
@@ -75,25 +72,25 @@ public static class TransformHelpers
|
||||
encoder.SetSoftwareBitmap(clipboardBitmap);
|
||||
await encoder.FlushAsync();
|
||||
|
||||
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png", cancellationToken);
|
||||
return await CreateDataPackageFromFileContentAsync(pngStream.AsStreamForRead(), "png");
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
private static async Task<DataPackage> ToTxtFileAsync(DataPackageView clipboardData)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var text = await clipboardData.GetTextOrHtmlTextAsync();
|
||||
return await CreateDataPackageFromFileContentAsync(text, "txt", cancellationToken);
|
||||
return await CreateDataPackageFromFileContentAsync(text, "txt");
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData, CancellationToken cancellationToken)
|
||||
private static async Task<DataPackage> ToHtmlFileAsync(DataPackageView clipboardData)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var cfHtml = await clipboardData.GetHtmlContentAsync();
|
||||
var html = RemoveHtmlMetadata(cfHtml);
|
||||
|
||||
return await CreateDataPackageFromFileContentAsync(html, "html", cancellationToken);
|
||||
return await CreateDataPackageFromFileContentAsync(html, "html");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -117,7 +114,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, CancellationToken cancellationToken)
|
||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(string data, string fileExtension)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
@@ -126,16 +123,16 @@ public static class TransformHelpers
|
||||
|
||||
var path = GetPasteAsFileTempFilePath(fileExtension);
|
||||
|
||||
await File.WriteAllTextAsync(path, data, cancellationToken);
|
||||
await File.WriteAllTextAsync(path, data);
|
||||
return await DataPackageHelpers.CreateFromFileAsync(path);
|
||||
}
|
||||
|
||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension, CancellationToken cancellationToken)
|
||||
private static async Task<DataPackage> CreateDataPackageFromFileContentAsync(Stream stream, string fileExtension)
|
||||
{
|
||||
var path = GetPasteAsFileTempFilePath(fileExtension);
|
||||
|
||||
using var fileStream = File.Create(path);
|
||||
await stream.CopyToAsync(fileStream, cancellationToken);
|
||||
await stream.CopyToAsync(fileStream);
|
||||
|
||||
return await DataPackageHelpers.CreateFromFileAsync(path);
|
||||
}
|
||||
|
||||
@@ -108,9 +108,7 @@ 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.TranscodeToMp3, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp3]),
|
||||
(PasteFormats.TranscodeToMp4, [sourceAdditionalActions.Transcode, sourceAdditionalActions.Transcode.TranscodeToMp4]),
|
||||
(PasteFormats.PasteAsHtmlFile, [sourceAdditionalActions.PasteAsFile, sourceAdditionalActions.PasteAsFile.PasteAsHtmlFile])
|
||||
];
|
||||
|
||||
_additionalActions.Clear();
|
||||
|
||||
@@ -13,7 +13,6 @@ public enum ClipboardFormat
|
||||
Text = 1 << 0,
|
||||
Html = 1 << 1,
|
||||
Audio = 1 << 2,
|
||||
Video = 1 << 3,
|
||||
Image = 1 << 4,
|
||||
File = 1 << 5, // output only for now
|
||||
Image = 1 << 3,
|
||||
File = 1 << 4, // output only for now
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class PasteActionError
|
||||
public static PasteActionError FromException(Exception ex) =>
|
||||
new()
|
||||
{
|
||||
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString(ex is OperationCanceledException ? "PasteActionCanceled" : "PasteError"),
|
||||
Text = ex is PasteActionException ? ex.Message : ResourceLoaderInstance.ResourceLoader.GetString("PasteError"),
|
||||
Details = (ex as PasteActionException)?.AIServiceMessage ?? string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,34 +82,12 @@ 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.Video | ClipboardFormat.Image,
|
||||
SupportedClipboardFormats = ClipboardFormat.Text | ClipboardFormat.Html | ClipboardFormat.Audio | ClipboardFormat.Image,
|
||||
RequiresPrompt = true)]
|
||||
KernelQuery,
|
||||
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
// 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, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
Task<string> TransformTextAsync(string prompt, string inputText);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
@@ -12,5 +10,5 @@ namespace AdvancedPaste.Services;
|
||||
|
||||
public interface IKernelService
|
||||
{
|
||||
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Models;
|
||||
@@ -13,5 +11,5 @@ namespace AdvancedPaste.Services;
|
||||
|
||||
public interface IPasteFormatExecutor
|
||||
{
|
||||
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source, CancellationToken cancellationToken, IProgress<double> progress);
|
||||
Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
// 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, CancellationToken cancellationToken);
|
||||
Task ValidateAsync(string fullPrompt);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -37,14 +36,12 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
|
||||
protected abstract AIServiceUsage GetAIServiceUsage(ChatMessageContent chatMessage);
|
||||
|
||||
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task<DataPackage> TransformClipboardAsync(string prompt, DataPackageView clipboardData, bool isSavedQuery)
|
||||
{
|
||||
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);
|
||||
@@ -54,7 +51,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
|
||||
try
|
||||
{
|
||||
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt, cancellationToken);
|
||||
(chatHistory, var usage) = cacheUsed ? await ExecuteCachedActionChain(kernel, maybeCacheValue.ActionChain) : await ExecuteAICompletion(kernel, prompt);
|
||||
|
||||
LogResult(cacheUsed, isSavedQuery, kernel.GetOrAddActionChain(), usage);
|
||||
|
||||
@@ -87,7 +84,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 or OperationCanceledException)
|
||||
if (ex is PasteActionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
@@ -130,7 +127,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, CancellationToken cancellationToken)
|
||||
private async Task<(ChatHistory ChatHistory, AIServiceUsage Usage)> ExecuteAICompletion(Kernel kernel, string prompt)
|
||||
{
|
||||
ChatHistory chatHistory = [];
|
||||
|
||||
@@ -144,10 +141,10 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
chatHistory.AddSystemMessage($"Available clipboard formats: {await kernel.GetDataFormatsAsync()}");
|
||||
chatHistory.AddUserMessage(prompt);
|
||||
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory), cancellationToken);
|
||||
await _promptModerationService.ValidateAsync(GetFullPrompt(chatHistory));
|
||||
|
||||
var chatResult = await kernel.GetRequiredService<IChatCompletionService>()
|
||||
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel, cancellationToken);
|
||||
.GetChatMessageContentAsync(chatHistory, PromptExecutionSettings, kernel);
|
||||
chatHistory.Add(chatResult);
|
||||
|
||||
var totalUsage = chatHistory.Select(GetAIServiceUsage)
|
||||
@@ -160,8 +157,6 @@ 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]);
|
||||
@@ -213,14 +208,14 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
async dataPackageView =>
|
||||
{
|
||||
var input = await dataPackageView.GetTextAsync();
|
||||
string output = await GetPromptBasedOutput(format, prompt, input, kernel.GetCancellationToken(), kernel.GetProgress());
|
||||
string output = await GetPromptBasedOutput(format, prompt, input);
|
||||
return DataPackageHelpers.CreateFromText(output);
|
||||
});
|
||||
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input, CancellationToken cancellationToken, IProgress<double> progress) =>
|
||||
private async Task<string> GetPromptBasedOutput(PasteFormats format, string prompt, string input) =>
|
||||
format switch
|
||||
{
|
||||
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => await _customTextTransformService.TransformTextAsync(prompt, input),
|
||||
_ => throw new ArgumentException($"Unsupported format {format} for prompt transform", nameof(format)),
|
||||
};
|
||||
|
||||
@@ -228,7 +223,7 @@ public abstract class KernelServiceBase(IKernelQueryCacheService queryCacheServi
|
||||
ExecuteTransformAsync(
|
||||
kernel,
|
||||
new ActionChainItem(format, Arguments: []),
|
||||
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView, kernel.GetCancellationToken(), kernel.GetProgress()));
|
||||
async dataPackageView => await TransformHelpers.TransformAsync(format, dataPackageView));
|
||||
|
||||
private static async Task<string> ExecuteTransformAsync(Kernel kernel, ActionChainItem actionChainItem, Func<DataPackageView, Task<DataPackage>> transformFunc)
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -24,11 +23,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, CancellationToken cancellationToken)
|
||||
private async Task<Completions> GetAICompletionAsync(string systemInstructions, string userMessage)
|
||||
{
|
||||
var fullPrompt = systemInstructions + "\n\n" + userMessage;
|
||||
|
||||
await _promptModerationService.ValidateAsync(fullPrompt, cancellationToken);
|
||||
await _promptModerationService.ValidateAsync(fullPrompt);
|
||||
|
||||
OpenAIClient azureAIClient = new(_aiCredentialsProvider.Key);
|
||||
|
||||
@@ -42,8 +41,7 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
||||
},
|
||||
Temperature = 0.01F,
|
||||
MaxTokens = 2000,
|
||||
},
|
||||
cancellationToken);
|
||||
});
|
||||
|
||||
if (response.Value.Choices[0].FinishReason == "length")
|
||||
{
|
||||
@@ -53,7 +51,7 @@ public sealed class CustomTextTransformService(IAICredentialsProvider aiCredenti
|
||||
return response;
|
||||
}
|
||||
|
||||
public async Task<string> TransformTextAsync(string prompt, string inputText, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task<string> TransformTextAsync(string prompt, string inputText)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prompt))
|
||||
{
|
||||
@@ -82,7 +80,7 @@ Output:
|
||||
|
||||
try
|
||||
{
|
||||
var response = await GetAICompletionAsync(systemInstructions, userMessage, cancellationToken);
|
||||
var response = await GetAICompletionAsync(systemInstructions, userMessage);
|
||||
|
||||
var usage = response.Usage;
|
||||
AdvancedPasteGenerateCustomFormatEvent telemetryEvent = new(usage.PromptTokens, usage.CompletionTokens, ModelName);
|
||||
@@ -100,7 +98,7 @@ Output:
|
||||
AdvancedPasteGenerateCustomErrorEvent errorEvent = new(ex is PasteActionModeratedException ? PasteActionModeratedException.ErrorDescription : ex.Message);
|
||||
PowerToysTelemetry.Log.WriteEvent(errorEvent);
|
||||
|
||||
if (ex is PasteActionException or OperationCanceledException)
|
||||
if (ex is PasteActionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ClientModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -19,12 +18,12 @@ public sealed class PromptModerationService(IAICredentialsProvider aiCredentials
|
||||
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider = aiCredentialsProvider;
|
||||
|
||||
public async Task ValidateAsync(string fullPrompt, CancellationToken cancellationToken)
|
||||
public async Task ValidateAsync(string fullPrompt)
|
||||
{
|
||||
try
|
||||
{
|
||||
ModerationClient moderationClient = new(ModelName, _aiCredentialsProvider.Key);
|
||||
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt, cancellationToken);
|
||||
var moderationClientResult = await moderationClient.ClassifyTextAsync(fullPrompt);
|
||||
var moderationResult = moderationClientResult.Value;
|
||||
|
||||
Logger.LogDebug($"{nameof(PromptModerationService)}.{nameof(ValidateAsync)} complete; {nameof(moderationResult.Flagged)}={moderationResult.Flagged}");
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -18,7 +17,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, CancellationToken cancellationToken, IProgress<double> progress)
|
||||
public async Task<DataPackage> ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
||||
{
|
||||
if (!pasteFormat.IsEnabled)
|
||||
{
|
||||
@@ -35,9 +34,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, cancellationToken, progress),
|
||||
PasteFormats.CustomTextTransformation => DataPackageHelpers.CreateFromText(await _customTextTransformService.TransformTextAsync(pasteFormat.Prompt, await clipboardData.GetTextAsync(), cancellationToken, progress)),
|
||||
_ => await TransformHelpers.TransformAsync(format, clipboardData, cancellationToken, progress),
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -135,9 +135,6 @@
|
||||
<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>
|
||||
@@ -191,19 +188,7 @@
|
||||
</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>
|
||||
@@ -222,9 +207,6 @@
|
||||
<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>
|
||||
@@ -234,9 +216,6 @@
|
||||
<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>
|
||||
|
||||
@@ -8,8 +8,6 @@ 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;
|
||||
@@ -31,7 +29,7 @@ using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
|
||||
namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
public sealed partial class OptionsViewModel : ObservableObject, IProgress<double>, IDisposable
|
||||
public sealed partial class OptionsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly DispatcherTimer _clipboardTimer;
|
||||
@@ -39,8 +37,6 @@ namespace AdvancedPaste.ViewModels
|
||||
private readonly IPasteFormatExecutor _pasteFormatExecutor;
|
||||
private readonly IAICredentialsProvider _aiCredentialsProvider;
|
||||
|
||||
private CancellationTokenSource _pasteActionCancellationTokenSource;
|
||||
|
||||
public DataPackageView ClipboardData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -69,11 +65,7 @@ namespace AdvancedPaste.ViewModels
|
||||
private bool _pasteFormatsDirty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isBusy;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasIndeterminateTransformProgress))]
|
||||
private double _transformProgress = double.NaN;
|
||||
private bool _busy;
|
||||
|
||||
public ObservableCollection<PasteFormat> StandardPasteFormats { get; } = [];
|
||||
|
||||
@@ -89,24 +81,9 @@ 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
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetMainWindow()?.Visible is true;
|
||||
}
|
||||
catch (COMException)
|
||||
{
|
||||
return false; // window is closed
|
||||
}
|
||||
}
|
||||
}
|
||||
private bool Visible => GetMainWindow()?.Visible is true;
|
||||
|
||||
public event EventHandler PreviewRequested;
|
||||
|
||||
@@ -212,12 +189,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
void UpdateFormats(ObservableCollection<PasteFormat> collection, IEnumerable<PasteFormat> pasteFormats)
|
||||
{
|
||||
// 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);
|
||||
}
|
||||
collection.Clear();
|
||||
|
||||
foreach (var format in FilterAndSort(pasteFormats))
|
||||
{
|
||||
@@ -242,13 +214,12 @@ namespace AdvancedPaste.ViewModels
|
||||
public void Dispose()
|
||||
{
|
||||
_clipboardTimer.Stop();
|
||||
_pasteActionCancellationTokenSource?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task ReadClipboardAsync()
|
||||
{
|
||||
if (IsBusy)
|
||||
if (Busy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -353,10 +324,6 @@ 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
|
||||
@@ -395,7 +362,7 @@ namespace AdvancedPaste.ViewModels
|
||||
|
||||
internal async Task ExecutePasteFormatAsync(PasteFormat pasteFormat, PasteActionSource source)
|
||||
{
|
||||
if (IsBusy)
|
||||
if (Busy)
|
||||
{
|
||||
Logger.LogWarning($"Execution of {pasteFormat.Format} from {source} suppressed as busy");
|
||||
return;
|
||||
@@ -410,18 +377,16 @@ namespace AdvancedPaste.ViewModels
|
||||
var elapsedWatch = Stopwatch.StartNew();
|
||||
Logger.LogDebug($"Started executing {pasteFormat.Format} from source {source}");
|
||||
|
||||
IsBusy = true;
|
||||
_pasteActionCancellationTokenSource = new();
|
||||
TransformProgress = double.NaN;
|
||||
Busy = true;
|
||||
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(1.5);
|
||||
var aiActionMinTaskTime = TimeSpan.FromSeconds(2);
|
||||
var delayTask = (Visible && source == PasteActionSource.GlobalKeyboardShortcut) ? Task.Delay(aiActionMinTaskTime) : Task.CompletedTask;
|
||||
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source, _pasteActionCancellationTokenSource.Token, this);
|
||||
var dataPackage = await _pasteFormatExecutor.ExecutePasteFormatAsync(pasteFormat, source);
|
||||
|
||||
await delayTask;
|
||||
|
||||
@@ -445,9 +410,7 @@ namespace AdvancedPaste.ViewModels
|
||||
PasteActionError = PasteActionError.FromException(ex);
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
_pasteActionCancellationTokenSource?.Dispose();
|
||||
_pasteActionCancellationTokenSource = null;
|
||||
Busy = false;
|
||||
elapsedWatch.Stop();
|
||||
Logger.LogDebug($"Finished executing {pasteFormat.Format} from source {source}; timeTakenMs={elapsedWatch.ElapsedMilliseconds}");
|
||||
}
|
||||
@@ -521,26 +484,5 @@ 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +88,6 @@ 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"TranscodeToMp3"), "TranscodeToMp3Hotkey"),
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"TranscodeToMp4"), "TranscodeToMp4Hotkey")
|
||||
TraceLoggingWideString(getAdditionalActionHotkeyCStr(L"PasteAsHtmlFile"), "PasteAsHtmlFileHotkey")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerToys.UITest;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@@ -18,18 +18,8 @@ namespace Hosts.UITests
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Test if Empty-view is shown when no entries are present.
|
||||
/// And 'Add an entry' button from Empty-view is functional.
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestEmptyView()
|
||||
@@ -38,25 +28,20 @@ namespace Hosts.UITests
|
||||
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");
|
||||
Assert.IsTrue(this.FindAll<Button>(By.Name("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.Find<Button>(By.Name("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");
|
||||
Assert.IsTrue(this.FindAll<Button>(By.Name("Delete")).Count == 1, "Should have one row now");
|
||||
Assert.IsTrue(this.FindAll<Button>(By.Name("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>
|
||||
/// Test if 'New entry' button is functional
|
||||
/// </summary>
|
||||
[TestMethod]
|
||||
public void TestAddingEntry()
|
||||
@@ -64,155 +49,11 @@ namespace Hosts.UITests
|
||||
this.CloseWarningDialog();
|
||||
this.RemoveAllEntries();
|
||||
|
||||
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0, "Should have no row after removing all");
|
||||
Assert.IsTrue(this.FindAll<Button>(By.Name("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");
|
||||
Assert.IsTrue(this.FindAll<Button>(By.Name("Delete")).Count == 1, "Should have one row now");
|
||||
}
|
||||
|
||||
private void AddEntry(string ip, string host, bool active = true, bool clickAddEntryButton = true)
|
||||
@@ -220,21 +61,21 @@ namespace Hosts.UITests
|
||||
if (clickAddEntryButton)
|
||||
{
|
||||
// Click 'Add an entry' for adding Host override rule
|
||||
this.Find<Button>("New entry").Click();
|
||||
this.Find<Button>(By.Name("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.IsFalse(this.Find<Button>(By.Name("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);
|
||||
Assert.IsTrue(this.Find<TextBox>(By.Name("Address")).SetText(ip, false).Text == ip);
|
||||
Assert.IsTrue(this.Find<TextBox>(By.Name("Hosts")).SetText(host, false).Text == host);
|
||||
|
||||
this.Find<ToggleSwitch>("Active").Toggle(active);
|
||||
this.Find<ToggleSwitch>(By.Name("Active")).Toggle(active);
|
||||
|
||||
Assert.IsTrue(this.Find<Button>("Add").Enabled, "Add button should be Enabled after providing valid inputs");
|
||||
Assert.IsTrue(this.Find<Button>(By.Name("Add")).Enabled, "Add button should be Enabled after providing valid inputs");
|
||||
|
||||
// Add the entry
|
||||
this.Find<Button>("Add").Click();
|
||||
this.Find<Button>(By.Name("Add")).Click();
|
||||
|
||||
// 0.5 second delay after adding an entry
|
||||
Task.Delay(500).Wait();
|
||||
@@ -243,25 +84,25 @@ namespace Hosts.UITests
|
||||
private void CloseWarningDialog()
|
||||
{
|
||||
// Find 'Accept' button which come in 'Warning' dialog
|
||||
if (this.FindAll("Warning").Count > 0 &&
|
||||
this.FindAll<Button>("Accept").Count > 0)
|
||||
if (this.FindAll<Window>(By.Name("Warning")).Count > 0 &&
|
||||
this.FindAll<Button>(By.Name("Accept")).Count > 0)
|
||||
{
|
||||
// Hide Warning dialog if any
|
||||
this.Find<Button>("Accept").Click();
|
||||
this.Find<Button>(By.Name("Accept")).Click();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAllEntries()
|
||||
{
|
||||
// Delete all existing host-override rules
|
||||
foreach (var deleteBtn in this.FindAll<Button>("Delete"))
|
||||
foreach (var deleteBtn in this.FindAll<Button>(By.Name("Delete")))
|
||||
{
|
||||
deleteBtn.Click();
|
||||
this.Find<Button>("Yes").Click();
|
||||
this.Find<Button>(By.Name("Yes")).Click();
|
||||
}
|
||||
|
||||
// Should have no row left, and no more delete button
|
||||
Assert.IsTrue(this.FindAll<Button>("Delete").Count == 0);
|
||||
Assert.IsTrue(this.FindAll<Button>(By.Name("Delete")).Count == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,4 @@
|
||||
<PackageReference Include="MSTest" />
|
||||
<ProjectReference Include="..\..\..\common\UITestAutomation\UITestAutomation.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -1,129 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
## 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**)
|
||||
@@ -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|arm64)__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
Regex packagedAppPathRegex = new Regex(@"(?<APPID>[^_]*)_\d+.\d+.\d+.\d+_x64__(?<PublisherID>[^\\]*)", RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
|
||||
Match match = packagedAppPathRegex.Match(appPath);
|
||||
_isPackagedApp = match.Success;
|
||||
if (match.Success)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -230,7 +230,7 @@ namespace ColorPicker.Helpers
|
||||
|
||||
public bool HandleEnterPressed()
|
||||
{
|
||||
if (!_colorPickerShown)
|
||||
if (!IsColorPickerVisible())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -241,13 +241,14 @@ namespace ColorPicker.Helpers
|
||||
|
||||
public bool HandleEscPressed()
|
||||
{
|
||||
if (!BlockEscapeKeyClosingColorPickerEditor
|
||||
&& (_colorPickerShown || (_colorEditorWindow != null && _colorEditorWindow.IsActive)))
|
||||
if (!BlockEscapeKeyClosingColorPickerEditor)
|
||||
{
|
||||
return EndUserSession();
|
||||
}
|
||||
|
||||
return false;
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void MoveCursor(int xOffset, int yOffset)
|
||||
|
||||
@@ -70,10 +70,17 @@ 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
|
||||
)
|
||||
{
|
||||
e.Handled = _appStateHandler.HandleEscPressed();
|
||||
return;
|
||||
if (_appStateHandler.IsColorPickerVisible()
|
||||
|| !AppStateHandler.BlockEscapeKeyClosingColorPickerEditor
|
||||
)
|
||||
{
|
||||
e.Handled = _appStateHandler.EndUserSession();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((virtualCode == KeyInterop.VirtualKeyFromKey(Key.Space) || virtualCode == KeyInterop.VirtualKeyFromKey(Key.Enter)) && (e.KeyboardState == GlobalKeyboardHook.KeyboardState.KeyDown))
|
||||
|
||||
@@ -487,10 +487,10 @@ namespace FancyZonesEditor
|
||||
}
|
||||
}
|
||||
|
||||
model.Delete();
|
||||
App.FancyZonesEditorIO.SerializeAppliedLayouts();
|
||||
App.FancyZonesEditorIO.SerializeCustomLayouts();
|
||||
App.FancyZonesEditorIO.SerializeDefaultLayouts();
|
||||
model.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
<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>
|
||||
@@ -10,55 +10,29 @@ using System.Windows.Data;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
|
||||
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
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
/// <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
|
||||
[ValueConversion(typeof(double), typeof(string))]
|
||||
internal class AutoDoubleConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
double d => d switch
|
||||
{
|
||||
double.NaN => "0",
|
||||
0 => (string)parameter == "Auto" ? Resources.Input_Auto : "0",
|
||||
_ => d.ToString(culture),
|
||||
},
|
||||
var d = (double)value;
|
||||
|
||||
_ => "0",
|
||||
};
|
||||
return d != 0
|
||||
? d.ToString(culture)
|
||||
: (string)parameter == "Auto"
|
||||
? Resources.Input_Auto
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
/// <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
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
null or "" => 0,
|
||||
string text when double.TryParse(text, NumberStyles.Any, culture, out double result) => result,
|
||||
_ => 0,
|
||||
};
|
||||
var text = (string)value;
|
||||
|
||||
return !string.IsNullOrEmpty(text)
|
||||
? double.Parse(text, culture)
|
||||
: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
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:v="clr-namespace:ImageResizer.Views">
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
@@ -115,12 +114,8 @@
|
||||
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"
|
||||
@@ -148,12 +143,8 @@
|
||||
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"
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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,
|
||||
};
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using 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();
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#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
|
||||
@@ -34,9 +34,6 @@
|
||||
<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>
|
||||
@@ -221,7 +218,6 @@
|
||||
<ClInclude Include="framework.h" />
|
||||
<ClInclude Include="KeyboardManagerEditorLibraryWrapper.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
@@ -235,9 +231,6 @@
|
||||
<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>
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
<ClInclude Include="KeyboardManagerEditorLibraryWrapper.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="resource.h">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp">
|
||||
@@ -42,9 +39,4 @@
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ResourceCompile Include="KeyboardManagerEditorLibraryWrapper.rc">
|
||||
<Filter>Resource Files</Filter>
|
||||
</ResourceCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//{{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
|
||||
//////////////////////////////
|
||||
@@ -7,8 +7,6 @@
|
||||
<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>
|
||||
|
||||
@@ -136,25 +136,13 @@ 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 | STGM_SHARE_DENY_NONE | STGM_TRANSACTED);
|
||||
((IPersistFile)link).Load(path, STGM_READ);
|
||||
}
|
||||
catch (System.IO.FileNotFoundException ex)
|
||||
{
|
||||
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);
|
||||
Log.Exception("Path could not be retrieved", ex, GetType(), path);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[] { "ξ" },
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.Win32;
|
||||
using RegistryPreviewUILib;
|
||||
|
||||
namespace RegistryPreview.FuzzTests
|
||||
{
|
||||
public class FuzzTests
|
||||
{
|
||||
private const string REGISTRYHEADER4 = "regedit4";
|
||||
private const string REGISTRYHEADER5 = "windows registry editor version 5.00";
|
||||
private const string KEYIMAGE = "ms-appx:///Assets/RegistryPreview/folder32.png";
|
||||
private const string DELETEDKEYIMAGE = "ms-appx:///Assets/RegistryPreview/deleted-folder32.png";
|
||||
|
||||
// Case 1: Fuzz test for CheckKeyLineForBrackets
|
||||
public static void FuzzCheckKeyLineForBrackets(ReadOnlySpan<byte> input)
|
||||
{
|
||||
string registryLine;
|
||||
|
||||
// Simulate registry file content as filenameText
|
||||
var filenameText = GenerateRegistryHeader(input);
|
||||
|
||||
string[] registryLines = filenameText.Split("\r");
|
||||
|
||||
if (registryLines.Length <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// REG files have to start with one of two headers and it's case-insensitive
|
||||
// The header in the registry file is either REGISTRYHEADER4 or REGISTRYHEADER5
|
||||
registryLine = registryLines[0];
|
||||
|
||||
// Check if the registry header is valid
|
||||
if (!IsValidRegistryHeader(registryLine))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int index = 1;
|
||||
registryLine = registryLines[index]; // Extract content after the header
|
||||
|
||||
ParseHelper.ProcessRegistryLine(registryLine);
|
||||
if (registryLine.StartsWith("[-", StringComparison.InvariantCulture))
|
||||
{
|
||||
// remove the - as we won't need it but it will get special treatment in the UI
|
||||
registryLine = registryLine.Remove(1, 1);
|
||||
|
||||
string imageName = DELETEDKEYIMAGE;
|
||||
|
||||
// Fuzz test for the CheckKeyLineForBrackets method
|
||||
ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
}
|
||||
else if (registryLine.StartsWith('['))
|
||||
{
|
||||
string imageName = KEYIMAGE;
|
||||
|
||||
// Fuzz test for the CheckKeyLineForBrackets method
|
||||
ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 1: Fuzz test for StripFirstAndLast
|
||||
public static void FuzzStripFirstAndLast(ReadOnlySpan<byte> input)
|
||||
{
|
||||
string registryLine;
|
||||
|
||||
var filenameText = GenerateRegistryHeader(input);
|
||||
|
||||
filenameText = filenameText.Replace("\r\n", "\r");
|
||||
string[] registryLines = filenameText.Split("\r");
|
||||
|
||||
if (registryLines.Length <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// REG files have to start with one of two headers and it's case-insensitive
|
||||
registryLine = registryLines[0];
|
||||
|
||||
if (!IsValidRegistryHeader(registryLine))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int index = 1;
|
||||
registryLine = registryLines[index];
|
||||
|
||||
ParseHelper.ProcessRegistryLine(registryLine);
|
||||
|
||||
if (registryLine.StartsWith("[-", StringComparison.InvariantCulture))
|
||||
{
|
||||
// remove the - as we won't need it but it will get special treatment in the UI
|
||||
registryLine = registryLine.Remove(1, 1);
|
||||
|
||||
string imageName = DELETEDKEYIMAGE;
|
||||
ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
|
||||
// Fuzz test for the StripFirstAndLast method
|
||||
registryLine = ParseHelper.StripFirstAndLast(registryLine);
|
||||
}
|
||||
else if (registryLine.StartsWith('['))
|
||||
{
|
||||
string imageName = KEYIMAGE;
|
||||
ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
|
||||
// Fuzz test for the StripFirstAndLast method
|
||||
registryLine = ParseHelper.StripFirstAndLast(registryLine);
|
||||
}
|
||||
else if (registryLine.StartsWith('"') && registryLine.EndsWith("=-", StringComparison.InvariantCulture))
|
||||
{
|
||||
registryLine = registryLine.Replace("=-", string.Empty);
|
||||
|
||||
// remove the "'s without removing all of them
|
||||
// Fuzz test for the StripFirstAndLast method
|
||||
registryLine = ParseHelper.StripFirstAndLast(registryLine);
|
||||
}
|
||||
else if (registryLine.StartsWith('"'))
|
||||
{
|
||||
int equal = registryLine.IndexOf('=');
|
||||
if ((equal < 0) || (equal > registryLine.Length - 1))
|
||||
{
|
||||
// something is very wrong
|
||||
return;
|
||||
}
|
||||
|
||||
// set the name and the value
|
||||
string name = registryLine.Substring(0, equal);
|
||||
|
||||
// trim the whitespace and quotes from the name
|
||||
name = name.Trim();
|
||||
|
||||
// Fuzz test for the StripFirstAndLast method
|
||||
name = ParseHelper.StripFirstAndLast(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GenerateRegistryHeader(ReadOnlySpan<byte> input)
|
||||
{
|
||||
string header = new Random().Next(2) == 0 ? REGISTRYHEADER4 : REGISTRYHEADER5;
|
||||
|
||||
string inputText = System.Text.Encoding.UTF8.GetString(input);
|
||||
string filenameText = header + "\r\n" + inputText;
|
||||
|
||||
return filenameText.Replace("\r\n", "\r");
|
||||
}
|
||||
|
||||
private static bool IsValidRegistryHeader(string line)
|
||||
{
|
||||
// Convert the line to lowercase once for comparison
|
||||
var lineLower = line.ToLowerInvariant();
|
||||
|
||||
switch (line)
|
||||
{
|
||||
case REGISTRYHEADER4:
|
||||
case REGISTRYHEADER5:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,4 @@
|
||||
// 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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.
|
||||
{
|
||||
"configVersion": 3,
|
||||
"entries": [
|
||||
{
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "RegistryPreview.FuzzTests.dll",
|
||||
"class": "RegistryPreview.FuzzTests.FuzzTests",
|
||||
"method": "FuzzCheckKeyLineForBrackets",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.RegistryPreview.dll"
|
||||
]
|
||||
},
|
||||
"adoTemplate": {
|
||||
// supply the values appropriate to your
|
||||
// project, where bugs will be filed
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
"skip": false,
|
||||
"rebootAfterSetup": false,
|
||||
"oneFuzzJobs": [
|
||||
// at least one job is required
|
||||
{
|
||||
"projectName": "RegistryPreview",
|
||||
"targetName": "RegistryPreview-dotnet-CheckKeyLineForBrackets-fuzzer"
|
||||
}
|
||||
],
|
||||
"jobDependencies": [
|
||||
// this should contain, at minimum,
|
||||
// the DLL and PDB files
|
||||
// you will need to add any other files required
|
||||
// (globs are supported)
|
||||
"RegistryPreview.FuzzTests.dll",
|
||||
"RegistryPreview.FuzzTests.pdb",
|
||||
"Microsoft.Windows.SDK.NET.dll",
|
||||
"WinRT.Runtime.dll"
|
||||
],
|
||||
"SdlWorkItemId": 49911822
|
||||
},
|
||||
{
|
||||
"fuzzer": {
|
||||
"$type": "libfuzzerDotNet",
|
||||
"dll": "RegistryPreview.FuzzTests.dll",
|
||||
"class": "RegistryPreview.FuzzTests.FuzzTests",
|
||||
"method": "FuzzStripFirstAndLast",
|
||||
"FuzzingTargetBinaries": [
|
||||
"PowerToys.RegistryPreview.dll"
|
||||
]
|
||||
},
|
||||
"adoTemplate": {
|
||||
// supply the values appropriate to your
|
||||
// project, where bugs will be filed
|
||||
"org": "microsoft",
|
||||
"project": "OS",
|
||||
"AssignedTo": "mengyuanchen@microsoft.com",
|
||||
"AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys",
|
||||
"IterationPath": "OS\\Future"
|
||||
},
|
||||
"jobNotificationEmail": "mengyuanchen@microsoft.com",
|
||||
"skip": false,
|
||||
"rebootAfterSetup": false,
|
||||
"oneFuzzJobs": [
|
||||
// at least one job is required
|
||||
{
|
||||
"projectName": "RegistryPreview",
|
||||
"targetName": "RegistryPreview-dotnet-StripFirstAndLasts-fuzzer"
|
||||
}
|
||||
],
|
||||
"jobDependencies": [
|
||||
// this should contain, at minimum,
|
||||
// the DLL and PDB files
|
||||
// you will need to add any other files required
|
||||
// (globs are supported)
|
||||
"RegistryPreview.FuzzTests.dll",
|
||||
"RegistryPreview.FuzzTests.pdb",
|
||||
"Microsoft.Windows.SDK.NET.dll",
|
||||
"WinRT.Runtime.dll"
|
||||
],
|
||||
"SdlWorkItemId": 49911822
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\tests\RegistryPreview.FuzzTests\</OutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\RegistryPreviewUILib\ParseHelper.cs" Link="ParseHelper.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="OneFuzzConfig.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MSTest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
112
src/modules/registrypreview/RegistryPreviewUILib/ParseHelper.cs
Normal file
112
src/modules/registrypreview/RegistryPreviewUILib/ParseHelper.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RegistryPreviewUILib
|
||||
{
|
||||
public class ParseHelper
|
||||
{
|
||||
private const string ERRORIMAGE = "ms-appx:///Assets/RegistryPreview/error32.png";
|
||||
|
||||
/// <summary>
|
||||
/// Checks a Key line for the closing bracket and treat it as an error if it cannot be found
|
||||
/// </summary>
|
||||
public static void CheckKeyLineForBrackets(ref string registryLine, ref string imageName)
|
||||
{
|
||||
// following the current behavior of the registry editor, find the last ] and treat everything else as ignorable
|
||||
int lastBracket = registryLine.LastIndexOf(']');
|
||||
if (lastBracket == -1)
|
||||
{
|
||||
// since we don't have a last bracket yet, add an extra space and continue processing
|
||||
registryLine += " ";
|
||||
imageName = ERRORIMAGE;
|
||||
}
|
||||
else
|
||||
{
|
||||
// having found the last ] and there is text after it, drop the rest of the string on the floor
|
||||
if (lastBracket < registryLine.Length - 1)
|
||||
{
|
||||
registryLine = registryLine.Substring(0, lastBracket + 1);
|
||||
}
|
||||
|
||||
if (CheckForKnownGoodBranches(registryLine) == false)
|
||||
{
|
||||
imageName = ERRORIMAGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make sure the root of a full path start with one of the five "hard coded" roots. Throw an error for the branch if it doesn't.
|
||||
/// </summary>
|
||||
private static bool CheckForKnownGoodBranches(string key)
|
||||
{
|
||||
if ((key.StartsWith("[HKEY_CLASSES_ROOT]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKEY_CURRENT_USER]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKEY_USERS]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKEY_LOCAL_MACHINE]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKEY_CURRENT_CONFIG]", StringComparison.InvariantCultureIgnoreCase) == false)
|
||||
&&
|
||||
(key.StartsWith(@"[HKEY_CLASSES_ROOT\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKEY_CURRENT_USER\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKEY_USERS\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKEY_LOCAL_MACHINE\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKEY_CURRENT_CONFIG\", StringComparison.InvariantCultureIgnoreCase) == false)
|
||||
&&
|
||||
(key.StartsWith("[HKCR]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKCU]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKU]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKLM]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKCC]", StringComparison.InvariantCultureIgnoreCase) == false)
|
||||
&&
|
||||
(key.StartsWith(@"[HKCR\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKCU\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKU\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKLM\", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith(@"[HKCC\", StringComparison.InvariantCultureIgnoreCase) == false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rip the first and last character off a string,
|
||||
/// checking that the string is at least 2 characters long to avoid errors
|
||||
/// </summary>
|
||||
public static string StripFirstAndLast(string line)
|
||||
{
|
||||
if (line.Length > 1)
|
||||
{
|
||||
line = line.Remove(line.Length - 1, 1);
|
||||
line = line.Remove(0, 1);
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
public static string ProcessRegistryLine(string registryLine)
|
||||
{
|
||||
if (registryLine.StartsWith("@=-", StringComparison.InvariantCulture))
|
||||
{
|
||||
// REG file has a callout to delete the @ Value which won't work *but* the Registry Editor will
|
||||
// clear the value of the @ Value instead, so it's still a valid line.
|
||||
registryLine = registryLine.Replace("@=-", "\"(Default)\"=\"\"");
|
||||
}
|
||||
else if (registryLine.StartsWith("@=", StringComparison.InvariantCulture))
|
||||
{
|
||||
// This is the Value called "(Default)" so we tweak the line for the UX
|
||||
registryLine = registryLine.Replace("@=", "\"(Default)\"=");
|
||||
}
|
||||
|
||||
return registryLine;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,9 @@ namespace RegistryPreviewUILib
|
||||
{
|
||||
// special case for when the registryLine begins with a @ - make some tweaks and
|
||||
// let the regular processing handle the rest.
|
||||
if (registryLine.StartsWith("@=-", StringComparison.InvariantCulture))
|
||||
registryLine = ParseHelper.ProcessRegistryLine(registryLine);
|
||||
|
||||
/* if (registryLine.StartsWith("@=-", StringComparison.InvariantCulture))
|
||||
{
|
||||
// REG file has a callout to delete the @ Value which won't work *but* the Registry Editor will
|
||||
// clear the value of the @ Value instead, so it's still a valid line.
|
||||
@@ -235,7 +237,7 @@ namespace RegistryPreviewUILib
|
||||
{
|
||||
// This is the Value called "(Default)" so we tweak the line for the UX
|
||||
registryLine = registryLine.Replace("@=", "\"(Default)\"=");
|
||||
}
|
||||
}*/
|
||||
|
||||
// continue until we have nothing left to read
|
||||
// switch logic, based off what the current line we're reading is
|
||||
@@ -245,10 +247,10 @@ namespace RegistryPreviewUILib
|
||||
registryLine = registryLine.Remove(1, 1);
|
||||
|
||||
string imageName = DELETEDKEYIMAGE;
|
||||
CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
|
||||
// this is a key, so remove the first [ and last ]
|
||||
registryLine = StripFirstAndLast(registryLine);
|
||||
registryLine = ParseHelper.StripFirstAndLast(registryLine);
|
||||
|
||||
// do not track the result of this node, since it should have no children
|
||||
AddTextToTree(registryLine, imageName);
|
||||
@@ -256,10 +258,10 @@ namespace RegistryPreviewUILib
|
||||
else if (registryLine.StartsWith('['))
|
||||
{
|
||||
string imageName = KEYIMAGE;
|
||||
CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
ParseHelper.CheckKeyLineForBrackets(ref registryLine, ref imageName);
|
||||
|
||||
// this is a key, so remove the first [ and last ]
|
||||
registryLine = StripFirstAndLast(registryLine);
|
||||
registryLine = ParseHelper.StripFirstAndLast(registryLine);
|
||||
|
||||
treeViewNode = AddTextToTree(registryLine, imageName);
|
||||
lastKeyPath = registryLine;
|
||||
@@ -270,7 +272,7 @@ namespace RegistryPreviewUILib
|
||||
registryLine = registryLine.Replace("=-", string.Empty);
|
||||
|
||||
// remove the "'s without removing all of them
|
||||
registryLine = StripFirstAndLast(registryLine);
|
||||
registryLine = ParseHelper.StripFirstAndLast(registryLine);
|
||||
|
||||
// Create a new listview item that will be used to display the delete value and store it
|
||||
registryValue = new RegistryValue(registryLine, string.Empty, string.Empty, lastKeyPath);
|
||||
@@ -300,7 +302,7 @@ namespace RegistryPreviewUILib
|
||||
|
||||
// trim the whitespace and quotes from the name
|
||||
name = name.Trim();
|
||||
name = StripFirstAndLast(name);
|
||||
name = ParseHelper.StripFirstAndLast(name);
|
||||
|
||||
// Clean out any escaped characters in the value, only for the preview
|
||||
name = StripEscapedCharacters(name);
|
||||
@@ -326,7 +328,7 @@ namespace RegistryPreviewUILib
|
||||
|
||||
if (value.StartsWith('"') && value.EndsWith('"'))
|
||||
{
|
||||
value = StripFirstAndLast(value);
|
||||
value = ParseHelper.StripFirstAndLast(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1038,7 +1040,7 @@ namespace RegistryPreviewUILib
|
||||
/// <summary>
|
||||
/// Checks a Key line for the closing bracket and treat it as an error if it cannot be found
|
||||
/// </summary>
|
||||
private void CheckKeyLineForBrackets(ref string registryLine, ref string imageName)
|
||||
/* private void CheckKeyLineForBrackets(ref string registryLine, ref string imageName)
|
||||
{
|
||||
// following the current behavior of the registry editor, find the last ] and treat everything else as ignorable
|
||||
int lastBracket = registryLine.LastIndexOf(']');
|
||||
@@ -1061,7 +1063,7 @@ namespace RegistryPreviewUILib
|
||||
imageName = ERRORIMAGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
/// <summary>
|
||||
/// Takes a binary registry value, sees if it has a ; and dumps the rest of the line - this does not work for REG_SZ values
|
||||
@@ -1082,7 +1084,7 @@ namespace RegistryPreviewUILib
|
||||
/// <summary>
|
||||
/// Make sure the root of a full path start with one of the five "hard coded" roots. Throw an error for the branch if it doesn't.
|
||||
/// </summary>
|
||||
private bool CheckForKnownGoodBranches(string key)
|
||||
/* private bool CheckForKnownGoodBranches(string key)
|
||||
{
|
||||
if ((key.StartsWith("[HKEY_CLASSES_ROOT]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
key.StartsWith("[HKEY_CURRENT_USER]", StringComparison.InvariantCultureIgnoreCase) == false &&
|
||||
@@ -1112,7 +1114,7 @@ namespace RegistryPreviewUILib
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} */
|
||||
|
||||
/// <summary>
|
||||
/// Turns the Open Key button in the command bar on/off, depending on if a key is selected
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
@@ -37,7 +36,4 @@ public sealed partial class AdvancedPasteAdditionalAction : Observable, IAdvance
|
||||
get => _isShown;
|
||||
set => Set(ref _isShown, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<IAdvancedPasteAction> SubActions => [];
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ 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)]
|
||||
@@ -23,22 +22,6 @@ public sealed class AdvancedPasteAdditionalActions
|
||||
[JsonPropertyName(PropertyNames.PasteAsFile)]
|
||||
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
[JsonIgnore]
|
||||
public IEnumerable<IAdvancedPasteAction> AllActions => new IAdvancedPasteAction[] { ImageToText, PasteAsFile }.Concat(PasteAsFile.SubActions);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user