Merge remote-tracking branch 'microsoft/main' into dev/feature/projects

This commit is contained in:
seraphima
2024-06-14 12:52:38 +02:00
205 changed files with 4323 additions and 1450 deletions

View File

@@ -39,6 +39,7 @@ nupkg
petabyte
resw
resx
srt
Stereolithography
terabyte
UYVY
@@ -127,6 +128,92 @@ XBUTTONDOWN
XBUTTONUP
XDOWN
# User32.SYSTEM_METRICS_INDEX.cs
CLEANBOOT
CMOUSEBUTTONS
CONVERTIBLESLATEMODE
CXBORDER
CXCURSOR
CXDLGFRAME
CXDLGFRAME
CXDOUBLECLK
CXDRAG
CXEDGE
CXFIXEDFRAME
CXFOCUSBORDER
CXFRAME
CXFRAME
CXFULLSCREEN
CXHSCROLL
CXHTHUMB
CXICON
CXICONSPACING
CXMAXIMIZED
CXMAXTRACK
CXMENUCHECK
CXMENUSIZE
CXMIN
CXMINIMIZED
CXMINSPACING
CXMINTRACK
CXPADDEDBORDER
CXSIZE
CXSIZEFRAME
CXSMSIZE
CXVSCROLL
CYBORDER
CYCAPTION
CYCURSOR
CYDLGFRAME
CYDLGFRAME
CYDOUBLECLK
CYDRAG
CYEDGE
CYFIXEDFRAME
CYFOCUSBORDER
CYFRAME
CYFRAME
CYFULLSCREEN
CYHSCROLL
CYICON
CYICONSPACING
CYKANJIWINDOW
CYMAXIMIZED
CYMAXTRACK
CYMENU
CYMENUCHECK
CYMENUSIZE
CYMIN
CYMINIMIZED
CYMINSPACING
CYMINTRACK
CYSIZE
CYSIZEFRAME
CYSMCAPTION
CYSMSIZE
CYVSCROLL
CYVTHUMB
DBCSENABLED
IMMENABLED
MAXIMUMTOUCHES
MEDIACENTER
MENUDROPALIGNMENT
MIDEASTENABLED
MOUSEHORIZONTALWHEELPRESENT
MOUSEPRESENT
MOUSEWHEELPRESENT
PENWINDOWS
REMOTECONTROL
REMOTESESSION
SAMEDISPLAYFORMA
SERVERR
SHOWSOUNDS
SHUTTINGDOWN
SLOWMACHINE
SWAPBUTTON
SYSTEMDOCKED
TABLETPC
# MATH

View File

@@ -33,10 +33,12 @@ Advaith
alekhyareddy
Aleks
angularsen
Anirudha
arjunbalgovind
Ashish
Baltazar
Bao
Bartosz
betadele
betsegaw
bricelam
@@ -51,6 +53,8 @@ crutkas
damienleroy
davidegiacometti
debian
Deibisu
Deibisu
Delimarsky
Deondre
DHowett
@@ -62,6 +66,7 @@ gabime
Galaxi
Garside
Gershaft
Giordani
Gokce
Guo
hanselman
@@ -70,12 +75,15 @@ Heiko
Hemmerlein
hlaueriksson
Horvalds
Howett
htcfreek
Huynh
Jaswal
jefflord
Jordi
jyuwono
Kairu
Kairu
Kamra
Kantarci
Karthick
@@ -92,7 +100,9 @@ martinmoene
Melman
Mikhayelyan
msft
Mykhailo
Myrvold
Naro
nathancartlidge
Nemeth
nielslaute
@@ -103,9 +113,13 @@ peteblois
phoboslab
Ponten
Pooja
Pylyp
quachpas
Quriz
randyrants
ricardosantos
riri
riri
ritchielawrence
robmikh
Rutkas
@@ -119,10 +133,12 @@ Seraphima
skttl
somil
Soref
Sosnowski
stefan
Szablewski
Tadele
talynone
Taras
TBM
tilovell
Triet
@@ -130,11 +146,9 @@ waaverecords
ycv
Yuniardi
yuyoyuppe
Zeol
Zoltan
Zykova
Kairu
Deibisu
riri
# OTHERS
@@ -169,4 +183,3 @@ xamlstyler
Xavalon
Xbox
Youdao

View File

@@ -125,6 +125,7 @@ bootstrapper
BOOTSTRAPPERINSTALLFOLDER
bostrot
BOTTOMALIGN
boxmodel
BPBF
bpmf
bpp
@@ -150,6 +151,7 @@ Cangjie
CANRENAME
CAPTUREBLT
CAPTURECHANGED
CARETBLINKING
CAtl
cch
CCHDEVICENAME
@@ -252,6 +254,7 @@ CREATESCHEDULEDTASK
CREATESTRUCT
CREATEWINDOWFAILED
CRECT
CRH
critsec
Crossdevice
CRSEL

View File

@@ -40,6 +40,9 @@
# tabs in c#
\$"\\t
# Hexadecimal character pattern in code
\\x[0-9a-fA-F][0-9a-fA-F]
# windows line breaks in strings
\\r\\n

View File

@@ -16,7 +16,7 @@ jobs:
run: |
release=$(curl https://api.github.com/repos/Microsoft/PowerToys/releases | jq '[.[]|select(.name | contains("Release"))][0]')
assets=$(jq -n "$release" | jq '.assets')
powerToysSetup=$(jq -n "$assets" | jq '[.[]|select(.name | contains("PowerToysSetup"))]')
powerToysSetup=$(jq -n "$assets" | jq '[.[]|select(.name | contains("PowerToysUserSetup"))]')
echo ::set-output name=powerToysInstallerX64Url::$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("x64"))][0].browser_download_url')
echo ::set-output name=powerToysInstallerArm64Url::$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("arm64"))][0].browser_download_url')

View File

@@ -60,7 +60,7 @@ jobs:
searchFolder: '$(Pipeline.Workspace)\build-${{ parameters.platform }}-${{ parameters.configuration }}'
vstestLocationMethod: 'location' # otherwise fails to find vstest.console.exe
#vstestLocation: '$(Agent.ToolsDirectory)\VsTest\**\${{ parameters.platform }}\tools\net462\Common7\IDE\Extensions\TestPlatform'
vstestLocation: '$(Agent.ToolsDirectory)\VsTest\17.10.0-release-24177-07\x64\tools\net462\Common7\IDE\Extensions\TestPlatform'
vstestLocation: '$(Agent.ToolsDirectory)\VsTest\17.10.0\x64\tools\net462\Common7\IDE\Extensions\TestPlatform'
uiTests: true
rerunFailedTests: true
testAssemblyVer2: |

View File

