mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-09 13:57:05 +01:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b7adbe6ac | ||
|
|
8b8c75b9a5 | ||
|
|
5c257fb3db | ||
|
|
6e141f89c9 | ||
|
|
62c7b0a66d | ||
|
|
9509d7c1cc | ||
|
|
6043898ee5 | ||
|
|
136b239f96 | ||
|
|
c148b51698 | ||
|
|
1ae8327a43 | ||
|
|
aa977f7579 | ||
|
|
dcbff83d8c | ||
|
|
5c631bd2c7 | ||
|
|
c252d87573 | ||
|
|
92483aee1a | ||
|
|
2eec4c5a98 | ||
|
|
109f0b210a | ||
|
|
f019163083 | ||
|
|
deb6234d72 | ||
|
|
cdf5677eb9 | ||
|
|
bc0811e6a1 | ||
|
|
fc32fe29ba | ||
|
|
2e85a14e72 | ||
|
|
1016154851 | ||
|
|
d7c021bf84 | ||
|
|
651f2e4bd8 | ||
|
|
3e07b9b8f4 | ||
|
|
d3aa5028aa | ||
|
|
638f91f75d | ||
|
|
e3f5fba870 | ||
|
|
126a03e3b0 | ||
|
|
bf42abc328 | ||
|
|
1d27c5b422 | ||
|
|
36d2f81e89 | ||
|
|
ed249bc0e1 | ||
|
|
fb7a85ec81 | ||
|
|
a80896d51b | ||
|
|
4badf2d24a | ||
|
|
d3d333319b | ||
|
|
b6f0c69e13 | ||
|
|
37059d4db0 | ||
|
|
eaf38ddcf6 | ||
|
|
114e53ecd6 | ||
|
|
a54dc0e52a | ||
|
|
949df59c9e | ||
|
|
e993ea3c64 | ||
|
|
4558d2e01b | ||
|
|
c2b6dceb18 | ||
|
|
c00f37c8d7 | ||
|
|
e2f1ad6d40 | ||
|
|
be89da899c | ||
|
|
1fb632d4a6 | ||
|
|
f859eac18a | ||
|
|
420e097e24 | ||
|
|
92e8b06068 | ||
|
|
4c9e18116c | ||
|
|
29ce15bb8a |
@@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"xamlstyler.console": {
|
||||
"version": "3.2206.4",
|
||||
"version": "3.2404.2",
|
||||
"commands": [
|
||||
"xstyler"
|
||||
]
|
||||
|
||||
91
.github/actions/spell-check/allow/code.txt
vendored
91
.github/actions/spell-check/allow/code.txt
vendored
@@ -128,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
|
||||
|
||||
@@ -135,3 +221,8 @@ artanh
|
||||
arsinh
|
||||
arcosh
|
||||
|
||||
# Linux
|
||||
|
||||
dbus
|
||||
anypass
|
||||
gpg
|
||||
|
||||
21
.github/actions/spell-check/allow/names.txt
vendored
21
.github/actions/spell-check/allow/names.txt
vendored
@@ -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
|
||||
|
||||
|
||||
11
.github/actions/spell-check/expect.txt
vendored
11
.github/actions/spell-check/expect.txt
vendored
@@ -95,6 +95,7 @@ AUTOUPDATE
|
||||
AValid
|
||||
awakeness
|
||||
AWAYMODE
|
||||
azcliversion
|
||||
azman
|
||||
backtracer
|
||||
bbwe
|
||||
@@ -119,11 +120,13 @@ BLURREGION
|
||||
bmi
|
||||
bms
|
||||
BNumber
|
||||
BODGY
|
||||
BOKMAL
|
||||
bootstrapper
|
||||
BOOTSTRAPPERINSTALLFOLDER
|
||||
bostrot
|
||||
BOTTOMALIGN
|
||||
boxmodel
|
||||
BPBF
|
||||
bpmf
|
||||
bpp
|
||||
@@ -149,6 +152,7 @@ Cangjie
|
||||
CANRENAME
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
CAtl
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
@@ -163,6 +167,7 @@ CENTERALIGN
|
||||
ceq
|
||||
certlm
|
||||
certmgr
|
||||
cfp
|
||||
cguid
|
||||
CHANGECBCHAIN
|
||||
changecursor
|
||||
@@ -250,6 +255,7 @@ CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
CREATEWINDOWFAILED
|
||||
CRECT
|
||||
CRH
|
||||
critsec
|
||||
Crossdevice
|
||||
CRSEL
|
||||
@@ -751,6 +757,7 @@ KEYEVENTF
|
||||
KEYIMAGE
|
||||
keynum
|
||||
keyremaps
|
||||
keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
Knownfolders
|
||||
@@ -1335,6 +1342,9 @@ RRF
|
||||
rrr
|
||||
rsop
|
||||
Rsp
|
||||
rstringalnum
|
||||
rstringalpha
|
||||
rstringdigit
|
||||
Rstrtmgr
|
||||
RTB
|
||||
RTLREADING
|
||||
@@ -1349,6 +1359,7 @@ runtimeclass
|
||||
runtimeobject
|
||||
runtimepack
|
||||
runtimes
|
||||
ruuid
|
||||
rvm
|
||||
rwin
|
||||
rwl
|
||||
|
||||
3
.github/actions/spell-check/patterns.txt
vendored
3
.github/actions/spell-check/patterns.txt
vendored
@@ -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
|
||||
|
||||
|
||||
96
.github/workflows/msstore-submissions.yml
vendored
96
.github/workflows/msstore-submissions.yml
vendored
@@ -5,56 +5,80 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
|
||||
microsoft_store:
|
||||
name: Publish Microsoft Store
|
||||
environment: store
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: BODGY - Set up Gnome Keyring for future Cert Auth
|
||||
run: |-
|
||||
sudo apt-get install -y gnome-keyring
|
||||
export $(dbus-launch --sh-syntax)
|
||||
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --unlock)
|
||||
export $(echo 'anypass_just_to_unlock' | gnome-keyring-daemon --start --components=gpg,pkcs11,secrets,ssh)
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
enable-AzPSSession: true
|
||||
|
||||
- name: Get latest URL from public releases
|
||||
id: releaseVars
|
||||
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"))]')
|
||||
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')
|
||||
powerToysSetup=$(jq -n "$assets" | jq '[.[]|select(.name | contains("PowerToysUserSetup"))]')
|
||||
echo powerToysInstallerX64Url=$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("x64"))][0].browser_download_url') >> $GITHUB_OUTPUT
|
||||
echo powerToysInstallerArm64Url=$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("arm64"))][0].browser_download_url') >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: microsoft/setup-msstore-cli@v1
|
||||
|
||||
- name: Fetch Store Credential
|
||||
uses: azure/cli@v2
|
||||
with:
|
||||
azcliversion: latest
|
||||
inlineScript: |-
|
||||
az keyvault secret download --vault-name ${{ secrets.AZURE_KEYVAULT_NAME }} -n ${{ secrets.AZURE_AUTH_CERT_NAME }} -f cert.pfx.b64
|
||||
base64 -d < cert.pfx.b64 > cert.pfx
|
||||
|
||||
- name: Configure Store Credentials
|
||||
uses: microsoft/store-submission@v1
|
||||
with:
|
||||
command: configure
|
||||
type: win32
|
||||
seller-id: ${{ secrets.SELLER_ID }}
|
||||
product-id: ${{ secrets.PRODUCT_ID }}
|
||||
tenant-id: ${{ secrets.TENANT_ID }}
|
||||
client-id: ${{ secrets.CLIENT_ID }}
|
||||
client-secret: ${{ secrets.CLIENT_SECRET }}
|
||||
run: |-
|
||||
msstore reconfigure -cfp cert.pfx -c ${{ secrets.AZURE_CLIENT_ID }} -t ${{ secrets.AZURE_TENANT_ID }} -s ${{ secrets.SELLER_ID }}
|
||||
|
||||
- name: Update draft submission
|
||||
uses: microsoft/store-submission@v1
|
||||
with:
|
||||
command: update
|
||||
product-update: '{
|
||||
"packages":[
|
||||
{
|
||||
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerX64Url }}",
|
||||
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
|
||||
"architectures":["X64"],
|
||||
"installerParameters":"/quiet /norestart",
|
||||
"isSilentInstall":true
|
||||
},
|
||||
{
|
||||
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerArm64Url }}",
|
||||
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
|
||||
"architectures":["Arm64"],
|
||||
"installerParameters":"/quiet /norestart",
|
||||
"isSilentInstall":true
|
||||
}
|
||||
]
|
||||
}'
|
||||
run: |-
|
||||
msstore submission update ${{ secrets.PRODUCT_ID }} '{
|
||||
"packages":[
|
||||
{
|
||||
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerX64Url }}",
|
||||
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
|
||||
"architectures":["X64"],
|
||||
"installerParameters":"/quiet /norestart",
|
||||
"isSilentInstall":true
|
||||
},
|
||||
{
|
||||
"packageUrl":"${{ steps.releaseVars.outputs.powerToysInstallerArm64Url }}",
|
||||
"languages":["zh-hans", "zh-hant", "en", "cs", "nl", "fr", "pt", "pt-br", "de", "hu", "it", "ja", "ko", "pl", "ru", "es", "tr"],
|
||||
"architectures":["Arm64"],
|
||||
"installerParameters":"/quiet /norestart",
|
||||
"isSilentInstall":true
|
||||
}
|
||||
]
|
||||
}'
|
||||
|
||||
- name: Publish Submission
|
||||
uses: microsoft/store-submission@v1
|
||||
with:
|
||||
command: publish
|
||||
run: |-
|
||||
msstore submission publish ${{ secrets.PRODUCT_ID }}
|
||||
|
||||
- name: Clean up auth certificate
|
||||
if: always()
|
||||
run: |-
|
||||
rm -f cert.pfx cert.pfx.b64
|
||||
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
pool:
|
||||
demands: ImageOverride -equals SHINE-VS17-Latest
|
||||
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
timeoutInMinutes: 120
|
||||
strategy:
|
||||
maxParallel: 10
|
||||
|
||||
@@ -3,10 +3,10 @@ jobs:
|
||||
- job: Precheck
|
||||
pool:
|
||||
demands: ImageOverride -equals SHINE-VS17-Latest
|
||||
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
variables:
|
||||
SrcPath: $(Build.Repository.LocalPath)
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
name: SHINE-OSS-Testing-x64
|
||||
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-Testing-x64
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-Testing-x64
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
|
||||
@@ -76,7 +76,7 @@ extends:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
IsPipeline: 1 # The installer uses this to detect whether it should pick up localizations
|
||||
SkipCppCodeAnalysis: 1 # Skip the code analysis to speed up release CI. It runs on PR CI, anyway
|
||||
IsExperimentationLive: 1 # The build and installer use this to turn on experimentation
|
||||
# IsExperimentationLive: 1 # The build and installer use this to turn on experimentation
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
|
||||
18
COMMUNITY.md
18
COMMUNITY.md
@@ -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
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<UsePrecompiledHeaders Condition="'$(TF_BUILD)' != ''">false</UsePrecompiledHeaders>
|
||||
|
||||
<!-- Change this to bust the cache -->
|
||||
<MSBuildCacheCacheUniverse Condition="'$(MSBuildCacheCacheUniverse)' == ''">202310210737</MSBuildCacheCacheUniverse>
|
||||
<MSBuildCacheCacheUniverse Condition="'$(MSBuildCacheCacheUniverse)' == ''">202406130737</MSBuildCacheCacheUniverse>
|
||||
|
||||
<!--
|
||||
Visual Studio telemetry reads various ApplicationInsights.config files and other files after the project is finished, likely in a detached process.
|
||||
|
||||
@@ -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" />
|
||||
@@ -86,7 +86,7 @@
|
||||
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="4.145.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="5.50.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Vanara.PInvoke.User32" Version="3.4.11" />
|
||||
<PackageVersion Include="Vanara.PInvoke.Shell32" Version="3.4.11" />
|
||||
@@ -98,4 +98,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -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
|
||||
@@ -1365,7 +1365,7 @@ EXHIBIT A -Mozilla Public License.
|
||||
- System.ServiceProcess.ServiceController 8.0.0
|
||||
- System.Text.Encoding.CodePages 8.0.0
|
||||
- UnicodeInformation 2.6.0
|
||||
- UnitsNet 4.145.0
|
||||
- UnitsNet 5.50.0
|
||||
- UTF.Unknown 2.5.1
|
||||
- Vanara.PInvoke.Shell32 3.4.11
|
||||
- Vanara.PInvoke.User32 3.4.11
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -47,7 +47,16 @@ registerAdditionalNewLanguage("id", [".fileExtension"], idDefinition(), monaco)
|
||||
|
||||
* The id can be anything. Recommended is one of the file extensions. For example "php" or "reg".
|
||||
|
||||
4. Execute the steps described in the [monaco_languages.json](#monaco_languagesjson) section.
|
||||
4. In case you wish to add a custom color for a token, you can do so by adding the following line to [`customTokenColors.js`](/src/common/FilePreviewCommon/Assets/Monaco/customTokenColors.js):
|
||||
```javascript
|
||||
{token: 'token-name', foreground: 'ff0000'}
|
||||
```
|
||||
> Replace `token-name` with the name of the token and `ff0000` with the hex code of the desired color.
|
||||
> Note: you can also specify a `background` and a `fontStyle` attribute for your token.
|
||||
|
||||
* Keep in mind that these rules apply to all languages. Therefore, you should not change the colors of any default tokens. Instead, create new tokens specific to the language you are adding.
|
||||
|
||||
5. Execute the steps described in the [monaco_languages.json](#monaco_languagesjson) section.
|
||||
|
||||
### Add a new file extension to an existing language
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ In order to test the remapping logic, a mocked keyboard input handler had to be
|
||||
The [`MockedInput`](https://github.com/microsoft/PowerToys/blob/main/src/modules/keyboardmanager/test/MockedInput.h) class uses a 256 size `bool` vector to store the key state for each key code. Identifying the foreground process is mocked by simply setting and getting a string value for the name of the current process.
|
||||
|
||||
[To mock the `SendInput` method](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/test/MockedInput.cpp#L10-L110), the steps for processing the input are as follows. This implementation is based on public documentation for SendInput and the behavior of key messages and keyboard hooks:
|
||||
- Iterate over all the inputs in the INPUT array argument
|
||||
- Iterate over all the inputs in the `INPUT` vector argument.
|
||||
- If the event is a key up event, then it is considered [`WM_SYSKEYUP`](https://learn.microsoft.com/windows/win32/inputdev/wm-syskeyup) if Alt is held down, otherwise it is `WM_KEYUP`.
|
||||
- If the event is a key down event, then it is considered [`WM_SYSKEYDOWN`](https://learn.microsoft.com/windows/win32/inputdev/wm-syskeydown) if either Alt is held down or if it is F10, otherwise it is `WM_KEYDOWN`.
|
||||
- An optional function which can be set on the `MockedInput` handler can be used to test for the number of times a key event is received by the system with a particular condition using [`sendVirtualInputCallCondition`](https://github.com/microsoft/PowerToys/blob/b80578b1b9a4b24c9945bddac33c771204280107/src/modules/keyboardmanager/test/MockedInput.cpp#L48-L52).
|
||||
|
||||
@@ -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.
|
||||
@@ -12,4 +12,4 @@ Eg: The maximum number of results drop down updates the maximum number of rows i
|
||||
## Keyboard Manager
|
||||
- The Settings process and keyboard manager share access to a common `default.json` file which contains information about the remapped keys and shortcuts.
|
||||
- To ensure that there is no contention while both processes try to access the common file, there is a named file mutex.
|
||||
- The settings process expects the keyboard manager process to create the `default.json` file if it does not exist. It does not create the file in case it is not present.
|
||||
- The settings process expects the keyboard manager process to create the `default.json` file if it does not exist. It does not create the file in case it is not present.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<DirectoryRef Id="INSTALLFOLDER" FileSource="$(var.BinDir)">
|
||||
<Component Id="powertoys_per_machine_comp" Win64="yes">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys">
|
||||
<RegistryValue Type="string" Name="InstallScope" Value="$(var.InstallScope)" />
|
||||
<RegistryValue Type="string" Name="InstallScope" Value="$(var.InstallScope)" />
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
<Component Id="powertoys_toast_clsid" Win64="yes">
|
||||
@@ -46,34 +46,19 @@
|
||||
</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" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
<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>
|
||||
</DirectoryRef>
|
||||
<?else?>
|
||||
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<!-- DSC module files for PerUser handled in InstallDSCModule custom action. -->
|
||||
<?else?>
|
||||
<DirectoryRef Id="ProgramFiles64Folder">
|
||||
<Directory Id="WindowsPowerShellFolder" Name="WindowsPowerShell">
|
||||
<Directory Id="PowerShellModulesFolder" Name="Modules">
|
||||
@@ -89,7 +74,7 @@
|
||||
</Directory>
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||
<Component Id="PowerToysStartMenuShortcut" >
|
||||
@@ -130,23 +115,27 @@
|
||||
</Fragment>
|
||||
<Fragment>
|
||||
<ComponentGroup Id="CoreComponents">
|
||||
<Component Id="RemoveCoreFolder" Guid="9330BD69-2D12-4D98-B0C7-66C99564D619" Directory="INSTALLFOLDER" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="RemoveCoreFolder" Value="" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall"/>
|
||||
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall"/>
|
||||
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall"/>
|
||||
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="powertoys_exe" />
|
||||
<ComponentRef Id="PowerToysStartMenuShortcut"/>
|
||||
<ComponentRef Id="powertoys_per_machine_comp" />
|
||||
<ComponentRef Id="powertoys_toast_clsid" />
|
||||
<ComponentRef Id="License_rtf" />
|
||||
<ComponentRef Id="Notice_md" />
|
||||
<ComponentRef Id="DesktopShortcut" />
|
||||
<ComponentRef Id="PowerToysDSC" />
|
||||
<Component Id="RemoveCoreFolder" Guid="9330BD69-2D12-4D98-B0C7-66C99564D619" Directory="INSTALLFOLDER" >
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<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"/>
|
||||
</Component>
|
||||
<ComponentRef Id="powertoys_exe" />
|
||||
<ComponentRef Id="PowerToysStartMenuShortcut"/>
|
||||
<ComponentRef Id="powertoys_per_machine_comp" />
|
||||
<ComponentRef Id="powertoys_toast_clsid" />
|
||||
<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>
|
||||
|
||||
@@ -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,7 +154,10 @@
|
||||
<!--<Custom Action="InstallEmbeddedMSIXTask" After="InstallFinalize">
|
||||
NOT Installed
|
||||
</Custom>-->
|
||||
<Custom Action="TelemetryLogInstallSuccess" After="InstallFinalize">
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<Custom Action="InstallDSCModule" After="InstallFiles"/>
|
||||
<?endif?>
|
||||
<Custom Action="TelemetryLogInstallSuccess" After="InstallFinalize">
|
||||
NOT Installed
|
||||
</Custom>
|
||||
<Custom Action="TelemetryLogUninstallSuccess" After="InstallFinalize">
|
||||
@@ -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>
|
||||
|
||||
@@ -21,7 +21,7 @@ $fileWxs = Get-Content $wxsFilePath;
|
||||
|
||||
$fileExclusionList = @("*.pdb", "*.lastcodeanalysissucceeded", "createdump.exe", "powertoys.exe")
|
||||
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "monacoSpecialLanguages.js", "*.pri")
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "monacoSpecialLanguages.js", "customTokenColors.js", "*.pri")
|
||||
|
||||
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -162,7 +179,7 @@ UINT __stdcall LaunchPowerToysCA(MSIHANDLE hInstall)
|
||||
BOOL isSystemUser = IsLocalSystem();
|
||||
|
||||
if (isSystemUser) {
|
||||
|
||||
|
||||
auto action = [&commandLine](HANDLE userToken) {
|
||||
STARTUPINFO startupInfo{ .cb = sizeof(STARTUPINFO), .wShowWindow = SW_SHOWNORMAL };
|
||||
PROCESS_INFORMATION processInformation;
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -18,10 +18,12 @@ EXPORTS
|
||||
CertifyVirtualCameraDriverCA
|
||||
InstallVirtualCameraDriverCA
|
||||
InstallEmbeddedMSIXCA
|
||||
InstallDSCModuleCA
|
||||
UnApplyModulesRegistryChangeSetsCA
|
||||
UninstallVirtualCameraDriverCA
|
||||
UnRegisterContextMenuPackagesCA
|
||||
UninstallEmbeddedMSIXCA
|
||||
UninstallDSCModuleCA
|
||||
UninstallServicesCA
|
||||
UninstallCommandNotFoundModuleCA
|
||||
UpgradeCommandNotFoundModuleCA
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/^#.*$/, 'comment'],
|
||||
[/^\s*!.*/, 'invalid'],
|
||||
[/^\s*[^#]+/, "tag"]
|
||||
[/.*((?<!(^|\/))\*\*.*|\*\*(?!(\/|$))).*/, 'invalid'],
|
||||
[/((?:^!\s*(?:\\\s|\S)+)?)((?:^\s*(?:\\\s|\S)+)?)((?:\s+(?:\\\s|\S)+)*)/, ['custom-gitignore.negation', 'tag', 'invalid']]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export const customTokenColors = [
|
||||
{token: 'custom-gitignore.negation', foreground: 'c00ce0'}
|
||||
];
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
// Get URL parameters:
|
||||
// `code` contains the code of the file in base64 encoded
|
||||
// `theme` can be "light" or "dark"
|
||||
// `theme` can be "vs" for light theme or "vs-dark" for dark theme
|
||||
// `lang` is the language of the file
|
||||
// `wrap` if the editor is wrapping or not
|
||||
|
||||
@@ -59,19 +59,29 @@
|
||||
<script src="http://[[PT_URL]]/monacoSpecialLanguages.js" type="module"></script>
|
||||
<script type="module">
|
||||
var editor;
|
||||
import { registerAdditionalLanguages } from "http://[[PT_URL]]/monacoSpecialLanguages.js"
|
||||
import { registerAdditionalLanguages } from 'http://[[PT_URL]]/monacoSpecialLanguages.js';
|
||||
import { customTokenColors } from 'http://[[PT_URL]]/customTokenColors.js';
|
||||
require.config({ paths: { vs: 'http://[[PT_URL]]/monacoSRC/min/vs' } });
|
||||
require(['vs/editor/editor.main'], async function () {
|
||||
await registerAdditionalLanguages(monaco)
|
||||
|
||||
// Creates a theme to handle custom tokens
|
||||
monaco.editor.defineTheme('theme', {
|
||||
base: theme, // Sets the base theme to "vs" or "vs-dark" depending on the user's preference
|
||||
inherit: true,
|
||||
rules: customTokenColors,
|
||||
colors: {} // `colors` is a required attribute
|
||||
});
|
||||
|
||||
// Creates the editor
|
||||
// For all parameters: https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
|
||||
// For all parameters: https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IStandaloneEditorConstructionOptions.html
|
||||
editor = monaco.editor.create(document.getElementById('container'), {
|
||||
value: code, // Sets content of the editor
|
||||
language: lang, // Sets language fof the code
|
||||
language: lang, // Sets language of the code
|
||||
readOnly: true, // Sets to readonly
|
||||
theme: theme, // Sets editor theme
|
||||
theme: 'theme', // Sets editor theme
|
||||
minimap: {enabled: false}, // Disables minimap
|
||||
lineNumbersMinChars: "3", //Width of the line numbers
|
||||
lineNumbersMinChars: '3', // Width of the line numbers
|
||||
scrollbar: {
|
||||
// Deactivate shadows
|
||||
shadows: false,
|
||||
@@ -79,17 +89,16 @@
|
||||
// Render scrollbar automatically
|
||||
vertical: 'auto',
|
||||
horizontal: 'auto',
|
||||
|
||||
},
|
||||
stickyScroll: {enabled: stickyScroll},
|
||||
fontSize: fontSize,
|
||||
wordWrap: (wrap?"on":"off") // Word wraps
|
||||
wordWrap: (wrap ? 'on' : 'off') // Word wraps
|
||||
});
|
||||
window.onresize = function (){
|
||||
window.onresize = () => {
|
||||
editor.layout();
|
||||
};
|
||||
|
||||
// Add switch wrap button to context menu
|
||||
// Add toggle wrap button to context menu
|
||||
editor.addAction({
|
||||
id: 'text-wrap',
|
||||
|
||||
@@ -101,17 +110,17 @@
|
||||
// A rule to evaluate on top of the precondition in order to dispatch the keybindings.
|
||||
keybindingContext: null,
|
||||
|
||||
contextMenuGroupId: 'cutcopypaste',
|
||||
contextMenuGroupId: 'cutcopypaste',
|
||||
|
||||
contextMenuOrder: 100,
|
||||
contextMenuOrder: 100,
|
||||
|
||||
// Method that will be executed when the action is triggered.
|
||||
// @param editor The editor instance is passed in as a convenience
|
||||
run: function (ed) {
|
||||
if(wrap){
|
||||
editor.updateOptions({ wordWrap: "off" })
|
||||
}else{
|
||||
editor.updateOptions({ wordWrap: "on" })
|
||||
if (wrap) {
|
||||
editor.updateOptions({ wordWrap: 'off' })
|
||||
} else {
|
||||
editor.updateOptions({ wordWrap: 'on' })
|
||||
}
|
||||
wrap = !wrap;
|
||||
}
|
||||
@@ -120,11 +129,11 @@
|
||||
onContextMenu();
|
||||
});
|
||||
|
||||
function onContextMenu(){
|
||||
function onContextMenu() {
|
||||
// Hide context menu items
|
||||
// Code modified from https://stackoverflow.com/questions/48745208/disable-cut-and-copy-in-context-menu-in-monaco-editor/65413517#65413517
|
||||
let menus = require('vs/platform/actions/common/actions').MenuRegistry._menuItems
|
||||
let contextMenuEntry = [...menus].find(entry => entry[0].id == "EditorContext")
|
||||
let contextMenuEntry = [...menus].find(entry => entry[0].id == 'EditorContext')
|
||||
let contextMenuLinks = contextMenuEntry[1]
|
||||
|
||||
let removableIds = ['editor.action.clipboardCutAction', 'editor.action.formatDocument', 'editor.action.formatSelection', 'editor.action.quickCommand', 'editor.action.quickOutline', 'editor.action.refactor', 'editor.action.sourceAction', 'editor.action.rename', undefined, 'editor.action.revealDefinition', 'editor.action.revealDeclaration', 'editor.action.goToTypeDefinition', 'editor.action.goToImplementation', 'editor.action.goToReferences', 'editor.action.changeAll']
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
<None Update="Assets\Monaco\monacoSpecialLanguages.js">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Assets\Monaco\customTokenColors.js">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Content Include="Assets\Monaco\monacoSRC\**">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Settings;
|
||||
using AdvancedPaste.ViewModels;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
using static AdvancedPaste.Helpers.NativeMethods;
|
||||
@@ -47,6 +46,7 @@ namespace AdvancedPaste
|
||||
Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) =>
|
||||
{
|
||||
services.AddSingleton<OptionsViewModel>();
|
||||
services.AddSingleton<IUserSettings, UserSettings>();
|
||||
}).Build();
|
||||
|
||||
viewModel = GetService<OptionsViewModel>();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -20,14 +19,13 @@ namespace AdvancedPaste.Controls
|
||||
public sealed partial class PromptBox : Microsoft.UI.Xaml.Controls.UserControl
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private UserSettings _userSettings;
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
public static readonly DependencyProperty PromptProperty = DependencyProperty.Register(
|
||||
nameof(Prompt),
|
||||
typeof(string),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: string.Empty));
|
||||
nameof(Prompt),
|
||||
typeof(string),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: string.Empty));
|
||||
|
||||
public OptionsViewModel ViewModel { get; private set; }
|
||||
|
||||
@@ -38,10 +36,10 @@ namespace AdvancedPaste.Controls
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(
|
||||
nameof(PlaceholderText),
|
||||
typeof(string),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: string.Empty));
|
||||
nameof(PlaceholderText),
|
||||
typeof(string),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: string.Empty));
|
||||
|
||||
public string PlaceholderText
|
||||
{
|
||||
@@ -50,10 +48,10 @@ namespace AdvancedPaste.Controls
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(
|
||||
nameof(Footer),
|
||||
typeof(object),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: null));
|
||||
nameof(Footer),
|
||||
typeof(object),
|
||||
typeof(PromptBox),
|
||||
new PropertyMetadata(defaultValue: null));
|
||||
|
||||
public object Footer
|
||||
{
|
||||
@@ -65,7 +63,7 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
_userSettings = new UserSettings();
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
|
||||
ViewModel = App.GetService<OptionsViewModel>();
|
||||
}
|
||||
@@ -80,8 +78,6 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomFormatEvent());
|
||||
|
||||
VisualStateManager.GoToState(this, "LoadingState", true);
|
||||
string inputInstructions = InputTxtBox.Text;
|
||||
ViewModel.SaveQuery(inputInstructions);
|
||||
|
||||
@@ -3,21 +3,20 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Settings;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Graphics;
|
||||
using WinUIEx;
|
||||
using WinUIEx.Messaging;
|
||||
using static AdvancedPaste.Helpers.NativeMethods;
|
||||
|
||||
namespace AdvancedPaste
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx, IDisposable
|
||||
{
|
||||
private WindowMessageMonitor _msgMonitor;
|
||||
private readonly WindowMessageMonitor _msgMonitor;
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
@@ -25,6 +24,8 @@ namespace AdvancedPaste
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
|
||||
AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico");
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetTitleBar(titleBar);
|
||||
@@ -32,6 +33,8 @@ namespace AdvancedPaste
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Title = loader.GetString("WindowTitle");
|
||||
|
||||
Activated += OnActivated;
|
||||
|
||||
_msgMonitor = new WindowMessageMonitor(this);
|
||||
_msgMonitor.WindowMessageReceived += (_, e) =>
|
||||
{
|
||||
@@ -47,6 +50,14 @@ namespace AdvancedPaste
|
||||
WindowHelpers.BringToForeground(this.GetWindowHandle());
|
||||
}
|
||||
|
||||
private void OnActivated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (_userSettings.CloseAfterLosingFocus && args.WindowActivationState == WindowActivationState.Deactivated)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
@@ -66,11 +77,15 @@ namespace AdvancedPaste
|
||||
|
||||
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
{
|
||||
Windows.Win32.PInvoke.ShowWindow((Windows.Win32.Foundation.HWND)this.GetWindowHandle(), Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
|
||||
Hide();
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void Hide()
|
||||
{
|
||||
Windows.Win32.PInvoke.ShowWindow(new Windows.Win32.Foundation.HWND(this.GetWindowHandle()), Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
}
|
||||
|
||||
public void SetFocus()
|
||||
{
|
||||
MainPage.CustomFormatTextBox.InputTxtBox.Focus(FocusState.Programmatic);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -86,6 +87,11 @@ namespace AdvancedPaste.Pages
|
||||
|
||||
_dispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
// Clear to avoid leaks due to Garbage Collection not clearing the bitmap from memory. Fix for https://github.com/microsoft/PowerToys/issues/33423
|
||||
clipboardHistory.Where(x => x.Image is not null)
|
||||
.ToList()
|
||||
.ForEach(x => x.Image.ClearValue(BitmapImage.UriSourceProperty));
|
||||
|
||||
clipboardHistory.Clear();
|
||||
|
||||
foreach (var item in items)
|
||||
@@ -225,6 +231,7 @@ namespace AdvancedPaste.Pages
|
||||
var item = e.ClickedItem as ClipboardItem;
|
||||
if (item is not null)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteClipboardItemClicked());
|
||||
if (!string.IsNullOrEmpty(item.Content))
|
||||
{
|
||||
ClipboardHelper.SetClipboardTextContent(item.Content);
|
||||
|
||||
@@ -33,6 +33,8 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
private string _openAIKey;
|
||||
|
||||
private string _modelName = "gpt-3.5-turbo-instruct";
|
||||
|
||||
public bool IsAIEnabled => !string.IsNullOrEmpty(this._openAIKey);
|
||||
|
||||
public AICompletionsHelper()
|
||||
@@ -69,14 +71,14 @@ namespace AdvancedPaste.Helpers
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GetAICompletion(string systemInstructions, string userMessage)
|
||||
private Response<Completions> GetAICompletion(string systemInstructions, string userMessage)
|
||||
{
|
||||
OpenAIClient azureAIClient = new OpenAIClient(_openAIKey);
|
||||
|
||||
var response = azureAIClient.GetCompletions(
|
||||
new CompletionsOptions()
|
||||
{
|
||||
DeploymentName = "gpt-3.5-turbo-instruct",
|
||||
DeploymentName = _modelName,
|
||||
Prompts =
|
||||
{
|
||||
systemInstructions + "\n\n" + userMessage,
|
||||
@@ -90,7 +92,7 @@ namespace AdvancedPaste.Helpers
|
||||
Console.WriteLine("Cut off due to length constraints");
|
||||
}
|
||||
|
||||
return response.Value.Choices[0].Text;
|
||||
return response;
|
||||
}
|
||||
|
||||
public AICompletionsResponse AIFormatString(string inputInstructions, string inputString)
|
||||
@@ -109,10 +111,16 @@ Output:
|
||||
";
|
||||
|
||||
string aiResponse = null;
|
||||
Response<Completions> rawAIResponse = null;
|
||||
int apiRequestStatus = (int)HttpStatusCode.OK;
|
||||
try
|
||||
{
|
||||
aiResponse = this.GetAICompletion(systemInstructions, userMessage);
|
||||
rawAIResponse = this.GetAICompletion(systemInstructions, userMessage);
|
||||
aiResponse = rawAIResponse.Value.Choices[0].Text;
|
||||
|
||||
int promptTokens = rawAIResponse.Value.Usage.PromptTokens;
|
||||
int completionTokens = rawAIResponse.Value.Usage.CompletionTokens;
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomFormatEvent(promptTokens, completionTokens, _modelName));
|
||||
}
|
||||
catch (Azure.RequestFailedException error)
|
||||
{
|
||||
|
||||
@@ -9,5 +9,7 @@ namespace AdvancedPaste.Settings
|
||||
public bool ShowCustomPreview { get; }
|
||||
|
||||
public bool SendPasteKeyCombination { get; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,14 @@ namespace AdvancedPaste.Helpers
|
||||
{
|
||||
internal static class JsonHelper
|
||||
{
|
||||
// Ini parts regex
|
||||
private static readonly Regex IniSectionNameRegex = new Regex(@"^\[(.+)\]");
|
||||
private static readonly Regex IniValueLineRegex = new Regex(@"(.+?)\s*=\s*(.*)");
|
||||
|
||||
// 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();
|
||||
@@ -39,6 +49,7 @@ namespace AdvancedPaste.Helpers
|
||||
{
|
||||
XmlDocument doc = new XmlDocument();
|
||||
doc.LoadXml(text);
|
||||
Logger.LogDebug("Converted from XML.");
|
||||
jsonText = JsonConvert.SerializeXmlNode(doc, Newtonsoft.Json.Formatting.Indented);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -46,6 +57,75 @@ namespace AdvancedPaste.Helpers
|
||||
Logger.LogError("Failed parsing input as xml", ex);
|
||||
}
|
||||
|
||||
// Try convert Ini
|
||||
// (Must come before CSV that ini is not false detected as CSV.)
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(jsonText))
|
||||
{
|
||||
var ini = new Dictionary<string, Dictionary<string, string>>();
|
||||
var lastSectionName = string.Empty;
|
||||
|
||||
string[] lines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Skipp comment lines.
|
||||
// (Comments are lines that starts with a semicolon.
|
||||
// This also skips commented key-value-pairs.)
|
||||
lines = lines.Where(l => !l.StartsWith(';')).ToArray();
|
||||
|
||||
// Validate content as ini
|
||||
// (First line is a section name and second line is a section name or a key-value-pair.
|
||||
// For the second line we check both, in case the first ini section is empty.)
|
||||
if (lines.Length >= 2 && IniSectionNameRegex.IsMatch(lines[0]) &&
|
||||
(IniSectionNameRegex.IsMatch(lines[1]) || IniValueLineRegex.IsMatch(lines[1])))
|
||||
{
|
||||
// Parse and convert Ini
|
||||
foreach (string line in lines)
|
||||
{
|
||||
Match lineSectionNameCheck = IniSectionNameRegex.Match(line);
|
||||
Match lineKeyValuePairCheck = IniValueLineRegex.Match(line);
|
||||
|
||||
if (lineSectionNameCheck.Success)
|
||||
{
|
||||
// Section name (Group 1)
|
||||
lastSectionName = lineSectionNameCheck.Groups[1].Value.Trim();
|
||||
if (string.IsNullOrWhiteSpace(lastSectionName))
|
||||
{
|
||||
throw new FormatException("Invalid ini file format: Empty section name.");
|
||||
}
|
||||
|
||||
ini.Add(lastSectionName, new Dictionary<string, string>());
|
||||
}
|
||||
else if (!lineKeyValuePairCheck.Success)
|
||||
{
|
||||
// Fail if it is not a key-value-pair (and was not detected as section name before).
|
||||
throw new FormatException("Invalid ini file format: Invalid line.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Key-value-pair (Group 1=Key; Group 2=Value)
|
||||
string iniKeyName = lineKeyValuePairCheck.Groups[1].Value.Trim();
|
||||
if (string.IsNullOrWhiteSpace(iniKeyName))
|
||||
{
|
||||
throw new FormatException("Invalid ini file format: Empty value name (key).");
|
||||
}
|
||||
|
||||
string iniValueData = lineKeyValuePairCheck.Groups[2].Value;
|
||||
ini[lastSectionName].Add(iniKeyName, iniValueData);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to JSON
|
||||
Logger.LogDebug("Converted from Ini.");
|
||||
jsonText = JsonConvert.SerializeObject(ini, Newtonsoft.Json.Formatting.Indented);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed parsing input as ini", ex);
|
||||
}
|
||||
|
||||
// Try convert CSV
|
||||
try
|
||||
{
|
||||
@@ -53,11 +133,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 +166,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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,15 @@ namespace AdvancedPaste.Settings
|
||||
|
||||
public bool SendPasteKeyCombination { get; private set; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; private set; }
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
|
||||
ShowCustomPreview = true;
|
||||
SendPasteKeyCombination = true;
|
||||
CloseAfterLosingFocus = false;
|
||||
|
||||
LoadSettingsFromJson();
|
||||
|
||||
@@ -61,6 +64,7 @@ namespace AdvancedPaste.Settings
|
||||
{
|
||||
ShowCustomPreview = settings.Properties.ShowCustomPreview;
|
||||
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
|
||||
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
|
||||
}
|
||||
|
||||
retry = false;
|
||||
|
||||
@@ -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>
|
||||
@@ -228,4 +228,4 @@
|
||||
<data name="OpenAIGpoDisabled" xml:space="preserve">
|
||||
<value>To custom with AI is disabled by your organization</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
|
||||
@@ -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.Diagnostics.Tracing;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.PowerToys.Telemetry.Events;
|
||||
|
||||
namespace AdvancedPaste.Telemetry
|
||||
{
|
||||
[EventData]
|
||||
public class AdvancedPasteClipboardItemClicked : EventBase, IEvent
|
||||
{
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,19 @@ namespace AdvancedPaste.Telemetry
|
||||
[EventData]
|
||||
public class AdvancedPasteGenerateCustomFormatEvent : EventBase, IEvent
|
||||
{
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
public string ModelName { get; set; }
|
||||
|
||||
public AdvancedPasteGenerateCustomFormatEvent(int promptTokens, int completionTokens, string modelName)
|
||||
{
|
||||
this.PromptTokens = promptTokens;
|
||||
this.CompletionTokens = completionTokens;
|
||||
ModelName = modelName;
|
||||
}
|
||||
|
||||
public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,12 @@ namespace AdvancedPaste.ViewModels
|
||||
public partial class OptionsViewModel : ObservableObject
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
private App app = App.Current as App;
|
||||
|
||||
private AICompletionsHelper aiHelper;
|
||||
|
||||
private UserSettings _userSettings;
|
||||
|
||||
public DataPackageView ClipboardData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -50,10 +49,10 @@ namespace AdvancedPaste.ViewModels
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxErrorText))]
|
||||
private int _apiRequestStatus;
|
||||
|
||||
public OptionsViewModel()
|
||||
public OptionsViewModel(IUserSettings userSettings)
|
||||
{
|
||||
aiHelper = new AICompletionsHelper();
|
||||
_userSettings = new UserSettings();
|
||||
_userSettings = userSettings;
|
||||
|
||||
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
x:Uid="Window"
|
||||
Width="680"
|
||||
MinWidth="480"
|
||||
MinWidth="520"
|
||||
MinHeight="320"
|
||||
mc:Ignorable="d">
|
||||
<Window.SystemBackdrop>
|
||||
|
||||
@@ -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)
|
||||
|
||||
165
src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs
Normal file
165
src/modules/Hosts/HostsUILib/Helpers/DuplicateService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/modules/Hosts/HostsUILib/Helpers/IDuplicateService.cs
Normal file
16
src/modules/Hosts/HostsUILib/Helpers/IDuplicateService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using HostsUILib.Models;
|
||||
|
||||
namespace HostsUILib.Helpers
|
||||
{
|
||||
public interface IHostsService : IDisposable
|
||||
public interface IHostsService
|
||||
{
|
||||
string HostsFilePath { get; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace HostsUILib.Settings
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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)),
|
||||
};
|
||||
@@ -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]
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -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}" +
|
||||
"}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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})";
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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})";
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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)
|
||||
@@ -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
|
||||
{
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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()
|
||||
{
|
||||
@@ -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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user