@@ -27,6 +27,9 @@ Heiko has helped triaging, discussing, and creating a substantial number of issu
### [@Jay-o-Way](https://github.com/Jay-o-Way) - Jay
Jay has helped triaging, discussing, creating a substantial number of issues and PRs.
### [@jefflord](https://github.com/Jjefflord) - Jeff Lord
Jeff added in multiple new features into Keyboard manager, such as key chord support and launching apps. He also contributed multiple features/fixes to PowerToys.
### [@TheJoeFin](https://github.com/TheJoeFin) - [Joe Finney](https://joefinapps.com)
Joe has helped triaging, discussing, issues as well as fixing bugs and building features for Text Extractor.
@@ -34,14 +37,12 @@ Joe has helped triaging, discussing, issues as well as fixing bugs and building
Helping keep our spelling correct :)
### [@martinchrzan](https://github.com/martinchrzan/) - Martin Chrzan
Color Picker is from Martin.
### [@mikeclayton](https://github.com/mikeclayton) - [Michael Clayton](https://michael-clayton.com)
Michael contributed the [initial version](https://github.com/microsoft/PowerToys/issues/23216) of the Mouse Jump tool and [a number of updates](https://github.com/microsoft/PowerToys/pulls?q=is%3Apr+author%3Amikeclayton) based on his FancyMouse utility.
### [@riverar](https://github.com/riverar) - [Rafael Rivera](https://withinrafael.com/)
Rafael has helped do the [upgrade from CppWinRT 1.x to 2.0](https://github.com/microsoft/PowerToys/issues/1907). He directly provided feedback to the CppWinRT team for bugs from this migration as well.
### [@royvou](https://github.com/royvou)
@@ -153,14 +154,25 @@ Other contributors:
## PowerToys core team
- [@crutkas](https://github.com/crutkas/) - Clint Rutkas - Lead
- [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Lead
- [@cinnamon-msft](https://github.com/cinnamon-msft) - Kayla Cinnamon - Product Manager
- [@ethanfangg](https://github.com/ethanfangg) - Ethan Fang - Product Manager
- [@plante-msft](https://github.com/plante-msft) - Connor Plante - Product Manager
- [@nguyen-dows](https://github.com/nguyen-dows) - Christopher Nguyen - Product Manager
- [@joadoumie](https://github.com/joadoumie) - Jordi Adoumie - Product Manager
- [@jaimecbernardo](https://github.com/jaimecbernardo) - Jaime Bernardo - Dev lead
- [@dhowett](https://github.com/dhowett) - Dustin Howett - Dev lead
- [@drawbyperpetual](https://github.com/drawbyperpetual) - Anirudha Shankar - Dev
- [@donlaci](https://github.com/donlaci) - Laszlo Nemeth - Dev
- [@gokcekantarci](https://github.com/gokcekantarci) - Gokce Kantarci - Dev
- [@SeraphimaZykova](https://github.com/SeraphimaZykova) - Seraphima Zykova - Dev
- [@stefansjfw](https://github.com/stefansjfw) - Stefan Markovic - Dev
# Former PowerToys core team members
- [@indierawk2k2](https://github.com/indierawk2k2) - Mike Harsh - Product Manager
- [@enricogior](https://github.com/enricogior) - Enrico Giordani - Dev Lead
- [@bzoz](https://github.com/bzoz) - Bartosz Sosnowski - Dev
- [@ivan100sic](https://github.com/ivan100sic) - Ivan Stošić - Dev
- [@mykhailopylyp](https://github.com/mykhailopylyp) - Mykhailo Pylyp - Dev
- [@taras-janea](https://github.com/taras-janea) - Taras Sich - Dev
- [@yuyoyuppe](https://github.com/yuyoyuppe) - Andrey Nekrasov - Dev

View File

@@ -46,7 +46,7 @@
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.0.4" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageVersion Include="Microsoft.Windows.SDK.Contracts" Version="10.0.19041.1" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.5.240428000" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />
@@ -75,7 +75,7 @@
<PackageVersion Include="System.Diagnostics.EventLog" Version="8.0.0" />
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="8.0.0" />
<PackageVersion Include="System.Drawing.Common" Version="8.0.5" />
<PackageVersion Include="System.Drawing.Common" Version="8.0.6" />
<PackageVersion Include="System.IO.Abstractions" Version="17.2.3" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="17.2.3" />
<PackageVersion Include="System.Management" Version="8.0.0" />

View File

@@ -1333,7 +1333,7 @@ EXHIBIT A -Mozilla Public License.
- Microsoft.Windows.CsWinRT 2.0.4
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
- Microsoft.Windows.SDK.Contracts 10.0.19041.1
- Microsoft.WindowsAppSDK 1.5.240311000
- Microsoft.WindowsAppSDK 1.5.240428000
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4
@@ -1355,7 +1355,7 @@ EXHIBIT A -Mozilla Public License.
- System.Data.SqlClient 4.8.6
- System.Diagnostics.EventLog 8.0.0
- System.Diagnostics.PerformanceCounter 8.0.0
- System.Drawing.Common 8.0.5
- System.Drawing.Common 8.0.6
- System.IO.Abstractions 17.2.3
- System.IO.Abstractions.TestingHelpers 17.2.3
- System.Management 8.0.0

View File

@@ -146,11 +146,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerLauncher", "src\module
{FDB3555B-58EF-4AE6-B5F1-904719637AB4} = {FDB3555B-58EF-4AE6-B5F1-904719637AB4}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E775CC2C-24CB-48D6-9C3A-BE4CCE0DB17A}"
ProjectSection(SolutionItems) = preProject
src\tests\win-app-driver\README.md = src\tests\win-app-driver\README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "previewpane", "previewpane", "{2F305555-C296-497E-AC20-5FA1B237996A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PreviewHandlerCommon", "src\modules\previewpane\Common\PreviewHandlerCommon.csproj", "{AF2349B8-E5B6-4004-9502-687C1C7730B1}"

199
README.md
View File

@@ -17,14 +17,15 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
| | Current utilities: | |
|--------------|--------------------|--------------|
| [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) |
| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) |
| [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) |
| [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) |
| [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [Peek](https://aka.ms/PowerToysOverview_Peek) |
| [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) |
| [Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) |
| [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) |
| [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) |
| [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) |
| [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) |
| [Peek](https://aka.ms/PowerToysOverview_Peek) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) |
| [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) |
| [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) |
| [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) |
## Installing and running Microsoft PowerToys
@@ -40,19 +41,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=project%3Amicrosoft%2FPowerToys%2F54
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F53
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysUserSetup-0.80.1-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysUserSetup-0.80.1-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysSetup-0.80.1-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysSetup-0.80.1-arm64.exe
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.82%22
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F54
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysUserSetup-0.81.0-x64.exe
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysUserSetup-0.81.0-arm64.exe
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysSetup-0.81.0-x64.exe
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.81.0/PowerToysSetup-0.81.0-arm64.exe
| Description | Filename | sha256 hash |
|----------------|----------|-------------|
| Per user - x64 | [PowerToysUserSetup-0.80.1-x64.exe][ptUserX64] | 23E35F7B33C6F24237BCA3D5E8EDF9B3BD4802DD656C402B40A4FC82670F8BE3 |
| Per user - ARM64 | [PowerToysUserSetup-0.80.1-arm64.exe][ptUserArm64] | C5EECF0D9D23AB8C14307F91CA28D2CF4DA5932D705F07AE93576C259F74B4D1 |
| Machine wide - x64 | [PowerToysSetup-0.80.1-x64.exe][ptMachineX64] | 62373A08BB8E1C1173D047509F3EA5DCC0BE1845787E07BCDA3F6A09DA2A0C17 |
| Machine wide - ARM64 | [PowerToysSetup-0.80.1-arm64.exe][ptMachineArm64] | 061EF8D1B10D68E69D04F98A2D8E1D8047436174C757770778ED23E01CC3B06C |
| Per user - x64 | [PowerToysUserSetup-0.81.0-x64.exe][ptUserX64] | E62B1EE81954A75355C04E7567B1C9AAD6034AA0C61AD22587F8746D0DC488C8 |
| Per user - ARM64 | [PowerToysUserSetup-0.81.0-arm64.exe][ptUserArm64] | 75330A2DB4F9EF9B548B3B58F8BF3262C8C67E680042639BBBBC87EA244F24E2 |
| Machine wide - x64 | [PowerToysSetup-0.81.0-x64.exe][ptMachineX64] | 29F151B01FE3C94D4FD75F2D6E8F09A6C0F0962385B83A5A733F6717312F639D |
| Machine wide - ARM64 | [PowerToysSetup-0.81.0-arm64.exe][ptMachineArm64] | FCE636220E1FB854771258D9558E07B7532728AD4C722A7920338DEE60DEECF7 |
This is our preferred method.
@@ -98,136 +99,158 @@ 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.80 - March 2024 Update
### 0.81 - Build 2024 Update
In this release, we focused on stability and improvements. The next release is planned to be released during [Microsoft Build 2024](https://build.microsoft.com/) (late May).
In this release, we focused on new features, stability and improvements.
**Highlights**
- New feature: Desired State Configuration support, allowing the use of winget configure for PowerToys. Check the [DSC documentation](https://aka.ms/powertoys-docs-dsc-configure) for more information.
- The Windows App SDK dependency was updated to 1.5.1, fixing many underlying UI issues.
- WebP/WebM files support was added to Peek. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Audio files support was added to Peek. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Automated UI testing for FancyZones Editor was added to CI.
- New utility: Advanced Paste - This is an evolution based on feedback of the Paste As Plain Text utility to do more. It can paste as plain text, markdown, or json directly with the new UX or with a direct keystroke invoke. These are fully locally executed. In addition, it now has an AI powered option as well if you wish with the free form text box. The AI feature is 100% opt-in and requires an Open AI key. This new system will allow us to have more freedom in the future to quickly add in new features like pasting an image directly to a file or handle additional meta data types past just text.
- Thanks [@craigloewen-msft](https://github.com/craigloewen-msft) for the core functionality and [@niels9001](https://github.com/niels9001) for the UI/UX design!
- Command Not Found now uses the PowerShell Gallery release and now supports ARM64. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
- Fixed most accessibility issues opened after the latest accessibility review.
- Refactored, packaged and released the main Environment Variables Editor, Hosts File Editor and Registry Preview utilities functionality as controls to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### General
- Added a Quick Access entry to access the flyout from PowerToys' tray icon right click menu. Thanks [@pekvasnovsky](https://github.com/pekvasnovsky)!
- Added support for Desired State Configuration in PowerToys, allowing the use of winget configure to configure many settings.
- Fixed crashes on older CPUS by updating .NET to 8.0.4. (This was a hotfix for 0.80)
### Awake
### Advanced Paste
- Fix an issue causing the "Keep screen on" option to disable after Awake deactivated itself.
- New utility: Advanced Paste - This is an evolution based on feedback of the Paste As Plain Text utility to do more. It can paste as plain text, markdown, or json directly with the new UX or with a direct keystroke invoke. These are fully locally executed. In addition, it now has an AI powered option as well if you wish with the free form text box. The AI feature is 100% opt-in and requires an Open AI key. This new system will allow us to have more freedom in the future to quickly add in new features like pasting an image directly to a file or handle additional meta data types past just text.
- Thanks [@craigloewen-msft](https://github.com/craigloewen-msft) for the core functionality and [@niels9001](https://github.com/niels9001) for the UI/UX design!
### AlwaysOnTop
- Enable border anti-aliasing. Thanks [@ewancg](https://github.com/ewancg)!
### Color Picker
- Fixed a UI issue causing the color picker modal to hide part of the color bar. Thanks [@TheChilledBuffalo](https://github.com/TheChilledBuffalo)!
- Improved accessibility by making the Settings and Copy to clipboard buttons focusable.
- Improved accessibility by supporting picking a color using the keyboard.
### Command Not Found
- Now tries to find a preview version of PowerShell if no stable version is found.
- Upgraded the Command Not Found to use the new PowerShell Gallery release and support ARM64. Thanks [@carlos-zamora](https://github.com/carlos-zamora)!
### Environment Variables Editor
- Refactored, packaged and released the main Environment Variables Editor functionality as a control to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### FancyZones
- Fixed a crash loading the editor when there's a layout with an empty name in the configuration file.
- Refactored layout internal data structures and common code to allow for automated testing.
- The pressing of the shift key is now detected through raw input to fix an issue causing the shift key to be locked for some users.
- Fixed window wrap around behavior when overriding Windows key and arrow shortcuts on single monitor scenarios. Thanks [@DanRosenberry](https://github.com/DanRosenberry)!
- Improved accessibility of the editor by listing the keyboard shortcuts in the Canvas Editor.
### File Explorer add-ons
- Fixed a crash occurring in the Monaco previewer when a file being previewed isn't found by the code behind.
- Fixed an issue in the Markdown previewer adding a leading space to code blocks. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Fixed wrong location and scaling of preview results on screens with different DPIs.
- Added better clean up code to thumbnail handlers to prevent locking files.
- Updated Monaco to 0.47 and added the new sticky scroll setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Added the new font size setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Added support for .srt (subtitle) file previewing in DevFiles viewer. Thanks [@PesBandi](https://github.com/PesBandi)!
### File Locksmith
### Hosts File Editor
- Allow multiple lines to wrap when viewing the modal with selected file paths. Thanks [@sanidhyas3s](https://github.com/sanidhyas3s)!
- Refactored, packaged and released the main Hosts File Editor functionality as a control to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### Image Resizer
- Supported narrator announcing the checkboxes in the UI and the sizes combobox. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Improved accessibility by increasing contrast in the text color of combobox items.
### Installer
- Fixed the final directory name of the PowerToys Run VSCode Workspaces plugin in the installation directory to match the plugin name. Thanks [@zetaloop](https://github.com/zetaloop)!
- Used more generic names for the bootstrap steps, so that "Installing PowerToys" is not shown when uninstalling.
- Fixed some install failures when the folders the DSC module is to be installed in isn't accessible by the WiX installer. (This was a hotfix for 0.80)
- Detecting install location for DSC now uses registry instead of WMI to improve performance. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed an error causing the machine scope installer to not install correctly in machines where the documents folder is in a UNC network path. We're still working in a fix for the user scope installer.
### Keyboard Manager
- Fixed an issue that would clear out KBM mappings when certain numpad keys were used as the second key of a chord.
- Added a comment in localization files so that translators won't translate "Text" as "SMS".
- Fixed startup crashes in the editor when the Visual C++ Redistributable wasn't installed. (This was a hotfix for 0.80)
- Fixed an accessibility issue where the first button wasn't focused after adding a new row in the editor.
- Environment Variables are now expanded in arguments of programs started through a shortcut. Thanks [@HydroH](https://github.com/HydroH)!
### Paste as Plain Text
- Paste as Plain Text was removed as a separate utility, since its functionality is now part of the Advanced Paste utility.
### Peek
- Added support to .WebP/.WebM files in the image/video previewer. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added support for audio files. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed an issue causing the open file button in the title bar to be un-clickable. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed an issue when previewing a folder with a dot in the name that caused Peek to try to preview it as a file. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Updated icons, tweaked UI and refactored internal code. Thanks [@Jay-o-Way](https://github.com/Jay-o-Way)!
- Updated Monaco to 0.47 and added the new sticky scroll setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Added the new font size setting for DevFiles viewer. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
- Upgrade the SharpCompress dependency to 0.37.2 and fixed archive parsing. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed aliasing in the image viewer.
- Added support for .srt (subtitle) file previewing in DevFiles viewer. Thanks [@PesBandi](https://github.com/PesBandi)!
### Power Rename
- Fixed the descriptions that were mixed up in the regex helper (\S and \w).
### PowerToys Run
- Added a setting to the Windows Search plugin to exclude files and patterns from the results. Thanks [@HydroH](https://github.com/HydroH)!
- Fixed an issue showing thumbnails caused by a hash collision between similar images.
- Added the "checkbox and multiline text box" additional property type for plugins and improved multiline text handling. Thanks [@htcfreek](https://github.com/htcfreek)!
- Added support for UNC paths starting with // in the Folder plugin. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed the plugin load failed message to list the failed plugins. Thanks [@belkiss](https://github.com/belkiss)!
- Icons for MSIX packages are now updated when a package update is detected. Thanks [@HydroH](https://github.com/HydroH)!
- Use Mica backdrop instead of Acrylic to fix random crashes caused by the Windows composition being momentarily turned off.
- Improved accessibility in the results list action buttons by improving contrast of hovered/focused buttons.
### Quick Accent
- Added the Schwa character to the Italian character set. Thanks [@damantioworks](https://github.com/damantioworks)!
- Added support for the Esperanto character set. Thanks [@salutontalk](https://github.com/salutontalk) and [@ccmywish](https://github.com/ccmywish)!
- Added the ǽ and ϑ characters. Thanks [@PesBandi](https://github.com/PesBandi)!
### Registry Preview
- Allow alternative valid names for the root keys. Thanks [@e-t-l](https://github.com/e-t-l)!
- Fixed an issue causing many pick file windows to be opened simultaneously. Thanks [@randyrants](https://github.com/randyrants)!
### Screen Ruler
- Updated the measure icons for clarity. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker) and [@niels9001](https://github.com/niels9001)!
### Shortcut Guide
- Updated the Emoji shortcut that is shown to the new Windows key + period (.) hotkey.
- Refactored, packaged and released the main Registry Preview functionality as a control to be integrated into DevHome. Thanks [@dabhattimsft](https://github.com/dabhattimsft) for validating and integrating into DevHome!
### Text Extractor
- Fixed issues creating the extract layout on certain monitor configurations.
### Video Conference Mute
- Added enable/disable telemetry to get usage data.
- Fixed an issue causing the Settings page to not be opened when clicking the Settings button in Text Extractor's overlay. (This was a hotfix for 0.80)
### Settings
- Added locks to some terms (like the name of some utilities) so that they aren't localized.
- Fixed some shortcuts not being shown properly in the Flyout and Dashboard. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Updated image for Color Picker and outdated animations for utilities in OOBE. Thanks [@niels9001](https://github.com/niels9001)!
- Improved UI ordering of the File Explorer add-ons. Thanks [@niels9001](https://github.com/niels9001)!
- Applied fixes to theme overriding and cleaned up unneeded code. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed misspells in references to the Hosts File Editor utility. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Improved accessibility of the Select Folder button in the Settings Backup UI.
- Improved accessibility by improving focus and tab navigation in the ColorPicker page. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Added a description to the fallback encoder setting in the Image Resizer page. Thanks [@Kissaki](https://github.com/Kissaki)!
- Refactored and improved performance in the PowerToys Run plugins UI in the Settings page. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
- Fixed a crash when a user cleared the contents of a Number Box in the PowerToys Run plugins additional options. Thanks [@htcfreek](https://github.com/htcfreek)!
- Update the PATH environment variables with the user scope PATH when entering the Command Not Found page to improve PowerShell detection.
### Documentation
- Added FastWeb plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@CCcat8059](https://github.com/CCcat8059)!
- Removed the old security link to MSRC from the create new issue page, since security.md is already linked there.
- Added clarity regarding unofficial plugins to the PowerToys Run thirdPartyRunPlugins.md docs.
- Added the WebSearchShortcut plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@Daydreamer-riri](https://github.com/Daydreamer-riri)!
- Updated COMMUNITY.md with the project managers that are part of the core team.
- Improved the DSC samples.
- Added the 1Password plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@KairuDeibisu](https://github.com/KairuDeibisu)!
- Added the UnicodeInput plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@nathancartlidge](https://github.com/nathancartlidge)!
### Development
- Updated System.Drawing.Common to 8.0.3 to fix CI builds after the .NET 8.0.3 upgrade was released.
- Adjusted the GitHub action names for releasing to winget and Microsoft Store so they're clearer in the UI.
- Upgraded WinAppSDK to 1.5.1, fixing many related issues.
- Consolidate the WebView2 version used by WinUI 2 in the Keyboard Manager Editor.
- Unified the use of Precompiled Headers when building on CI. Thanks [@dfederm](https://github.com/dfederm)!
- Added UI tests for FancyZones Editor in CI.
- Added a GitHub bot to identify possible duplicates when a new issue is created. Thanks [@craigloewen-msft](https://github.com/craigloewen-msft)!
- Updated the WiX installer dependency to 3.14.1 to fix possible security issues.
- Changed the pipelines to use pipeline artifacts instead of build artifacts. Thanks [@dfederm](https://github.com/dfederm)!
- Added the -graph parameter for pipelines. Thanks [@dfederm](https://github.com/dfederm)!
- Tests in the pipelines now run as part of the build step to save on CI time. Thanks [@dfederm](https://github.com/dfederm)!
- Updated System.Drawing.Common to 8.0.5 to fix CI builds after the .NET 8.0.5 upgrade was released.
- Fixed file permissions when doing a build using cache on PR CI. Thanks [@dfederm](https://github.com/dfederm)!
- Removed the Test SDK reference on ARM64 to fix local building for ARM64. Thanks [@dfederm](https://github.com/dfederm)!
- Replaced make_pair with RemapBufferRow in Keyboard Manager internal code. Thanks [@masaru-iritani](https://github.com/masaru-iritani)!
- Added CODEOWNERS file to protect sensitive parts of the repo. Thanks [@htcfreek](https://github.com/htcfreek) for the help in figuring out how to make the spellcheck folder an exception!
- Added comments in code. to make it clear what the error badge in PowerToys Run plugin list in Settings means. Thanks [@Jay-o-Way](https://github.com/Jay-o-Way)!
- Enabled caching by default in the PR CI pipelines. Thanks [@dfederm](https://github.com/dfederm)!
- Disabled caching for PR started from forks, since those were failing. Thanks [@dfederm](https://github.com/dfederm)!
- Removed baseline files for policy checking and turned on the "TSA" process in the release pipelines instead.
- Added caching of nuget packages in the PR CI pipelines. Thanks [@dfederm](https://github.com/dfederm)!
- Updated the release CI pipelines TouchdownBuildTask to v3.
- Moved the release CI pipelines to ESRPv5.
- Added a policy for GitHub Copilot Workspaces for the repo on GitHub. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
#### What is being planned for version 0.81
#### What is being planned for version 0.82
For [v0.81][github-next-release-work], we'll work on the items below:
For [v0.82][github-next-release-work], we'll work on the items below:
- Stability / bug fixes
- Language selection
- New module: File Actions Menu
The next release is planned to be released during Microsoft Build 2024.
## PowerToys Community
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldnt be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month by month, you directly help make PowerToys a better piece of software.

View File

@@ -2,7 +2,7 @@
## Through runner
- The settings process communicates changes in the UI to most modules using the runner through delegates.
- More details on this are mentioned in [`runner-ipc.md`](settingsv2/runner-ipc.md).
- More details on this are mentioned in [`runner-ipc.md`](runner-ipc.md).
## PT Run
- Any changes to the UI are saved by the settings process in the `settings.json` file located within the `/Local/Microsoft/PowerToys/Launcher/` folder.

View File

@@ -27,7 +27,6 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| ------ | ------ | ----------- |
| [BrowserSearch](https://github.com/TBM13/BrowserSearch) | [TBM13](https://github.com/TBM13) | Search your browser history |
| [GitHub Emoji](https://github.com/hlaueriksson/GEmojiSharp) | [hlaueriksson](https://github.com/hlaueriksson) | Search GitHub Emoji |
| [Guid](https://github.com/skttl/ptrun-guid) | [skttl](https://github.com/skttl) | Guid generator |
| [PowerTranslator](https://github.com/N0I0C0K/PowerTranslator) | [N0I0C0K](https://github.com/N0I0C0K) | Text translator based on Youdao |
| [Quick Lookup](https://github.com/GTGalaxi/quick-lookup-ptrun) | [gtgalaxi](https://github.com/GTGalaxi) | Search across multiple cyber security tools |
| [Input Typer](https://github.com/CoreyHayward/PowerToys-Run-InputTyper) | [CoreyHayward](https://github.com/CoreyHayward) | Type the input as if sent from a keyboard |
@@ -36,6 +35,7 @@ Contact the developers of a plugin directly for assistance with a specific plugi
| [FastWeb](https://github.com/CCcat8059/FastWeb) | [CCcat](https://github.com/CCcat8059) | Open website in browser |
| [WebSearchShortcut](https://github.com/Daydreamer-riri/PowerToys-Run-WebSearchShortcut) | [Riri](https://github.com/Daydreamer-riri) | Select a specific search engine to perform searches. |
| [UnicodeInput](https://github.com/nathancartlidge/powertoys-run-unicode) | [nathancartlidge](https://github.com/nathancartlidge) | Copy Unicode characters to the clipboard |
| [PowerHexInspector](https://github.com/NaroZeol/PowerHexInspector) | [NaroZeol](https://github.com/NaroZeol) | Peek other forms of an input number |
## Extending software plugins
@@ -44,6 +44,7 @@ Below are community created plugins that target a website or software. They are
| Plugin | Author | Description |
| ------ | ------ | ----------- |
| [Edge Favorite](https://github.com/davidegiacometti/PowerToys-Run-EdgeFavorite) | [davidegiacometti](https://github.com/davidegiacometti) | Open Microsoft Edge favorites |
| [Edge Workspaces](https://github.com/quachpas/PowerToys-Run-EdgeWorkspaces) | [quachpas](https://github.com/quachpas) | Open Microsoft Edge workspaces|
| [Everything](https://github.com/lin-ycv/EverythingPowerToys) | [Yu Chieh (Victor) Lin](https://github.com/Lin-ycv) | Get search results from Everything |
| [GitKraken](https://github.com/davidegiacometti/PowerToys-Run-GitKraken) | [davidegiacometti](https://github.com/davidegiacometti) | Open GitKraken repositories |
| [Visual Studio Recents](https://github.com/davidegiacometti/PowerToys-Run-VisualStudio) | [davidegiacometti](https://github.com/davidegiacometti) | Open Visual Studio recents |

View File

@@ -46,33 +46,18 @@
</Component>
</DirectoryRef>
<?if $(var.PerUser) = "true" ?>
<DirectoryRef Id="PersonalFolder">
<Directory Id="WindowsPowerShellFolder" Name="PowerShell">
<Directory Id="PowerShellModulesFolder" Name="Modules">
<Directory Id="PowerToysDscFolder" Name="Microsoft.PowerToys.Configure">
<Directory Id="PowerToysDscVerFolder" Name="$(var.Version)">
<Component Id="PowerToysDSC" Win64="yes" Guid="4A033E3B-6590-43FD-8FBD-27F9DF557F7F">
<RegistryValue Root="HKCU"
Key="Software\[Manufacturer]\[ProductName]"
Name="DSCInstalled"
Type="integer"
Value="1"
KeyPath="yes"/>
<!-- Don't fail installation because of DSC. Files are marked as not vital. -->
<File Vital="no" Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version)\Microsoft.PowerToys.Configure.psd1" Id="PTConf.psd1" />
<File Vital="no" Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version)\Microsoft.PowerToys.Configure.psm1" Id="PTConf.psm1" />
<RemoveFolder Id="RemoveThisFolder" On="uninstall" />
<RemoveFolder Id="RemovePowerToysDscVerFolder" Directory="PowerToysDscVerFolder" On="uninstall" />
<RemoveFolder Id="RemovePowerToysDscFolder" Directory="PowerToysDscFolder" On="uninstall" />
<RemoveFolder Id="RemovePowerShellModulesFolder" Directory="PowerShellModulesFolder" On="uninstall" />
<RemoveFolder Id="RemoveWindowsPowerShellFolder" Directory="WindowsPowerShellFolder" On="uninstall" />
<DirectoryRef Id="DSCModulesReferenceFolder">
<Component Id="PowerToysDSCReference" Win64="yes" Guid="40869ACB-0BEB-4911-AE41-5E73BC1586A9">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="DSCModulesReference" Value="" KeyPath="yes"/>
</RegistryKey>
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version)\Microsoft.PowerToys.Configure.psd1" Id="PTConfReference.psd1" />
<File Source="$(var.RepoDir)\src\dsc\Microsoft.PowerToys.Configure\Generated\Microsoft.PowerToys.Configure\$(var.Version)\Microsoft.PowerToys.Configure.psm1" Id="PTConfReference.psm1" />
</Component>
</Directory>
</Directory>
</Directory>
</Directory>
</DirectoryRef>
<?if $(var.PerUser) = "true" ?>
<!-- DSC module files for PerUser handled in InstallDSCModule custom action. -->
<?else?>
<DirectoryRef Id="ProgramFiles64Folder">
<Directory Id="WindowsPowerShellFolder" Name="WindowsPowerShell">
@@ -135,6 +120,7 @@
<RegistryValue Type="string" Name="RemoveCoreFolder" Value="" KeyPath="yes"/>
</RegistryKey>
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall"/>
<RemoveFolder Id="RemoveDSCModulesReferenceFolder" Directory="DSCModulesReferenceFolder" On="uninstall"/>
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall"/>
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall"/>
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall"/>
@@ -146,7 +132,10 @@
<ComponentRef Id="License_rtf" />
<ComponentRef Id="Notice_md" />
<ComponentRef Id="DesktopShortcut" />
<ComponentRef Id="PowerToysDSCReference" />
<?if $(var.PerUser) = "false" ?>
<ComponentRef Id="PowerToysDSC" />
<?endif?>
</ComponentGroup>
</Fragment>
</Wix>

View File

@@ -136,6 +136,11 @@
<Custom Action="SetUninstallCommandNotFoundParam" Before="UninstallCommandNotFound" />
<Custom Action="SetUpgradeCommandNotFoundParam" Before="UpgradeCommandNotFound" />
<Custom Action="SetApplyModulesRegistryChangeSetsParam" Before="ApplyModulesRegistryChangeSets" />
<?if $(var.PerUser) = "true" ?>
<Custom Action="SetInstallDSCModuleParam" Before="InstallDSCModule" />
<?endif?>
<Custom Action="SetUnApplyModulesRegistryChangeSetsParam" Before="UnApplyModulesRegistryChangeSets" />
<Custom Action="CheckGPO" After="InstallInitialize">
NOT Installed
@@ -149,6 +154,9 @@
<!--<Custom Action="InstallEmbeddedMSIXTask" After="InstallFinalize">
NOT Installed
</Custom>-->
<?if $(var.PerUser) = "true" ?>
<Custom Action="InstallDSCModule" After="InstallFiles"/>
<?endif?>
<Custom Action="TelemetryLogInstallSuccess" After="InstallFinalize">
NOT Installed
</Custom>
@@ -177,8 +185,12 @@
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
Installed AND (REMOVE="ALL")
</Custom>-->
<?if $(var.PerUser) = "true" ?>
<Custom Action="UninstallDSCModule" After="InstallFinalize">
Installed AND (REMOVE="ALL")
</Custom>
<?endif?>
<Custom Action="TerminateProcesses" Before="InstallValidate" />
<Custom Action="LaunchPowerToys" Before="InstallFinalize">NOT Installed</Custom>
</InstallExecuteSequence>
@@ -211,6 +223,10 @@
Property="UnApplyModulesRegistryChangeSets"
Value="[INSTALLFOLDER]" />
<CustomAction Id="SetInstallDSCModuleParam"
Property="InstallDSCModule"
Value="[INSTALLFOLDER]" />
<CustomAction Id="SetUninstallCommandNotFoundParam"
Property="UninstallCommandNotFound"
Value="[INSTALLFOLDER]" />
@@ -265,6 +281,21 @@
DllEntry="UninstallEmbeddedMSIXCA"
/>
<CustomAction Id="InstallDSCModule"
Return="ignore"
Impersonate="yes"
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="InstallDSCModuleCA"
/>
<CustomAction Id="UninstallDSCModule"
Return="ignore"
Impersonate="yes"
BinaryKey="PTCustomActions"
DllEntry="UninstallDSCModuleCA"
/>
<CustomAction Id="UninstallServicesTask"
Return="ignore"
Impersonate="yes"
@@ -407,6 +438,7 @@
<Directory Id="INSTALLFOLDER" Name="PowerToys">
<Directory Id="BaseApplicationsAssetsFolder" Name="Assets">
</Directory>
<Directory Id="DSCModulesReferenceFolder" Name="DSCModules" />
<Directory Id="WinUI3AppsInstallFolder" Name="WinUI3Apps">
<Directory Id="WinUI3AppsMicrosoftUIXamlInstallFolder" Name="Microsoft.UI.Xaml">
<Directory Id="WinUI3AppsMicrosoftUIXamlAssetsInstallFolder" Name="Assets" />
@@ -421,9 +453,6 @@
<Directory Id="ApplicationProgramsFolder" Name="PowerToys (Preview)"/>
</Directory>
<Directory Id="DesktopFolder" Name="Desktop" />
<?if $(var.PerUser) = "true" ?>
<Directory Id="PersonalFolder" Name="UserHomeDocuments" />
<?endif?>
</Directory>
</Fragment>
</Wix>

View File

@@ -139,6 +139,23 @@ LExit:
return SUCCEEDED(hr);
}
static std::filesystem::path GetUserPowerShellModulesPath()
{
PWSTR myDocumentsBlockPtr;
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Documents, 0, NULL, &myDocumentsBlockPtr)))
{
const std::wstring myDocuments{ myDocumentsBlockPtr };
CoTaskMemFree(myDocumentsBlockPtr);
return std::filesystem::path(myDocuments) / "PowerShell" / "Modules";
}
else
{
CoTaskMemFree(myDocumentsBlockPtr);
return {};
}
}
UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
@@ -317,6 +334,125 @@ LExit:
return WcaFinalize(er);
}
const wchar_t* DSC_CONFIGURE_PSD1_NAME = L"Microsoft.PowerToys.Configure.psd1";
const wchar_t* DSC_CONFIGURE_PSM1_NAME = L"Microsoft.PowerToys.Configure.psm1";
UINT __stdcall InstallDSCModuleCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
std::wstring installationFolder;
hr = WcaInitialize(hInstall, "InstallDSCModuleCA");
ExitOnFailure(hr, "Failed to initialize");
hr = getInstallFolder(hInstall, installationFolder);
ExitOnFailure(hr, "Failed to get installFolder.");
{
const auto baseModulesPath = GetUserPowerShellModulesPath();
if (baseModulesPath.empty())
{
hr = E_FAIL;
ExitOnFailure(hr, "Unable to determine Powershell modules path");
}
const auto modulesPath = baseModulesPath / L"Microsoft.PowerToys.Configure" / get_product_version();
std::error_code errorCode;
fs::create_directories(modulesPath, errorCode);
if (errorCode)
{
hr = E_FAIL;
ExitOnFailure(hr, "Unable to create Powershell modules folder");
}
for (const auto* filename : { DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME })
{
fs::copy_file(fs::path(installationFolder) / "DSCModules" / filename, modulesPath / filename, fs::copy_options::overwrite_existing, errorCode);
if (errorCode)
{
hr = E_FAIL;
ExitOnFailure(hr, "Unable to copy Powershell modules file");
}
}
}
LExit:
if (SUCCEEDED(hr))
{
er = ERROR_SUCCESS;
Logger::info(L"DSC module was installed!");
}
else
{
er = ERROR_INSTALL_FAILURE;
Logger::error(L"Couldn't install DSC module!");
}
return WcaFinalize(er);
}
UINT __stdcall UninstallDSCModuleCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
hr = WcaInitialize(hInstall, "UninstallDSCModuleCA");
ExitOnFailure(hr, "Failed to initialize");
{
const auto baseModulesPath = GetUserPowerShellModulesPath();
if (baseModulesPath.empty())
{
hr = E_FAIL;
ExitOnFailure(hr, "Unable to determine Powershell modules path");
}
const auto powerToysModulePath = baseModulesPath / L"Microsoft.PowerToys.Configure";
const auto versionedModulePath = powerToysModulePath / get_product_version();
std::error_code errorCode;
for (const auto* filename : { DSC_CONFIGURE_PSD1_NAME, DSC_CONFIGURE_PSM1_NAME })
{
fs::remove(versionedModulePath / filename, errorCode);
if (errorCode)
{
hr = E_FAIL;
ExitOnFailure(hr, "Unable to delete DSC file");
}
}
for (const auto* modulePath : { &versionedModulePath, &powerToysModulePath })
{
fs::remove(*modulePath, errorCode);
if (errorCode)
{
hr = E_FAIL;
ExitOnFailure(hr, "Unable to delete DSC folder");
}
}
}
LExit:
if (SUCCEEDED(hr))
{
er = ERROR_SUCCESS;
Logger::info(L"DSC module was uninstalled!");
}
else
{
er = ERROR_INSTALL_FAILURE;
Logger::error(L"Couldn't uninstall DSC module!");
}
return WcaFinalize(er);
}
UINT __stdcall InstallEmbeddedMSIXCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
@@ -472,9 +608,19 @@ UINT __stdcall UninstallCommandNotFoundModuleCA(MSIHANDLE hInstall)
hr = getInstallFolder(hInstall, installationFolder);
ExitOnFailure(hr, "Failed to get installFolder.");
#ifdef _M_ARM64
command = "powershell.exe";
command += " ";
command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted";
command += " -Command ";
command += "\"[Environment]::SetEnvironmentVariable('PATH', [Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('PATH', 'User'), 'Process');";
command += "pwsh.exe -NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File '" + winrt::to_string(installationFolder) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\DisableModule.ps1" + "'\"";
#else
command = "pwsh.exe";
command += " ";
command += "-NoProfile -NonInteractive -NoLogo -WindowStyle Hidden -ExecutionPolicy Unrestricted -File \"" + winrt::to_string(installationFolder) + "\\WinUI3Apps\\Assets\\Settings\\Scripts\\DisableModule.ps1" + "\"";
#endif
system(command.c_str());

View File

@@ -18,10 +18,12 @@ EXPORTS
CertifyVirtualCameraDriverCA
InstallVirtualCameraDriverCA
InstallEmbeddedMSIXCA
InstallDSCModuleCA
UnApplyModulesRegistryChangeSetsCA
UninstallVirtualCameraDriverCA
UnRegisterContextMenuPackagesCA
UninstallEmbeddedMSIXCA
UninstallDSCModuleCA
UninstallServicesCA
UninstallCommandNotFoundModuleCA
UpgradeCommandNotFoundModuleCA

View File

@@ -1,4 +1,4 @@
#include "pch.h"
#include "pch.h"
#include "GPOWrapper.h"
#include "GPOWrapper.g.cpp"
@@ -172,6 +172,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredQoiThumbnailsEnabledValue());
}
GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOnlineAIModelsValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue());
}
GpoRuleConfigured GPOWrapper::GetConfiguredProjectsEnabledValue()
{
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredProjectsEnabledValue());

View File

@@ -1,4 +1,4 @@
#pragma once
#pragma once
#include "GPOWrapper.g.h"
#include <common/utils/gpo.h>
@@ -49,6 +49,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue();
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
static GpoRuleConfigured GetConfiguredProjectsEnabledValue();
};
}

View File

@@ -53,6 +53,7 @@ namespace PowerToys
static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue();
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
static GpoRuleConfigured GetConfiguredProjectsEnabledValue();
}
}

View File

@@ -71,7 +71,7 @@ namespace powertoys_gpo {
// The registry value names for other PowerToys policies.
const std::wstring POLICY_ALLOW_EXPERIMENTATION = L"AllowExperimentation";
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS = L"PowerLauncherAllPluginsEnabledState";
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS = L"AllowPowerToysAdvancedPasteOnlineAIModels";
inline std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name)
{
@@ -476,4 +476,9 @@ namespace powertoys_gpo {
{
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_QOI_THUMBNAILS);
}
inline gpo_rule_configured_t getAllowedAdvancedPasteOnlineAIModelsValue()
{
return getUtilityEnabledValue(POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS);
}
}

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.9" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.10" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyNamespaces>
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
</policyNamespaces>
<resources minRequiredRevision="1.9"/><!-- Last changed with PowerToys v0.81.0 -->
<resources minRequiredRevision="1.10"/><!-- Last changed with PowerToys v0.81.1 -->
<supportedOn>
<definitions>
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
@@ -18,6 +18,7 @@
<definition name="SUPPORTED_POWERTOYS_0_77_0" displayName="$(string.SUPPORTED_POWERTOYS_0_77_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_78_0" displayName="$(string.SUPPORTED_POWERTOYS_0_78_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_81_0" displayName="$(string.SUPPORTED_POWERTOYS_0_81_0)"/>
<definition name="SUPPORTED_POWERTOYS_0_81_1" displayName="$(string.SUPPORTED_POWERTOYS_0_81_1)"/>
</definitions>
</supportedOn>
<categories>
@@ -28,6 +29,9 @@
<category name="PowerToysRun" displayName="$(string.PowerToysRun)">
<parentCategory ref="PowerToys" />
</category>
<category name="AdvancedPaste" displayName="$(string.AdvancedPaste)">
<parentCategory ref="PowerToys" />
</category>
</categories>
<policies>
@@ -499,5 +503,15 @@
<list id="PowerToysRunIndividualPluginEnabledList" explicitValue="true" />
</elements>
</policy>
<policy name="AllowPowerToysAdvancedPasteOnlineAIModels" class="Both" displayName="$(string.AllowPowerToysAdvancedPasteOnlineAIModels)" explainText="$(string.AllowPowerToysAdvancedPasteOnlineAIModelsDescription)" key="Software\Policies\PowerToys" valueName="AllowPowerToysAdvancedPasteOnlineAIModels">
<parentCategory ref="AdvancedPaste" />
<supportedOn ref="SUPPORTED_POWERTOYS_0_81_1" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
</policies>
</policyDefinitions>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.9" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.10" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
<displayName>PowerToys</displayName>
<description>PowerToys</description>
<resources>
@@ -9,6 +9,7 @@
<string id="PowerToys">Microsoft PowerToys</string>
<string id="InstallerUpdates">Installer and Updates</string>
<string id="PowerToysRun">PowerToys Run</string>
<string id="AdvancedPaste">Advanced Paste</string>
<string id="Projects">Projects</string>
<string id="SUPPORTED_POWERTOYS_0_64_0">PowerToys version 0.64.0 or later</string>
@@ -21,6 +22,7 @@
<string id="SUPPORTED_POWERTOYS_0_77_0">PowerToys version 0.77.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_78_0">PowerToys version 0.78.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_81_0">PowerToys version 0.81.0 or later</string>
<string id="SUPPORTED_POWERTOYS_0_81_1">PowerToys version 0.81.1 or later</string>
<string id="ConfigureGlobalUtilityEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
@@ -125,6 +127,12 @@ If you disable or don't configure this policy, either the user or the policy "Co
You can set the enabled state for all plugins not configured by this policy using the policy "Configure enabled state for all plugins".
Note: Changes require a restart of PowerToys Run.
</string>
<string id="AllowPowerToysAdvancedPasteOnlineAIModelsDescription">This policy configures the enabled disable state for using Advanced Paste online AI models.
If you enable or don't configure this policy, the user takes control over the enabled state of the Enable paste with AI Advanced Paste setting.
If you disable this policy, the user won't be able to enable Enable paste with AI Advanced Paste setting and use Advanced Paste AI prompt nor set up the Open AI key in PowerToys Settings.
</string>
<string id="ConfigureGlobalUtilityEnabledState">Configure global utility enabled state</string>
<string id="ConfigureEnabledUtilityAdvancedPaste">Advanced Paste: Configure enabled state</string>
@@ -173,6 +181,7 @@ Note: Changes require a restart of PowerToys Run.
<string id="PowerToysRunIndividualPluginEnabledState">Configure enabled state for individual plugins</string>
<string id="ConfigureEnabledUtilityFileExplorerQOIPreview">QOI file preview: Configure enabled state</string>
<string id="ConfigureEnabledUtilityFileExplorerQOIThumbnails">QOI file thumbnail: Configure enabled state</string>
<string id="AllowPowerToysAdvancedPasteOnlineAIModels">Advanced Paste: Allow using online AI models</string>
</stringTable>
<presentationTable>

View File

@@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using ManagedCommon;
@@ -14,6 +16,10 @@ namespace AdvancedPaste.Helpers
{
internal static class JsonHelper
{
// List of supported CSV delimiters and Regex to detect separator property
private static readonly char[] CsvDelimArry = [',', ';', '\t'];
private static readonly Regex CsvSepIdentifierRegex = new Regex(@"^sep=(.)$", RegexOptions.IgnoreCase);
internal static string ToJsonFromXmlOrCsv(DataPackageView clipboardData)
{
Logger.LogTrace();
@@ -53,11 +59,31 @@ namespace AdvancedPaste.Helpers
{
var csv = new List<string[]>();
foreach (var line in text.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
string[] lines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
// Detect the csv delimiter and the count of occurrence based on the first two csv lines.
GetCsvDelimiter(lines, out char delim, out int delimCount);
foreach (var line in lines)
{
csv.Add(line.Split(","));
// If line is separator property line, then skip it
if (CsvSepIdentifierRegex.IsMatch(line))
{
continue;
}
// A CSV line is valid, if the delimiter occurs more or equal times in every line compared to the first data line. (More because sometimes the delimiter occurs in a data string.)
if (line.Count(x => x == delim) >= delimCount)
{
csv.Add(line.Split(delim));
}
else
{
throw new FormatException("Invalid CSV format: Number of delimiters wrong in the current line.");
}
}
Logger.LogDebug("Convert from csv.");
jsonText = JsonConvert.SerializeObject(csv, Newtonsoft.Json.Formatting.Indented);
}
}
@@ -66,7 +92,79 @@ namespace AdvancedPaste.Helpers
Logger.LogError("Failed parsing input as csv", ex);
}
// Try convert Plain Text
try
{
if (string.IsNullOrEmpty(jsonText))
{
var plainText = new List<string>();
foreach (var line in text.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
{
plainText.Add(line);
}
Logger.LogDebug("Convert from plain text.");
jsonText = JsonConvert.SerializeObject(plainText, Newtonsoft.Json.Formatting.Indented);
}
}
catch (Exception ex)
{
Logger.LogError("Failed parsing input as plain text", ex);
}
return string.IsNullOrEmpty(jsonText) ? text : jsonText;
}
private static void GetCsvDelimiter(in string[] csvLines, out char delimiter, out int delimiterCount)
{
delimiter = '\0'; // Unicode "null" character.
delimiterCount = 0;
if (csvLines.Length > 1)
{
// Try to select the delimiter based on the separator property.
Match matchChar = CsvSepIdentifierRegex.Match(csvLines[0]);
if (matchChar.Success)
{
// We can do matchChar[0] as the match only returns one character.
// We get the count from the second line, as the first one only contains the character definition and not a CSV data line.
char delimChar = matchChar.Groups[1].Value.Trim()[0];
delimiter = delimChar;
delimiterCount = csvLines[1].Count(x => x == delimChar);
}
}
if (csvLines.Length > 0 && delimiterCount == 0)
{
// Try to select the correct delimiter based on the first two CSV lines from a list of predefined delimiters.
foreach (char c in CsvDelimArry)
{
int cntFirstLine = csvLines[0].Count(x => x == c);
int cntNextLine = 0; // Default to 0 that the 'second line' check is always true.
// Additional count if we have more than one line
if (csvLines.Length >= 2)
{
cntNextLine = csvLines[1].Count(x => x == c);
}
// The delimiter is found if the count is bigger as from the last selected delimiter
// and if the next csv line does not exist or has the same number or more occurrences of the delimiter.
// (We check the next line to prevent false positives.)
if (cntFirstLine > delimiterCount && (cntNextLine == 0 || cntNextLine >= cntFirstLine))
{
delimiter = c;
delimiterCount = cntFirstLine;
}
}
}
// If the delimiter count is 0, we can't detect it and it is no valid CSV.
if (delimiterCount == 0)
{
throw new FormatException("Invalid CSV format: Failed to detect the delimiter.");
}
}
}
}

View File

@@ -127,7 +127,7 @@
<value>Clipboard data is not text</value>
</data>
<data name="OpenAINotConfigured" xml:space="preserve">
<value>To custom with AI not enabled</value>
<value>To custom with AI is not enabled</value>
</data>
<data name="OpenAIApiKeyUnauthorized" xml:space="preserve">
<value>Invalid API key or endpoint</value>
@@ -225,4 +225,7 @@
<data name="TermsLink.Text" xml:space="preserve">
<value>OpenAI Terms</value>
</data>
<data name="OpenAIGpoDisabled" xml:space="preserve">
<value>To custom with AI is disabled by your organization</value>
</data>
</root>

View File

@@ -16,7 +16,6 @@ using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using WinUIEx;
@@ -81,6 +80,13 @@ namespace AdvancedPaste.ViewModels
{
GetClipboardData();
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
IsCustomAIEnabled = false;
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
}
else
{
var openAIKey = AICompletionsHelper.LoadOpenAIKey();
var currentKey = aiHelper.GetKey();
bool keyChanged = openAIKey != currentKey;
@@ -108,6 +114,7 @@ namespace AdvancedPaste.ViewModels
{
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
}
}
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
GeneratedResponses.Clear();
@@ -146,7 +153,11 @@ namespace AdvancedPaste.ViewModels
{
app.GetMainWindow().ClearInputText();
if (!aiHelper.IsAIEnabled)
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
}
else if (!aiHelper.IsAIEnabled)
{
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
}

View File

@@ -127,7 +127,7 @@
<value>New profile</value>
</data>
<data name="ProfilesDescriptionLbl.Text" xml:space="preserve">
<value>You can create profiles to quickly apply a set of preconfigured variables</value>
<value>Create profiles to quickly apply a set of preconfigured variables. Profile variables have precedence over User and System variables.</value>
</data>
<data name="ProfilesLbl.Text" xml:space="preserve">
<value>Profiles</value>
@@ -194,7 +194,7 @@
<value>Add</value>
</data>
<data name="AppliedVariablesDescriptionLbl.Text" xml:space="preserve">
<value>List of applied variables</value>
<value>Applied variables list shows the current state of the environment, including Profile, User, and System variables.</value>
</data>
<data name="AppliedVariablesLbl.Text" xml:space="preserve">
<value>Applied variables</value>
@@ -242,7 +242,7 @@
<value>Add variable</value>
</data>
<data name="DefaultVariablesDescriptionLbl.Text" xml:space="preserve">
<value>Add, remove or edit USER and SYSTEM variables</value>
<value>Add, edit, or remove User and System variables.</value>
</data>
<data name="EditItem.Text" xml:space="preserve">
<value>Edit</value>

View File

@@ -47,6 +47,7 @@ namespace Hosts
services.AddSingleton<IHostsService, HostsService>();
services.AddSingleton<IUserSettings, Hosts.Settings.UserSettings>();
services.AddSingleton<IElevationHelper, ElevationHelper>();
services.AddSingleton<IDuplicateService, DuplicateService>();
// Views and ViewModels
services.AddSingleton<ILogger, LoggerWrapper>();

View File

@@ -8,7 +8,7 @@
xmlns:winuiex="using:WinUIEx"
x:Uid="Window"
Width="680"
MinWidth="480"
MinWidth="520"
MinHeight="320"
mc:Ignorable="d">
<Window.SystemBackdrop>

View File

@@ -5,7 +5,6 @@
using System;
using System.IO.Abstractions;
using System.Threading;
using HostsUILib.Helpers;
using HostsUILib.Settings;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
@@ -45,6 +44,8 @@ namespace Hosts.Settings
// Moved from Settings.UI.Library
public HostsEncoding Encoding { get; set; }
public event EventHandler LoopbackDuplicatesChanged;
public UserSettings()
{
_settingsUtils = new SettingsUtils();
@@ -58,8 +59,6 @@ namespace Hosts.Settings
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
}
public event EventHandler LoopbackDuplicatesChanged;
private void LoadSettingsFromJson()
{
lock (_loadingSettingsLock)

View File

@@ -0,0 +1,165 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using HostsUILib.Models;
using HostsUILib.Settings;
using Microsoft.UI.Dispatching;
namespace HostsUILib.Helpers
{
public class DuplicateService : IDuplicateService, IDisposable
{
private record struct Check(string Address, string[] Hosts);
private readonly IUserSettings _userSettings;
private readonly DispatcherQueue _dispatcherQueue;
private readonly Queue<Check> _checkQueue;
private readonly ManualResetEvent _checkEvent;
private readonly Thread _queueThread;
private readonly string[] _loopbackAddresses =
{
"0.0.0.0",
"::",
"::0",
"0:0:0:0:0:0:0:0",
"127.0.0.1",
"::1",
"0:0:0:0:0:0:0:1",
};
private ReadOnlyCollection<Entry> _entries;
private bool _disposed;
public DuplicateService(IUserSettings userSettings)
{
_userSettings = userSettings;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_checkQueue = new Queue<Check>();
_checkEvent = new ManualResetEvent(false);
_queueThread = new Thread(ProcessQueue);
_queueThread.IsBackground = true;
_queueThread.Start();
}
public void Initialize(IList<Entry> entries)
{
_entries = entries.AsReadOnly();
if (_checkQueue.Count > 0)
{
_checkQueue.Clear();
}
foreach (var entry in _entries)
{
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
{
continue;
}
_checkQueue.Enqueue(new Check(entry.Address, entry.SplittedHosts));
}
_checkEvent.Set();
}
public void CheckDuplicates(string address, string[] hosts)
{
_checkQueue.Enqueue(new Check(address, hosts));
_checkEvent.Set();
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void ProcessQueue()
{
while (true)
{
_checkEvent.WaitOne();
while (_checkQueue.Count > 0)
{
var check = _checkQueue.Dequeue();
FindDuplicates(check.Address, check.Hosts);
}
_checkEvent.Reset();
}
}
private void FindDuplicates(string address, string[] hosts)
{
var entries = _entries.Where(e =>
string.Equals(e.Address, address, StringComparison.OrdinalIgnoreCase)
|| hosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any());
foreach (var entry in entries)
{
SetDuplicate(entry);
}
}
private void SetDuplicate(Entry entry)
{
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
{
_dispatcherQueue.TryEnqueue(() =>
{
entry.Duplicate = false;
});
return;
}
var duplicate = false;
/*
* Duplicate are based on the following criteria:
* Entries with the same type and at least one host in common
* Entries with the same type and address, except when there is only one entry with less than 9 hosts for that type and address
*/
if (_entries.Any(e => e != entry
&& e.Type == entry.Type
&& entry.SplittedHosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any()))
{
duplicate = true;
}
else if (_entries.Any(e => e != entry
&& e.Type == entry.Type
&& string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase)))
{
duplicate = entry.SplittedHosts.Length < Consts.MaxHostsCount
&& _entries.Count(e => e.Type == entry.Type
&& string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase)
&& e.SplittedHosts.Length < Consts.MaxHostsCount) > 1;
}
_dispatcherQueue.TryEnqueue(() => entry.Duplicate = duplicate);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_checkEvent?.Dispose();
_disposed = true;
}
}
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using HostsUILib.Models;
namespace HostsUILib.Helpers
{
public interface IDuplicateService
{
void Initialize(IList<Entry> entries);
void CheckDuplicates(string address, string[] hosts);
}
}

View File

@@ -9,7 +9,7 @@ using HostsUILib.Models;
namespace HostsUILib.Helpers
{
public interface IHostsService : IDisposable
public interface IHostsService
{
string HostsFilePath { get; }

View File

@@ -412,23 +412,23 @@
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Entry">
<Grid
Margin="0"
AutomationProperties.Name="{x:Bind Address, Mode=OneWay}"
Background="Transparent"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="256" />
<ColumnDefinition Width="*" MinWidth="150" />
<!-- Address -->
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" MinWidth="120" />
<!-- Comment -->
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="20" />
<!-- Status -->
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="20" />
<!-- Duplicate -->
<ColumnDefinition Width="Auto" />
<!-- ToggleSwitch -->
<ColumnDefinition Width="Auto" />
<!-- DeleteEntry -->
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
@@ -511,7 +511,7 @@
Grid.Column="4"
Width="40"
MinWidth="0"
HorizontalAlignment="Right"
HorizontalAlignment="Center"
GotFocus="Entries_GotFocus"
IsOn="{x:Bind Active, Mode=TwoWay}"
OffContent=""
@@ -705,10 +705,13 @@
Padding="16,0"
HorizontalAlignment="Stretch"
AcceptsReturn="True"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollMode="Auto"
ScrollViewer.IsHorizontalRailEnabled="True"
ScrollViewer.IsVerticalRailEnabled="True"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.VerticalScrollMode="Enabled"
TextWrapping="Wrap" />
TextWrapping="NoWrap" />
</ContentDialog>
<TeachingTip

View File

@@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Net;
namespace HostsUILib.Settings
{

View File

@@ -8,7 +8,6 @@ using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -23,21 +22,14 @@ using static HostsUILib.Settings.IUserSettings;
namespace HostsUILib.ViewModels
{
public partial class MainViewModel : ObservableObject, IDisposable
public partial class MainViewModel : ObservableObject
{
private readonly IHostsService _hostsService;
private readonly IUserSettings _userSettings;
private readonly IDuplicateService _duplicateService;
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
private readonly string[] _loopbackAddresses =
{
"127.0.0.1",
"::1",
"0:0:0:0:0:0:0:1",
};
private bool _readingHosts;
private bool _disposed;
private CancellationTokenSource _tokenSource;
[ObservableProperty]
private Entry _selected;
@@ -95,10 +87,16 @@ namespace HostsUILib.ViewModels
private OpenSettingsFunction _openSettingsFunction;
public MainViewModel(IHostsService hostService, IUserSettings userSettings, ILogger logger, OpenSettingsFunction openSettingsFunction)
public MainViewModel(
IHostsService hostService,
IUserSettings userSettings,
IDuplicateService duplicateService,
ILogger logger,
OpenSettingsFunction openSettingsFunction)
{
_hostsService = hostService;
_userSettings = userSettings;
_duplicateService = duplicateService;
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
@@ -111,8 +109,7 @@ namespace HostsUILib.ViewModels
{
entry.PropertyChanged += Entry_PropertyChanged;
_entries.Add(entry);
FindDuplicates(entry.Address, entry.SplittedHosts);
_duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
}
public void Update(int index, Entry entry)
@@ -126,8 +123,8 @@ namespace HostsUILib.ViewModels
existingEntry.Hosts = entry.Hosts;
existingEntry.Active = entry.Active;
FindDuplicates(oldAddress, oldHosts);
FindDuplicates(entry.Address, entry.SplittedHosts);
_duplicateService.CheckDuplicates(oldAddress, oldHosts);
_duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
}
public void DeleteSelected()
@@ -135,8 +132,7 @@ namespace HostsUILib.ViewModels
var address = Selected.Address;
var hosts = Selected.SplittedHosts;
_entries.Remove(Selected);
FindDuplicates(address, hosts);
_duplicateService.CheckDuplicates(address, hosts);
}
public void UpdateAdditionalLines(string lines)
@@ -169,8 +165,7 @@ namespace HostsUILib.ViewModels
var address = entry.Address;
var hosts = entry.SplittedHosts;
_entries.Remove(entry);
FindDuplicates(address, hosts);
_duplicateService.CheckDuplicates(address, hosts);
}
}
@@ -213,9 +208,7 @@ namespace HostsUILib.ViewModels
});
_readingHosts = false;
_tokenSource?.Cancel();
_tokenSource = new CancellationTokenSource();
FindDuplicates(_tokenSource.Token);
_duplicateService.Initialize(_entries);
});
}
@@ -294,12 +287,6 @@ namespace HostsUILib.ViewModels
_ = Task.Run(SaveAsync);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Entry_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (Filtered && (e.PropertyName == nameof(Entry.Hosts)
@@ -326,82 +313,6 @@ namespace HostsUILib.ViewModels
_ = Task.Run(SaveAsync);
}
private void FindDuplicates(CancellationToken cancellationToken)
{
foreach (var entry in _entries)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
{
continue;
}
SetDuplicate(entry);
}
catch (OperationCanceledException)
{
LoggerInstance.Logger.LogInfo("FindDuplicates cancelled");
return;
}
}
}
private void FindDuplicates(string address, IEnumerable<string> hosts)
{
var entries = _entries.Where(e =>
string.Equals(e.Address, address, StringComparison.OrdinalIgnoreCase)
|| hosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any());
foreach (var entry in entries)
{
SetDuplicate(entry);
}
}
private void SetDuplicate(Entry entry)
{
if (!_userSettings.LoopbackDuplicates && _loopbackAddresses.Contains(entry.Address))
{
_dispatcherQueue.TryEnqueue(() =>
{
entry.Duplicate = false;
});
return;
}
var duplicate = false;
/*
* Duplicate are based on the following criteria:
* Entries with the same type and at least one host in common
* Entries with the same type and address, except when there is only one entry with less than 9 hosts for that type and address
*/
if (_entries.Any(e => e != entry
&& e.Type == entry.Type
&& entry.SplittedHosts.Intersect(e.SplittedHosts, StringComparer.OrdinalIgnoreCase).Any()))
{
duplicate = true;
}
else if (_entries.Any(e => e != entry
&& e.Type == entry.Type
&& string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase)))
{
duplicate = entry.SplittedHosts.Length < Consts.MaxHostsCount
&& _entries.Count(e => e.Type == entry.Type
&& string.Equals(e.Address, entry.Address, StringComparison.OrdinalIgnoreCase)
&& e.SplittedHosts.Length < Consts.MaxHostsCount) > 1;
}
_dispatcherQueue.TryEnqueue(() =>
{
entry.Duplicate = duplicate;
});
}
private async Task SaveAsync()
{
bool error = true;
@@ -444,17 +355,5 @@ namespace HostsUILib.ViewModels
IsReadOnly = isReadOnly;
});
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_hostsService?.Dispose();
_disposed = true;
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
@@ -141,7 +141,7 @@
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
@@ -152,7 +152,7 @@
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.231216.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240311000\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.targets'))" />
</Target>
</Project>

View File

@@ -3,5 +3,5 @@
<package id="Microsoft.Windows.CppWinRT" version="2.0.240111.5" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231216.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22621.2428" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.5.240311000" targetFramework="native" />
<package id="Microsoft.WindowsAppSDK" version="1.5.240428000" targetFramework="native" />
</packages>

View File

@@ -82,9 +82,9 @@ private:
{
Logger::info("MeasureTool is going to use default shortcut");
m_hotkey.win = true;
m_hotkey.ctrl = true;
m_hotkey.alt = false;
m_hotkey.shift = true;
m_hotkey.ctrl = false;
m_hotkey.key = 'M';
}
}

View File

@@ -65,6 +65,7 @@ protected:
bool m_destroyed = false;
FindMyMouseActivationMethod m_activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
bool m_includeWinKey = FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY;
bool m_doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
int m_sonarRadius = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS;
int m_sonarZoomFactor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM;
@@ -146,6 +147,7 @@ private:
void OnMouseTimer();
void DetectShake();
bool KeyboardInputCanActivate();
void StartSonar();
void StopSonar();
@@ -352,7 +354,7 @@ void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
break;
case SonarState::ControlUp1:
if (pressed)
if (pressed && KeyboardInputCanActivate())
{
auto now = GetTickCount64();
auto doubleClickInterval = now - m_lastKeyTime;
@@ -438,6 +440,12 @@ void SuperSonar<D>::DetectShake()
}
template<typename D>
bool SuperSonar<D>::KeyboardInputCanActivate()
{
return !m_includeWinKey || (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000);
}
template<typename D>
void SuperSonar<D>::OnSonarMouseInput(RAWINPUT const& input)
{
@@ -762,6 +770,7 @@ public:
m_backgroundColor = settings.backgroundColor;
m_spotlightColor = settings.spotlightColor;
m_activationMethod = settings.activationMethod;
m_includeWinKey = settings.includeWinKey;
m_doNotActivateOnGameMode = settings.doNotActivateOnGameMode;
m_fadeDuration = settings.animationDurationMs > 0 ? settings.animationDurationMs : 1;
m_finalAlphaNumerator = settings.overlayOpacity;
@@ -791,6 +800,7 @@ public:
m_backgroundColor = localSettings.backgroundColor;
m_spotlightColor = localSettings.spotlightColor;
m_activationMethod = localSettings.activationMethod;
m_includeWinKey = localSettings.includeWinKey;
m_doNotActivateOnGameMode = localSettings.doNotActivateOnGameMode;
m_fadeDuration = localSettings.animationDurationMs > 0 ? localSettings.animationDurationMs : 1;
m_finalAlphaNumerator = localSettings.overlayOpacity;

View File

@@ -18,6 +18,7 @@ constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_RADIUS = 100;
constexpr int FIND_MY_MOUSE_DEFAULT_ANIMATION_DURATION_MS = 500;
constexpr int FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_INITIAL_ZOOM = 9;
constexpr FindMyMouseActivationMethod FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD = FindMyMouseActivationMethod::DoubleLeftControlKey;
constexpr bool FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY = false;
constexpr int FIND_MY_MOUSE_DEFAULT_SHAKE_MINIMUM_DISTANCE = 1000;
constexpr int FIND_MY_MOUSE_DEFAULT_SHAKE_INTERVAL_MS = 1000;
constexpr int FIND_MY_MOUSE_DEFAULT_SHAKE_FACTOR = 400; // 400 percent
@@ -25,6 +26,7 @@ constexpr int FIND_MY_MOUSE_DEFAULT_SHAKE_FACTOR = 400; // 400 percent
struct FindMyMouseSettings
{
FindMyMouseActivationMethod activationMethod = FIND_MY_MOUSE_DEFAULT_ACTIVATION_METHOD;
bool includeWinKey = FIND_MY_MOUSE_DEFAULT_INCLUDE_WIN_KEY;
bool doNotActivateOnGameMode = FIND_MY_MOUSE_DEFAULT_DO_NOT_ACTIVATE_ON_GAME_MODE;
winrt::Windows::UI::Color backgroundColor = FIND_MY_MOUSE_DEFAULT_BACKGROUND_COLOR;
winrt::Windows::UI::Color spotlightColor = FIND_MY_MOUSE_DEFAULT_SPOTLIGHT_COLOR;

View File

@@ -14,6 +14,7 @@ namespace
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_ACTIVATION_METHOD[] = L"activation_method";
const wchar_t JSON_KEY_INCLUDE_WIN_KEY[] = L"include_win_key";
const wchar_t JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE[] = L"do_not_activate_on_game_mode";
const wchar_t JSON_KEY_BACKGROUND_COLOR[] = L"background_color";
const wchar_t JSON_KEY_SPOTLIGHT_COLOR[] = L"spotlight_color";
@@ -237,6 +238,15 @@ void FindMyMouse::parse_settings(PowerToysSettings::PowerToyValues& settings)
Logger::warn("Failed to initialize Activation Method from settings. Will use default value");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_INCLUDE_WIN_KEY);
findMyMouseSettings.includeWinKey = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to get 'include windows key with ctrl' setting");
}
try
{
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_DO_NOT_ACTIVATE_ON_GAME_MODE);
findMyMouseSettings.doNotActivateOnGameMode = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);

View File

@@ -0,0 +1,152 @@
// 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.Drawing;
using System.Linq;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Common.Helpers;
using MouseJumpUI.Common.Imaging;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Styles;
using MouseJumpUI.Helpers;
namespace MouseJumpUI.UnitTests.Common.Helpers;
[TestClass]
public static class DrawingHelperTests
{
[TestClass]
public sealed class GetPreviewLayoutTests
{
public sealed class TestCase
{
public TestCase(PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation, string desktopImageFilename, string expectedImageFilename)
{
this.PreviewStyle = previewStyle;
this.Screens = screens;
this.ActivatedLocation = activatedLocation;
this.DesktopImageFilename = desktopImageFilename;
this.ExpectedImageFilename = expectedImageFilename;
}
public PreviewStyle PreviewStyle { get; }
public List<RectangleInfo> Screens { get; }
public PointInfo ActivatedLocation { get; }
public string DesktopImageFilename { get; }
public string ExpectedImageFilename { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
/* 4-grid */
yield return new object[]
{
new TestCase(
previewStyle: StyleHelper.DefaultPreviewStyle,
screens: new List<RectangleInfo>()
{
new(0, 0, 500, 500),
new(500, 0, 500, 500),
new(500, 500, 500, 500),
new(0, 500, 500, 500),
},
activatedLocation: new(x: 50, y: 50),
desktopImageFilename: "Common/Helpers/_test-4grid-desktop.png",
expectedImageFilename: "Common/Helpers/_test-4grid-expected.png"),
};
/* win 11 */
yield return new object[]
{
new TestCase(
previewStyle: StyleHelper.DefaultPreviewStyle,
screens: new List<RectangleInfo>()
{
new(5120, 349, 1920, 1080),
new(0, 0, 5120, 1440),
},
activatedLocation: new(x: 50, y: 50),
desktopImageFilename: "Common/Helpers/_test-win11-desktop.png",
expectedImageFilename: "Common/Helpers/_test-win11-expected.png"),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
// load the fake desktop image
using var desktopImage = GetPreviewLayoutTests.LoadImageResource(data.DesktopImageFilename);
// draw the preview image
var previewLayout = LayoutHelper.GetPreviewLayout(
previewStyle: data.PreviewStyle,
screens: data.Screens,
activatedLocation: data.ActivatedLocation);
var imageCopyService = new StaticImageRegionCopyService(desktopImage);
using var actual = DrawingHelper.RenderPreview(previewLayout, imageCopyService);
// load the expected image
var expected = GetPreviewLayoutTests.LoadImageResource(data.ExpectedImageFilename);
// compare the images
var screens = System.Windows.Forms.Screen.AllScreens;
AssertImagesEqual(expected, actual);
}
private static Bitmap LoadImageResource(string filename)
{
var assembly = Assembly.GetExecutingAssembly();
var assemblyName = new AssemblyName(assembly.FullName ?? throw new InvalidOperationException());
var resourceName = $"Microsoft.{assemblyName.Name}.{filename.Replace("/", ".")}";
var resourceNames = assembly.GetManifestResourceNames();
if (!resourceNames.Contains(resourceName))
{
throw new InvalidOperationException($"Embedded resource '{resourceName}' does not exist.");
}
var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException();
var image = (Bitmap)Image.FromStream(stream);
return image;
}
/// <summary>
/// Naive / brute force image comparison - we can optimise this later :-)
/// </summary>
private static void AssertImagesEqual(Bitmap expected, Bitmap actual)
{
Assert.AreEqual(
expected.Width,
actual.Width,
$"expected width: {expected.Width}, actual width: {actual.Width}");
Assert.AreEqual(
expected.Height,
actual.Height,
$"expected height: {expected.Height}, actual height: {actual.Height}");
for (var y = 0; y < expected.Height; y++)
{
for (var x = 0; x < expected.Width; x++)
{
var expectedPixel = expected.GetPixel(x, y);
var actualPixel = actual.GetPixel(x, y);
// allow a small tolerance for rounding differences in gdi
Assert.IsTrue(
(Math.Abs(expectedPixel.A - actualPixel.A) <= 1) &&
(Math.Abs(expectedPixel.R - actualPixel.R) <= 1) &&
(Math.Abs(expectedPixel.G - actualPixel.G) <= 1) &&
(Math.Abs(expectedPixel.B - actualPixel.B) <= 1),
$"images differ at pixel ({x}, {y}) - expected: {expectedPixel}, actual: {actualPixel}");
}
}
}
}
}

View File

@@ -0,0 +1,452 @@
// 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.Drawing;
using System.Text.Json;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Common.Helpers;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Layout;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.UnitTests.Common.Helpers;
[TestClass]
public static class LayoutHelperTests
{
/*
[TestClass]
public sealed class OldLayoutTests
{
public static IEnumerable<object[]> GetTestCases()
{
// check we handle rounding errors in scaling the preview form
// that might make the form *larger* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1014 / 7168) * 7168 = 1014.0000000000000000000000002
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7168, 1440),
screens: new List<ScreenInfo>
{
new(HMONITOR.Null, false, new(6144, 0, 1024, 768), new(6144, 0, 1024, 768)),
new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6656, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(6144, 277.14732M, 1024, 213.70535M),
previewBounds: new(0, 0, 1014, 203.70535M),
screenBounds: new List<RectangleInfo>
{
new(869.14285M, 0, 144.85714M, 108.642857M),
new(0, 0, 869.142857M, 203.705357M),
},
activatedScreenBounds: new(6144, 0, 1024, 768));
yield return new object[] { new TestCase(layoutConfig, layoutInfo) };
// check we handle rounding errors in scaling the preview form
// that might make the form a pixel *smaller* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1280 / 7424) * 7424 = 1279.9999999999999999999999999
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7424, 1440),
screens: new List<ScreenInfo>
{
new(HMONITOR.Null, false, new(6144, 0, 1280, 768), new(6144, 0, 1280, 768)),
new(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6784, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(
6144,
255.83189M, // (768 - (((decimal)(1280-10) / 7424 * 1440) + 10)) / 2
1280,
256.33620M // ((decimal)(1280 - 10) / 7424 * 1440) + 10
),
previewBounds: new(0, 0, 1270, 246.33620M),
screenBounds: new List<RectangleInfo>
{
new(1051.03448M, 0, 218.96551M, 131.37931M),
new(0, 0M, 1051.03448M, 246.33620M),
},
activatedScreenBounds: new(6144, 0, 1280, 768));
yield return new object[] { new TestCase(layoutConfig, layoutInfo) };
}
}
*/
[TestClass]
public sealed class GetPreviewLayoutTests
{
public sealed class TestCase
{
public TestCase(PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation, PreviewLayout expectedResult)
{
this.PreviewStyle = previewStyle;
this.Screens = screens;
this.ActivatedLocation = activatedLocation;
this.ExpectedResult = expectedResult;
}
public PreviewStyle PreviewStyle { get; }
public List<RectangleInfo> Screens { get; }
public PointInfo ActivatedLocation { get; }
public PreviewLayout ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// happy path - single screen with 50% scaling,
// *has* a preview borders but *no* screenshot borders
//
// +----------------+
// | |
// | 0 |
// | |
// +----------------+
var previewStyle = new PreviewStyle(
canvasSize: new(
width: 524,
height: 396
),
canvasStyle: new(
marginStyle: MarginStyle.Empty,
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: new(
all: 1),
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
),
screenStyle: BoxStyle.Empty);
var screens = new List<RectangleInfo>
{
new(0, 0, 1024, 768),
};
var activatedLocation = new PointInfo(512, 384);
var previewLayout = new PreviewLayout(
virtualScreen: new(0, 0, 1024, 768),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(250, 186, 524, 396),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 524, 396),
marginBounds: new(0, 0, 524, 396),
borderBounds: new(0, 0, 524, 396),
paddingBounds: new(5, 5, 514, 386),
contentBounds: new(6, 6, 512, 384)
),
screenshotBounds: new()
{
new(
outerBounds: new(6, 6, 512, 384),
marginBounds: new(6, 6, 512, 384),
borderBounds: new(6, 6, 512, 384),
paddingBounds: new(6, 6, 512, 384),
contentBounds: new(6, 6, 512, 384)
),
});
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) };
// happy path - single screen with 50% scaling,
// *no* preview borders but *has* screenshot borders
//
// +----------------+
// | |
// | 0 |
// | |
// +----------------+
previewStyle = new PreviewStyle(
canvasSize: new(
width: 512,
height: 384
),
canvasStyle: BoxStyle.Empty,
screenStyle: new(
marginStyle: new(
all: 1),
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: PaddingStyle.Empty,
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
));
screens = new List<RectangleInfo>
{
new(0, 0, 1024, 768),
};
activatedLocation = new PointInfo(512, 384);
previewLayout = new PreviewLayout(
virtualScreen: new(0, 0, 1024, 768),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(256, 192, 512, 384),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 512, 384),
marginBounds: new(0, 0, 512, 384),
borderBounds: new(0, 0, 512, 384),
paddingBounds: new(0, 0, 512, 384),
contentBounds: new(0, 0, 512, 384)
),
screenshotBounds: new()
{
new(
outerBounds: new(0, 0, 512, 384),
marginBounds: new(0, 0, 512, 384),
borderBounds: new(1, 1, 510, 382),
paddingBounds: new(6, 6, 500, 372),
contentBounds: new(6, 6, 500, 372)
),
});
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) };
// primary monitor not topmost / leftmost - if there are screens
// that are further left or higher up than the primary monitor
// they'll have negative coordinates which has caused some
// issues with calculations in the past. this test will make
// sure we handle screens with negative coordinates gracefully
//
// +-------+
// | 0 +----------------+
// +-------+ |
// | 1 |
// | |
// +----------------+
previewStyle = new PreviewStyle(
canvasSize: new(
width: 716,
height: 204
),
canvasStyle: new(
marginStyle: MarginStyle.Empty,
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: new(
all: 1),
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
),
screenStyle: new(
marginStyle: new(
all: 1),
borderStyle: new(
color: SystemColors.Highlight,
all: 5,
depth: 3),
paddingStyle: PaddingStyle.Empty,
backgroundStyle: new(
color1: Color.FromArgb(13, 87, 210), // light blue
color2: Color.FromArgb(3, 68, 192) // darker blue
)
));
screens = new List<RectangleInfo>
{
new(-1920, -480, 1920, 1080),
new(0, 0, 5120, 1440),
};
activatedLocation = new(-960, 60);
previewLayout = new PreviewLayout(
virtualScreen: new(-1920, -480, 7040, 1920),
screens: screens,
activatedScreenIndex: 0,
formBounds: new(-1318, -42, 716, 204),
previewStyle: previewStyle,
previewBounds: new(
outerBounds: new(0, 0, 716, 204),
marginBounds: new(0, 0, 716, 204),
borderBounds: new(0, 0, 716, 204),
paddingBounds: new(5, 5, 706, 194),
contentBounds: new(6, 6, 704, 192)
),
screenshotBounds: new()
{
new(
outerBounds: new(6, 6, 192, 108),
marginBounds: new(6, 6, 192, 108),
borderBounds: new(7, 7, 190, 106),
paddingBounds: new(12, 12, 180, 96),
contentBounds: new(12, 12, 180, 96)
),
new(
outerBounds: new(198, 54, 512, 144),
marginBounds: new(198, 54, 512, 144),
borderBounds: new(199, 55, 510, 142),
paddingBounds: new(204, 60, 500, 132),
contentBounds: new(204, 60, 500, 132)
),
});
yield return new object[] { new TestCase(previewStyle, screens, activatedLocation, previewLayout) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
// note - even if values are within 0.0001M of each other they could
// still round to different values - e.g.
// (int)1279.999999999999 -> 1279
// vs
// (int)1280.000000000000 -> 1280
// so we'll compare the raw values, *and* convert to an int-based
// Rectangle to compare rounded values
var actual = LayoutHelper.GetPreviewLayout(data.PreviewStyle, data.Screens, data.ActivatedLocation);
var expected = data.ExpectedResult;
var options = new JsonSerializerOptions
{
WriteIndented = true,
};
Assert.AreEqual(
JsonSerializer.Serialize(expected, options),
JsonSerializer.Serialize(actual, options));
}
}
[TestClass]
public sealed class GetBoxBoundsFromContentBoundsTests
{
public sealed class TestCase
{
public TestCase(RectangleInfo contentBounds, BoxStyle boxStyle, BoxBounds expectedResult)
{
this.ContentBounds = contentBounds;
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
}
public RectangleInfo ContentBounds { get; set; }
public BoxStyle BoxStyle { get; set; }
public BoxBounds ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new[]
{
new TestCase(
contentBounds: new(100, 100, 800, 600),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = LayoutHelper.GetBoxBoundsFromContentBounds(data.ContentBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
}
}
[TestClass]
public sealed class GetBoxBoundsFromOuterBoundsTests
{
public sealed class TestCase
{
public TestCase(RectangleInfo outerBounds, BoxStyle boxStyle, BoxBounds expectedResult)
{
this.OuterBounds = outerBounds;
this.BoxStyle = boxStyle;
this.ExpectedResult = expectedResult;
}
public RectangleInfo OuterBounds { get; set; }
public BoxStyle BoxStyle { get; set; }
public BoxBounds ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
yield return new[]
{
new TestCase(
outerBounds: new(85, 85, 830, 630),
boxStyle: new(
marginStyle: new(3),
borderStyle: new(Color.Red, 5, 0),
paddingStyle: new(7),
backgroundStyle: BackgroundStyle.Empty),
expectedResult: new(
outerBounds: new(85, 85, 830, 630),
marginBounds: new(85, 85, 830, 630),
borderBounds: new(88, 88, 824, 624),
paddingBounds: new(93, 93, 814, 614),
contentBounds: new(100, 100, 800, 600))),
};
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = LayoutHelper.GetBoxBoundsFromOuterBounds(data.OuterBounds, data.BoxStyle);
var expected = data.ExpectedResult;
Assert.AreEqual(
JsonSerializer.Serialize(expected),
JsonSerializer.Serialize(actual));
}
}
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Common.Helpers;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.UnitTests.Common.Helpers;
[TestClass]
public static class MouseHelperTests
{
[TestClass]
public sealed class GetJumpLocationTests
{
public sealed class TestCase
{
public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult)
{
this.PreviewLocation = previewLocation;
this.PreviewSize = previewSize;
this.DesktopBounds = desktopBounds;
this.ExpectedResult = expectedResult;
}
public PointInfo PreviewLocation { get; }
public SizeInfo PreviewSize { get; }
public RectangleInfo DesktopBounds { get; }
public PointInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// screen corners and midpoint with a zero origin
yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) };
yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) };
yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) };
yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) };
yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) };
// screen corners and midpoint with a positive origin
yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) };
yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) };
yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) };
yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) };
yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) };
// screen corners and midpoint with a negative origin
yield return new object[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) };
yield return new object[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) };
yield return new object[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) };
yield return new object[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) };
yield return new object[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = MouseHelper.GetJumpLocation(
data.PreviewLocation,
data.PreviewSize,
data.DesktopBounds);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.X, actual.X);
Assert.AreEqual(expected.Y, actual.Y);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

@@ -4,9 +4,9 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.UnitTests.Models.Drawing;
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
[TestClass]
public static class RectangleInfoTests
@@ -23,30 +23,30 @@ public static class RectangleInfoTests
this.ExpectedResult = expectedResult;
}
public RectangleInfo Rectangle { get; set; }
public RectangleInfo Rectangle { get; }
public PointInfo Point { get; set; }
public PointInfo Point { get; }
public RectangleInfo ExpectedResult { get; set; }
public RectangleInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// zero-sized
yield return new[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
yield return new object[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
// zero-origin
yield return new[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
yield return new object[] { new TestCase(new(0, 0, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new object[] { new TestCase(new(0, 0, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new object[] { new TestCase(new(0, 0, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
// non-zero origin
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(100, 100), new(0, 0, 200, 200)), };
yield return new object[] { new TestCase(new(1000, 2000, 200, 200), new(500, 500), new(400, 400, 200, 200)), };
yield return new object[] { new TestCase(new(1000, 2000, 800, 600), new(1000, 1000), new(600, 700, 800, 600)), };
// negative result
yield return new[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
yield return new object[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
}
[TestMethod]
@@ -74,53 +74,53 @@ public static class RectangleInfoTests
this.ExpectedResult = expectedResult;
}
public RectangleInfo Inner { get; set; }
public RectangleInfo Inner { get; }
public RectangleInfo Outer { get; set; }
public RectangleInfo Outer { get; }
public RectangleInfo ExpectedResult { get; set; }
public RectangleInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// already inside - obj fills bounds exactly
yield return new[]
yield return new object[]
{
new TestCase(new(0, 0, 100, 100), new(0, 0, 100, 100), new(0, 0, 100, 100)),
};
// already inside - obj exactly in each corner
yield return new[]
yield return new object[]
{
new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(100, 100, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
};
// move inside - obj outside each corner
yield return new[]
yield return new object[]
{
new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
};
yield return new[]
yield return new object[]
{
new TestCase(new(150, 150, 100, 100), new(0, 0, 200, 200), new(100, 100, 100, 100)),
};

View File

@@ -4,9 +4,9 @@
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.UnitTests.Drawing;
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
[TestClass]
public static class SizeInfoTests
@@ -23,28 +23,28 @@ public static class SizeInfoTests
this.ExpectedResult = expectedResult;
}
public SizeInfo Obj { get; set; }
public SizeInfo Obj { get; }
public SizeInfo Bounds { get; set; }
public SizeInfo Bounds { get; }
public SizeInfo ExpectedResult { get; set; }
public SizeInfo ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// identity tests
yield return new[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), };
yield return new object[] { new TestCase(new(512, 384), new(512, 384), new(512, 384)), };
yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), new(1024, 768)), };
// general tests
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), new(2048, 1536)), };
yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), new(1024, 768)), };
// scale to fit width
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
// scale to fit height
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
}
[TestMethod]
@@ -70,28 +70,28 @@ public static class SizeInfoTests
this.ExpectedResult = expectedResult;
}
public SizeInfo Obj { get; set; }
public SizeInfo Obj { get; }
public SizeInfo Bounds { get; set; }
public SizeInfo Bounds { get; }
public decimal ExpectedResult { get; set; }
public decimal ExpectedResult { get; }
}
public static IEnumerable<object[]> GetTestCases()
{
// identity tests
yield return new[] { new TestCase(new(512, 384), new(512, 384), 1), };
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
yield return new object[] { new TestCase(new(512, 384), new(512, 384), 1), };
yield return new object[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
// general tests
yield return new[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
yield return new[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 1536), 4), };
yield return new object[] { new TestCase(new(2048, 1536), new(1024, 768), 0.5M), };
// scale to fit width
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
// scale to fit height
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
}
[TestMethod]

View File

@@ -1,229 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Helpers;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Layout;
using MouseJumpUI.Models.Screen;
using static MouseJumpUI.NativeMethods.Core;
namespace MouseJumpUI.UnitTests.Helpers;
[TestClass]
public static class DrawingHelperTests
{
[TestClass]
public sealed class CalculateLayoutInfoTests
{
public sealed class TestCase
{
public TestCase(LayoutConfig layoutConfig, LayoutInfo expectedResult)
{
this.LayoutConfig = layoutConfig;
this.ExpectedResult = expectedResult;
}
public LayoutConfig LayoutConfig { get; set; }
public LayoutInfo ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
// happy path - check the preview form is shown
// at the correct size and position on a single screen
//
// +----------------+
// | |
// | 0 |
// | |
// +----------------+
var layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 5120, 1440),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 5120, 1440), new(0, 0, 5120, 1440)),
},
activatedLocation: new(5120 / 2, 1440 / 2),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
var layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(1760, 491.40625M, 1600, 457.1875M),
previewBounds: new(0, 0, 1590, 447.1875M),
screenBounds: new List<RectangleInfo>
{
new(0, 0, 1590, 447.1875M),
},
activatedScreenBounds: new(0, 0, 5120, 1440));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
// primary monitor not topmost / leftmost - if there are screens
// that are further left or higher than the primary monitor
// they'll have negative coordinates which has caused some
// issues with calculations in the past. this test will make
// sure we handle negative coordinates gracefully
//
// +-------+
// | 0 +----------------+
// +-------+ |
// | 1 |
// | |
// +----------------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(-1920, -472, 7040, 1912),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(-1920, -472, 1920, 1080), new(-1920, -472, 1920, 1080)),
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 5120, 1440), new(0, 0, 5120, 1440)),
},
activatedLocation: new(-960, -236),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(
-1760,
-456.91477M, // -236 - (((decimal)(1600-10) / 7040 * 1912) + 10) / 2
1600,
441.829545M // ((decimal)(1600-10) / 7040 * 1912) + 10
),
previewBounds: new(0, 0, 1590, 431.829545M),
screenBounds: new List<RectangleInfo>
{
new(0, 0, 433.63636M, 243.92045M),
new(433.63636M, 106.602270M, 1156.36363M, 325.22727M),
},
activatedScreenBounds: new(-1920, -472, 1920, 1080));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
// check we handle rounding errors in scaling the preview form
// that might make the form *larger* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1014 / 7168) * 7168 = 1014.0000000000000000000000002
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7168, 1440),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(6144, 0, 1024, 768), new(6144, 0, 1024, 768)),
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6656, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(6144, 277.14732M, 1024, 213.70535M),
previewBounds: new(0, 0, 1014, 203.70535M),
screenBounds: new List<RectangleInfo>
{
new(869.14285M, 0, 144.85714M, 108.642857M),
new(0, 0, 869.142857M, 203.705357M),
},
activatedScreenBounds: new(6144, 0, 1024, 768));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
// check we handle rounding errors in scaling the preview form
// that might make the form a pixel *smaller* than the current screen -
// e.g. a desktop 7168 x 1440 scaled to a screen 1024 x 768
// with a 5px form padding border:
//
// ((decimal)1280 / 7424) * 7424 = 1279.9999999999999999999999999
//
// +----------------+
// | |
// | 1 +-------+
// | | 0 |
// +----------------+-------+
layoutConfig = new LayoutConfig(
virtualScreenBounds: new(0, 0, 7424, 1440),
screens: new List<ScreenInfo>
{
new ScreenInfo(HMONITOR.Null, false, new(6144, 0, 1280, 768), new(6144, 0, 1280, 768)),
new ScreenInfo(HMONITOR.Null, false, new(0, 0, 6144, 1440), new(0, 0, 6144, 1440)),
},
activatedLocation: new(6784, 384),
activatedScreenIndex: 0,
activatedScreenNumber: 1,
maximumFormSize: new(1600, 1200),
formPadding: new(5, 5, 5, 5),
previewPadding: new(0, 0, 0, 0));
layoutInfo = new LayoutInfo(
layoutConfig: layoutConfig,
formBounds: new(
6144,
255.83189M, // (768 - (((decimal)(1280-10) / 7424 * 1440) + 10)) / 2
1280,
256.33620M // ((decimal)(1280 - 10) / 7424 * 1440) + 10
),
previewBounds: new(0, 0, 1270, 246.33620M),
screenBounds: new List<RectangleInfo>
{
new(1051.03448M, 0, 218.96551M, 131.37931M),
new(0, 0M, 1051.03448M, 246.33620M),
},
activatedScreenBounds: new(6144, 0, 1280, 768));
yield return new[] { new TestCase(layoutConfig, layoutInfo) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
// note - even if values are within 0.0001M of each other they could
// still round to different values - e.g.
// (int)1279.999999999999 -> 1279
// vs
// (int)1280.000000000000 -> 1280
// so we'll compare the raw values, *and* convert to an int-based
// Rectangle to compare rounded values
var actual = LayoutHelper.CalculateLayoutInfo(data.LayoutConfig);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.FormBounds.X, actual.FormBounds.X, 0.00001M, "FormBounds.X");
Assert.AreEqual(expected.FormBounds.Y, actual.FormBounds.Y, 0.00001M, "FormBounds.Y");
Assert.AreEqual(expected.FormBounds.Width, actual.FormBounds.Width, 0.00001M, "FormBounds.Width");
Assert.AreEqual(expected.FormBounds.Height, actual.FormBounds.Height, 0.00001M, "FormBounds.Height");
Assert.AreEqual(expected.FormBounds.ToRectangle(), actual.FormBounds.ToRectangle(), "FormBounds.ToRectangle");
Assert.AreEqual(expected.PreviewBounds.X, actual.PreviewBounds.X, 0.00001M, "PreviewBounds.X");
Assert.AreEqual(expected.PreviewBounds.Y, actual.PreviewBounds.Y, 0.00001M, "PreviewBounds.Y");
Assert.AreEqual(expected.PreviewBounds.Width, actual.PreviewBounds.Width, 0.00001M, "PreviewBounds.Width");
Assert.AreEqual(expected.PreviewBounds.Height, actual.PreviewBounds.Height, 0.00001M, "PreviewBounds.Height");
Assert.AreEqual(expected.PreviewBounds.ToRectangle(), actual.PreviewBounds.ToRectangle(), "PreviewBounds.ToRectangle");
Assert.AreEqual(expected.ScreenBounds.Count, actual.ScreenBounds.Count, "ScreenBounds.Count");
for (var i = 0; i < expected.ScreenBounds.Count; i++)
{
Assert.AreEqual(expected.ScreenBounds[i].X, actual.ScreenBounds[i].X, 0.00001M, $"ScreenBounds[{i}].X");
Assert.AreEqual(expected.ScreenBounds[i].Y, actual.ScreenBounds[i].Y, 0.00001M, $"ScreenBounds[{i}].Y");
Assert.AreEqual(expected.ScreenBounds[i].Width, actual.ScreenBounds[i].Width, 0.00001M, $"ScreenBounds[{i}].Width");
Assert.AreEqual(expected.ScreenBounds[i].Height, actual.ScreenBounds[i].Height, 0.00001M, $"ScreenBounds[{i}].Height");
Assert.AreEqual(expected.ScreenBounds[i].ToRectangle(), actual.ScreenBounds[i].ToRectangle(), "ActivatedScreen.ToRectangle");
}
Assert.AreEqual(expected.ActivatedScreenBounds.X, actual.ActivatedScreenBounds.X, "ActivatedScreen.X");
Assert.AreEqual(expected.ActivatedScreenBounds.Y, actual.ActivatedScreenBounds.Y, "ActivatedScreen.Y");
Assert.AreEqual(expected.ActivatedScreenBounds.Width, actual.ActivatedScreenBounds.Width, "ActivatedScreen.Width");
Assert.AreEqual(expected.ActivatedScreenBounds.Height, actual.ActivatedScreenBounds.Height, "ActivatedScreen.Height");
Assert.AreEqual(expected.ActivatedScreenBounds.ToRectangle(), actual.ActivatedScreenBounds.ToRectangle(), "ActivatedScreen.ToRectangle");
}
}
}

View File

@@ -1,74 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.Helpers;
using MouseJumpUI.Models.Drawing;
namespace MouseJumpUI.UnitTests.Helpers;
[TestClass]
public static class MouseHelperTests
{
[TestClass]
public sealed class GetJumpLocationTests
{
public sealed class TestCase
{
public TestCase(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds, PointInfo expectedResult)
{
this.PreviewLocation = previewLocation;
this.PreviewSize = previewSize;
this.DesktopBounds = desktopBounds;
this.ExpectedResult = expectedResult;
}
public PointInfo PreviewLocation { get; set; }
public SizeInfo PreviewSize { get; set; }
public RectangleInfo DesktopBounds { get; set; }
public PointInfo ExpectedResult { get; set; }
}
public static IEnumerable<object[]> GetTestCases()
{
// screen corners and midpoint with a zero origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(0, 0, 1600, 1200), new(0, 0)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(0, 0, 1600, 1200), new(1600, 0)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(0, 0, 1600, 1200), new(0, 1200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(0, 0, 1600, 1200), new(1600, 1200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(0, 0, 1600, 1200), new(800, 600)) };
// screen corners and midpoint with a positive origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(1000, 2200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(1000, 1000, 1600, 1200), new(2600, 2200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(1000, 1000, 1600, 1200), new(1800, 1600)) };
// screen corners and midpoint with a negative origin
yield return new[] { new TestCase(new(0, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, -1000)) };
yield return new[] { new TestCase(new(160, 0), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, -1000)) };
yield return new[] { new TestCase(new(0, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(-1000, 200)) };
yield return new[] { new TestCase(new(160, 120), new(160, 120), new(-1000, -1000, 1600, 1200), new(600, 200)) };
yield return new[] { new TestCase(new(80, 60), new(160, 120), new(-1000, -1000, 1600, 1200), new(-200, -400)) };
}
[TestMethod]
[DynamicData(nameof(GetTestCases), DynamicDataSourceType.Method)]
public void RunTestCases(TestCase data)
{
var actual = MouseHelper.GetJumpLocation(
data.PreviewLocation,
data.PreviewSize,
data.DesktopBounds);
var expected = data.ExpectedResult;
Assert.AreEqual(expected.X, actual.X);
Assert.AreEqual(expected.Y, actual.Y);
}
}
}

View File

@@ -28,6 +28,13 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Common\Helpers\_test-4grid-desktop.png" />
<EmbeddedResource Include="Common\Helpers\_test-4grid-expected.png" />
<EmbeddedResource Include="Common\Helpers\_test-win11-desktop.png" />
<EmbeddedResource Include="Common\Helpers\_test-win11-expected.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MouseJumpUI\MouseJumpUI.csproj" />
</ItemGroup>

View File

@@ -0,0 +1,248 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Linq;
using MouseJumpUI.Common.Imaging;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Layout;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Common.Helpers;
internal static class DrawingHelper
{
public static Bitmap RenderPreview(
PreviewLayout previewLayout,
IImageRegionCopyService imageCopyService,
Action<Bitmap>? previewImageCreatedCallback = null,
Action? previewImageUpdatedCallback = null)
{
var stopwatch = Stopwatch.StartNew();
// initialize the preview image
var previewBounds = previewLayout.PreviewBounds.OuterBounds.ToRectangle();
var previewImage = new Bitmap(previewBounds.Width, previewBounds.Height, PixelFormat.Format32bppPArgb);
var previewGraphics = Graphics.FromImage(previewImage);
previewImageCreatedCallback?.Invoke(previewImage);
DrawingHelper.DrawRaisedBorder(previewGraphics, previewLayout.PreviewStyle.CanvasStyle, previewLayout.PreviewBounds);
DrawingHelper.DrawBackgroundFill(
previewGraphics,
previewLayout.PreviewStyle.CanvasStyle,
previewLayout.PreviewBounds,
[]);
// sort the source and target screen areas into the order we want to
// draw them, putting the activated screen first (we need to capture
// and draw the activated screen before we show the form because
// otherwise we'll capture the form as part of the screenshot!)
var sourceScreens = new List<RectangleInfo> { previewLayout.Screens[previewLayout.ActivatedScreenIndex] }
.Concat(previewLayout.Screens.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex))
.ToList();
var targetScreens = new List<BoxBounds> { previewLayout.ScreenshotBounds[previewLayout.ActivatedScreenIndex] }
.Concat(previewLayout.ScreenshotBounds.Where((_, idx) => idx != previewLayout.ActivatedScreenIndex))
.ToList();
// draw all the screenshot bezels
foreach (var screenshotBounds in previewLayout.ScreenshotBounds)
{
DrawingHelper.DrawRaisedBorder(
previewGraphics, previewLayout.PreviewStyle.ScreenStyle, screenshotBounds);
}
var refreshRequired = false;
var placeholdersDrawn = false;
for (var i = 0; i < sourceScreens.Count; i++)
{
imageCopyService.CopyImageRegion(previewGraphics, sourceScreens[i], targetScreens[i].ContentBounds);
refreshRequired = true;
// show the placeholder images and show the form if it looks like it might take
// a while to capture the remaining screenshot images (but only if there are any)
if (stopwatch.ElapsedMilliseconds > 250)
{
// draw placeholder backgrounds for any undrawn screens
if (!placeholdersDrawn)
{
DrawingHelper.DrawScreenPlaceholders(
previewGraphics,
previewLayout.PreviewStyle.ScreenStyle,
targetScreens.GetRange(i + 1, targetScreens.Count - i - 1));
placeholdersDrawn = true;
}
previewImageUpdatedCallback?.Invoke();
refreshRequired = false;
}
}
if (refreshRequired)
{
previewImageUpdatedCallback?.Invoke();
}
stopwatch.Stop();
return previewImage;
}
/// <summary>
/// Draws a border shape with an optional raised 3d highlight and shadow effect.
/// </summary>
private static void DrawRaisedBorder(
Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds)
{
var borderStyle = boxStyle.BorderStyle;
if ((borderStyle.Horizontal == 0) || (borderStyle.Vertical == 0))
{
return;
}
// draw the main box border
using var borderBrush = new SolidBrush(borderStyle.Color);
var borderRegion = new Region(boxBounds.BorderBounds.ToRectangle());
borderRegion.Exclude(boxBounds.PaddingBounds.ToRectangle());
graphics.FillRegion(borderBrush, borderRegion);
// draw the highlight and shadow
var bounds = boxBounds.BorderBounds.ToRectangle();
using var highlight = new Pen(Color.FromArgb(0x44, 0xFF, 0xFF, 0xFF));
using var shadow = new Pen(Color.FromArgb(0x44, 0x00, 0x00, 0x00));
var outer = (
Left: bounds.Left,
Top: bounds.Top,
Right: bounds.Right - 1,
Bottom: bounds.Bottom - 1
);
var inner = (
Left: bounds.Left + (int)borderStyle.Left - 1,
Top: bounds.Top + (int)borderStyle.Top - 1,
Right: bounds.Right - (int)borderStyle.Right,
Bottom: bounds.Bottom - (int)borderStyle.Bottom
);
for (var i = 0; i < borderStyle.Depth; i++)
{
// left edge
if (borderStyle.Left >= i * 2)
{
graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Left, outer.Bottom);
graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Left, inner.Bottom);
}
// top edge
if (borderStyle.Top >= i * 2)
{
graphics.DrawLine(highlight, outer.Left, outer.Top, outer.Right, outer.Top);
graphics.DrawLine(shadow, inner.Left, inner.Top, inner.Right, inner.Top);
}
// right edge
if (borderStyle.Right >= i * 2)
{
graphics.DrawLine(highlight, inner.Right, inner.Top, inner.Right, inner.Bottom);
graphics.DrawLine(shadow, outer.Right, outer.Top, outer.Right, outer.Bottom);
}
// bottom edge
if (borderStyle.Bottom >= i * 2)
{
graphics.DrawLine(highlight, inner.Left, inner.Bottom, inner.Right, inner.Bottom);
graphics.DrawLine(shadow, outer.Left, outer.Bottom, outer.Right, outer.Bottom);
}
// shrink the outer border for the next iteration
outer = (
outer.Left + 1,
outer.Top + 1,
outer.Right - 1,
outer.Bottom - 1
);
// enlarge the inner border for the next iteration
inner = (
inner.Left - 1,
inner.Top - 1,
inner.Right + 1,
inner.Bottom + 1
);
}
}
/// <summary>
/// Draws a gradient-filled background shape.
/// </summary>
private static void DrawBackgroundFill(
Graphics graphics, BoxStyle boxStyle, BoxBounds boxBounds, IEnumerable<RectangleInfo> excludeBounds)
{
var backgroundBounds = boxBounds.PaddingBounds;
using var backgroundBrush = DrawingHelper.GetBackgroundStyleBrush(boxStyle.BackgroundStyle, backgroundBounds);
if (backgroundBrush == null)
{
return;
}
// it's faster to build a region with the screen areas excluded
// and fill that than it is to fill the entire bounding rectangle
var backgroundRegion = new Region(backgroundBounds.ToRectangle());
foreach (var exclude in excludeBounds)
{
backgroundRegion.Exclude(exclude.ToRectangle());
}
graphics.FillRegion(backgroundBrush, backgroundRegion);
}
/// <summary>
/// Draws placeholder background images for the specified screens on the preview.
/// </summary>
private static void DrawScreenPlaceholders(
Graphics graphics, BoxStyle screenStyle, IList<BoxBounds> screenBounds)
{
if (screenBounds.Count == 0)
{
return;
}
if (screenStyle?.BackgroundStyle?.Color1 == null)
{
return;
}
using var brush = new SolidBrush(screenStyle.BackgroundStyle.Color1.Value);
graphics.FillRectangles(brush, screenBounds.Select(bounds => bounds.PaddingBounds.ToRectangle()).ToArray());
}
private static Brush? GetBackgroundStyleBrush(BackgroundStyle backgroundStyle, RectangleInfo backgroundBounds)
{
var backgroundBrush = backgroundStyle switch
{
{ Color1: not null, Color2: not null } =>
/* draw a gradient fill if both colors are specified */
new LinearGradientBrush(
backgroundBounds.ToRectangle(),
backgroundStyle.Color1.Value,
backgroundStyle.Color2.Value,
LinearGradientMode.ForwardDiagonal),
{ Color1: not null } =>
/* draw a solid fill if only one color is specified */
new SolidBrush(
backgroundStyle.Color1.Value),
{ Color2: not null } =>
/* draw a solid fill if only one color is specified */
new SolidBrush(
backgroundStyle.Color2.Value),
_ => (Brush?)null,
};
return backgroundBrush;
}
}

View File

@@ -0,0 +1,159 @@
// 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 MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Layout;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Common.Helpers;
internal static class LayoutHelper
{
public static PreviewLayout GetPreviewLayout(
PreviewStyle previewStyle, List<RectangleInfo> screens, PointInfo activatedLocation)
{
ArgumentNullException.ThrowIfNull(previewStyle);
ArgumentNullException.ThrowIfNull(screens);
if (screens.Count == 0)
{
throw new ArgumentException("Value must contain at least one item.", nameof(screens));
}
var builder = new PreviewLayout.Builder();
builder.Screens = screens.ToList();
// calculate the bounding rectangle for the virtual screen
builder.VirtualScreen = LayoutHelper.GetCombinedScreenBounds(builder.Screens);
// find the screen that contains the activated location - this is the
// one we'll show the preview form on
var activatedScreen = builder.Screens.Single(
screen => screen.Contains(activatedLocation));
builder.ActivatedScreenIndex = builder.Screens.IndexOf(activatedScreen);
// work out the maximum allowed size of the preview form:
// * can't be bigger than the activated screen
// * can't be bigger than the configured canvas size
var maxPreviewSize = activatedScreen.Size
.Intersect(previewStyle.CanvasSize);
// the "content area" (i.e. drawing area) for screenshots is inside the
// preview border and inside the preview padding (if any)
var maxContentSize = maxPreviewSize
.Shrink(previewStyle.CanvasStyle.MarginStyle)
.Shrink(previewStyle.CanvasStyle.BorderStyle)
.Shrink(previewStyle.CanvasStyle.PaddingStyle);
// scale the virtual screen to fit inside the content area
var screenScalingRatio = builder.VirtualScreen.Size
.ScaleToFitRatio(maxContentSize);
// work out the actual size of the "content area" by scaling the virtual screen
// to fit inside the maximum content area while maintaining its aspect ration.
// we'll also offset it to allow for any margins, borders and padding
var contentBounds = builder.VirtualScreen.Size
.Scale(screenScalingRatio)
.Floor()
.PlaceAt(0, 0)
.Offset(previewStyle.CanvasStyle.MarginStyle.Left, previewStyle.CanvasStyle.MarginStyle.Top)
.Offset(previewStyle.CanvasStyle.BorderStyle.Left, previewStyle.CanvasStyle.BorderStyle.Top)
.Offset(previewStyle.CanvasStyle.PaddingStyle.Left, previewStyle.CanvasStyle.PaddingStyle.Top);
// now we know the actual size of the content area we can work outwards to
// get the size of the background bounds including margins, borders and padding
builder.PreviewStyle = previewStyle;
builder.PreviewBounds = LayoutHelper.GetBoxBoundsFromContentBounds(
contentBounds,
previewStyle.CanvasStyle);
// ... and then the size and position of the preview form on the activated screen
// * center the form to the activated position, but nudge it back
// inside the visible area of the activated screen if it falls outside
var formBounds = builder.PreviewBounds.OuterBounds
.Center(activatedLocation)
.Clamp(activatedScreen);
builder.FormBounds = formBounds;
// now calculate the positions of each of the screenshot images on the preview
builder.ScreenshotBounds = builder.Screens
.Select(
screen => LayoutHelper.GetBoxBoundsFromOuterBounds(
screen
.Offset(builder.VirtualScreen.Location.ToSize().Invert())
.Scale(screenScalingRatio)
.Offset(builder.PreviewBounds.ContentBounds.Location.ToSize())
.Truncate(),
previewStyle.ScreenStyle))
.ToList();
return builder.Build();
}
internal static RectangleInfo GetCombinedScreenBounds(List<RectangleInfo> screens)
{
return screens.Skip(1).Aggregate(
seed: screens.First(),
(bounds, screen) => bounds.Union(screen));
}
/// <summary>
/// Calculates the bounds of the various areas of a box, given the content bounds and the box style.
/// Starts with the content bounds and works outward, enlarging the content bounds by the padding, border, and margin sizes to calculate the outer bounds of the box.
/// </summary>
/// <param name="contentBounds">The content bounds of the box.</param>
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="contentBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
internal static BoxBounds GetBoxBoundsFromContentBounds(
RectangleInfo contentBounds,
BoxStyle boxStyle)
{
ArgumentNullException.ThrowIfNull(contentBounds);
ArgumentNullException.ThrowIfNull(boxStyle);
if (boxStyle.PaddingStyle == null || boxStyle.BorderStyle == null || boxStyle.MarginStyle == null)
{
throw new ArgumentException(null, nameof(boxStyle));
}
var paddingBounds = contentBounds.Enlarge(boxStyle.PaddingStyle);
var borderBounds = paddingBounds.Enlarge(boxStyle.BorderStyle);
var marginBounds = borderBounds.Enlarge(boxStyle.MarginStyle);
var outerBounds = marginBounds;
return new(
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
}
/// <summary>
/// Calculates the bounds of the various areas of a box, given the outer bounds and the box style.
/// This method starts with the outer bounds and works inward, shrinking the outer bounds by the margin, border, and padding sizes to calculate the content bounds of the box.
/// </summary>
/// <param name="outerBounds">The outer bounds of the box.</param>
/// <param name="boxStyle">The style of the box, which includes the sizes of the margin, border, and padding areas.</param>
/// <returns>A <see cref="BoxBounds"/> object that represents the bounds of the different areas of the box.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="outerBounds"/> or <paramref name="boxStyle"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when any of the styles in <paramref name="boxStyle"/> is null.</exception>
internal static BoxBounds GetBoxBoundsFromOuterBounds(
RectangleInfo outerBounds,
BoxStyle boxStyle)
{
ArgumentNullException.ThrowIfNull(outerBounds);
ArgumentNullException.ThrowIfNull(boxStyle);
if (outerBounds == null || boxStyle.MarginStyle == null || boxStyle.BorderStyle == null || boxStyle.PaddingStyle == null)
{
throw new ArgumentException(null, nameof(boxStyle));
}
var marginBounds = outerBounds;
var borderBounds = marginBounds.Shrink(boxStyle.MarginStyle);
var paddingBounds = borderBounds.Shrink(boxStyle.BorderStyle);
var contentBounds = paddingBounds.Shrink(boxStyle.PaddingStyle);
return new(
outerBounds, marginBounds, borderBounds, paddingBounds, contentBounds);
}
}

View File

@@ -4,12 +4,12 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.NativeMethods;
using static MouseJumpUI.Common.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.User32;
namespace MouseJumpUI.Helpers;
namespace MouseJumpUI.Common.Helpers;
internal static class MouseHelper
{
@@ -22,7 +22,7 @@ internal static class MouseHelper
/// or even negative if the primary monitor is not the at the top-left of the
/// entire desktop rectangle, so results may contain negative coordinates.
/// </remarks>
public static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
internal static PointInfo GetJumpLocation(PointInfo previewLocation, SizeInfo previewSize, RectangleInfo desktopBounds)
{
return previewLocation
.Scale(previewSize.ScaleToFitRatio(desktopBounds.Size))
@@ -32,7 +32,7 @@ internal static class MouseHelper
/// <summary>
/// Get the current position of the cursor.
/// </summary>
public static PointInfo GetCursorPosition()
internal static PointInfo GetCursorPosition()
{
var lpPoint = new LPPOINT(new POINT(0, 0));
var result = User32.GetCursorPos(lpPoint);
@@ -55,7 +55,7 @@ internal static class MouseHelper
/// <remarks>
/// See https://github.com/mikeclayton/FancyMouse/pull/3
/// </remarks>
public static void SetCursorPosition(PointInfo location)
internal static void SetCursorPosition(PointInfo location)
{
// set the new cursor position *twice* - the cursor sometimes end up in
// the wrong place if we try to cross the dead space between non-aligned
@@ -73,15 +73,21 @@ internal static class MouseHelper
//
// setting the position a second time seems to fix this and moves the
// cursor to the expected location (b)
var point = location.ToPoint();
var target = location.ToPoint();
for (var i = 0; i < 2; i++)
{
var result = User32.SetCursorPos(point.X, point.Y);
var result = User32.SetCursorPos(target.X, target.Y);
if (!result)
{
throw new Win32Exception(
Marshal.GetLastWin32Error());
}
var current = MouseHelper.GetCursorPosition();
if ((current.X == target.X) || (current.Y == target.Y))
{
break;
}
}
// temporary workaround for issue #1273
@@ -95,25 +101,25 @@ internal static class MouseHelper
/// See https://github.com/microsoft/PowerToys/issues/24523
/// https://github.com/microsoft/PowerToys/pull/24527
/// </remarks>
public static void SimulateMouseMovementEvent(PointInfo location)
internal static void SimulateMouseMovementEvent(PointInfo location)
{
var inputs = new User32.INPUT[]
{
new(
type: User32.INPUT_TYPE.INPUT_MOUSE,
data: new User32.INPUT.DUMMYUNIONNAME(
mi: new User32.MOUSEINPUT(
type: INPUT_TYPE.INPUT_MOUSE,
data: new INPUT.DUMMYUNIONNAME(
mi: new MOUSEINPUT(
dx: (int)MouseHelper.CalculateAbsoluteCoordinateX(location.X),
dy: (int)MouseHelper.CalculateAbsoluteCoordinateY(location.Y),
mouseData: 0,
dwFlags: User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | User32.MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
dwFlags: MOUSE_EVENT_FLAGS.MOUSEEVENTF_MOVE | MOUSE_EVENT_FLAGS.MOUSEEVENTF_ABSOLUTE,
time: 0,
dwExtraInfo: ULONG_PTR.Null))),
};
var result = User32.SendInput(
(uint)inputs.Length,
new User32.LPINPUT(inputs),
User32.INPUT.Size * inputs.Length);
(UINT)inputs.Length,
new LPINPUT(inputs),
INPUT.Size * inputs.Length);
if (result != inputs.Length)
{
throw new Win32Exception(
@@ -125,13 +131,13 @@ internal static class MouseHelper
{
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
return (x * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CXSCREEN);
return (x * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
}
internal static decimal CalculateAbsoluteCoordinateY(decimal y)
private static decimal CalculateAbsoluteCoordinateY(decimal y)
{
// If MOUSEEVENTF_ABSOLUTE value is specified, dx and dy contain normalized absolute coordinates between 0 and 65,535.
// see https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-mouseinput
return (y * 65535) / User32.GetSystemMetrics(User32.SYSTEM_METRICS_INDEX.SM_CYSCREEN);
return (y * 65535) / User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
}
}

View File

@@ -5,13 +5,13 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using MouseJumpUI.Models.Drawing;
using MouseJumpUI.Models.Screen;
using MouseJumpUI.NativeMethods;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.NativeMethods.User32;
using System.Linq;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.NativeMethods;
using static MouseJumpUI.Common.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.User32;
namespace MouseJumpUI.Helpers;
namespace MouseJumpUI.Common.Helpers;
internal static class ScreenHelper
{
@@ -28,22 +28,21 @@ internal static class ScreenHelper
User32.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYVIRTUALSCREEN));
}
public static IEnumerable<ScreenInfo> GetAllScreens()
internal static IEnumerable<ScreenInfo> GetAllScreens()
{
// enumerate the monitors attached to the system
var hMonitors = new List<HMONITOR>();
var result = User32.EnumDisplayMonitors(
HDC.Null,
LPCRECT.Null,
(unnamedParam1, unnamedParam2, unnamedParam3, unnamedParam4) =>
var callback = new User32.MONITORENUMPROC(
(hMonitor, hdcMonitor, lprcMonitor, dwData) =>
{
hMonitors.Add(unnamedParam1);
hMonitors.Add(hMonitor);
return true;
},
LPARAM.Null);
});
var result = User32.EnumDisplayMonitors(HDC.Null, LPCRECT.Null, callback, LPARAM.Null);
if (!result)
{
throw new Win32Exception(
result.Value,
$"{nameof(User32.EnumDisplayMonitors)} failed with return code {result.Value}");
}
@@ -51,11 +50,12 @@ internal static class ScreenHelper
foreach (var hMonitor in hMonitors)
{
var monitorInfoPtr = new LPMONITORINFO(
new MONITORINFO((uint)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
new MONITORINFO((DWORD)MONITORINFO.Size, RECT.Empty, RECT.Empty, 0));
result = User32.GetMonitorInfoW(hMonitor, monitorInfoPtr);
if (!result)
{
throw new Win32Exception(
result.Value,
$"{nameof(User32.GetMonitorInfoW)} failed with return code {result.Value}");
}
@@ -78,9 +78,11 @@ internal static class ScreenHelper
}
}
public static HMONITOR MonitorFromPoint(
internal static ScreenInfo GetScreenFromPoint(
List<ScreenInfo> screens,
PointInfo pt)
{
// get the monitor handle from the point
var hMonitor = User32.MonitorFromPoint(
new((int)pt.X, (int)pt.Y),
User32.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTONEAREST);
@@ -89,6 +91,9 @@ internal static class ScreenHelper
throw new InvalidOperationException($"no monitor found for point {pt}");
}
return hMonitor;
// find the screen with the given monitor handle
var screen = screens
.Single(item => item.Handle == hMonitor);
return screen;
}
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Drawing;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.NativeMethods;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.Common.Imaging;
/// <summary>
/// Implements an IImageRegionCopyService that uses the current desktop window as the copy source.
/// This is used during the main application runtime to generate preview images of the desktop.
/// </summary>
internal sealed class DesktopImageRegionCopyService : IImageRegionCopyService
{
/// <summary>
/// Copies the source region from the current desktop window
/// to the target region on the specified Graphics object.
/// </summary>
public void CopyImageRegion(
Graphics targetGraphics,
RectangleInfo sourceBounds,
RectangleInfo targetBounds)
{
var stopwatch = Stopwatch.StartNew();
var (desktopHwnd, desktopHdc) = DesktopImageRegionCopyService.GetDesktopDeviceContext();
var previewHdc = DesktopImageRegionCopyService.GetGraphicsDeviceContext(
targetGraphics, Gdi32.STRETCH_BLT_MODE.STRETCH_HALFTONE);
stopwatch.Stop();
var source = sourceBounds.ToRectangle();
var target = targetBounds.ToRectangle();
var result = Gdi32.StretchBlt(
previewHdc,
target.X,
target.Y,
target.Width,
target.Height,
desktopHdc,
source.X,
source.Y,
source.Width,
source.Height,
Gdi32.ROP_CODE.SRCCOPY);
if (!result)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.StretchBlt)} returned {result.Value}");
}
// we need to release the graphics device context handle before anything
// else tries to use the Graphics object otherwise it'll give an error
// from GDI saying "Object is currently in use elsewhere"
DesktopImageRegionCopyService.FreeGraphicsDeviceContext(targetGraphics, ref previewHdc);
DesktopImageRegionCopyService.FreeDesktopDeviceContext(ref desktopHwnd, ref desktopHdc);
}
private static (HWND DesktopHwnd, HDC DesktopHdc) GetDesktopDeviceContext()
{
var desktopHwnd = User32.GetDesktopWindow();
var desktopHdc = User32.GetWindowDC(desktopHwnd);
if (desktopHdc.IsNull)
{
throw new InvalidOperationException(
$"{nameof(User32.GetWindowDC)} returned null");
}
return (desktopHwnd, desktopHdc);
}
private static void FreeDesktopDeviceContext(ref HWND desktopHwnd, ref HDC desktopHdc)
{
if (!desktopHwnd.IsNull && !desktopHdc.IsNull)
{
var result = User32.ReleaseDC(desktopHwnd, desktopHdc);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(User32.ReleaseDC)} returned {result}");
}
}
desktopHwnd = HWND.Null;
desktopHdc = HDC.Null;
}
/// <summary>
/// Checks if the target device context handle exists, and creates a new one from the
/// specified Graphics object if not.
/// </summary>
private static HDC GetGraphicsDeviceContext(Graphics graphics, Gdi32.STRETCH_BLT_MODE mode)
{
var graphicsHdc = (HDC)graphics.GetHdc();
var result = Gdi32.SetStretchBltMode(graphicsHdc, mode);
if (result == 0)
{
throw new InvalidOperationException(
$"{nameof(Gdi32.SetStretchBltMode)} returned {result}");
}
return graphicsHdc;
}
/// <summary>
/// Free the specified device context handle if it exists.
/// </summary>
private static void FreeGraphicsDeviceContext(Graphics graphics, ref HDC graphicsHdc)
{
if (graphicsHdc.IsNull)
{
return;
}
graphics.ReleaseHdc(graphicsHdc.Value);
graphicsHdc = HDC.Null;
}
}

View File

@@ -0,0 +1,24 @@
// 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.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.Common.Imaging;
internal interface IImageRegionCopyService
{
/// <summary>
/// Copies the source region from the provider's source image (e.g. the interactive desktop,
/// a static image, etc) to the target region on the specified Graphics object.
/// </summary>
/// <remarks>
/// Implementations of this interface are used to capture regions of the interactive desktop
/// during runtime, or to capture regions of a static reference image during unit tests.
/// </remarks>
void CopyImageRegion(
Graphics targetGraphics,
RectangleInfo sourceBounds,
RectangleInfo targetBounds);
}

View File

@@ -0,0 +1,42 @@
// 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.Drawing;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.Common.Imaging;
/// <summary>
/// Implements an IImageRegionCopyService that uses the specified image as the copy source.
/// This is used for testing the DrawingHelper rather than as part of the main application.
/// </summary>
internal sealed class StaticImageRegionCopyService : IImageRegionCopyService
{
public StaticImageRegionCopyService(Image sourceImage)
{
this.SourceImage = sourceImage ?? throw new ArgumentNullException(nameof(sourceImage));
}
private Image SourceImage
{
get;
}
/// <summary>
/// Copies the source region from the static source image
/// to the target region on the specified Graphics object.
/// </summary>
public void CopyImageRegion(
Graphics targetGraphics,
RectangleInfo sourceBounds,
RectangleInfo targetBounds)
{
targetGraphics.DrawImage(
image: this.SourceImage,
destRect: targetBounds.ToRectangle(),
srcRect: sourceBounds.ToRectangle(),
srcUnit: GraphicsUnit.Pixel);
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MouseJumpUI.Common.Models.Drawing;
public sealed class BoxBounds
{
/*
see https://www.w3schools.com/css/css_boxmodel.asp
+--------------[bounds]---------------+
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ [content] ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
+-------------------------------------+
*/
internal BoxBounds(
RectangleInfo outerBounds,
RectangleInfo marginBounds,
RectangleInfo borderBounds,
RectangleInfo paddingBounds,
RectangleInfo contentBounds)
{
this.OuterBounds = outerBounds ?? throw new ArgumentNullException(nameof(outerBounds));
this.MarginBounds = marginBounds ?? throw new ArgumentNullException(nameof(marginBounds));
this.BorderBounds = borderBounds ?? throw new ArgumentNullException(nameof(borderBounds));
this.PaddingBounds = paddingBounds ?? throw new ArgumentNullException(nameof(paddingBounds));
this.ContentBounds = contentBounds ?? throw new ArgumentNullException(nameof(contentBounds));
}
/// <summary>
/// Gets the outer bounds of this layout box.
/// </summary>
public RectangleInfo OuterBounds
{
get;
}
public RectangleInfo MarginBounds
{
get;
}
public RectangleInfo BorderBounds
{
get;
}
public RectangleInfo PaddingBounds
{
get;
}
/// <summary>
/// Gets the bounds of the content area for this layout box.
/// </summary>
public RectangleInfo ContentBounds
{
get;
}
}

View File

@@ -0,0 +1,80 @@
// 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.Drawing;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Drawing.Point object with some extra utility methods.
/// </summary>
public sealed class PointInfo
{
public PointInfo(decimal x, decimal y)
{
this.X = x;
this.Y = y;
}
public PointInfo(Point point)
: this(point.X, point.Y)
{
}
public decimal X
{
get;
}
public decimal Y
{
get;
}
/// <summary>
/// Moves this PointInfo inside the specified RectangleInfo.
/// </summary>
public PointInfo Clamp(RectangleInfo outer)
{
return new(
x: Math.Clamp(this.X, outer.X, outer.Right),
y: Math.Clamp(this.Y, outer.Y, outer.Bottom));
}
public PointInfo Scale(decimal scalingFactor) => new(this.X * scalingFactor, this.Y * scalingFactor);
public PointInfo Offset(PointInfo amount) => new(this.X + amount.X, this.Y + amount.Y);
public Point ToPoint() => new((int)this.X, (int)this.Y);
public SizeInfo ToSize()
{
return new((int)this.X, (int)this.Y);
}
/// <summary>
/// Stretches the point to the same proportional position in targetBounds as
/// it currently is in sourceBounds
/// </summary>
public PointInfo Stretch(RectangleInfo source, RectangleInfo target)
{
return new PointInfo(
x: ((this.X - source.X) / source.Width * target.Width) + target.X,
y: ((this.Y - source.Y) / source.Height * target.Height) + target.Y);
}
public PointInfo Truncate() =>
new(
(int)this.X,
(int)this.Y);
public override string ToString()
{
return "{" +
$"{nameof(this.X)}={this.X}," +
$"{nameof(this.Y)}={this.Y}" +
"}";
}
}

View File

@@ -0,0 +1,300 @@
// 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.Drawing;
using System.Text.Json.Serialization;
using MouseJumpUI.Common.Models.Styles;
using BorderStyle = MouseJumpUI.Common.Models.Styles.BorderStyle;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Drawing.Rectangle object with some extra utility methods.
/// </summary>
public sealed class RectangleInfo
{
public static readonly RectangleInfo Empty = new(0, 0, 0, 0);
public RectangleInfo(decimal x, decimal y, decimal width, decimal height)
{
this.X = x;
this.Y = y;
this.Width = width;
this.Height = height;
}
public RectangleInfo(Rectangle rectangle)
: this(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)
{
}
public RectangleInfo(Point location, SizeInfo size)
: this(location.X, location.Y, size.Width, size.Height)
{
}
public RectangleInfo(SizeInfo size)
: this(0, 0, size.Width, size.Height)
{
}
public decimal X
{
get;
}
public decimal Y
{
get;
}
public decimal Width
{
get;
}
public decimal Height
{
get;
}
[JsonIgnore]
public decimal Left =>
this.X;
[JsonIgnore]
public decimal Top =>
this.Y;
[JsonIgnore]
public decimal Right =>
this.X + this.Width;
[JsonIgnore]
public decimal Bottom =>
this.Y + this.Height;
[JsonIgnore]
public decimal Area =>
this.Width * this.Height;
[JsonIgnore]
public PointInfo Location =>
new(this.X, this.Y);
[JsonIgnore]
public PointInfo Midpoint =>
new(
x: this.X + (this.Width / 2),
y: this.Y + (this.Height / 2));
[JsonIgnore]
public SizeInfo Size => new(this.Width, this.Height);
/// <summary>
/// Centers the rectangle around a specified point.
/// </summary>
/// <param name="point">The <see cref="PointInfo"/> around which the rectangle will be centered.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is centered around the specified point.</returns>
public RectangleInfo Center(PointInfo point) =>
new(
x: point.X - (this.Width / 2),
y: point.Y - (this.Height / 2),
width: this.Width,
height: this.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is moved within the bounds of the specified outer rectangle.
/// If the current rectangle is larger than the outer rectangle, an exception is thrown.
/// </summary>
/// <param name="outer">The outer <see cref="RectangleInfo"/> within which to confine this rectangle.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is the result of moving this rectangle within the bounds of the outer rectangle.</returns>
/// <exception cref="ArgumentException">Thrown when the current rectangle is larger than the outer rectangle.</exception>
public RectangleInfo Clamp(RectangleInfo outer)
{
if ((this.Width > outer.Width) || (this.Height > outer.Height))
{
throw new ArgumentException($"Value cannot be larger than {nameof(outer)}.");
}
return new(
x: Math.Clamp(this.X, outer.X, outer.Right - this.Width),
y: Math.Clamp(this.Y, outer.Y, outer.Bottom - this.Height),
width: this.Width,
height: this.Height);
}
/// <remarks>
/// Adapted from https://github.com/dotnet/runtime
/// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs
/// </remarks>
public bool Contains(decimal x, decimal y) =>
this.X <= x && x < this.X + this.Width && this.Y <= y && y < this.Y + this.Height;
/// <remarks>
/// Adapted from https://github.com/dotnet/runtime
/// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs
/// </remarks>
public bool Contains(PointInfo pt) =>
this.Contains(pt.X, pt.Y);
/// <remarks>
/// Adapted from https://github.com/dotnet/runtime
/// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs
/// </remarks>
public bool Contains(RectangleInfo rect) =>
(this.X <= rect.X) && (rect.X + rect.Width <= this.X + this.Width) &&
(this.Y <= rect.Y) && (rect.Y + rect.Height <= this.Y + this.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is larger than the current rectangle.
/// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the border.
/// </summary>
/// <param name="border">The <see cref="BorderStyle"/> that specifies the amount to enlarge the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is larger than the current rectangle by the specified border amounts.</returns>
public RectangleInfo Enlarge(BorderStyle border) =>
new(
this.X - border.Left,
this.Y - border.Top,
this.Width + border.Horizontal,
this.Height + border.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is larger than the current rectangle.
/// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the margin.
/// </summary>
/// <param name="margin">The <see cref="MarginStyle"/> that specifies the amount to enlarge the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is larger than the current rectangle by the specified margin amounts.</returns>
public RectangleInfo Enlarge(MarginStyle margin) =>
new(
this.X - margin.Left,
this.Y - margin.Top,
this.Width + margin.Horizontal,
this.Height + margin.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is larger than the current rectangle.
/// The dimensions of the new rectangle are calculated by enlarging the current rectangle's dimensions by the size of the padding.
/// </summary>
/// <param name="padding">The <see cref="PaddingStyle"/> that specifies the amount to enlarge the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is larger than the current rectangle by the specified padding amounts.</returns>
public RectangleInfo Enlarge(PaddingStyle padding) =>
new(
this.X - padding.Left,
this.Y - padding.Top,
this.Width + padding.Horizontal,
this.Height + padding.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is offset by the specified amount.
/// </summary>
/// <param name="amount">The <see cref="SizeInfo"/> representing the amount to offset in both the X and Y directions.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is offset by the specified amount.</returns>
public RectangleInfo Offset(SizeInfo amount) =>
this.Offset(amount.Width, amount.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is offset by the specified X and Y distances.
/// </summary>
/// <param name="dx">The distance to offset the rectangle along the X-axis.</param>
/// <param name="dy">The distance to offset the rectangle along the Y-axis.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is offset by the specified X and Y distances.</returns>
public RectangleInfo Offset(decimal dx, decimal dy) =>
new(this.X + dx, this.Y + dy, this.Width, this.Height);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is a scaled version of the current rectangle.
/// The dimensions of the new rectangle are calculated by multiplying the current rectangle's dimensions by the scaling factor.
/// </summary>
/// <param name="scalingFactor">The factor by which to scale the rectangle's dimensions.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is a scaled version of the current rectangle.</returns>
public RectangleInfo Scale(decimal scalingFactor) =>
new(
this.X * scalingFactor,
this.Y * scalingFactor,
this.Width * scalingFactor,
this.Height * scalingFactor);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is smaller than the current rectangle.
/// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the border.
/// </summary>
/// <param name="border">The <see cref="BorderStyle"/> that specifies the amount to shrink the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is smaller than the current rectangle by the specified border amounts.</returns>
public RectangleInfo Shrink(BorderStyle border) =>
new(
this.X + border.Left,
this.Y + border.Top,
this.Width - border.Horizontal,
this.Height - border.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is smaller than the current rectangle.
/// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the margin.
/// </summary>
/// <param name="margin">The <see cref="MarginStyle"/> that specifies the amount to shrink the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is smaller than the current rectangle by the specified margin amounts.</returns>
public RectangleInfo Shrink(MarginStyle margin) =>
new(
this.X + margin.Left,
this.Y + margin.Top,
this.Width - margin.Horizontal,
this.Height - margin.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> that is smaller than the current rectangle.
/// The dimensions of the new rectangle are calculated by shrinking the current rectangle's dimensions by the size of the padding.
/// </summary>
/// <param name="padding">The <see cref="PaddingStyle"/> that specifies the amount to shrink the rectangle on each side.</param>
/// <returns>A new <see cref="RectangleInfo"/> that is smaller than the current rectangle by the specified padding amounts.</returns>
public RectangleInfo Shrink(PaddingStyle padding) =>
new(
this.X + padding.Left,
this.Y + padding.Top,
this.Width - padding.Horizontal,
this.Height - padding.Vertical);
/// <summary>
/// Returns a new <see cref="RectangleInfo"/> where the X, Y, Width, and Height properties of the current rectangle are truncated to integers.
/// </summary>
/// <returns>A new <see cref="RectangleInfo"/> with the X, Y, Width, and Height properties of the current rectangle truncated to integers.</returns>
public RectangleInfo Truncate() =>
new(
(int)this.X,
(int)this.Y,
(int)this.Width,
(int)this.Height);
/// <remarks>
/// Adapted from https://github.com/dotnet/runtime
/// See https://github.com/dotnet/runtime/blob/dfd618dc648ba9b11dd0f8034f78113d69f223cd/src/libraries/System.Drawing.Primitives/src/System/Drawing/Rectangle.cs
/// </remarks>
public RectangleInfo Union(RectangleInfo rect)
{
var x1 = Math.Min(this.X, rect.X);
var x2 = Math.Max(this.X + this.Width, rect.X + rect.Width);
var y1 = Math.Min(this.Y, rect.Y);
var y2 = Math.Max(this.Y + this.Height, rect.Y + rect.Height);
return new RectangleInfo(x1, y1, x2 - x1, y2 - y1);
}
public Rectangle ToRectangle() =>
new(
(int)this.X,
(int)this.Y,
(int)this.Width,
(int)this.Height);
public override string ToString()
{
return "{" +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Width)}={this.Width}," +
$"{nameof(this.Height)}={this.Height}" +
"}";
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Windows.Forms.Screen object so we don't need to
/// take a dependency on WinForms just for screen info.
/// </summary>
internal sealed class ScreenInfo
{
internal ScreenInfo(HMONITOR handle, bool primary, RectangleInfo displayArea, RectangleInfo workingArea)
{
this.Handle = handle;
this.Primary = primary;
this.DisplayArea = displayArea ?? throw new ArgumentNullException(nameof(displayArea));
this.WorkingArea = workingArea ?? throw new ArgumentNullException(nameof(workingArea));
}
public int Handle
{
get;
}
public bool Primary
{
get;
}
public RectangleInfo DisplayArea
{
get;
}
public RectangleInfo WorkingArea
{
get;
}
}

View File

@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Drawing;
using MouseJumpUI.Common.Models.Styles;
using BorderStyle = MouseJumpUI.Common.Models.Styles.BorderStyle;
namespace MouseJumpUI.Common.Models.Drawing;
/// <summary>
/// Immutable version of a System.Drawing.Size object with some extra utility methods.
/// </summary>
public sealed class SizeInfo
{
public SizeInfo(decimal width, decimal height)
{
this.Width = width;
this.Height = height;
}
public SizeInfo(Size size)
: this(size.Width, size.Height)
{
}
public decimal Width
{
get;
}
public decimal Height
{
get;
}
public SizeInfo Enlarge(BorderStyle border) =>
new(
this.Width + border.Horizontal,
this.Height + border.Vertical);
public SizeInfo Enlarge(PaddingStyle padding) =>
new(
this.Width + padding.Horizontal,
this.Height + padding.Vertical);
/// <summary>
/// Calculates the intersection of this size with another size, resulting in a size that represents
/// the overlapping dimensions. Both sizes must be non-negative.
/// </summary>
/// <param name="size">The size to intersect with this instance.</param>
/// <returns>A new <see cref="SizeInfo"/> instance representing the intersection of the two sizes.</returns>
/// <exception cref="ArgumentException">Thrown when either this size or the specified size has negative dimensions.</exception>
public SizeInfo Intersect(SizeInfo size)
{
if ((this.Width < 0) || (this.Height < 0) || (size.Width < 0) || (size.Height < 0))
{
throw new ArgumentException("Sizes must be non-negative");
}
return new(
Math.Min(this.Width, size.Width),
Math.Min(this.Height, size.Height));
}
/// <summary>
/// Creates a new <see cref="SizeInfo"/> instance with the width and height negated, effectively inverting its dimensions.
/// </summary>
/// <returns>A new <see cref="SizeInfo"/> instance with inverted dimensions.</returns>
public SizeInfo Invert() =>
new(-this.Width, -this.Height);
public SizeInfo Scale(decimal scalingFactor) => new(
this.Width * scalingFactor,
this.Height * scalingFactor);
public SizeInfo Shrink(BorderStyle border) =>
new(this.Width - border.Horizontal, this.Height - border.Vertical);
public SizeInfo Shrink(MarginStyle margin) =>
new(this.Width - margin.Horizontal, this.Height - margin.Vertical);
public SizeInfo Shrink(PaddingStyle padding) =>
new(this.Width - padding.Horizontal, this.Height - padding.Vertical);
/// <summary>
/// Creates a new <see cref="RectangleInfo"/> instance representing a rectangle with this size,
/// positioned at the specified coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the upper-left corner of the rectangle.</param>
/// <param name="y">The y-coordinate of the upper-left corner of the rectangle.</param>
/// <returns>A new <see cref="RectangleInfo"/> instance representing the positioned rectangle.</returns>
public RectangleInfo PlaceAt(decimal x, decimal y) =>
new(x, y, this.Width, this.Height);
/// <summary>
/// Scales this size to fit within the bounds of another size, while maintaining the aspect ratio.
/// </summary>
/// <param name="bounds">The size to fit this size into.</param>
/// <returns>A new <see cref="SizeInfo"/> instance representing the scaled size.</returns>
public SizeInfo ScaleToFit(SizeInfo bounds)
{
var widthRatio = bounds.Width / this.Width;
var heightRatio = bounds.Height / this.Height;
return widthRatio.CompareTo(heightRatio) switch
{
< 0 => new(bounds.Width, this.Height * widthRatio),
0 => bounds,
> 0 => new(this.Width * heightRatio, bounds.Height),
};
}
/// <summary>
/// Rounds down the width and height of this size to the nearest whole number.
/// </summary>
/// <returns>A new <see cref="SizeInfo"/> instance with floored dimensions.</returns>
public SizeInfo Floor()
{
return new SizeInfo(
Math.Floor(this.Width),
Math.Floor(this.Height));
}
/// <summary>
/// Calculates the scaling ratio needed to fit this size within the bounds of another size without distorting the aspect ratio.
/// </summary>
/// <param name="bounds">The size to fit this size into.</param>
/// <returns>The scaling ratio as a decimal.</returns>
/// <exception cref="ArgumentException">Thrown if the width or height of the bounds is zero.</exception>
public decimal ScaleToFitRatio(SizeInfo bounds)
{
if (bounds.Width == 0 || bounds.Height == 0)
{
throw new ArgumentException($"{nameof(bounds.Width)} or {nameof(bounds.Height)} cannot be zero", nameof(bounds));
}
var widthRatio = bounds.Width / this.Width;
var heightRatio = bounds.Height / this.Height;
var scalingRatio = Math.Min(widthRatio, heightRatio);
return scalingRatio;
}
public Size ToSize() => new((int)this.Width, (int)this.Height);
public Point ToPoint() => new((int)this.Width, (int)this.Height);
public override string ToString()
{
return "{" +
$"{nameof(this.Width)}={this.Width}," +
$"{nameof(this.Height)}={this.Height}" +
"}";
}
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using MouseJumpUI.Common.Models.Drawing;
using MouseJumpUI.Common.Models.Styles;
namespace MouseJumpUI.Common.Models.Layout;
public sealed class PreviewLayout
{
public sealed class Builder
{
public Builder()
{
this.Screens = new();
this.ScreenshotBounds = new();
}
public PreviewStyle? PreviewStyle
{
get;
set;
}
public RectangleInfo? VirtualScreen
{
get;
set;
}
public List<RectangleInfo> Screens
{
get;
set;
}
public int ActivatedScreenIndex
{
get;
set;
}
public RectangleInfo? FormBounds
{
get;
set;
}
public BoxBounds? PreviewBounds
{
get;
set;
}
public List<BoxBounds> ScreenshotBounds
{
get;
set;
}
public PreviewLayout Build()
{
return new PreviewLayout(
previewStyle: this.PreviewStyle ?? throw new InvalidOperationException($"{nameof(this.PreviewStyle)} must be initialized before calling {nameof(this.Build)}."),
virtualScreen: this.VirtualScreen ?? throw new InvalidOperationException($"{nameof(this.VirtualScreen)} must be initialized before calling {nameof(this.Build)}."),
screens: this.Screens ?? throw new InvalidOperationException($"{nameof(this.Screens)} must be initialized before calling {nameof(this.Build)}."),
activatedScreenIndex: this.ActivatedScreenIndex,
formBounds: this.FormBounds ?? throw new InvalidOperationException($"{nameof(this.FormBounds)} must be initialized before calling {nameof(this.Build)}."),
previewBounds: this.PreviewBounds ?? throw new InvalidOperationException($"{nameof(this.PreviewBounds)} must be initialized before calling {nameof(this.Build)}."),
screenshotBounds: this.ScreenshotBounds ?? throw new InvalidOperationException($"{nameof(this.ScreenshotBounds)} must be initialized before calling {nameof(this.Build)}."));
}
}
public PreviewLayout(
PreviewStyle previewStyle,
RectangleInfo virtualScreen,
List<RectangleInfo> screens,
int activatedScreenIndex,
RectangleInfo formBounds,
BoxBounds previewBounds,
List<BoxBounds> screenshotBounds)
{
this.PreviewStyle = previewStyle ?? throw new ArgumentNullException(nameof(previewStyle));
this.VirtualScreen = virtualScreen ?? throw new ArgumentNullException(nameof(virtualScreen));
this.Screens = (screens ?? throw new ArgumentNullException(nameof(screens)))
.ToList().AsReadOnly();
this.ActivatedScreenIndex = activatedScreenIndex;
this.FormBounds = formBounds ?? throw new ArgumentNullException(nameof(formBounds));
this.PreviewBounds = previewBounds ?? throw new ArgumentNullException(nameof(previewBounds));
this.ScreenshotBounds = (screenshotBounds ?? throw new ArgumentNullException(nameof(screenshotBounds)))
.ToList().AsReadOnly();
}
public PreviewStyle PreviewStyle
{
get;
}
public RectangleInfo VirtualScreen
{
get;
}
public ReadOnlyCollection<RectangleInfo> Screens
{
get;
}
public int ActivatedScreenIndex
{
get;
}
public RectangleInfo FormBounds
{
get;
}
public BoxBounds PreviewBounds
{
get;
}
public ReadOnlyCollection<BoxBounds> ScreenshotBounds
{
get;
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Drawing;
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the background fill style for a drawing object.
/// </summary>
public sealed class BackgroundStyle
{
public static readonly BackgroundStyle Empty = new(
Color.Transparent,
Color.Transparent
);
public BackgroundStyle(
Color? color1,
Color? color2)
{
this.Color1 = color1;
this.Color2 = color2;
}
public Color? Color1
{
get;
}
public Color? Color2
{
get;
}
public override string ToString()
{
return "{" +
$"{nameof(this.Color1)}={this.Color1}," +
$"{nameof(this.Color2)}={this.Color2}" +
"}";
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Drawing;
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the border style for a drawing object.
/// </summary>
public sealed class BorderStyle
{
public static readonly BorderStyle Empty = new(Color.Transparent, 0, 0);
public BorderStyle(Color color, decimal all, decimal depth)
: this(color, all, all, all, all, depth)
{
}
public BorderStyle(Color color, decimal left, decimal top, decimal right, decimal bottom, decimal depth)
{
this.Color = color;
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
this.Depth = depth;
}
public Color Color
{
get;
}
public decimal Left
{
get;
}
public decimal Top
{
get;
}
public decimal Right
{
get;
}
public decimal Bottom
{
get;
}
/// <summary>
/// Gets the "depth" of the 3d highlight and shadow effect on the border.
/// </summary>
public decimal Depth
{
get;
}
public decimal Horizontal => this.Left + this.Right;
public decimal Vertical => this.Top + this.Bottom;
public override string ToString()
{
return "{" +
$"{nameof(this.Color)}={this.Color}," +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Right)}={this.Right}," +
$"{nameof(this.Bottom)}={this.Bottom}," +
$"{nameof(this.Depth)}={this.Depth}" +
"}";
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the styles to apply to a simple box-layout based drawing object.
/// </summary>
public sealed class BoxStyle
{
/*
see https://www.w3schools.com/css/css_boxmodel.asp
+--------------[bounds]---------------+
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒[margin]▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓[border]▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▓▓░░░░░░░░░░[padding]░░░░░░░░░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ [content] ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░ ░░▓▓▒▒|
|▒▒▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▒▒|
|▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒|
|▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒|
+-------------------------------------+
*/
public static readonly BoxStyle Empty = new(MarginStyle.Empty, BorderStyle.Empty, PaddingStyle.Empty, BackgroundStyle.Empty);
public BoxStyle(
MarginStyle marginStyle,
BorderStyle borderStyle,
PaddingStyle paddingStyle,
BackgroundStyle backgroundStyle)
{
this.MarginStyle = marginStyle ?? throw new ArgumentNullException(nameof(marginStyle));
this.BorderStyle = borderStyle ?? throw new ArgumentNullException(nameof(borderStyle));
this.PaddingStyle = paddingStyle ?? throw new ArgumentNullException(nameof(paddingStyle));
this.BackgroundStyle = backgroundStyle ?? throw new ArgumentNullException(nameof(backgroundStyle));
}
/// <summary>
/// Gets the margin style for this layout box.
/// </summary>
public MarginStyle MarginStyle
{
get;
}
/// <summary>
/// Gets the border style for this layout box.
/// </summary>
public BorderStyle BorderStyle
{
get;
}
/// <summary>
/// Gets the padding style for this layout box.
/// </summary>
public PaddingStyle PaddingStyle
{
get;
}
/// <summary>
/// Gets the background fill style for the content area of this layout box.
/// </summary>
public BackgroundStyle BackgroundStyle
{
get;
}
}

View File

@@ -0,0 +1,60 @@
// 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 MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the margin style for a drawing object.
/// </summary>
public sealed class MarginStyle
{
public static readonly MarginStyle Empty = new(0);
public MarginStyle(decimal all)
: this(all, all, all, all)
{
}
public MarginStyle(decimal left, decimal top, decimal right, decimal bottom)
{
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
}
public decimal Left
{
get;
}
public decimal Top
{
get;
}
public decimal Right
{
get;
}
public decimal Bottom
{
get;
}
public decimal Horizontal => this.Left + this.Right;
public decimal Vertical => this.Top + this.Bottom;
public override string ToString()
{
return "{" +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Right)}={this.Right}," +
$"{nameof(this.Bottom)}={this.Bottom}" +
"}";
}
}

View File

@@ -0,0 +1,60 @@
// 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 MouseJumpUI.Common.Models.Styles;
/// <summary>
/// Represents the margin style for a drawing object.
/// </summary>
public sealed class PaddingStyle
{
public static readonly PaddingStyle Empty = new(0);
public PaddingStyle(decimal all)
: this(all, all, all, all)
{
}
public PaddingStyle(decimal left, decimal top, decimal right, decimal bottom)
{
this.Left = left;
this.Top = top;
this.Right = right;
this.Bottom = bottom;
}
public decimal Left
{
get;
}
public decimal Top
{
get;
}
public decimal Right
{
get;
}
public decimal Bottom
{
get;
}
public decimal Horizontal => this.Left + this.Right;
public decimal Vertical => this.Top + this.Bottom;
public override string ToString()
{
return "{" +
$"{nameof(this.Left)}={this.Left}," +
$"{nameof(this.Top)}={this.Top}," +
$"{nameof(this.Right)}={this.Right}," +
$"{nameof(this.Bottom)}={this.Bottom}" +
"}";
}
}

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using MouseJumpUI.Common.Models.Drawing;
namespace MouseJumpUI.Common.Models.Styles;
public sealed class PreviewStyle
{
public PreviewStyle(
SizeInfo canvasSize,
BoxStyle canvasStyle,
BoxStyle screenStyle)
{
this.CanvasSize = canvasSize ?? throw new ArgumentNullException(nameof(canvasSize));
this.CanvasStyle = canvasStyle ?? throw new ArgumentNullException(nameof(canvasStyle));
this.ScreenStyle = screenStyle ?? throw new ArgumentNullException(nameof(screenStyle));
}
public SizeInfo CanvasSize
{
get;
}
public BoxStyle CanvasStyle
{
get;
}
public BoxStyle ScreenStyle
{
get;
}
}

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{

View File

@@ -5,7 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -25,6 +25,15 @@ internal static partial class Core
public readonly LONG right;
public readonly LONG bottom;
public CRECT(
int left, int top, int right, int bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public CRECT(
LONG left, LONG top, LONG right, LONG bottom)
{

View File

@@ -2,7 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
using System.Runtime.InteropServices;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -23,10 +25,17 @@ internal static partial class Core
this.Value = value;
}
public static int Size =>
Marshal.SizeOf(typeof(DWORD));
public static implicit operator uint(DWORD value) => value.Value;
public static implicit operator DWORD(uint value) => new(value);
public static explicit operator int(DWORD value) => (int)value.Value;
public static explicit operator DWORD(int value) => new((uint)value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";

View File

@@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -31,7 +31,7 @@ internal static partial class Core
public static implicit operator IntPtr(HANDLE value) => value.Value;
public static implicit operator HANDLE(IntPtr value) => new(value);
public static explicit operator HANDLE(IntPtr value) => new(value);
public override string ToString()
{

View File

@@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -29,6 +29,10 @@ internal static partial class Core
public bool IsNull => this.Value == HDC.Null.Value;
public static implicit operator IntPtr(HDC value) => value.Value;
public static explicit operator HDC(IntPtr value) => new(value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";

View File

@@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -31,15 +31,15 @@ internal static partial class Core
public static implicit operator int(HMONITOR value) => value.Value.ToInt32();
public static implicit operator HMONITOR(int value) => new(value);
public static explicit operator HMONITOR(int value) => new(value);
public static implicit operator IntPtr(HMONITOR value) => value.Value;
public static implicit operator HMONITOR(IntPtr value) => new(value);
public static explicit operator HMONITOR(IntPtr value) => new(value);
public static implicit operator HANDLE(HMONITOR value) => new(value.Value);
public static implicit operator HMONITOR(HANDLE value) => new(value.Value);
public static explicit operator HMONITOR(HANDLE value) => new(value.Value);
public override string ToString()
{

View File

@@ -3,8 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -20,6 +21,9 @@ internal static partial class Core
{
public static readonly HWND Null = new(IntPtr.Zero);
[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Name and value taken from Win32Api")]
public static readonly HWND HWND_MESSAGE = new(-3);
public readonly IntPtr Value;
public HWND(IntPtr value)

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -26,9 +26,11 @@ internal static partial class Core
this.Value = value;
}
public bool IsNull => this.Value == LPARAM.Null.Value;
public static implicit operator IntPtr(LPARAM value) => value.Value;
public static implicit operator LPARAM(IntPtr value) => new(value);
public static explicit operator LPARAM(IntPtr value) => new(value);
public override string ToString()
{

View File

@@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -25,6 +25,8 @@ internal static partial class Core
this.Value = LPCRECT.ToPtr(value);
}
public bool IsNull => this.Value == LPCRECT.Null.Value;
private static IntPtr ToPtr(CRECT value)
{
var ptr = Marshal.AllocHGlobal(CRECT.Size);
@@ -39,7 +41,7 @@ internal static partial class Core
public static implicit operator IntPtr(LPCRECT value) => value.Value;
public static implicit operator LPCRECT(IntPtr value) => new(value);
public static explicit operator LPCRECT(IntPtr value) => new(value);
public override string ToString()
{

View File

@@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -25,6 +25,8 @@ internal static partial class Core
this.Value = LPPOINT.ToPtr(value);
}
public bool IsNull => this.Value == LPPOINT.Null.Value;
private static IntPtr ToPtr(POINT value)
{
var ptr = Marshal.AllocHGlobal(POINT.Size);
@@ -44,7 +46,7 @@ internal static partial class Core
public static implicit operator IntPtr(LPPOINT value) => value.Value;
public static implicit operator LPPOINT(IntPtr value) => new(value);
public static explicit operator LPPOINT(IntPtr value) => new(value);
public override string ToString()
{

View File

@@ -4,7 +4,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -24,6 +24,8 @@ internal static partial class Core
this.Value = LPRECT.ToPtr(value);
}
public bool IsNull => this.Value == LPRECT.Null.Value;
private static IntPtr ToPtr(RECT value)
{
var ptr = Marshal.AllocHGlobal(RECT.Size);
@@ -38,7 +40,7 @@ internal static partial class Core
public static implicit operator IntPtr(LPRECT value) => value.Value;
public static implicit operator LPRECT(IntPtr value) => new(value);
public static explicit operator LPRECT(IntPtr value) => new(value);
public override string ToString()
{

View File

@@ -5,7 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -28,6 +28,14 @@ internal static partial class Core
/// </summary>
public readonly LONG y;
public POINT(
int x,
int y)
{
this.x = x;
this.y = y;
}
public POINT(
LONG x,
LONG y)

View File

@@ -5,7 +5,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -25,6 +25,15 @@ internal static partial class Core
public readonly LONG right;
public readonly LONG bottom;
public RECT(
int left, int top, int right, int bottom)
{
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
public RECT(
LONG left, LONG top, LONG right, LONG bottom)
{

View File

@@ -1,7 +1,8 @@
// 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 MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -26,6 +27,10 @@ internal static partial class Core
public static implicit operator UINT(uint value) => new(value);
public static explicit operator int(UINT value) => (int)value.Value;
public static explicit operator UINT(int value) => new((uint)value);
public override string ToString()
{
return $"{this.GetType().Name}({this.Value})";

View File

@@ -4,7 +4,7 @@
using System;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{
@@ -34,7 +34,7 @@ internal static partial class Core
public static implicit operator UIntPtr(ULONG_PTR value) => value.Value;
public static implicit operator ULONG_PTR(UIntPtr value) => new(value);
public static explicit operator ULONG_PTR(UIntPtr value) => new(value);
public override string ToString()
{

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Core
{

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class Gdi32
{

View File

@@ -2,7 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static class Libraries
{

View File

@@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information.
using System.Runtime.InteropServices;
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@@ -5,7 +5,7 @@
using System;
using System.Runtime.InteropServices;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

View File

@@ -2,9 +2,9 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using static MouseJumpUI.NativeMethods.Core;
using static MouseJumpUI.Common.NativeMethods.Core;
namespace MouseJumpUI.NativeMethods;
namespace MouseJumpUI.Common.NativeMethods;
internal static partial class User32
{

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