mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-07 12:57:03 +01:00
Compare commits
2 Commits
V0.82.1
...
dev/crloew
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9518dece7f | ||
|
|
8dc95c991e |
@@ -9,7 +9,7 @@
|
||||
]
|
||||
},
|
||||
"xamlstyler.console": {
|
||||
"version": "3.2404.2",
|
||||
"version": "3.2206.4",
|
||||
"commands": [
|
||||
"xstyler"
|
||||
]
|
||||
|
||||
92
.github/actions/spell-check/allow/code.txt
vendored
92
.github/actions/spell-check/allow/code.txt
vendored
@@ -39,7 +39,6 @@ nupkg
|
||||
petabyte
|
||||
resw
|
||||
resx
|
||||
srt
|
||||
Stereolithography
|
||||
terabyte
|
||||
UYVY
|
||||
@@ -128,92 +127,6 @@ 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
|
||||
|
||||
@@ -221,8 +134,3 @@ 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,12 +33,10 @@ Advaith
|
||||
alekhyareddy
|
||||
Aleks
|
||||
angularsen
|
||||
Anirudha
|
||||
arjunbalgovind
|
||||
Ashish
|
||||
Baltazar
|
||||
Bao
|
||||
Bartosz
|
||||
betadele
|
||||
betsegaw
|
||||
bricelam
|
||||
@@ -53,8 +51,6 @@ crutkas
|
||||
damienleroy
|
||||
davidegiacometti
|
||||
debian
|
||||
Deibisu
|
||||
Deibisu
|
||||
Delimarsky
|
||||
Deondre
|
||||
DHowett
|
||||
@@ -66,7 +62,6 @@ gabime
|
||||
Galaxi
|
||||
Garside
|
||||
Gershaft
|
||||
Giordani
|
||||
Gokce
|
||||
Guo
|
||||
hanselman
|
||||
@@ -75,15 +70,12 @@ Heiko
|
||||
Hemmerlein
|
||||
hlaueriksson
|
||||
Horvalds
|
||||
Howett
|
||||
htcfreek
|
||||
Huynh
|
||||
Jaswal
|
||||
jefflord
|
||||
Jordi
|
||||
jyuwono
|
||||
Kairu
|
||||
Kairu
|
||||
Kamra
|
||||
Kantarci
|
||||
Karthick
|
||||
@@ -100,9 +92,7 @@ martinmoene
|
||||
Melman
|
||||
Mikhayelyan
|
||||
msft
|
||||
Mykhailo
|
||||
Myrvold
|
||||
Naro
|
||||
nathancartlidge
|
||||
Nemeth
|
||||
nielslaute
|
||||
@@ -113,13 +103,9 @@ peteblois
|
||||
phoboslab
|
||||
Ponten
|
||||
Pooja
|
||||
Pylyp
|
||||
quachpas
|
||||
Quriz
|
||||
randyrants
|
||||
ricardosantos
|
||||
riri
|
||||
riri
|
||||
ritchielawrence
|
||||
robmikh
|
||||
Rutkas
|
||||
@@ -133,12 +119,10 @@ Seraphima
|
||||
skttl
|
||||
somil
|
||||
Soref
|
||||
Sosnowski
|
||||
stefan
|
||||
Szablewski
|
||||
Tadele
|
||||
talynone
|
||||
Taras
|
||||
TBM
|
||||
tilovell
|
||||
Triet
|
||||
@@ -146,9 +130,11 @@ waaverecords
|
||||
ycv
|
||||
Yuniardi
|
||||
yuyoyuppe
|
||||
Zeol
|
||||
Zoltan
|
||||
Zykova
|
||||
Kairu
|
||||
Deibisu
|
||||
riri
|
||||
|
||||
# OTHERS
|
||||
|
||||
@@ -183,3 +169,4 @@ xamlstyler
|
||||
Xavalon
|
||||
Xbox
|
||||
Youdao
|
||||
|
||||
|
||||
11
.github/actions/spell-check/expect.txt
vendored
11
.github/actions/spell-check/expect.txt
vendored
@@ -95,7 +95,6 @@ AUTOUPDATE
|
||||
AValid
|
||||
awakeness
|
||||
AWAYMODE
|
||||
azcliversion
|
||||
azman
|
||||
backtracer
|
||||
bbwe
|
||||
@@ -120,13 +119,11 @@ BLURREGION
|
||||
bmi
|
||||
bms
|
||||
BNumber
|
||||
BODGY
|
||||
BOKMAL
|
||||
bootstrapper
|
||||
BOOTSTRAPPERINSTALLFOLDER
|
||||
bostrot
|
||||
BOTTOMALIGN
|
||||
boxmodel
|
||||
BPBF
|
||||
bpmf
|
||||
bpp
|
||||
@@ -152,7 +149,6 @@ Cangjie
|
||||
CANRENAME
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
CAtl
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
@@ -167,7 +163,6 @@ CENTERALIGN
|
||||
ceq
|
||||
certlm
|
||||
certmgr
|
||||
cfp
|
||||
cguid
|
||||
CHANGECBCHAIN
|
||||
changecursor
|
||||
@@ -255,7 +250,6 @@ CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
CREATEWINDOWFAILED
|
||||
CRECT
|
||||
CRH
|
||||
critsec
|
||||
Crossdevice
|
||||
CRSEL
|
||||
@@ -757,7 +751,6 @@ KEYEVENTF
|
||||
KEYIMAGE
|
||||
keynum
|
||||
keyremaps
|
||||
keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
Knownfolders
|
||||
@@ -1342,9 +1335,6 @@ RRF
|
||||
rrr
|
||||
rsop
|
||||
Rsp
|
||||
rstringalnum
|
||||
rstringalpha
|
||||
rstringdigit
|
||||
Rstrtmgr
|
||||
RTB
|
||||
RTLREADING
|
||||
@@ -1359,7 +1349,6 @@ 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,9 +40,6 @@
|
||||
# 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,80 +5,56 @@ 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("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
|
||||
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')
|
||||
|
||||
- name: Configure Store Credentials
|
||||
run: |-
|
||||
msstore reconfigure -cfp cert.pfx -c ${{ secrets.AZURE_CLIENT_ID }} -t ${{ secrets.AZURE_TENANT_ID }} -s ${{ secrets.SELLER_ID }}
|
||||
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 }}
|
||||
|
||||
- name: Update draft submission
|
||||
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
|
||||
}
|
||||
]
|
||||
}'
|
||||
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
|
||||
}
|
||||
]
|
||||
}'
|
||||
|
||||
- name: Publish Submission
|
||||
run: |-
|
||||
msstore submission publish ${{ secrets.PRODUCT_ID }}
|
||||
|
||||
- name: Clean up auth certificate
|
||||
if: always()
|
||||
run: |-
|
||||
rm -f cert.pfx cert.pfx.b64
|
||||
uses: microsoft/store-submission@v1
|
||||
with:
|
||||
command: publish
|
||||
|
||||
@@ -41,6 +41,6 @@ jobs:
|
||||
platform: arm64
|
||||
${{ if eq(variables['System.PullRequest.IsFork'], 'False') }}:
|
||||
enableCaching: true
|
||||
# - template: ./templates/run-ui-tests-ci.yml
|
||||
# parameters:
|
||||
# platform: x64
|
||||
- template: ./templates/run-ui-tests-ci.yml
|
||||
parameters:
|
||||
platform: x64
|
||||
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=16384
|
||||
pool:
|
||||
demands: ImageOverride -equals SHINE-VS17-Latest
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
name: SHINE-INT-L
|
||||
timeoutInMinutes: 120
|
||||
strategy:
|
||||
maxParallel: 10
|
||||
|
||||
@@ -3,10 +3,10 @@ jobs:
|
||||
- job: Precheck
|
||||
pool:
|
||||
demands: ImageOverride -equals SHINE-VS17-Latest
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-L
|
||||
${{ else }}:
|
||||
${{ if eq(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if ne(variables['System.CollectionUri'], 'https://dev.azure.com/ms/') }}:
|
||||
name: SHINE-INT-L
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
variables:
|
||||
SrcPath: $(Build.Repository.LocalPath)
|
||||
pool:
|
||||
${{ if eq(variables['System.CollectionId'], 'cb55739e-4afe-46a3-970f-1b49d8ee7564') }}:
|
||||
name: SHINE-INT-Testing-x64
|
||||
${{ else }}:
|
||||
${{ 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/') }}:
|
||||
name: SHINE-INT-Testing-x64
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
searchFolder: '$(Pipeline.Workspace)\build-${{ parameters.platform }}-${{ parameters.configuration }}'
|
||||
vstestLocationMethod: 'location' # otherwise fails to find vstest.console.exe
|
||||
#vstestLocation: '$(Agent.ToolsDirectory)\VsTest\**\${{ parameters.platform }}\tools\net462\Common7\IDE\Extensions\TestPlatform'
|
||||
vstestLocation: '$(Agent.ToolsDirectory)\VsTest\17.10.0\x64\tools\net462\Common7\IDE\Extensions\TestPlatform'
|
||||
vstestLocation: '$(Agent.ToolsDirectory)\VsTest\17.10.0-release-24177-07\x64\tools\net462\Common7\IDE\Extensions\TestPlatform'
|
||||
uiTests: true
|
||||
rerunFailedTests: true
|
||||
testAssemblyVer2: |
|
||||
|
||||
@@ -43,7 +43,7 @@ steps:
|
||||
inputs:
|
||||
solution: "**/installer/PowerToysSetup.sln"
|
||||
vsVersion: 17.0
|
||||
msbuildArgs: /p:CIBuild=true /p:BuildProjectReferences=false /target:PowerToysInstaller /bl:$(Build.SourcesDirectory)\msbuild.binlog /p:RunBuildEvents=false /p:PerUser=${{parameters.perUserArg}}
|
||||
msbuildArgs: /p:CIBuild=true /target:PowerToysInstaller /bl:$(Build.SourcesDirectory)\msbuild.binlog /p:RunBuildEvents=false /p:PerUser=${{parameters.perUserArg}}
|
||||
platform: $(BuildPlatform)
|
||||
configuration: $(BuildConfiguration)
|
||||
clean: false # don't undo our hard work above by deleting the CustomActions dll
|
||||
|
||||
@@ -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,9 +27,6 @@ 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.
|
||||
|
||||
@@ -37,12 +34,14 @@ 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)
|
||||
@@ -154,25 +153,14 @@ 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)' == ''">202407100737</MSBuildCacheCacheUniverse>
|
||||
<MSBuildCacheCacheUniverse Condition="'$(MSBuildCacheCacheUniverse)' == ''">202310210737</MSBuildCacheCacheUniverse>
|
||||
|
||||
<!--
|
||||
Visual Studio telemetry reads various ApplicationInsights.config files and other files after the project is finished, likely in a detached process.
|
||||
|
||||
@@ -35,7 +35,10 @@
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.17.3" />
|
||||
<PackageVersion Include="Microsoft.ML.OnnxRuntime.Extensions" Version="0.10.0" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.2" />
|
||||
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2365.46" />
|
||||
<!-- Package Microsoft.Win32.SystemEvents 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.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
|
||||
@@ -46,7 +49,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.240428000" />
|
||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
|
||||
<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" />
|
||||
@@ -58,6 +61,7 @@
|
||||
<PackageVersion Include="NLog" Version="5.0.4" />
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="NReco.VideoConverter" Version="1.2.1" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.37.2" />
|
||||
@@ -75,10 +79,11 @@
|
||||
<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.6" />
|
||||
<PackageVersion Include="System.Drawing.Common" Version="8.0.4" />
|
||||
<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" />
|
||||
<PackageVersion Include="System.Management.Automation" Version="7.4.0" />
|
||||
<PackageVersion Include="System.Reactive" Version="6.0.0-preview.9" />
|
||||
<PackageVersion Include="System.Runtime.Caching" Version="8.0.0" />
|
||||
<!-- Package System.Security.Cryptography.ProtectedData 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. -->
|
||||
@@ -86,7 +91,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="5.50.0" />
|
||||
<PackageVersion Include="UnitsNet" Version="4.145.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 +103,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.240428000
|
||||
- Microsoft.WindowsAppSDK 1.5.240311000
|
||||
- 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.6
|
||||
- System.Drawing.Common 8.0.5
|
||||
- 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 5.50.0
|
||||
- UnitsNet 4.145.0
|
||||
- UTF.Unknown 2.5.1
|
||||
- Vanara.PInvoke.Shell32 3.4.11
|
||||
- Vanara.PInvoke.User32 3.4.11
|
||||
|
||||
@@ -146,6 +146,11 @@ 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}"
|
||||
|
||||
200
README.md
200
README.md
@@ -17,15 +17,14 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
|
||||
|
||||
| | Current utilities: | |
|
||||
|--------------|--------------------|--------------|
|
||||
| [Advanced Paste](https://aka.ms/PowerToysOverview_AdvancedPaste) | [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) |
|
||||
| [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) | [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) |
|
||||
| [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) | [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) |
|
||||
| [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) | [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) |
|
||||
| [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) | [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) |
|
||||
| [Peek](https://aka.ms/PowerToysOverview_Peek) | [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) |
|
||||
| [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) | [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) |
|
||||
| [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) | [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) |
|
||||
| [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) |
|
||||
| [Always on Top](https://aka.ms/PowerToysOverview_AoT) | [PowerToys Awake](https://aka.ms/PowerToysOverview_Awake) | [Command Not Found](https://aka.ms/PowerToysOverview_CmdNotFound) |
|
||||
| [Color Picker](https://aka.ms/PowerToysOverview_ColorPicker) | [Crop And Lock](https://aka.ms/PowerToysOverview_CropAndLock) | [Environment Variables](https://aka.ms/PowerToysOverview_EnvironmentVariables) |
|
||||
| [FancyZones](https://aka.ms/PowerToysOverview_FancyZones) | [File Explorer Add-ons](https://aka.ms/PowerToysOverview_FileExplorerAddOns) | [File Locksmith](https://aka.ms/PowerToysOverview_FileLocksmith) |
|
||||
| [Hosts File Editor](https://aka.ms/PowerToysOverview_HostsFileEditor) | [Image Resizer](https://aka.ms/PowerToysOverview_ImageResizer) | [Keyboard Manager](https://aka.ms/PowerToysOverview_KeyboardManager) |
|
||||
| [Mouse utilities](https://aka.ms/PowerToysOverview_MouseUtilities) | [Mouse Without Borders](https://aka.ms/PowerToysOverview_MouseWithoutBorders) | [Peek](https://aka.ms/PowerToysOverview_Peek) |
|
||||
| [Paste as Plain Text](https://aka.ms/PowerToysOverview_PastePlain) | [PowerRename](https://aka.ms/PowerToysOverview_PowerRename) | [PowerToys Run](https://aka.ms/PowerToysOverview_PowerToysRun) |
|
||||
| [Quick Accent](https://aka.ms/PowerToysOverview_QuickAccent) | [Registry Preview](https://aka.ms/PowerToysOverview_RegistryPreview) | [Screen Ruler](https://aka.ms/PowerToysOverview_ScreenRuler) |
|
||||
| [Shortcut Guide](https://aka.ms/PowerToysOverview_ShortcutGuide) | [Text Extractor](https://aka.ms/PowerToysOverview_TextExtractor) | [Video Conference Mute](https://aka.ms/PowerToysOverview_VideoConference) |
|
||||
|
||||
## Installing and running Microsoft PowerToys
|
||||
|
||||
@@ -41,19 +40,19 @@ Microsoft PowerToys is a set of utilities for power users to tune and streamline
|
||||
Go to the [Microsoft PowerToys GitHub releases page][github-release-link] and click on `Assets` at the bottom to show the files available in the release. Please use the appropriate PowerToys installer that matches your machine's architecture and install scope. For most, it is `x64` and per-user.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.83%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.82%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.82.0/PowerToysUserSetup-0.82.0-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.82.0/PowerToysUserSetup-0.82.0-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.82.0/PowerToysSetup-0.82.0-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.82.0/PowerToysSetup-0.82.0-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F54
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=project%3Amicrosoft%2FPowerToys%2F53
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysUserSetup-0.80.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysUserSetup-0.80.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysSetup-0.80.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.80.1/PowerToysSetup-0.80.1-arm64.exe
|
||||
|
||||
| Description | Filename | sha256 hash |
|
||||
|----------------|----------|-------------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.82.0-x64.exe][ptUserX64] | 295E2A4622C7E347D3E1BAEA6B36BECC328B566496678F1F87DE3F8A12A7F89A |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.82.0-arm64.exe][ptUserArm64] | 55D25D068C6148F0A15C7806B9F813224ABA9C461943F42BB2A8B0E22D28240C |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.82.0-x64.exe][ptMachineX64] | 01B59C00BB43C25BEFEF274755875717AB4DEAB57C0354AB96CF5B1DA4837C9A |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.82.0-arm64.exe][ptMachineArm64] | 1F642B50962516127793C4D3556BF4FC24B9738BAC2F362CAA3BFF8B0C3AF97F |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.80.1-x64.exe][ptUserX64] | 23E35F7B33C6F24237BCA3D5E8EDF9B3BD4802DD656C402B40A4FC82670F8BE3 |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.80.1-arm64.exe][ptUserArm64] | C5EECF0D9D23AB8C14307F91CA28D2CF4DA5932D705F07AE93576C259F74B4D1 |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.80.1-x64.exe][ptMachineX64] | 62373A08BB8E1C1173D047509F3EA5DCC0BE1845787E07BCDA3F6A09DA2A0C17 |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.80.1-arm64.exe][ptMachineArm64] | 061EF8D1B10D68E69D04F98A2D8E1D8047436174C757770778ED23E01CC3B06C |
|
||||
|
||||
This is our preferred method.
|
||||
|
||||
@@ -99,137 +98,136 @@ For guidance on developing for PowerToys, please read the [developer docs](/doc/
|
||||
|
||||
Our [prioritized roadmap][roadmap] of features and utilities that the core team is focusing on.
|
||||
|
||||
### 0.82 - June 2024 Update
|
||||
### 0.80 - March 2024 Update
|
||||
|
||||
In this release, we focused on stability and improvements.
|
||||
In this release, we focused on stability and improvements. The next release is planned to be released during [Microsoft Build 2024](https://build.microsoft.com/) (late May).
|
||||
|
||||
**Highlights**
|
||||
|
||||
- New feature added to PowerRename to allow using sequences of random characters and UUIDs when renaming files. Thanks [@jhirvioja](https://github.com/jhirvioja)!
|
||||
- Improvements in the Paste As JSON feature to better handle other CSV delimiters and converting from ini files. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Fixed UI issues that were reported after upgrading to WPF UI on Color Picker and PowerToys Run.
|
||||
- Bug fixes and stability.
|
||||
- New feature: Desired State Configuration support, allowing the use of winget configure for PowerToys. Check the [DSC documentation](https://aka.ms/powertoys-docs-dsc-configure) for more information.
|
||||
- The Windows App SDK dependency was updated to 1.5.1, fixing many underlying UI issues.
|
||||
- WebP/WebM files support was added to Peek. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Audio files support was added to Peek. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Automated UI testing for FancyZones Editor was added to CI.
|
||||
|
||||
### Advanced Paste
|
||||
### General
|
||||
|
||||
- Fixed an issue causing external applications triggering Advanced Paste. (This was a hotfix for 0.81)
|
||||
- Added a GPO rule to disallow using online models in Advanced Paste. (This was a hotfix for 0.81)
|
||||
- Improved CSV delimiter handling and plain text parsing for the Paste as JSON feature. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Added support to convert from ini in the Paste as JSON feature. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Fixed a memory leak caused by images not being properly cleaned out from clipboard history.
|
||||
- Added an option to hide the UI when it loses focus. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Improved telemetry to get better data about token usage and if clipboard history is a popular feature. Thanks [@craigloewen-msft](https://github.com/craigloewen-msft)!
|
||||
- Added a Quick Access entry to access the flyout from PowerToys' tray icon right click menu. Thanks [@pekvasnovsky](https://github.com/pekvasnovsky)!
|
||||
- Added support for Desired State Configuration in PowerToys, allowing the use of winget configure to configure many settings.
|
||||
|
||||
### Awake
|
||||
|
||||
- Fix an issue causing the "Keep screen on" option to disable after Awake deactivated itself.
|
||||
|
||||
### Color Picker
|
||||
|
||||
- Fixed the opaque background corners in the picker that were introduced after the upgrade to WPFUI.
|
||||
- Fixed a UI issue causing the color picker modal to hide part of the color bar. Thanks [@TheChilledBuffalo](https://github.com/TheChilledBuffalo)!
|
||||
|
||||
### Developer Files Preview (Monaco)
|
||||
### Command Not Found
|
||||
|
||||
- Improved the syntax highlight for .gitignore files. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Checking for the sticky scroll option in code behind was being done twice. Removed one of the checks. Thanks [@downarowiczd](https://github.com/downarowiczd)!
|
||||
- Now tries to find a preview version of PowerShell if no stable version is found.
|
||||
|
||||
### Environment Variables Editor
|
||||
### FancyZones
|
||||
|
||||
- Added clarity to the UI section tooltips. Thanks [@anson-poon](https://github.com/anson-poon)!
|
||||
- Fixed a crash loading the editor when there's a layout with an empty name in the configuration file.
|
||||
- Refactored layout internal data structures and common code to allow for automated testing.
|
||||
- The pressing of the shift key is now detected through raw input to fix an issue causing the shift key to be locked for some users.
|
||||
|
||||
### File Explorer add-ons
|
||||
|
||||
- Fixed a crash when the preview handlers received a 64-bit handle from the OS. Thanks [@z4pf1sh](https://github.com/z4pf1sh)!
|
||||
- Fixed a crash when trying to update window bounds and File Explorer already disposed the preview.
|
||||
- Fixed a crash occurring in the Monaco previewer when a file being previewed isn't found by the code behind.
|
||||
- Fixed an issue in the Markdown previewer adding a leading space to code blocks. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker)!
|
||||
- Fixed wrong location and scaling of preview results on screens with different DPIs.
|
||||
- Added better clean up code to thumbnail handlers to prevent locking files.
|
||||
|
||||
### Find My Mouse
|
||||
### File Locksmith
|
||||
|
||||
- Added the option to have to use the Windows + Control keys to activate. Thanks [@Gentoli](https://github.com/Gentoli)!
|
||||
|
||||
### Hosts File Editor
|
||||
|
||||
- Improved spacing definitions in the UI so that hosts name are not hidden when resizing and icons are well aligned. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Changed the additional lines dialog to show the horizontal scrollbar instead of wrapping contents. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Improved the duplication check's logic to improve performance and take into account features that were introduced after it. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Allow multiple lines to wrap when viewing the modal with selected file paths. Thanks [@sanidhyas3s](https://github.com/sanidhyas3s)!
|
||||
|
||||
### Installer
|
||||
|
||||
- Fixed the remaining install failures when the folders the DSC module is to be installed in isn't accessible by the WiX installer for user scope installations.
|
||||
- Fixed an issue causing ARM 64 uninstall process to not correctly finding powershell 7 to run uninstall scripts.
|
||||
- Fixed the final directory name of the PowerToys Run VSCode Workspaces plugin in the installation directory to match the plugin name. Thanks [@zetaloop](https://github.com/zetaloop)!
|
||||
- Used more generic names for the bootstrap steps, so that "Installing PowerToys" is not shown when uninstalling.
|
||||
|
||||
### Keyboard Manager
|
||||
|
||||
- Fixed an issue that would clear out KBM mappings when certain numpad keys were used as the second key of a chord.
|
||||
- Added a comment in localization files so that translators won't translate "Text" as "SMS".
|
||||
|
||||
### Peek
|
||||
|
||||
- Prevent activating Peek when the user is renaming a file. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Added support to preview special folders like Recycle Bin and My PC instead of throwing an error.
|
||||
- Fixed a crash caused by double releasing a COM object from the module interface.
|
||||
|
||||
### Power Rename
|
||||
|
||||
- Improved apostrophe character handling for the Capitalize and Titlecase renaming flags. Thanks [@anthonymonforte](https://github.com/anthonymonforte)!
|
||||
- Added a feature to allow using sequences of random characters or UUIDs when renaming files. Thanks [@jhirvioja](https://github.com/jhirvioja)!
|
||||
- Added support to .WebP/.WebM files in the image/video previewer. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Added support for audio files. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed an issue causing the open file button in the title bar to be un-clickable. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed an issue when previewing a folder with a dot in the name that caused Peek to try to preview it as a file. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### PowerToys Run
|
||||
|
||||
- Improved the plugin descriptions for consistency in the UI. Thanks [@HydroH](https://github.com/HydroH)!
|
||||
- Fixed UI scaling for different dpi scenarios.
|
||||
- Fixed crash on a racing condition when updating UWP icon paths in the Program plugin. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Fixed PowerToys Run hanging when trying to close an unresponsive window in the WindowWalker plugin. Thanks [@GhostVaibhav](https://github.com/GhostVaibhav)!
|
||||
- Fixed the example in the UnitConverter description to reduce confusion with the inches abbreviation (now uses "to" instead of "in"). Thanks [@acekirkpatrick](https://github.com/acekirkpatrick)!
|
||||
- Brought the acrylic background back and applied a proper fix to the titlebar accent showing through transparency.
|
||||
- Fixed an issue causing the transparency from the UI disappearing after some time.
|
||||
- Added a setting to the Windows Search plugin to exclude files and patterns from the results. Thanks [@HydroH](https://github.com/HydroH)!
|
||||
- Fixed an issue showing thumbnails caused by a hash collision between similar images.
|
||||
- Added the "checkbox and multiline text box" additional property type for plugins and improved multiline text handling. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
|
||||
### Quick Accent
|
||||
|
||||
- Added support for the Crimean Tatar character set. Thanks [@cor-bee](https://github.com/cor-bee)!
|
||||
- Added the Numero symbol and double acute accent character. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Added the International Phonetic Alphabet characters. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Fixed the character description center positioning. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Added feminine and masculine ordinal indicator characters to the Portuguese character set. Thanks [@octastylos-pseudodipteros](https://github.com/octastylos-pseudodipteros)!
|
||||
- Added the Schwa character to the Italian character set. Thanks [@damantioworks](https://github.com/damantioworks)!
|
||||
|
||||
### Registry Preview
|
||||
|
||||
- Allow alternative valid names for the root keys. Thanks [@e-t-l](https://github.com/e-t-l)!
|
||||
- Fixed an issue causing many pick file windows to be opened simultaneously. Thanks [@randyrants](https://github.com/randyrants)!
|
||||
|
||||
### Screen Ruler
|
||||
|
||||
- Updated the default activation hotkey to Win+Control+Shift+M, in order to not conflict with the Windows shortcut that restores minimized windows (Win+Shift+M). Thanks [@nx-frost](https://github.com/nx-frost)!
|
||||
- Updated the measure icons for clarity. Thanks [@Aaron-Junker](https://github.com/Aaron-Junker) and [@niels9001](https://github.com/niels9001)!
|
||||
|
||||
### Shortcut Guide
|
||||
|
||||
- Updated the Emoji shortcut that is shown to the new Windows key + period (.) hotkey.
|
||||
|
||||
### Text Extractor
|
||||
|
||||
- Fixed issues creating the extract layout on certain monitor configurations.
|
||||
|
||||
### Video Conference Mute
|
||||
|
||||
- Added enable/disable telemetry to get usage data.
|
||||
|
||||
### Settings
|
||||
|
||||
- Disabled the UI to enable/disable clipboard history in the Advanced Paste settings page when clipboard history is disabled by GPO in the system. (This was a hotfix for 0.81)
|
||||
- Updated Advanced Paste's Settings and OOBE page to clarify that the AI use is optional and opt-in. (This was a hotfix for 0.81)
|
||||
- Corrected a spelling fix in Advanced Paste's settings page. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Added localization support for the "Configure OpenAI Key" button in Advanced Paste's settings page. Thanks [@zetaloop](https://github.com/zetaloop)!
|
||||
- Fixed extra GPO warnings being shown in Advanced Paste's settings page even if the module is disabled. Thanks [@htcfreek](https://github.com/htcfreek)!
|
||||
- Fixed a crash when a PowerToys Run plugin icon path is badly formed.
|
||||
- Disabled the experimentation paths in code behind to improve performance, since there's no current experimentation going on.
|
||||
- Added locks to some terms (like the name of some utilities) so that they aren't localized.
|
||||
- Fixed some shortcuts not being shown properly in the Flyout and Dashboard. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
- Updated image for Color Picker and outdated animations for utilities in OOBE. Thanks [@niels9001](https://github.com/niels9001)!
|
||||
|
||||
### Documentation
|
||||
|
||||
- Adjusted the readme and release notes to clarify use of AI on Advanced Paste. (This was a hotfix for 0.81)
|
||||
- Added the Edge Workspaces plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@quachpas](https://github.com/quachpas)!
|
||||
- Removed the deprecated Guid plugin from PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@abduljawada](https://github.com/abduljawada)!
|
||||
- Added the PowerHexInspector plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@NaroZeol](https://github.com/NaroZeol)!
|
||||
- Fixed a broken link in the communication-with-modules.md file. Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
- Updated COMMUNITY.md with missing and former members.
|
||||
- Added FastWeb plugin to PowerToys Run thirdPartyRunPlugins.md docs. Thanks [@CCcat8059](https://github.com/CCcat8059)!
|
||||
- Removed the old security link to MSRC from the create new issue page, since security.md is already linked there.
|
||||
- Added clarity regarding unofficial plugins to the PowerToys Run thirdPartyRunPlugins.md docs.
|
||||
|
||||
### Development
|
||||
|
||||
- Fixed ci UI tests to point to the correct Visual Studio vstest location after a Visual Studio upgrade. (This was a hotfix for 0.81)
|
||||
- Updated System.Drawing.Common to 8.0.6 to fix CI builds after the .NET 8.0.6 upgrade was released.
|
||||
- Removed an incorrect file reference to long removed documentation from the solution file. Thanks [@Kissaki](https://github.com/Kissaki)!
|
||||
- Upgraded Windows App SDK to 1.5.3.
|
||||
- Removed use of the BinaryFormatter API from Mouse Without Borders, which is expected to be deprecated in .NET 9.
|
||||
- The user scope installer is now sent to the Microsoft store instead of the machine scope installer.
|
||||
- Refactored Mouse Jump's internal code to allow for a future introduction of customizable appearance features. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
- Removed a noisy error from spell check ci runs.
|
||||
- Improved the ci agent pool selection code.
|
||||
- Updated Xamlstyler.console to 3.2404.2. Thanks [@Jvr2022](https://github.com/Jvr2022)!
|
||||
- Updated UnitsNet to 5.50.0 Thanks [@Jvr2022](https://github.com/Jvr2022)!
|
||||
- Replaced LPINPUT with std::vector of INPUT instances in Keyboard Manager internal code. Thanks [@masaru-iritani](https://github.com/masaru-iritani)!
|
||||
- Improved the Microsoft Store submission ci action to use the proper cli and authentication.
|
||||
- Updated System.Drawing.Common to 8.0.3 to fix CI builds after the .NET 8.0.3 upgrade was released.
|
||||
- Adjusted the GitHub action names for releasing to winget and Microsoft Store so they're clearer in the UI.
|
||||
- Upgraded WinAppSDK to 1.5.1, fixing many related issues.
|
||||
- Consolidate the WebView2 version used by WinUI 2 in the Keyboard Manager Editor.
|
||||
- Unified the use of Precompiled Headers when building on CI. Thanks [@dfederm](https://github.com/dfederm)!
|
||||
- Added UI tests for FancyZones Editor in CI.
|
||||
- Added a GitHub bot to identify possible duplicates when a new issue is created. Thanks [@craigloewen-msft](https://github.com/craigloewen-msft)!
|
||||
- Updated the WiX installer dependency to 3.14.1 to fix possible security issues.
|
||||
- Changed the pipelines to use pipeline artifacts instead of build artifacts. Thanks [@dfederm](https://github.com/dfederm)!
|
||||
- Added the -graph parameter for pipelines. Thanks [@dfederm](https://github.com/dfederm)!
|
||||
- Tests in the pipelines now run as part of the build step to save on CI time. Thanks [@dfederm](https://github.com/dfederm)!
|
||||
|
||||
#### What is being planned for version 0.83
|
||||
#### What is being planned for version 0.81
|
||||
|
||||
For [v0.83][github-next-release-work], we'll work on the items below:
|
||||
For [v0.81][github-next-release-work], we'll work on the items below:
|
||||
|
||||
- Stability / bug fixes
|
||||
- New utility: Dev Projects
|
||||
- Language selection
|
||||
- New module: File Actions Menu
|
||||
|
||||
The next release is planned to be released during Microsoft Build 2024.
|
||||
|
||||
## PowerToys Community
|
||||
|
||||
The PowerToys team is extremely grateful to have the [support of an amazing active community][community-link]. The work you do is incredibly important. PowerToys wouldn’t be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month by month, you directly help make PowerToys a better piece of software.
|
||||
|
||||
@@ -47,16 +47,7 @@ registerAdditionalNewLanguage("id", [".fileExtension"], idDefinition(), monaco)
|
||||
|
||||
* The id can be anything. Recommended is one of the file extensions. For example "php" or "reg".
|
||||
|
||||
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.
|
||||
4. 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` vector argument.
|
||||
- Iterate over all the inputs in the INPUT array 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`](runner-ipc.md).
|
||||
- More details on this are mentioned in [`runner-ipc.md`](settingsv2/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,6 +27,7 @@ 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 |
|
||||
@@ -35,7 +36,6 @@ 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,7 +44,6 @@ 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,19 +46,34 @@
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<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>
|
||||
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<!-- DSC module files for PerUser handled in InstallDSCModule custom action. -->
|
||||
<?else?>
|
||||
<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>
|
||||
<?else?>
|
||||
<DirectoryRef Id="ProgramFiles64Folder">
|
||||
<Directory Id="WindowsPowerShellFolder" Name="WindowsPowerShell">
|
||||
<Directory Id="PowerShellModulesFolder" Name="Modules">
|
||||
@@ -74,7 +89,7 @@
|
||||
</Directory>
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||
<Component Id="PowerToysStartMenuShortcut" >
|
||||
@@ -115,27 +130,23 @@
|
||||
</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="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?>
|
||||
<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" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -136,11 +136,6 @@
|
||||
<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
|
||||
@@ -154,10 +149,7 @@
|
||||
<!--<Custom Action="InstallEmbeddedMSIXTask" After="InstallFinalize">
|
||||
NOT Installed
|
||||
</Custom>-->
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<Custom Action="InstallDSCModule" After="InstallFiles"/>
|
||||
<?endif?>
|
||||
<Custom Action="TelemetryLogInstallSuccess" After="InstallFinalize">
|
||||
<Custom Action="TelemetryLogInstallSuccess" After="InstallFinalize">
|
||||
NOT Installed
|
||||
</Custom>
|
||||
<Custom Action="TelemetryLogUninstallSuccess" After="InstallFinalize">
|
||||
@@ -185,12 +177,8 @@
|
||||
<!--<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>
|
||||
@@ -223,10 +211,6 @@
|
||||
Property="UnApplyModulesRegistryChangeSets"
|
||||
Value="[INSTALLFOLDER]" />
|
||||
|
||||
<CustomAction Id="SetInstallDSCModuleParam"
|
||||
Property="InstallDSCModule"
|
||||
Value="[INSTALLFOLDER]" />
|
||||
|
||||
<CustomAction Id="SetUninstallCommandNotFoundParam"
|
||||
Property="UninstallCommandNotFound"
|
||||
Value="[INSTALLFOLDER]" />
|
||||
@@ -281,21 +265,6 @@
|
||||
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"
|
||||
@@ -438,7 +407,6 @@
|
||||
<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" />
|
||||
@@ -453,6 +421,9 @@
|
||||
<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", "customTokenColors.js", "*.pri")
|
||||
$fileInclusionList = @("*.dll", "*.exe", "*.json", "*.msix", "*.png", "*.gif", "*.ico", "*.cur", "*.svg", "index.html", "reg.js", "gitignore.js", "monacoSpecialLanguages.js", "*.pri")
|
||||
|
||||
$dllsToIgnore = @("System.CodeDom.dll", "WindowsBase.dll")
|
||||
|
||||
|
||||
@@ -139,23 +139,6 @@ 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;
|
||||
@@ -179,7 +162,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;
|
||||
@@ -334,125 +317,6 @@ 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(false);
|
||||
|
||||
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(false);
|
||||
|
||||
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;
|
||||
@@ -608,19 +472,9 @@ 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,12 +18,10 @@ EXPORTS
|
||||
CertifyVirtualCameraDriverCA
|
||||
InstallVirtualCameraDriverCA
|
||||
InstallEmbeddedMSIXCA
|
||||
InstallDSCModuleCA
|
||||
UnApplyModulesRegistryChangeSetsCA
|
||||
UninstallVirtualCameraDriverCA
|
||||
UnRegisterContextMenuPackagesCA
|
||||
UninstallEmbeddedMSIXCA
|
||||
UninstallDSCModuleCA
|
||||
UninstallServicesCA
|
||||
UninstallCommandNotFoundModuleCA
|
||||
UpgradeCommandNotFoundModuleCA
|
||||
|
||||
@@ -12,10 +12,5 @@ namespace Common.UI
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= 22000;
|
||||
}
|
||||
|
||||
public static bool IsGreaterThanWindows11_21H2()
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build > 22000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/^#.*$/, 'comment'],
|
||||
[/.*((?<!(^|\/))\*\*.*|\*\*(?!(\/|$))).*/, 'invalid'],
|
||||
[/((?:^!\s*(?:\\\s|\S)+)?)((?:^\s*(?:\\\s|\S)+)?)((?:\s+(?:\\\s|\S)+)*)/, ['custom-gitignore.negation', 'tag', 'invalid']]
|
||||
[/^\s*!.*/, 'invalid'],
|
||||
[/^\s*[^#]+/, "tag"]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
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 "vs" for light theme or "vs-dark" for dark theme
|
||||
// `theme` can be "light" or "dark"
|
||||
// `lang` is the language of the file
|
||||
// `wrap` if the editor is wrapping or not
|
||||
|
||||
@@ -59,29 +59,19 @@
|
||||
<script src="http://[[PT_URL]]/monacoSpecialLanguages.js" type="module"></script>
|
||||
<script type="module">
|
||||
var editor;
|
||||
import { registerAdditionalLanguages } from 'http://[[PT_URL]]/monacoSpecialLanguages.js';
|
||||
import { customTokenColors } from 'http://[[PT_URL]]/customTokenColors.js';
|
||||
import { registerAdditionalLanguages } from "http://[[PT_URL]]/monacoSpecialLanguages.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/typedoc/interfaces/editor.IStandaloneEditorConstructionOptions.html
|
||||
// For all parameters: https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandaloneeditorconstructionoptions.html
|
||||
editor = monaco.editor.create(document.getElementById('container'), {
|
||||
value: code, // Sets content of the editor
|
||||
language: lang, // Sets language of the code
|
||||
language: lang, // Sets language fof 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,
|
||||
@@ -89,16 +79,17 @@
|
||||
// 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 = () => {
|
||||
window.onresize = function (){
|
||||
editor.layout();
|
||||
};
|
||||
|
||||
// Add toggle wrap button to context menu
|
||||
// Add switch wrap button to context menu
|
||||
editor.addAction({
|
||||
id: 'text-wrap',
|
||||
|
||||
@@ -110,17 +101,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;
|
||||
}
|
||||
@@ -129,11 +120,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,9 +34,6 @@
|
||||
<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>
|
||||
|
||||
@@ -172,8 +172,4 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getConfiguredQoiThumbnailsEnabledValue());
|
||||
}
|
||||
GpoRuleConfigured GPOWrapper::GetAllowedAdvancedPasteOnlineAIModelsValue()
|
||||
{
|
||||
return static_cast<GpoRuleConfigured>(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
|
||||
static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ namespace PowerToys
|
||||
static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
|
||||
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
|
||||
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace powertoys_gpo {
|
||||
// The registry value names for other PowerToys policies.
|
||||
const std::wstring POLICY_ALLOW_EXPERIMENTATION = L"AllowExperimentation";
|
||||
const std::wstring POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS = L"PowerLauncherAllPluginsEnabledState";
|
||||
const std::wstring POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS = L"AllowPowerToysAdvancedPasteOnlineAIModels";
|
||||
|
||||
|
||||
inline std::optional<std::wstring> readRegistryStringValue(HKEY hRootKey, const std::wstring& subKey, const std::wstring& value_name)
|
||||
{
|
||||
@@ -470,9 +470,4 @@ namespace powertoys_gpo {
|
||||
{
|
||||
return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_QOI_THUMBNAILS);
|
||||
}
|
||||
|
||||
inline gpo_rule_configured_t getAllowedAdvancedPasteOnlineAIModelsValue()
|
||||
{
|
||||
return getUtilityEnabledValue(POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,18 +27,18 @@ enum class version_architecture
|
||||
version_architecture get_current_architecture();
|
||||
const wchar_t* get_architecture_string(const version_architecture);
|
||||
|
||||
inline std::wstring get_product_version(bool includeV = true)
|
||||
inline std::wstring get_product_version()
|
||||
{
|
||||
static std::wstring version = (includeV ? L"v" : L"") + std::to_wstring(VERSION_MAJOR) +
|
||||
static std::wstring version = L"v" + std::to_wstring(VERSION_MAJOR) +
|
||||
L"." + std::to_wstring(VERSION_MINOR) +
|
||||
L"." + std::to_wstring(VERSION_REVISION);
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
inline std::wstring get_std_product_version(bool includeV = true)
|
||||
inline std::wstring get_std_product_version()
|
||||
{
|
||||
static std::wstring version = (includeV ? L"v" : L"") + std::to_wstring(VERSION_MAJOR) +
|
||||
static std::wstring version = L"v" + std::to_wstring(VERSION_MAJOR) +
|
||||
L"." + std::to_wstring(VERSION_MINOR) +
|
||||
L"." + std::to_wstring(VERSION_REVISION) + L".0";
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License. -->
|
||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.10" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.9" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyNamespaces>
|
||||
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
|
||||
</policyNamespaces>
|
||||
<resources minRequiredRevision="1.10"/><!-- Last changed with PowerToys v0.81.1 -->
|
||||
<resources minRequiredRevision="1.9"/><!-- Last changed with PowerToys v0.81.0 -->
|
||||
<supportedOn>
|
||||
<definitions>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
|
||||
@@ -18,7 +18,6 @@
|
||||
<definition name="SUPPORTED_POWERTOYS_0_77_0" displayName="$(string.SUPPORTED_POWERTOYS_0_77_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_78_0" displayName="$(string.SUPPORTED_POWERTOYS_0_78_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_81_0" displayName="$(string.SUPPORTED_POWERTOYS_0_81_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_81_1" displayName="$(string.SUPPORTED_POWERTOYS_0_81_1)"/>
|
||||
</definitions>
|
||||
</supportedOn>
|
||||
<categories>
|
||||
@@ -29,9 +28,6 @@
|
||||
<category name="PowerToysRun" displayName="$(string.PowerToysRun)">
|
||||
<parentCategory ref="PowerToys" />
|
||||
</category>
|
||||
<category name="AdvancedPaste" displayName="$(string.AdvancedPaste)">
|
||||
<parentCategory ref="PowerToys" />
|
||||
</category>
|
||||
</categories>
|
||||
<policies>
|
||||
|
||||
@@ -493,15 +489,5 @@
|
||||
<list id="PowerToysRunIndividualPluginEnabledList" explicitValue="true" />
|
||||
</elements>
|
||||
</policy>
|
||||
<policy name="AllowPowerToysAdvancedPasteOnlineAIModels" class="Both" displayName="$(string.AllowPowerToysAdvancedPasteOnlineAIModels)" explainText="$(string.AllowPowerToysAdvancedPasteOnlineAIModelsDescription)" key="Software\Policies\PowerToys" valueName="AllowPowerToysAdvancedPasteOnlineAIModels">
|
||||
<parentCategory ref="AdvancedPaste" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_81_1" />
|
||||
<enabledValue>
|
||||
<decimal value="1" />
|
||||
</enabledValue>
|
||||
<disabledValue>
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
</policies>
|
||||
</policyDefinitions>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License. -->
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.10" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.9" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<displayName>PowerToys</displayName>
|
||||
<description>PowerToys</description>
|
||||
<resources>
|
||||
@@ -9,7 +9,6 @@
|
||||
<string id="PowerToys">Microsoft PowerToys</string>
|
||||
<string id="InstallerUpdates">Installer and Updates</string>
|
||||
<string id="PowerToysRun">PowerToys Run</string>
|
||||
<string id="AdvancedPaste">Advanced Paste</string>
|
||||
|
||||
<string id="SUPPORTED_POWERTOYS_0_64_0">PowerToys version 0.64.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_68_0">PowerToys version 0.68.0 or later</string>
|
||||
@@ -21,7 +20,6 @@
|
||||
<string id="SUPPORTED_POWERTOYS_0_77_0">PowerToys version 0.77.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_78_0">PowerToys version 0.78.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_81_0">PowerToys version 0.81.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_81_1">PowerToys version 0.81.1 or later</string>
|
||||
|
||||
<string id="ConfigureGlobalUtilityEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
|
||||
|
||||
@@ -120,12 +118,6 @@ If you disable or don't configure this policy, either the user or the policy "Co
|
||||
You can set the enabled state for all plugins not configured by this policy using the policy "Configure enabled state for all plugins".
|
||||
|
||||
Note: Changes require a restart of PowerToys Run.
|
||||
</string>
|
||||
<string id="AllowPowerToysAdvancedPasteOnlineAIModelsDescription">This policy configures the enabled disable state for using Advanced Paste online AI models.
|
||||
|
||||
If you enable or don't configure this policy, the user takes control over the enabled state of the Enable paste with AI Advanced Paste setting.
|
||||
|
||||
If you disable this policy, the user won't be able to enable Enable paste with AI Advanced Paste setting and use Advanced Paste AI prompt nor set up the Open AI key in PowerToys Settings.
|
||||
</string>
|
||||
<string id="ConfigureGlobalUtilityEnabledState">Configure global utility enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityAdvancedPaste">Advanced Paste: Configure enabled state</string>
|
||||
@@ -173,7 +165,6 @@ If you disable this policy, the user won't be able to enable Enable paste with A
|
||||
<string id="PowerToysRunIndividualPluginEnabledState">Configure enabled state for individual plugins</string>
|
||||
<string id="ConfigureEnabledUtilityFileExplorerQOIPreview">QOI file preview: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFileExplorerQOIThumbnails">QOI file thumbnail: Configure enabled state</string>
|
||||
<string id="AllowPowerToysAdvancedPasteOnlineAIModels">Advanced Paste: Allow using online AI models</string>
|
||||
</stringTable>
|
||||
|
||||
<presentationTable>
|
||||
|
||||
1
src/modules/AdvancedPaste/AdvancedPaste/AIModelAssets/.gitignore
vendored
Normal file
1
src/modules/AdvancedPaste/AdvancedPaste/AIModelAssets/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.onnx
|
||||
@@ -0,0 +1,49 @@
|
||||
## Setting up the ML models
|
||||
|
||||
PowerToys Advanced Paste uses ML models for on device AI actions such as transcribing an audio or video file. Before you can use these features, you will need to download the models and place them in the right directory before building the project.
|
||||
|
||||
Here is how your directory structure should look like with the models included
|
||||
|
||||
```
|
||||
src\modules\AdvancedPaste\AdvancedPaste
|
||||
├── AIModelAssets
|
||||
│ ├── whisper
|
||||
│ | ├── silero_vad.onnx
|
||||
│ │ ├── whisper-small.onnx
|
||||
```
|
||||
|
||||
There are two models that are used in this project.
|
||||
1. Silero VAD - a voice activity detection model that is used to detect speech in an audio file and used to chunk a long audio file into smaller segments
|
||||
2. Whisper Small - an automatic speech recognition model that is used to transcribe the audio or audio file segments
|
||||
|
||||
### Silero VAD
|
||||
|
||||
1. Download the pre-trained model from this [GitHub repo](https://github.com/snakers4/silero-vad). The model is available in the `files` directory, named `silero_vad.onnx`.
|
||||
2. Place the `silero_vad.onnx` file in the `src\modules\AdvancedPaste\AdvancedPaste\AIModelAssets\whisper` directory. Create the directory if it does not exist.
|
||||
|
||||
### Whisper
|
||||
|
||||
1. Generate an optimized model with Olive [following these instructions](https://github.com/microsoft/Olive/blob/main/examples/whisper/README.md). Here the commands we used to generate the model (assuming you already have python installed):
|
||||
``` bash
|
||||
# Clone the Olive repository and navigate to the whisper example folder
|
||||
git clone https://github.com/microsoft/Olive
|
||||
cd Olive/examples/whisper
|
||||
|
||||
# Install the required packages
|
||||
pip install olive-ai
|
||||
python -m pip install -r requirements.txt
|
||||
pip install onnxruntime onnxruntime_extensions
|
||||
|
||||
# prepare the whisper model (note, you can use other whisper variants as well, e.g. whisper-tiny)
|
||||
python prepare_whisper_configs.py --model_name openai/whisper-small --multilingual --enable_timestamps
|
||||
|
||||
# Run the Olive workflow to generate the optimized model
|
||||
olive run --config whisper_cpu_int8.json --setup
|
||||
olive run --config whisper_cpu_int8.json
|
||||
```
|
||||
|
||||
|
||||
The generated model will be in the `.\models\conversion-transformers_optimization-onnx_dynamic_quantization-insert_beam_search-prepost` folder.
|
||||
|
||||
2. Rename the `whisper_cpu_int8_cpu-cpu_model.onnx` file that was generated to `whisper-small.onnx`and place it in the `src\modules\AdvancedPaste\AdvancedPaste\AIModelAssets\whisper` directory. Create the directory if it does not exist.
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Peek.FilePreviewer.Models;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers;
|
||||
|
||||
public interface ISpecialFolderPreviewer : IPreviewer
|
||||
namespace AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
public SpecialFolderPreviewData? Preview { get; }
|
||||
public class DetectionResult
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
||||
public double Seconds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// 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;
|
||||
|
||||
namespace AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
public class SlieroVadDetector : IDisposable
|
||||
{
|
||||
private readonly SlieroVadOnnxModel model;
|
||||
private readonly float startThreshold;
|
||||
private readonly float endThreshold;
|
||||
private readonly int samplingRate;
|
||||
private readonly float minSilenceSamples;
|
||||
private readonly float speechPadSamples;
|
||||
private bool triggered;
|
||||
private int tempEnd;
|
||||
private int currentSample;
|
||||
|
||||
public SlieroVadDetector(
|
||||
float startThreshold,
|
||||
float endThreshold,
|
||||
int samplingRate,
|
||||
int minSilenceDurationMs,
|
||||
int speechPadMs)
|
||||
{
|
||||
if (samplingRate != 8000 && samplingRate != 16000)
|
||||
{
|
||||
throw new ArgumentException("does not support sampling rates other than [8000, 16000]");
|
||||
}
|
||||
|
||||
this.model = new SlieroVadOnnxModel();
|
||||
this.startThreshold = startThreshold;
|
||||
this.endThreshold = endThreshold;
|
||||
this.samplingRate = samplingRate;
|
||||
this.minSilenceSamples = samplingRate * minSilenceDurationMs / 1000f;
|
||||
this.speechPadSamples = samplingRate * speechPadMs / 1000f;
|
||||
|
||||
Reset();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
model.ResetStates();
|
||||
triggered = false;
|
||||
tempEnd = 0;
|
||||
currentSample = 0;
|
||||
}
|
||||
|
||||
public Dictionary<string, double> Apply(byte[] data, bool returnSeconds)
|
||||
{
|
||||
float[] audioData = new float[data.Length / 2];
|
||||
for (int i = 0; i < audioData.Length; i++)
|
||||
{
|
||||
audioData[i] = ((data[i * 2] & 0xff) | (data[(i * 2) + 1] << 8)) / 32767.0f;
|
||||
}
|
||||
|
||||
int windowSizeSamples = audioData.Length;
|
||||
currentSample += windowSizeSamples;
|
||||
|
||||
float speechProb = 0;
|
||||
try
|
||||
{
|
||||
speechProb = model.Call(new float[][] { audioData }, samplingRate)[0];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("An error occurred while calling the model", ex);
|
||||
}
|
||||
|
||||
if (speechProb >= startThreshold && tempEnd != 0)
|
||||
{
|
||||
tempEnd = 0;
|
||||
}
|
||||
|
||||
if (speechProb >= startThreshold && !triggered)
|
||||
{
|
||||
triggered = true;
|
||||
int speechStart = (int)(currentSample - speechPadSamples);
|
||||
speechStart = Math.Max(speechStart, 0);
|
||||
|
||||
Dictionary<string, double> result = new Dictionary<string, double>();
|
||||
if (returnSeconds)
|
||||
{
|
||||
double speechStartSeconds = speechStart / (double)samplingRate;
|
||||
double roundedSpeechStart = Math.Round(speechStartSeconds, 1, MidpointRounding.AwayFromZero);
|
||||
result["start"] = roundedSpeechStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
result["start"] = speechStart;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (speechProb < endThreshold && triggered)
|
||||
{
|
||||
if (tempEnd == 0)
|
||||
{
|
||||
tempEnd = currentSample;
|
||||
}
|
||||
|
||||
if (currentSample - tempEnd < minSilenceSamples)
|
||||
{
|
||||
return new Dictionary<string, double>();
|
||||
}
|
||||
else
|
||||
{
|
||||
int speechEnd = (int)(tempEnd + speechPadSamples);
|
||||
tempEnd = 0;
|
||||
triggered = false;
|
||||
|
||||
Dictionary<string, double> result = new Dictionary<string, double>();
|
||||
|
||||
if (returnSeconds)
|
||||
{
|
||||
double speechEndSeconds = speechEnd / (double)samplingRate;
|
||||
double roundedSpeechEnd = Math.Round(speechEndSeconds, 1, MidpointRounding.AwayFromZero);
|
||||
result["end"] = roundedSpeechEnd;
|
||||
}
|
||||
else
|
||||
{
|
||||
result["end"] = speechEnd;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return new Dictionary<string, double>();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Reset();
|
||||
model.Close();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
this.model.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// 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 Microsoft.ML.OnnxRuntime;
|
||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
|
||||
namespace AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
public class SlieroVadOnnxModel : IDisposable
|
||||
{
|
||||
private readonly InferenceSession session;
|
||||
private OrtValue h;
|
||||
private OrtValue c;
|
||||
private int lastSr;
|
||||
private int lastBatchSize;
|
||||
private static readonly List<int> SampleRates = new List<int> { 8000, 16000 };
|
||||
|
||||
public SlieroVadOnnxModel()
|
||||
{
|
||||
var modelPath = $@"{AppDomain.CurrentDomain.BaseDirectory}AIModelAssets\whisper\silero_vad.onnx";
|
||||
|
||||
var options = new SessionOptions();
|
||||
options.InterOpNumThreads = 1;
|
||||
options.IntraOpNumThreads = 1;
|
||||
options.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_EXTENDED;
|
||||
session = new InferenceSession(modelPath, options);
|
||||
ResetStates();
|
||||
}
|
||||
|
||||
public void ResetStates()
|
||||
{
|
||||
try
|
||||
{
|
||||
var hTensor = new DenseTensor<float>(new[] { 2, 1, 64 });
|
||||
var cTensor = new DenseTensor<float>(new[] { 2, 1, 64 });
|
||||
h = OrtValue.CreateTensorValueFromMemory<float>(OrtMemoryInfo.DefaultInstance, hTensor.Buffer, [2, 1, 64]);
|
||||
c = OrtValue.CreateTensorValueFromMemory<float>(OrtMemoryInfo.DefaultInstance, cTensor.Buffer, [2, 1, 64]);
|
||||
lastSr = 0;
|
||||
lastBatchSize = 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
session.Dispose();
|
||||
}
|
||||
|
||||
public class ValidationResult
|
||||
{
|
||||
public float[][] X { get; private set; }
|
||||
|
||||
public int Sr { get; private set; }
|
||||
|
||||
public ValidationResult(float[][] x, int sr)
|
||||
{
|
||||
X = x;
|
||||
Sr = sr;
|
||||
}
|
||||
}
|
||||
|
||||
private ValidationResult ValidateInput(float[][] x, int sr)
|
||||
{
|
||||
if (x.Length == 1)
|
||||
{
|
||||
x = new float[][] { x[0] };
|
||||
}
|
||||
|
||||
if (x.Length > 2)
|
||||
{
|
||||
throw new ArgumentException($"Incorrect audio data dimension: {x.Length}");
|
||||
}
|
||||
|
||||
if (sr != 16000 && sr % 16000 == 0)
|
||||
{
|
||||
int step = sr / 16000;
|
||||
float[][] reducedX = x.Select(row => row.Where((_, i) => i % step == 0).ToArray()).ToArray();
|
||||
x = reducedX;
|
||||
sr = 16000;
|
||||
}
|
||||
|
||||
if (!SampleRates.Contains(sr))
|
||||
{
|
||||
throw new ArgumentException($"Only supports sample rates {string.Join(", ", SampleRates)} (or multiples of 16000)");
|
||||
}
|
||||
|
||||
if ((float)sr / x[0].Length > 31.25)
|
||||
{
|
||||
throw new ArgumentException("Input audio is too short");
|
||||
}
|
||||
|
||||
return new ValidationResult(x, sr);
|
||||
}
|
||||
|
||||
public float[] Call(float[][] x, int sr)
|
||||
{
|
||||
var result = ValidateInput(x, sr);
|
||||
x = result.X;
|
||||
sr = result.Sr;
|
||||
|
||||
int batchSize = x.Length;
|
||||
int sampleSize = x[0].Length; // Assuming all subarrays have identical length
|
||||
|
||||
if (lastBatchSize == 0 || lastSr != sr || lastBatchSize != batchSize)
|
||||
{
|
||||
ResetStates();
|
||||
}
|
||||
|
||||
// Flatten the jagged array and create the tensor with the correct shape
|
||||
var flatArray = x.SelectMany(inner => inner).ToArray();
|
||||
|
||||
var input = new Dictionary<string, OrtValue>
|
||||
{
|
||||
{ "input", OrtValue.CreateTensorValueFromMemory(flatArray, [batchSize, sampleSize]) },
|
||||
{ "sr", OrtValue.CreateTensorValueFromMemory(new long[] { sr }, [1]) },
|
||||
{ "h", h },
|
||||
{ "c", c },
|
||||
};
|
||||
|
||||
var runOptions = new RunOptions();
|
||||
|
||||
try
|
||||
{
|
||||
using (var results = session.Run(runOptions, input, session.OutputNames))
|
||||
{
|
||||
var output = results[0].GetTensorDataAsSpan<float>().ToArray();
|
||||
h = OrtValue.CreateTensorValueFromMemory(results.ElementAt(1).GetTensorDataAsSpan<float>().ToArray(), [2, 1, 64]);
|
||||
c = OrtValue.CreateTensorValueFromMemory(results.ElementAt(2).GetTensorDataAsSpan<float>().ToArray(), [2, 1, 64]);
|
||||
|
||||
lastSr = sr;
|
||||
lastBatchSize = batchSize;
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException("An error occurred while calling the model", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
session?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
public class WhisperChunk
|
||||
{
|
||||
public double Start { get; set; }
|
||||
|
||||
public double End { get; set; }
|
||||
|
||||
public WhisperChunk(double start, double end)
|
||||
{
|
||||
this.Start = start;
|
||||
this.End = end;
|
||||
}
|
||||
|
||||
public double Length => End - Start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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.Linq;
|
||||
|
||||
namespace AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
public static class WhisperChunking
|
||||
{
|
||||
private static readonly int SAMPLERATE = 16000;
|
||||
private static readonly float STARTTHRESHOLD = 0.25f;
|
||||
private static readonly float ENDTHRESHOLD = 0.25f;
|
||||
private static readonly int MINSILENCEDURATIONMS = 1000;
|
||||
private static readonly int SPEECHPADMS = 400;
|
||||
private static readonly int WINDOWSIZESAMPLES = 3200;
|
||||
|
||||
private static readonly double MAXCHUNKS = 29;
|
||||
private static readonly double MINCHUNKS = 5;
|
||||
|
||||
public static List<WhisperChunk> SmartChunking(byte[] audioBytes)
|
||||
{
|
||||
SlieroVadDetector vadDetector;
|
||||
vadDetector = new SlieroVadDetector(STARTTHRESHOLD, ENDTHRESHOLD, SAMPLERATE, MINSILENCEDURATIONMS, SPEECHPADMS);
|
||||
|
||||
int bytesPerSample = 2;
|
||||
int bytesPerWindow = WINDOWSIZESAMPLES * bytesPerSample;
|
||||
|
||||
float totalSeconds = audioBytes.Length / (SAMPLERATE * 2);
|
||||
var result = new List<DetectionResult>();
|
||||
for (int offset = 0; offset + bytesPerWindow <= audioBytes.Length; offset += bytesPerWindow)
|
||||
{
|
||||
byte[] data = new byte[bytesPerWindow];
|
||||
Array.Copy(audioBytes, offset, data, 0, bytesPerWindow);
|
||||
|
||||
// Simulating the process as if data was being read in chunks
|
||||
try
|
||||
{
|
||||
var detectResult = vadDetector.Apply(data, true);
|
||||
|
||||
// iterate over detectResult and apply the data to result:
|
||||
foreach (var (key, value) in detectResult)
|
||||
{
|
||||
result.Add(new DetectionResult { Type = key, Seconds = value });
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Depending on the need, you might want to break out of the loop or just report the error
|
||||
Console.Error.WriteLine($"Error applying VAD detector: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var stamps = GetTimeStamps(result, totalSeconds, MAXCHUNKS, MINCHUNKS);
|
||||
return stamps;
|
||||
}
|
||||
|
||||
private static List<WhisperChunk> GetTimeStamps(List<DetectionResult> voiceAreas, double totalSeconds, double maxChunkLength, double minChunkLength)
|
||||
{
|
||||
if (totalSeconds <= maxChunkLength)
|
||||
{
|
||||
return new List<WhisperChunk> { new WhisperChunk(0, totalSeconds) };
|
||||
}
|
||||
|
||||
voiceAreas = voiceAreas.OrderBy(va => va.Seconds).ToList();
|
||||
|
||||
List<WhisperChunk> chunks = new List<WhisperChunk>();
|
||||
|
||||
double nextChunkStart = 0.0;
|
||||
while (nextChunkStart < totalSeconds)
|
||||
{
|
||||
double idealChunkEnd = nextChunkStart + maxChunkLength;
|
||||
double chunkEnd = idealChunkEnd > totalSeconds ? totalSeconds : idealChunkEnd;
|
||||
|
||||
var validVoiceAreas = voiceAreas.Where(va => va.Seconds > nextChunkStart && va.Seconds <= chunkEnd).ToList();
|
||||
|
||||
if (validVoiceAreas.Count != 0)
|
||||
{
|
||||
chunkEnd = validVoiceAreas.Last().Seconds;
|
||||
}
|
||||
|
||||
chunks.Add(new WhisperChunk(nextChunkStart, chunkEnd));
|
||||
nextChunkStart = chunkEnd + 0.1;
|
||||
}
|
||||
|
||||
return MergeSmallChunks(chunks, maxChunkLength, minChunkLength);
|
||||
}
|
||||
|
||||
private static List<WhisperChunk> MergeSmallChunks(List<WhisperChunk> chunks, double maxChunkLength, double minChunkLength)
|
||||
{
|
||||
for (int i = 1; i < chunks.Count; i++)
|
||||
{
|
||||
// Check if current chunk is small and can be merged with previous
|
||||
if (chunks[i].Length < minChunkLength)
|
||||
{
|
||||
double prevChunkLength = chunks[i - 1].Length;
|
||||
double combinedLength = prevChunkLength + chunks[i].Length;
|
||||
|
||||
if (combinedLength <= maxChunkLength)
|
||||
{
|
||||
chunks[i - 1].End = chunks[i].End; // Merge with previous chunk
|
||||
chunks.RemoveAt(i); // Remove current chunk
|
||||
i--; // Adjust index to recheck current position now pointing to next chunk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
// 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.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.ML.OnnxRuntime;
|
||||
using Microsoft.ML.OnnxRuntime.Tensors;
|
||||
using NReco.VideoConverter;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
public static class Whisper
|
||||
{
|
||||
private static InferenceSession _inferenceSession;
|
||||
|
||||
private static InferenceSession InitializeModel()
|
||||
{
|
||||
// model generated from https://github.com/microsoft/Olive/blob/main/examples/whisper/README.md
|
||||
// var modelPath = $@"{AppDomain.CurrentDomain.BaseDirectory}AIModelAssets\whisper\whisper_tiny.onnx";
|
||||
var modelPath = $@"{AppDomain.CurrentDomain.BaseDirectory}AIModelAssets\whisper\whisper_small.onnx";
|
||||
|
||||
SessionOptions options = new SessionOptions();
|
||||
options.RegisterOrtExtensions();
|
||||
options.GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL;
|
||||
|
||||
var session = new InferenceSession(modelPath, options);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private static List<WhisperTranscribedChunk> TranscribeChunkAsync(byte[] pcmAudioData, string inputLanguage, WhisperTaskType taskType, int offsetSeconds = 30)
|
||||
{
|
||||
#pragma warning disable CA1861 // Avoid constant arrays as arguments
|
||||
if (_inferenceSession == null)
|
||||
{
|
||||
_inferenceSession = InitializeModel();
|
||||
}
|
||||
|
||||
var audioTensor = new DenseTensor<byte>(pcmAudioData, [1, pcmAudioData.Length]);
|
||||
var timestampsEnableTensor = new DenseTensor<int>(1);
|
||||
timestampsEnableTensor.Fill(1);
|
||||
|
||||
int task = (int)taskType;
|
||||
int langCode = WhisperUtils.GetLangId(inputLanguage);
|
||||
var decoderInputIds = new int[] { 50258, langCode, task };
|
||||
var langAndModeTensor = new DenseTensor<int>(decoderInputIds, [1, 3]);
|
||||
|
||||
var minLengthTensor = new DenseTensor<int>(1);
|
||||
minLengthTensor.Fill(0);
|
||||
|
||||
var maxLengthTensor = new DenseTensor<int>(1);
|
||||
maxLengthTensor.Fill(448);
|
||||
|
||||
var numBeamsTensor = new DenseTensor<int>(1);
|
||||
numBeamsTensor.Fill(1);
|
||||
|
||||
var numReturnSequencesTensor = new DenseTensor<int>(1);
|
||||
numReturnSequencesTensor.Fill(1);
|
||||
|
||||
var lengthPenaltyTensor = new DenseTensor<float>(1);
|
||||
lengthPenaltyTensor.Fill(1.0f);
|
||||
|
||||
var repetitionPenaltyTensor = new DenseTensor<float>(1);
|
||||
repetitionPenaltyTensor.Fill(1.2f);
|
||||
|
||||
var inputs = new List<NamedOnnxValue>
|
||||
{
|
||||
NamedOnnxValue.CreateFromTensor("audio_stream", audioTensor),
|
||||
NamedOnnxValue.CreateFromTensor("min_length", minLengthTensor),
|
||||
NamedOnnxValue.CreateFromTensor("max_length", maxLengthTensor),
|
||||
NamedOnnxValue.CreateFromTensor("num_beams", numBeamsTensor),
|
||||
NamedOnnxValue.CreateFromTensor("num_return_sequences", numReturnSequencesTensor),
|
||||
NamedOnnxValue.CreateFromTensor("length_penalty", lengthPenaltyTensor),
|
||||
NamedOnnxValue.CreateFromTensor("repetition_penalty", repetitionPenaltyTensor),
|
||||
NamedOnnxValue.CreateFromTensor("logits_processor", timestampsEnableTensor),
|
||||
NamedOnnxValue.CreateFromTensor("decoder_input_ids", langAndModeTensor),
|
||||
};
|
||||
#pragma warning restore CA1861 // Avoid constant arrays as arguments
|
||||
|
||||
// for multithread need to try AsyncRun
|
||||
try
|
||||
{
|
||||
using var results = _inferenceSession.Run(inputs);
|
||||
var result = results[0].AsTensor<string>().GetValue(0);
|
||||
return WhisperUtils.ProcessTranscriptionWithTimestamps(result, offsetSeconds);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// return empty list in case of exception
|
||||
return new List<WhisperTranscribedChunk>();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<WhisperTranscribedChunk> TranscribeAsync(StorageFile audioFile, int startSeconds, int durationSeconds, EventHandler<float> progress = null)
|
||||
{
|
||||
var transcribedChunks = new List<WhisperTranscribedChunk>();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var audioBytes = LoadAudioBytes(audioFile.Path, startSeconds, durationSeconds);
|
||||
|
||||
sw.Stop();
|
||||
Debug.WriteLine($"Loading took {sw.ElapsedMilliseconds} ms");
|
||||
sw.Start();
|
||||
|
||||
var dynamicChunks = WhisperChunking.SmartChunking(audioBytes);
|
||||
|
||||
sw.Stop();
|
||||
Debug.WriteLine($"Chunking took {sw.ElapsedMilliseconds} ms");
|
||||
|
||||
for (var i = 0; i < dynamicChunks.Count; i++)
|
||||
{
|
||||
var chunk = dynamicChunks[i];
|
||||
|
||||
var audioSegment = ExtractAudioSegment(audioFile.Path, chunk.Start, chunk.End - chunk.Start);
|
||||
|
||||
var transcription = TranscribeChunkAsync(audioSegment, "en", WhisperTaskType.Transcribe, (int)chunk.Start);
|
||||
|
||||
transcribedChunks.AddRange(transcription);
|
||||
|
||||
progress?.Invoke(null, (float)i / dynamicChunks.Count);
|
||||
}
|
||||
|
||||
return transcribedChunks;
|
||||
}
|
||||
|
||||
private static byte[] LoadAudioBytes(string file, int startSeconds, int durationSeconds)
|
||||
{
|
||||
var ffmpeg = new FFMpegConverter();
|
||||
var output = new MemoryStream();
|
||||
|
||||
var extension = Path.GetExtension(file).Substring(1);
|
||||
|
||||
// Convert to PCM
|
||||
if (startSeconds == 0 && durationSeconds == 0)
|
||||
{
|
||||
ffmpeg.ConvertMedia(
|
||||
inputFile: file,
|
||||
inputFormat: null,
|
||||
outputStream: output,
|
||||
outputFormat: "s16le",
|
||||
new ConvertSettings()
|
||||
{
|
||||
AudioCodec = "pcm_s16le",
|
||||
AudioSampleRate = 16000,
|
||||
CustomOutputArgs = "-ac 1",
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
ffmpeg.ConvertMedia(
|
||||
inputFile: file,
|
||||
inputFormat: null,
|
||||
outputStream: output,
|
||||
outputFormat: "s16le",
|
||||
new ConvertSettings()
|
||||
{
|
||||
Seek = (float?)startSeconds,
|
||||
MaxDuration = (float?)durationSeconds,
|
||||
AudioCodec = "pcm_s16le",
|
||||
AudioSampleRate = 16000,
|
||||
CustomOutputArgs = "-ac 1",
|
||||
});
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static byte[] ExtractAudioSegment(string inPath, double startTimeInSeconds, double segmentDurationInSeconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var extension = System.IO.Path.GetExtension(inPath).Substring(1);
|
||||
var output = new MemoryStream();
|
||||
|
||||
var convertSettings = new ConvertSettings
|
||||
{
|
||||
Seek = (float?)startTimeInSeconds,
|
||||
MaxDuration = (float?)segmentDurationInSeconds,
|
||||
AudioSampleRate = 16000,
|
||||
CustomOutputArgs = "-vn -ac 1",
|
||||
};
|
||||
|
||||
var ffMpegConverter = new FFMpegConverter();
|
||||
ffMpegConverter.ConvertMedia(
|
||||
inputFile: inPath,
|
||||
inputFormat: null,
|
||||
outputStream: output,
|
||||
outputFormat: "wav",
|
||||
convertSettings);
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Error during the audio extraction: " + ex.Message);
|
||||
return Array.Empty<byte>(); // Return an empty array in case of exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal enum WhisperTaskType
|
||||
{
|
||||
Translate = 50358,
|
||||
Transcribe = 50359,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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 AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
public class WhisperTranscribedChunk
|
||||
{
|
||||
public string Text { get; set; }
|
||||
|
||||
public double Start { get; set; }
|
||||
|
||||
public double End { get; set; }
|
||||
|
||||
public double Length => End - Start;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AdvancedPaste.AIModels.Whisper
|
||||
{
|
||||
internal static class WhisperUtils
|
||||
{
|
||||
private static Dictionary<string, string> languageCodes = new()
|
||||
{
|
||||
{ "English", "en" },
|
||||
{ "Serbian", "sr" },
|
||||
{ "Hindi", "hi" },
|
||||
{ "Spanish", "es" },
|
||||
{ "Russian", "ru" },
|
||||
{ "Korean", "ko" },
|
||||
{ "French", "fr" },
|
||||
{ "Japanese", "ja" },
|
||||
{ "Portuguese", "pt" },
|
||||
{ "Turkish", "tr" },
|
||||
{ "Polish", "pl" },
|
||||
{ "Catalan", "ca" },
|
||||
{ "Dutch", "nl" },
|
||||
{ "Arabic", "ar" },
|
||||
{ "Swedish", "sv" },
|
||||
{ "Italian", "it" },
|
||||
{ "Indonesian", "id" },
|
||||
{ "Macedonian", "mk" },
|
||||
{ "Mandarin", "zh" },
|
||||
};
|
||||
|
||||
public static int GetLangId(string languageString)
|
||||
{
|
||||
int langId = 50259;
|
||||
Dictionary<string, int> langToId = new Dictionary<string, int>
|
||||
{
|
||||
{ "af", 50327 },
|
||||
{ "am", 50334 },
|
||||
{ "ar", 50272 },
|
||||
{ "as", 50350 },
|
||||
{ "az", 50304 },
|
||||
{ "ba", 50355 },
|
||||
{ "be", 50330 },
|
||||
{ "bg", 50292 },
|
||||
{ "bn", 50302 },
|
||||
{ "bo", 50347 },
|
||||
{ "br", 50309 },
|
||||
{ "bs", 50315 },
|
||||
{ "ca", 50270 },
|
||||
{ "cs", 50283 },
|
||||
{ "cy", 50297 },
|
||||
{ "da", 50285 },
|
||||
{ "de", 50261 },
|
||||
{ "el", 50281 },
|
||||
{ "en", 50259 },
|
||||
{ "es", 50262 },
|
||||
{ "et", 50307 },
|
||||
{ "eu", 50310 },
|
||||
{ "fa", 50300 },
|
||||
{ "fi", 50277 },
|
||||
{ "fo", 50338 },
|
||||
{ "fr", 50265 },
|
||||
{ "gl", 50319 },
|
||||
{ "gu", 50333 },
|
||||
{ "haw", 50352 },
|
||||
{ "ha", 50354 },
|
||||
{ "he", 50279 },
|
||||
{ "hi", 50276 },
|
||||
{ "hr", 50291 },
|
||||
{ "ht", 50339 },
|
||||
{ "hu", 50286 },
|
||||
{ "hy", 50312 },
|
||||
{ "id", 50275 },
|
||||
{ "is", 50311 },
|
||||
{ "it", 50274 },
|
||||
{ "ja", 50266 },
|
||||
{ "jw", 50356 },
|
||||
{ "ka", 50329 },
|
||||
{ "kk", 50316 },
|
||||
{ "km", 50323 },
|
||||
{ "kn", 50306 },
|
||||
{ "ko", 50264 },
|
||||
{ "la", 50294 },
|
||||
{ "lb", 50345 },
|
||||
{ "ln", 50353 },
|
||||
{ "lo", 50336 },
|
||||
{ "lt", 50293 },
|
||||
{ "lv", 50301 },
|
||||
{ "mg", 50349 },
|
||||
{ "mi", 50295 },
|
||||
{ "mk", 50308 },
|
||||
{ "ml", 50296 },
|
||||
{ "mn", 50314 },
|
||||
{ "mr", 50320 },
|
||||
{ "ms", 50282 },
|
||||
{ "mt", 50343 },
|
||||
{ "my", 50346 },
|
||||
{ "ne", 50313 },
|
||||
{ "nl", 50271 },
|
||||
{ "nn", 50342 },
|
||||
{ "no", 50288 },
|
||||
{ "oc", 50328 },
|
||||
{ "pa", 50321 },
|
||||
{ "pl", 50269 },
|
||||
{ "ps", 50340 },
|
||||
{ "pt", 50267 },
|
||||
{ "ro", 50284 },
|
||||
{ "ru", 50263 },
|
||||
{ "sa", 50344 },
|
||||
{ "sd", 50332 },
|
||||
{ "si", 50322 },
|
||||
{ "sk", 50298 },
|
||||
{ "sl", 50305 },
|
||||
{ "sn", 50324 },
|
||||
{ "so", 50326 },
|
||||
{ "sq", 50317 },
|
||||
{ "sr", 50303 },
|
||||
{ "su", 50357 },
|
||||
{ "sv", 50273 },
|
||||
{ "sw", 50318 },
|
||||
{ "ta", 50287 },
|
||||
{ "te", 50299 },
|
||||
{ "tg", 50331 },
|
||||
{ "th", 50289 },
|
||||
{ "tk", 50341 },
|
||||
{ "tl", 50325 },
|
||||
{ "tr", 50268 },
|
||||
{ "tt", 50335 },
|
||||
{ "ug", 50348 },
|
||||
{ "uk", 50260 },
|
||||
{ "ur", 50337 },
|
||||
{ "uz", 50351 },
|
||||
{ "vi", 50278 },
|
||||
{ "xh", 50322 },
|
||||
{ "yi", 50305 },
|
||||
{ "yo", 50324 },
|
||||
{ "zh", 50258 },
|
||||
{ "zu", 50321 },
|
||||
};
|
||||
|
||||
if (languageCodes.TryGetValue(languageString, out string langCode))
|
||||
{
|
||||
langId = langToId[langCode];
|
||||
}
|
||||
|
||||
return langId;
|
||||
}
|
||||
|
||||
public static List<WhisperTranscribedChunk> ProcessTranscriptionWithTimestamps(string transcription, double offsetSeconds = 0)
|
||||
{
|
||||
Regex pattern = new Regex(@"<\|([\d.]+)\|>([^<]+)<\|([\d.]+)\|>");
|
||||
MatchCollection matches = pattern.Matches(transcription);
|
||||
List<WhisperTranscribedChunk> list = new();
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
// Parse the original start and end times
|
||||
#pragma warning disable CA1305 // Specify IFormatProvider
|
||||
double start = double.Parse(matches[i].Groups[1].Value);
|
||||
double end = double.Parse(matches[i].Groups[3].Value);
|
||||
#pragma warning restore CA1305 // Specify IFormatProvider
|
||||
string subtitle = string.IsNullOrEmpty(matches[i].Groups[2].Value) ? string.Empty : matches[i].Groups[2].Value.Trim();
|
||||
WhisperTranscribedChunk chunk = new()
|
||||
{
|
||||
Text = subtitle,
|
||||
Start = start + offsetSeconds,
|
||||
End = end + offsetSeconds,
|
||||
};
|
||||
list.Add(chunk);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static List<WhisperTranscribedChunk> MergeTranscribedChunks(List<WhisperTranscribedChunk> chunks)
|
||||
{
|
||||
List<WhisperTranscribedChunk> list = new();
|
||||
WhisperTranscribedChunk transcribedChunk = chunks[0];
|
||||
|
||||
for (int i = 1; i < chunks.Count; i++)
|
||||
{
|
||||
char lastCharOfPrev = transcribedChunk.Text[transcribedChunk.Text.Length - 1];
|
||||
char firstCharOfNext = chunks[i].Text[0];
|
||||
|
||||
// Approach 1: Get full sentences together
|
||||
// Approach 2: Sliding window of desired duration
|
||||
if (char.IsLower(firstCharOfNext) || (lastCharOfPrev != '.' && lastCharOfPrev != '?' && lastCharOfPrev != '!'))
|
||||
{
|
||||
transcribedChunk.End = chunks[i].End;
|
||||
transcribedChunk.Text += " " + chunks[i].Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(transcribedChunk);
|
||||
transcribedChunk = chunks[i];
|
||||
}
|
||||
}
|
||||
|
||||
list.Add(transcribedChunk);
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,17 +62,22 @@
|
||||
<PackageReference Include="Azure.AI.OpenAI" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime" />
|
||||
<PackageReference Include="Microsoft.ML.OnnxRuntime.Extensions" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWinRT" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="NReco.VideoConverter" />
|
||||
<PackageReference Include="ReverseMarkdown" />
|
||||
<!-- HACK: To align Microsoft.Bcl.AsyncInterfaces.dll version with PowerToys.Settings.csproj. -->
|
||||
<PackageReference Include="StreamJsonRpc" />
|
||||
<!-- HACK: To align deps versions. -->
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<PackageReference Include="WinUIEx" />
|
||||
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
@@ -102,6 +107,13 @@
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Copy the model over -->
|
||||
<ItemGroup>
|
||||
<None Update="AIModelAssets\**">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
// 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.Pages;
|
||||
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;
|
||||
@@ -46,7 +48,6 @@ 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>();
|
||||
@@ -100,14 +101,12 @@ namespace AdvancedPaste
|
||||
|
||||
private void OnAdvancedPasteJsonHotkey()
|
||||
{
|
||||
viewModel.GetClipboardData();
|
||||
viewModel.ToJsonFunction(true);
|
||||
viewModel.ToJson();
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteMarkdownHotkey()
|
||||
{
|
||||
viewModel.GetClipboardData();
|
||||
viewModel.ToMarkdownFunction(true);
|
||||
viewModel.ToMarkdown();
|
||||
}
|
||||
|
||||
private void OnAdvancedPasteHotkey()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<UserControl
|
||||
<UserControl
|
||||
x:Class="AdvancedPaste.Controls.PromptBox"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
@@ -19,13 +20,14 @@ namespace AdvancedPaste.Controls
|
||||
public sealed partial class PromptBox : Microsoft.UI.Xaml.Controls.UserControl
|
||||
{
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
private UserSettings _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; }
|
||||
|
||||
@@ -36,10 +38,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
|
||||
{
|
||||
@@ -48,10 +50,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
|
||||
{
|
||||
@@ -63,7 +65,7 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
_userSettings = new UserSettings();
|
||||
|
||||
ViewModel = App.GetService<OptionsViewModel>();
|
||||
}
|
||||
@@ -78,6 +80,8 @@ namespace AdvancedPaste.Controls
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteGenerateCustomFormatEvent());
|
||||
|
||||
VisualStateManager.GoToState(this, "LoadingState", true);
|
||||
string inputInstructions = InputTxtBox.Text;
|
||||
ViewModel.SaveQuery(inputInstructions);
|
||||
@@ -133,6 +137,10 @@ namespace AdvancedPaste.Controls
|
||||
private void InputTxtBox_TextChanging(Microsoft.UI.Xaml.Controls.TextBox sender, TextBoxTextChangingEventArgs args)
|
||||
{
|
||||
SendBtn.Visibility = InputTxtBox.Text.Length > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// Sort available options
|
||||
string input = InputTxtBox.Text;
|
||||
ViewModel.FilterOptionsFromInput(input);
|
||||
}
|
||||
|
||||
private void InputTxtBox_KeyDown(object sender, Microsoft.UI.Xaml.Input.KeyRoutedEventArgs e)
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:pages="using:AdvancedPaste.Pages"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Width="420"
|
||||
Height="308"
|
||||
Width="600"
|
||||
Height="320"
|
||||
MinWidth="420"
|
||||
MinHeight="308"
|
||||
Closed="WindowEx_Closed"
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
// 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 readonly WindowMessageMonitor _msgMonitor;
|
||||
private readonly IUserSettings _userSettings;
|
||||
private WindowMessageMonitor _msgMonitor;
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
@@ -24,8 +25,6 @@ namespace AdvancedPaste
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
_userSettings = App.GetService<IUserSettings>();
|
||||
|
||||
AppWindow.SetIcon("Assets/AdvancedPaste/AdvancedPaste.ico");
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
this.SetTitleBar(titleBar);
|
||||
@@ -33,8 +32,6 @@ namespace AdvancedPaste
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Title = loader.GetString("WindowTitle");
|
||||
|
||||
Activated += OnActivated;
|
||||
|
||||
_msgMonitor = new WindowMessageMonitor(this);
|
||||
_msgMonitor.WindowMessageReceived += (_, e) =>
|
||||
{
|
||||
@@ -50,14 +47,6 @@ 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)
|
||||
@@ -77,13 +66,9 @@ namespace AdvancedPaste
|
||||
|
||||
private void WindowEx_Closed(object sender, Microsoft.UI.Xaml.WindowEventArgs args)
|
||||
{
|
||||
Hide();
|
||||
args.Handled = true;
|
||||
}
|
||||
Windows.Win32.PInvoke.ShowWindow((Windows.Win32.Foundation.HWND)this.GetWindowHandle(), Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
|
||||
private void Hide()
|
||||
{
|
||||
Windows.Win32.PInvoke.ShowWindow(new Windows.Win32.Foundation.HWND(this.GetWindowHandle()), Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void SetFocus()
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
ItemClick="PasteOptionsListView_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemsSource="{x:Bind pasteFormats, Mode=OneWay}"
|
||||
ContainerContentChanging="PasteFormatListContentChanging"
|
||||
SelectionMode="None"
|
||||
TabIndex="1">
|
||||
<ListView.ItemTemplate>
|
||||
|
||||
@@ -25,28 +25,39 @@ namespace AdvancedPaste.Pages
|
||||
public sealed partial class MainPage : Page
|
||||
{
|
||||
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
|
||||
private readonly ObservableCollection<PasteFormat> pasteFormats;
|
||||
private readonly Microsoft.UI.Dispatching.DispatcherQueue _dispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private string _filterText;
|
||||
|
||||
private ObservableCollection<PasteFormat> pasteFormats = new();
|
||||
|
||||
private bool _pasteAsPlainEnabled;
|
||||
private bool _pasteAsMarkdownEnabled;
|
||||
private bool _pasteAsJsonEnabled;
|
||||
private bool _pasteAudioToTextEnabled;
|
||||
private bool _pasteAsFileEnabled;
|
||||
|
||||
public OptionsViewModel ViewModel { get; private set; }
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
pasteFormats =
|
||||
[
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE8E9" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsPlainText"), Format = PasteFormats.PlainText },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\ue8a5" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsMarkdown"), Format = PasteFormats.Markdown },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE943" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsJson"), Format = PasteFormats.Json },
|
||||
];
|
||||
|
||||
ViewModel = App.GetService<OptionsViewModel>();
|
||||
|
||||
clipboardHistory = new ObservableCollection<ClipboardItem>();
|
||||
|
||||
LoadClipboardHistoryEvent(null, null);
|
||||
LoadClipboardHistoryAsync();
|
||||
Clipboard.HistoryChanged += LoadClipboardHistoryEvent;
|
||||
ViewModel.FormatsChanged += FormatsChangedHandler;
|
||||
ViewModel.WindowShown += WindowShownHandler;
|
||||
this.EnablePasteOptions();
|
||||
}
|
||||
|
||||
private bool WindowShownHandler()
|
||||
{
|
||||
EnablePasteOptions();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LoadClipboardHistoryEvent(object sender, object e)
|
||||
@@ -57,6 +68,72 @@ namespace AdvancedPaste.Pages
|
||||
});
|
||||
}
|
||||
|
||||
private void GenerateFormatList()
|
||||
{
|
||||
List<PasteFormat> pasteFormatFullList =
|
||||
[
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE8AC" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsPlainText"), Format = PasteFormats.PlainText, Enabled = _pasteAsPlainEnabled },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\ue8a5" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsMarkdown"), Format = PasteFormats.Markdown, Enabled = _pasteAsMarkdownEnabled },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE943" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsJson"), Format = PasteFormats.Json, Enabled = _pasteAsJsonEnabled },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE943" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAudioToText"), Format = PasteFormats.AudioToText, Enabled = _pasteAudioToTextEnabled },
|
||||
new PasteFormat { Icon = new FontIcon() { Glyph = "\uE943" }, Name = ResourceLoaderInstance.ResourceLoader.GetString("PasteAsFile"), Format = PasteFormats.File, Enabled = _pasteAsFileEnabled },
|
||||
];
|
||||
|
||||
ObservableCollection<PasteFormat> toAddFormats;
|
||||
|
||||
if (_filterText != null)
|
||||
{
|
||||
toAddFormats = new ObservableCollection<PasteFormat>(pasteFormatFullList.Where(pasteFormat => pasteFormat.Name.Contains(_filterText, StringComparison.OrdinalIgnoreCase)).OrderByDescending(pasteFormat => pasteFormat.Enabled));
|
||||
}
|
||||
else
|
||||
{
|
||||
toAddFormats = new ObservableCollection<PasteFormat>(pasteFormatFullList.OrderByDescending(pasteFormat => pasteFormat.Enabled));
|
||||
}
|
||||
|
||||
pasteFormats.Clear();
|
||||
|
||||
foreach (var format in toAddFormats)
|
||||
{
|
||||
pasteFormats.Add(format);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnablePasteOptions()
|
||||
{
|
||||
Logger.LogInfo("Enabling paste options");
|
||||
|
||||
_pasteAsPlainEnabled = false;
|
||||
_pasteAsMarkdownEnabled = false;
|
||||
_pasteAsJsonEnabled = false;
|
||||
_pasteAudioToTextEnabled = false;
|
||||
_pasteAsFileEnabled = false;
|
||||
|
||||
if (ViewModel.ClipboardHasText)
|
||||
{
|
||||
_pasteAsJsonEnabled = true;
|
||||
_pasteAsPlainEnabled = true;
|
||||
_pasteAsFileEnabled = true;
|
||||
}
|
||||
|
||||
if (ViewModel.ClipboardHasHtml)
|
||||
{
|
||||
_pasteAsMarkdownEnabled = true;
|
||||
_pasteAsFileEnabled = true;
|
||||
}
|
||||
|
||||
if (ViewModel.ClipboardHasImage)
|
||||
{
|
||||
_pasteAsFileEnabled = true;
|
||||
}
|
||||
|
||||
if (ViewModel.ClipboardHasAudio)
|
||||
{
|
||||
_pasteAudioToTextEnabled = true;
|
||||
}
|
||||
|
||||
GenerateFormatList();
|
||||
}
|
||||
|
||||
public async void LoadClipboardHistoryAsync()
|
||||
{
|
||||
try
|
||||
@@ -87,11 +164,6 @@ 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)
|
||||
@@ -137,17 +209,27 @@ namespace AdvancedPaste.Pages
|
||||
|
||||
private void PasteAsPlain()
|
||||
{
|
||||
ViewModel.ToPlainTextFunction();
|
||||
ViewModel.ToPlainText();
|
||||
}
|
||||
|
||||
private void PasteAsMarkdown()
|
||||
{
|
||||
ViewModel.ToMarkdownFunction();
|
||||
ViewModel.ToMarkdown();
|
||||
}
|
||||
|
||||
private void PasteAsJson()
|
||||
{
|
||||
ViewModel.ToJsonFunction();
|
||||
ViewModel.ToJson();
|
||||
}
|
||||
|
||||
private void AudioToText()
|
||||
{
|
||||
ViewModel.AudioToText();
|
||||
}
|
||||
|
||||
private void PasteAsFile()
|
||||
{
|
||||
ViewModel.ToFile();
|
||||
}
|
||||
|
||||
private void PasteOptionsListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
@@ -176,6 +258,20 @@ namespace AdvancedPaste.Pages
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.Json));
|
||||
break;
|
||||
}
|
||||
|
||||
case PasteFormats.AudioToText:
|
||||
{
|
||||
AudioToText();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.AudioToText));
|
||||
return;
|
||||
}
|
||||
|
||||
case PasteFormats.File:
|
||||
{
|
||||
PasteAsFile();
|
||||
PowerToysTelemetry.Log.WriteEvent(new Telemetry.AdvancedPasteFormatClickedEvent(PasteFormats.File));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +327,6 @@ 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);
|
||||
@@ -244,5 +339,24 @@ namespace AdvancedPaste.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PasteFormatListContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
|
||||
{
|
||||
var listViewItem = args.ItemContainer;
|
||||
|
||||
if (listViewItem != null)
|
||||
{
|
||||
var model = (PasteFormat)args.Item;
|
||||
|
||||
listViewItem.IsEnabled = model.Enabled;
|
||||
}
|
||||
}
|
||||
|
||||
private bool FormatsChangedHandler(string input)
|
||||
{
|
||||
_filterText = input;
|
||||
GenerateFormatList();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using AdvancedPaste.Models;
|
||||
using Azure;
|
||||
using Azure.AI.OpenAI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Security.Credentials;
|
||||
|
||||
namespace AdvancedPaste.Helpers
|
||||
@@ -33,8 +35,6 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
private string _openAIKey;
|
||||
|
||||
private string _modelName = "gpt-3.5-turbo-instruct";
|
||||
|
||||
public bool IsAIEnabled => !string.IsNullOrEmpty(this._openAIKey);
|
||||
|
||||
public AICompletionsHelper()
|
||||
@@ -71,14 +71,14 @@ namespace AdvancedPaste.Helpers
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private Response<Completions> GetAICompletion(string systemInstructions, string userMessage)
|
||||
public string GetAICompletion(string systemInstructions, string userMessage)
|
||||
{
|
||||
OpenAIClient azureAIClient = new OpenAIClient(_openAIKey);
|
||||
|
||||
var response = azureAIClient.GetCompletions(
|
||||
new CompletionsOptions()
|
||||
{
|
||||
DeploymentName = _modelName,
|
||||
DeploymentName = "gpt-3.5-turbo-instruct",
|
||||
Prompts =
|
||||
{
|
||||
systemInstructions + "\n\n" + userMessage,
|
||||
@@ -92,35 +92,16 @@ namespace AdvancedPaste.Helpers
|
||||
Console.WriteLine("Cut off due to length constraints");
|
||||
}
|
||||
|
||||
return response;
|
||||
return response.Value.Choices[0].Text;
|
||||
}
|
||||
|
||||
public AICompletionsResponse AIFormatString(string inputInstructions, string inputString)
|
||||
private AICompletionsResponse TryAICompletion(string systemInstructions, string userMessage)
|
||||
{
|
||||
string systemInstructions = $@"You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it.
|
||||
|
||||
Do not output anything else besides the reformatted clipboard content.";
|
||||
|
||||
string userMessage = $@"User instructions:
|
||||
{inputInstructions}
|
||||
|
||||
Clipboard Content:
|
||||
{inputString}
|
||||
|
||||
Output:
|
||||
";
|
||||
|
||||
string aiResponse = null;
|
||||
Response<Completions> rawAIResponse = null;
|
||||
int apiRequestStatus = (int)HttpStatusCode.OK;
|
||||
try
|
||||
{
|
||||
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));
|
||||
aiResponse = this.GetAICompletion(systemInstructions, userMessage);
|
||||
}
|
||||
catch (Azure.RequestFailedException error)
|
||||
{
|
||||
@@ -137,5 +118,170 @@ Output:
|
||||
|
||||
return new AICompletionsResponse(aiResponse, apiRequestStatus);
|
||||
}
|
||||
|
||||
public AICompletionsResponse AIFormatString(string inputInstructions, string inputString)
|
||||
{
|
||||
string systemInstructions = $@"You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to edit their clipboard content as they have requested it.
|
||||
Ensure that you do all that is requested of you in the instructions. If the user has multiple instructions in their prompt be sure that both are all completed.
|
||||
Your output can include HTML if necessary, but it is not required.";
|
||||
|
||||
string userMessage = $@"User instructions:
|
||||
{inputInstructions}
|
||||
|
||||
Clipboard Content:
|
||||
{inputString}
|
||||
|
||||
Output:
|
||||
";
|
||||
|
||||
return TryAICompletion(systemInstructions, userMessage);
|
||||
}
|
||||
|
||||
public string AIFormatStringAsHTML(string inputInstructions, string inputString)
|
||||
{
|
||||
string systemInstructions = $@"You are tasked with reformatting user's clipboard data. Use the user's instructions, and the content of their clipboard below to reformat their clipboard content as they have requested it.
|
||||
Ensure that you do all that is requested of you in the instructions. If the user has multiple instructions in their prompt be sure that both are all completed.
|
||||
Do not use <code> blocks or classes to style the HTML, instead format directly into the HTML with inline styles wherever possible.
|
||||
Your output needs to be in HTML format.";
|
||||
|
||||
string userMessage = $@"User instructions:
|
||||
{inputInstructions}
|
||||
|
||||
Clipboard Content:
|
||||
{inputString}
|
||||
|
||||
Output:
|
||||
";
|
||||
|
||||
return TryAICompletion(systemInstructions, userMessage).Response;
|
||||
}
|
||||
|
||||
public string AIGetHTMLOrPlainTextOutput(string inputInstructions, string inputString)
|
||||
{
|
||||
string systemInstructions = $@"You are tasked with determining the output format for a user's request to reformat the clipboard data.
|
||||
You can choose between the output of 'HTML' or 'PlainText'. Your answer can only be those two options, do not put any other output.
|
||||
|
||||
Use these examples below to inform you.
|
||||
|
||||
Example user instructions:
|
||||
Make this pretty
|
||||
|
||||
Example clipboard content:
|
||||
var x = 5;
|
||||
|
||||
Example output:
|
||||
HTML
|
||||
|
||||
Example user instructions:
|
||||
Change to a pirate speaking in markdown
|
||||
|
||||
Example clipboard content:
|
||||
Hello my good friend.
|
||||
|
||||
Example output:
|
||||
PlainText
|
||||
|
||||
Example user instructions:
|
||||
Show this data as a table.
|
||||
|
||||
Example clipboard content:
|
||||
T-Rex, 5, 10
|
||||
Velociraptor, 7, 15
|
||||
|
||||
Example output:
|
||||
HTML
|
||||
|
||||
Now output the real answer.";
|
||||
|
||||
string userMessage = $@"User instructions:
|
||||
{inputInstructions}
|
||||
|
||||
Clipboard Content:
|
||||
{inputString}
|
||||
|
||||
Output:
|
||||
";
|
||||
|
||||
return TryAICompletion(systemInstructions, userMessage).Response;
|
||||
}
|
||||
|
||||
public string GetOperationsFromAI(string inputInstructions, bool hasText, bool hasImage, bool hasHtml, bool hasFile, bool hasAudio)
|
||||
{
|
||||
string availableFormatString = "(string inputInstructions";
|
||||
if (hasText)
|
||||
{
|
||||
availableFormatString += ", string clipboardText";
|
||||
}
|
||||
|
||||
if (hasImage)
|
||||
{
|
||||
availableFormatString += ", Image clipboardImage";
|
||||
}
|
||||
|
||||
if (hasHtml)
|
||||
{
|
||||
availableFormatString += ", HtmlData clipboardHTML";
|
||||
}
|
||||
|
||||
if (hasFile)
|
||||
{
|
||||
availableFormatString += ", File clipboardFile";
|
||||
}
|
||||
|
||||
if (hasAudio)
|
||||
{
|
||||
availableFormatString += ", Audio clipboardAudio";
|
||||
}
|
||||
|
||||
availableFormatString += ")";
|
||||
|
||||
string systemInstructions = $@"You are tasked with determining what operations are needed to reformat a user's clipboard data. Use the user's instructions, available functions, and clipboard data content to output the list of operations needed.
|
||||
You will output youre response as a function in C# ONLY using the functions provided (Do not use any other C# functions other than what is provided below!)
|
||||
|
||||
Available functions:
|
||||
- string ToJSON(string clipboardText)
|
||||
- Returns a string formatted into JSON from the clipboard content, only accepts text
|
||||
- Only to be used if the user explicitly requests JSON.
|
||||
- string ToPlainText(string clipboardText)
|
||||
- Returns a string with the clipboard content formatted into plain text, only accepts text
|
||||
- string ToCustomWithAI(string inputInstructions, string clipboardText)
|
||||
- Returns a string with the clipboard content formatted according to the input instructions, only accepts text.
|
||||
- Use this function to do custom processing of the text if another function above does not meet the requirements. Feel free to modify the user's instructions as needed to input to this function.
|
||||
- string ToFile(string clipboardText)
|
||||
- Returns a string of the filename of the file created from the input clipboard text
|
||||
- string ToFile(Image clipboardImage)
|
||||
- Returns a string of the filename of the file created from the input clipboard image
|
||||
- string AudioToText(Audio clipboardAudio, int seekSeconds, int maxDurationSeconds)
|
||||
- Returns a string with the clipboard audio content formatted into text, only accepts audio
|
||||
- seekSeconds is the number of seconds to skip from the start of the audio file
|
||||
- maxDurationSeconds is the maximum number of seconds to process from the audio file
|
||||
- If seekSeconds and maxDurationSeconds are 0 and 0 the entire file will be processed.
|
||||
|
||||
Example available arguments:
|
||||
(string inputInstructions, Audio clipboardAudio)
|
||||
|
||||
Example user instructions:
|
||||
To text, convert to Python, and highlight syntax with VS Code highlighting
|
||||
|
||||
Example output:
|
||||
public string ReformatClipboard(string inputInstructions, Audio clipboardAudio)
|
||||
{{
|
||||
string audioText = AudioToText(clipboardAudio, 0, 0);
|
||||
string customFormattedText = ToCustomWithAI('Convert to Python', imageText);
|
||||
string customFormattedText2 = ToCustomWithAI('Highlight syntax with VS Code highlighting', imageText);
|
||||
return customFormattedText2;
|
||||
}}";
|
||||
|
||||
string userMessage = $@"Available arguments:
|
||||
{availableFormatString}
|
||||
|
||||
User instructions:
|
||||
{inputInstructions}
|
||||
|
||||
Output:
|
||||
";
|
||||
|
||||
return TryAICompletion(systemInstructions, userMessage).Response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.AIModels.Whisper;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace AdvancedPaste.Helpers
|
||||
{
|
||||
public class AILocalModelsHelper
|
||||
{
|
||||
public Task<string> DoWhisperInference(StorageFile file)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var results = Whisper.TranscribeAsync(file, 0, 0);
|
||||
return string.Join("\n", results.Select(r => r.Text));
|
||||
});
|
||||
}
|
||||
|
||||
public Task<string> DoWhisperInference(StorageFile file, int startSeconds, int durationSeconds)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var results = Whisper.TranscribeAsync(file, startSeconds, durationSeconds);
|
||||
return string.Join("\n", results.Select(r => r.Text));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,16 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.System;
|
||||
|
||||
@@ -15,6 +20,16 @@ namespace AdvancedPaste.Helpers
|
||||
{
|
||||
internal static class ClipboardHelper
|
||||
{
|
||||
public enum ClipboardContentFormats
|
||||
{
|
||||
Text,
|
||||
Image,
|
||||
File,
|
||||
HTML,
|
||||
Audio,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
internal static void SetClipboardTextContent(string text)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
@@ -89,6 +104,81 @@ namespace AdvancedPaste.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
internal static string ConvertHTMLToPlainText(string inputHTML)
|
||||
{
|
||||
return System.Net.WebUtility.HtmlDecode(System.Text.RegularExpressions.Regex.Replace(inputHTML, "<.*?>", string.Empty));
|
||||
}
|
||||
|
||||
internal static async Task<bool> SetClipboardFile(string fileName)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (fileName != null)
|
||||
{
|
||||
StorageFile storageFile = await StorageFile.GetFileFromPathAsync(fileName).AsTask();
|
||||
|
||||
DataPackage output = new();
|
||||
output.SetStorageItems(new[] { storageFile });
|
||||
Clipboard.SetContent(output);
|
||||
|
||||
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
|
||||
// Calling inside a loop makes it work.
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Clipboard.Flush() failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void SetClipboardHTMLContent(string htmlContent)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (htmlContent != null)
|
||||
{
|
||||
// Set htmlContent to output
|
||||
DataPackage output = new();
|
||||
output.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(htmlContent));
|
||||
|
||||
// Extract plain text from HTML
|
||||
string plainText = ConvertHTMLToPlainText(htmlContent);
|
||||
|
||||
output.SetText(plainText);
|
||||
Clipboard.SetContent(output);
|
||||
|
||||
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
|
||||
// Calling inside a loop makes it work.
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.Flush();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Clipboard.Flush() failed", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Error");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to send a single key event
|
||||
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
|
||||
{
|
||||
@@ -135,5 +225,91 @@ namespace AdvancedPaste.Helpers
|
||||
|
||||
Logger.LogInfo("Paste sent");
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardTextContent(DataPackageView clipboardData)
|
||||
{
|
||||
if (clipboardData != null)
|
||||
{
|
||||
if (clipboardData.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
return await Task.Run(async () =>
|
||||
{
|
||||
string plainText = await clipboardData.GetTextAsync() as string;
|
||||
return plainText;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardHTMLContent(DataPackageView clipboardData)
|
||||
{
|
||||
if (clipboardData != null)
|
||||
{
|
||||
if (clipboardData.Contains(StandardDataFormats.Html))
|
||||
{
|
||||
return await Task.Run(async () =>
|
||||
{
|
||||
string htmlText = await clipboardData.GetHtmlFormatAsync() as string;
|
||||
return htmlText;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardFileName(DataPackageView clipboardData)
|
||||
{
|
||||
if (clipboardData != null)
|
||||
{
|
||||
if (clipboardData.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
return await Task.Run(async () =>
|
||||
{
|
||||
var storageItems = await clipboardData.GetStorageItemsAsync();
|
||||
var file = storageItems[0] as StorageFile;
|
||||
return file.Path;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
internal static async Task<SoftwareBitmap> GetClipboardImageContent(DataPackageView clipboardData)
|
||||
{
|
||||
SoftwareBitmap softwareBitmap = null;
|
||||
|
||||
// Check if the clipboard contains a file reference
|
||||
if (clipboardData.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var storageItems = await clipboardData.GetStorageItemsAsync();
|
||||
var file = storageItems[0] as StorageFile;
|
||||
if (file != null)
|
||||
{
|
||||
using (var stream = await file.OpenReadAsync())
|
||||
{
|
||||
// Get image stream and create a software bitmap
|
||||
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);
|
||||
softwareBitmap = await decoder.GetSoftwareBitmapAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (clipboardData.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
// If it's not a file reference, get bitmap directly
|
||||
var imageStreamReference = await clipboardData.GetBitmapAsync();
|
||||
var imageStream = await imageStreamReference.OpenReadAsync();
|
||||
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(imageStream);
|
||||
softwareBitmap = await decoder.GetSoftwareBitmapAsync();
|
||||
}
|
||||
}
|
||||
|
||||
return softwareBitmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,5 @@ namespace AdvancedPaste.Settings
|
||||
public bool ShowCustomPreview { get; }
|
||||
|
||||
public bool SendPasteKeyCombination { get; }
|
||||
|
||||
public bool CloseAfterLosingFocus { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using ManagedCommon;
|
||||
@@ -16,40 +14,17 @@ 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)
|
||||
internal static string ToJsonFromXmlOrCsv(string inputText)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (clipboardData == null || !clipboardData.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
Logger.LogWarning("Clipboard does not contain text data");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
string text = Task.Run(async () =>
|
||||
{
|
||||
string plainText = await clipboardData.GetTextAsync() as string;
|
||||
return plainText;
|
||||
}).Result;
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
|
||||
string jsonText = string.Empty;
|
||||
|
||||
// Try convert XML
|
||||
try
|
||||
{
|
||||
XmlDocument doc = new XmlDocument();
|
||||
doc.LoadXml(text);
|
||||
Logger.LogDebug("Converted from XML.");
|
||||
doc.LoadXml(inputText);
|
||||
jsonText = JsonConvert.SerializeXmlNode(doc, Newtonsoft.Json.Formatting.Indented);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -57,75 +32,6 @@ 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
|
||||
{
|
||||
@@ -133,31 +39,11 @@ namespace AdvancedPaste.Helpers
|
||||
{
|
||||
var csv = new List<string[]>();
|
||||
|
||||
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)
|
||||
foreach (var line in inputText.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
// 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.");
|
||||
}
|
||||
csv.Add(line.Split(","));
|
||||
}
|
||||
|
||||
Logger.LogDebug("Convert from csv.");
|
||||
jsonText = JsonConvert.SerializeObject(csv, Newtonsoft.Json.Formatting.Indented);
|
||||
}
|
||||
}
|
||||
@@ -166,79 +52,7 @@ 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.");
|
||||
}
|
||||
return string.IsNullOrEmpty(jsonText) ? inputText : jsonText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,69 +14,6 @@ namespace AdvancedPaste.Helpers
|
||||
{
|
||||
internal static class MarkdownHelper
|
||||
{
|
||||
public static string ToMarkdown(DataPackageView clipboardData)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (clipboardData == null)
|
||||
{
|
||||
Logger.LogWarning("Clipboard does not contain data");
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string data = string.Empty;
|
||||
|
||||
if (clipboardData.Contains(StandardDataFormats.Html))
|
||||
{
|
||||
data = Task.Run(async () =>
|
||||
{
|
||||
string data = await clipboardData.GetHtmlFormatAsync() as string;
|
||||
return data;
|
||||
}).Result;
|
||||
}
|
||||
else if (clipboardData.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
data = Task.Run(async () =>
|
||||
{
|
||||
string plainText = await clipboardData.GetTextAsync() as string;
|
||||
return plainText;
|
||||
}).Result;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(data))
|
||||
{
|
||||
string cleanedHtml = CleanHtml(data);
|
||||
|
||||
return ConvertHtmlToMarkdown(cleanedHtml);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public static string PasteAsPlainTextFromClipboard(DataPackageView clipboardData)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (clipboardData != null)
|
||||
{
|
||||
if (!clipboardData.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
Logger.LogWarning("Clipboard does not contain text data");
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
string plainText = await clipboardData.GetTextAsync() as string;
|
||||
return plainText;
|
||||
}).Result;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string CleanHtml(string html)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
@@ -158,13 +95,15 @@ namespace AdvancedPaste.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertHtmlToMarkdown(string html)
|
||||
internal static string ConvertHtmlToMarkdown(string data)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
string cleanedHtml = CleanHtml(data);
|
||||
|
||||
// Perform the conversion from HTML to Markdown using your chosen library or method
|
||||
var converter = new ReverseMarkdown.Converter();
|
||||
string markdown = converter.Convert(html);
|
||||
string markdown = converter.Convert(cleanedHtml);
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,12 @@ 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();
|
||||
|
||||
@@ -64,7 +61,6 @@ namespace AdvancedPaste.Settings
|
||||
{
|
||||
ShowCustomPreview = settings.Properties.ShowCustomPreview;
|
||||
SendPasteKeyCombination = settings.Properties.SendPasteKeyCombination;
|
||||
CloseAfterLosingFocus = settings.Properties.CloseAfterLosingFocus;
|
||||
}
|
||||
|
||||
retry = false;
|
||||
|
||||
@@ -13,5 +13,7 @@ namespace AdvancedPaste.Models
|
||||
public string Name { get; set; }
|
||||
|
||||
public PasteFormats Format { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,7 @@ namespace AdvancedPaste.Models
|
||||
Markdown,
|
||||
Json,
|
||||
Custom,
|
||||
AudioToText,
|
||||
File,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
<value>Clipboard data is not text</value>
|
||||
</data>
|
||||
<data name="OpenAINotConfigured" xml:space="preserve">
|
||||
<value>To custom with AI is not enabled</value>
|
||||
<value>To custom with AI not enabled</value>
|
||||
</data>
|
||||
<data name="OpenAIApiKeyUnauthorized" xml:space="preserve">
|
||||
<value>Invalid API key or endpoint</value>
|
||||
@@ -165,6 +165,12 @@
|
||||
<data name="PasteAsPlainText" xml:space="preserve">
|
||||
<value>Paste as plain text</value>
|
||||
</data>
|
||||
<data name="PasteAudioToText" xml:space="preserve">
|
||||
<value>Paste audio to text</value>
|
||||
</data>
|
||||
<data name="PasteAsFile" xml:space="preserve">
|
||||
<value>Paste as file</value>
|
||||
</data>
|
||||
<data name="PasteButtonAutomation.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Paste</value>
|
||||
</data>
|
||||
@@ -225,7 +231,4 @@
|
||||
<data name="TermsLink.Text" xml:space="preserve">
|
||||
<value>OpenAI Terms</value>
|
||||
</data>
|
||||
<data name="OpenAIGpoDisabled" xml:space="preserve">
|
||||
<value>To custom with AI is disabled by your organization</value>
|
||||
</data>
|
||||
</root>
|
||||
</root>
|
||||
@@ -1,16 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.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,19 +11,6 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Formats.Tar;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Xps.Packaging;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using AdvancedPaste.Settings;
|
||||
@@ -16,27 +27,54 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.Win32;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
using WinUIEx;
|
||||
using static AdvancedPaste.Helpers.NativeMethods;
|
||||
using Application = Microsoft.UI.Xaml.Application;
|
||||
using BitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder;
|
||||
using BitmapEncoder = Windows.Graphics.Imaging.BitmapEncoder;
|
||||
using Clipboard = Windows.ApplicationModel.DataTransfer.Clipboard;
|
||||
|
||||
namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
public partial class OptionsViewModel : ObservableObject
|
||||
{
|
||||
internal struct SavedClipboardItem
|
||||
{
|
||||
public ClipboardHelper.ClipboardContentFormats Format { get; set; }
|
||||
|
||||
public string Text { get; set; }
|
||||
|
||||
public string HTML { get; set; }
|
||||
|
||||
public string Filename { get; set; }
|
||||
|
||||
public SoftwareBitmap Image { get; set; }
|
||||
}
|
||||
|
||||
private static readonly string[] FunctionNames =
|
||||
{
|
||||
"ToCustomWithAI",
|
||||
"RemoveBackground",
|
||||
"ToJSON",
|
||||
"ToPlainText",
|
||||
"ToMarkdown",
|
||||
"ToFile",
|
||||
"AudioToText",
|
||||
};
|
||||
|
||||
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
private readonly IUserSettings _userSettings;
|
||||
|
||||
private App app = App.Current as App;
|
||||
|
||||
private AICompletionsHelper aiHelper;
|
||||
|
||||
public DataPackageView ClipboardData { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
[NotifyPropertyChangedFor(nameof(IsCustomAIEnabled))]
|
||||
private bool _isClipboardDataText;
|
||||
private UserSettings _userSettings;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxPlaceholderText))]
|
||||
@@ -49,83 +87,37 @@ namespace AdvancedPaste.ViewModels
|
||||
[NotifyPropertyChangedFor(nameof(InputTxtBoxErrorText))]
|
||||
private int _apiRequestStatus;
|
||||
|
||||
public OptionsViewModel(IUserSettings userSettings)
|
||||
{
|
||||
aiHelper = new AICompletionsHelper();
|
||||
_userSettings = userSettings;
|
||||
[ObservableProperty]
|
||||
private string _customFormatResult;
|
||||
|
||||
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
[ObservableProperty]
|
||||
private bool _customFormatIsHTML;
|
||||
|
||||
ApiRequestStatus = (int)HttpStatusCode.OK;
|
||||
[ObservableProperty]
|
||||
private DataPackageView _clipboardContent;
|
||||
|
||||
GeneratedResponses = new ObservableCollection<string>();
|
||||
GeneratedResponses.CollectionChanged += (s, e) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(HasMultipleResponses));
|
||||
OnPropertyChanged(nameof(CurrentIndexDisplay));
|
||||
};
|
||||
[ObservableProperty]
|
||||
private bool _clipboardHasText;
|
||||
|
||||
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
|
||||
GetClipboardData();
|
||||
}
|
||||
[ObservableProperty]
|
||||
private bool _clipboardHasHtml;
|
||||
|
||||
public void GetClipboardData()
|
||||
{
|
||||
ClipboardData = Clipboard.GetContent();
|
||||
IsClipboardDataText = ClipboardData.Contains(StandardDataFormats.Text);
|
||||
}
|
||||
[ObservableProperty]
|
||||
private bool _clipboardHasImage;
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
GetClipboardData();
|
||||
[ObservableProperty]
|
||||
private bool _clipboardHasFile;
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
IsCustomAIEnabled = false;
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
}
|
||||
else
|
||||
{
|
||||
var openAIKey = AICompletionsHelper.LoadOpenAIKey();
|
||||
var currentKey = aiHelper.GetKey();
|
||||
bool keyChanged = openAIKey != currentKey;
|
||||
|
||||
if (keyChanged)
|
||||
{
|
||||
app.GetMainWindow().StartLoading();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
aiHelper.SetOpenAIKey(openAIKey);
|
||||
}).ContinueWith(
|
||||
(t) =>
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
app.GetMainWindow().FinishLoading(aiHelper.IsAIEnabled);
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
});
|
||||
},
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCustomAIEnabled = IsClipboardDataText && aiHelper.IsAIEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
|
||||
GeneratedResponses.Clear();
|
||||
}
|
||||
[ObservableProperty]
|
||||
private bool _clipboardHasAudio;
|
||||
|
||||
// List to store generated responses
|
||||
public ObservableCollection<string> GeneratedResponses { get; set; } = new ObservableCollection<string>();
|
||||
internal ObservableCollection<SavedClipboardItem> GeneratedResponses { get; set; } = new ObservableCollection<SavedClipboardItem>();
|
||||
|
||||
// Index to keep track of the current response
|
||||
private int _currentResponseIndex;
|
||||
|
||||
public int CurrentResponseIndex
|
||||
internal int CurrentResponseIndex
|
||||
{
|
||||
get => _currentResponseIndex;
|
||||
set
|
||||
@@ -133,7 +125,7 @@ namespace AdvancedPaste.ViewModels
|
||||
if (value >= 0 && value < GeneratedResponses.Count)
|
||||
{
|
||||
SetProperty(ref _currentResponseIndex, value);
|
||||
CustomFormatResult = GeneratedResponses[_currentResponseIndex];
|
||||
CustomFormatResult = GeneratedResponses[_currentResponseIndex].Text;
|
||||
OnPropertyChanged(nameof(CurrentIndexDisplay));
|
||||
}
|
||||
}
|
||||
@@ -152,18 +144,10 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
app.GetMainWindow().ClearInputText();
|
||||
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetAllowedAdvancedPasteOnlineAIModelsValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIGpoDisabled");
|
||||
}
|
||||
else if (!aiHelper.IsAIEnabled)
|
||||
if (!aiHelper.IsAIEnabled)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAINotConfigured");
|
||||
}
|
||||
else if (!IsClipboardDataText)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("ClipboardDataTypeMismatchWarning");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("CustomFormatTextBox/PlaceholderText");
|
||||
@@ -179,15 +163,15 @@ namespace AdvancedPaste.ViewModels
|
||||
{
|
||||
if (ApiRequestStatus == (int)HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyTooManyRequests");
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIAPIKeyTooManyRequests");
|
||||
}
|
||||
else if (ApiRequestStatus == (int)HttpStatusCode.Unauthorized)
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyUnauthorized");
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIAPIKeyUnauthorized");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIApiKeyError") + ApiRequestStatus.ToString(CultureInfo.InvariantCulture);
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("OpenAIAPIKeyError") + ApiRequestStatus.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,13 +179,142 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
private string _customFormatResult;
|
||||
private AILocalModelsHelper aiLocalModelsHelper;
|
||||
|
||||
[RelayCommand]
|
||||
public void PasteCustom()
|
||||
public event Func<string, bool> FormatsChanged;
|
||||
|
||||
public event Func<bool> WindowShown;
|
||||
|
||||
public OptionsViewModel()
|
||||
{
|
||||
PasteCustomFunction(GeneratedResponses[CurrentResponseIndex]);
|
||||
aiHelper = new AICompletionsHelper();
|
||||
_userSettings = new UserSettings();
|
||||
|
||||
IsCustomAIEnabled = aiHelper.IsAIEnabled;
|
||||
|
||||
ApiRequestStatus = (int)HttpStatusCode.OK;
|
||||
|
||||
GeneratedResponses = new ObservableCollection<SavedClipboardItem>();
|
||||
GeneratedResponses.CollectionChanged += (s, e) =>
|
||||
{
|
||||
OnPropertyChanged(nameof(HasMultipleResponses));
|
||||
OnPropertyChanged(nameof(CurrentIndexDisplay));
|
||||
};
|
||||
|
||||
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
|
||||
aiLocalModelsHelper = new AILocalModelsHelper();
|
||||
}
|
||||
|
||||
public void GetClipboardData()
|
||||
{
|
||||
ClipboardContent = Clipboard.GetContent();
|
||||
|
||||
ClipboardHasText = false;
|
||||
ClipboardHasHtml = false;
|
||||
ClipboardHasImage = false;
|
||||
ClipboardHasFile = false;
|
||||
ClipboardHasAudio = false;
|
||||
|
||||
if (ClipboardContent == null)
|
||||
{
|
||||
Logger.LogWarning("Clipboard does not contain any data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ClipboardContent.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
ClipboardHasText = true;
|
||||
}
|
||||
|
||||
if (ClipboardContent.Contains(StandardDataFormats.Html))
|
||||
{
|
||||
ClipboardHasHtml = true;
|
||||
}
|
||||
|
||||
if (ClipboardContent.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
ClipboardHasImage = true;
|
||||
}
|
||||
|
||||
if (ClipboardContent.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
// Get storage items and iterate through their file names to find endings
|
||||
// to enable audio and image to text
|
||||
ClipboardHasFile = true;
|
||||
try
|
||||
{
|
||||
var storageItemsAwaiter = ClipboardContent.GetStorageItemsAsync();
|
||||
storageItemsAwaiter.AsTask().Wait();
|
||||
var storageItems = storageItemsAwaiter.GetResults();
|
||||
foreach (var storageItem in storageItems)
|
||||
{
|
||||
if (storageItem is Windows.Storage.StorageFile file)
|
||||
{
|
||||
if (file.ContentType.Contains("audio") || file.Name.EndsWith("waptt", StringComparison.InvariantCulture))
|
||||
{
|
||||
if (file.ContentType.Contains("audio"))
|
||||
{
|
||||
ClipboardHasAudio = true;
|
||||
}
|
||||
else if (file.ContentType.Contains("image"))
|
||||
{
|
||||
ClipboardHasImage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("Error getting storage items", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnShow()
|
||||
{
|
||||
GetClipboardData();
|
||||
|
||||
var openAIKey = AICompletionsHelper.LoadOpenAIKey();
|
||||
var currentKey = aiHelper.GetKey();
|
||||
bool keyChanged = openAIKey != currentKey;
|
||||
|
||||
if (keyChanged)
|
||||
{
|
||||
app.GetMainWindow().StartLoading();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
aiHelper.SetOpenAIKey(openAIKey);
|
||||
}).ContinueWith(
|
||||
(t) =>
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
app.GetMainWindow().FinishLoading(aiHelper.IsAIEnabled);
|
||||
OnPropertyChanged(nameof(InputTxtBoxPlaceholderText));
|
||||
IsCustomAIEnabled = aiHelper.IsAIEnabled;
|
||||
});
|
||||
},
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
IsCustomAIEnabled = aiHelper.IsAIEnabled;
|
||||
}
|
||||
|
||||
ClipboardHistoryEnabled = IsClipboardHistoryEnabled();
|
||||
GeneratedResponses.Clear();
|
||||
WindowShown?.Invoke();
|
||||
}
|
||||
|
||||
private void HideWindow()
|
||||
{
|
||||
if (app.GetMainWindow() != null)
|
||||
{
|
||||
Windows.Win32.Foundation.HWND hwnd = (Windows.Win32.Foundation.HWND)app.GetMainWindow().GetWindowHandle();
|
||||
Windows.Win32.PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
// Command to select the previous custom format
|
||||
@@ -232,6 +345,12 @@ namespace AdvancedPaste.ViewModels
|
||||
(App.Current as App).GetMainWindow().Close();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void PasteCustom()
|
||||
{
|
||||
_ = PasteCustomFunction(GeneratedResponses[CurrentResponseIndex]);
|
||||
}
|
||||
|
||||
private void SetClipboardContentAndHideWindow(string content)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(content))
|
||||
@@ -239,20 +358,16 @@ namespace AdvancedPaste.ViewModels
|
||||
ClipboardHelper.SetClipboardTextContent(content);
|
||||
}
|
||||
|
||||
if (app.GetMainWindow() != null)
|
||||
{
|
||||
Windows.Win32.Foundation.HWND hwnd = (Windows.Win32.Foundation.HWND)app.GetMainWindow().GetWindowHandle();
|
||||
Windows.Win32.PInvoke.ShowWindow(hwnd, Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_HIDE);
|
||||
}
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
internal void ToPlainTextFunction()
|
||||
internal async void ToPlainText()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
string outputString = MarkdownHelper.PasteAsPlainTextFromClipboard(ClipboardData);
|
||||
string outputString = await ClipboardHelper.GetClipboardTextContent(ClipboardContent);
|
||||
|
||||
SetClipboardContentAndHideWindow(outputString);
|
||||
|
||||
@@ -266,13 +381,24 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal void ToMarkdownFunction(bool pasteAlways = false)
|
||||
internal async void ToMarkdown(bool pasteAlways = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
string outputString = MarkdownHelper.ToMarkdown(ClipboardData);
|
||||
string inputString = string.Empty;
|
||||
|
||||
if (ClipboardHasHtml)
|
||||
{
|
||||
inputString = await ClipboardHelper.GetClipboardHTMLContent(ClipboardContent);
|
||||
}
|
||||
else if (ClipboardHasText)
|
||||
{
|
||||
inputString = await ClipboardHelper.GetClipboardTextContent(ClipboardContent);
|
||||
}
|
||||
|
||||
string outputString = ToMarkdownFunction(inputString);
|
||||
|
||||
SetClipboardContentAndHideWindow(outputString);
|
||||
|
||||
@@ -286,13 +412,20 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal void ToJsonFunction(bool pasteAlways = false)
|
||||
internal string ToMarkdownFunction(string inputHTML)
|
||||
{
|
||||
return MarkdownHelper.ConvertHtmlToMarkdown(inputHTML);
|
||||
}
|
||||
|
||||
internal async void ToJson(bool pasteAlways = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
string jsonText = JsonHelper.ToJsonFromXmlOrCsv(ClipboardData);
|
||||
string inputText = await ClipboardHelper.GetClipboardTextContent(ClipboardContent);
|
||||
|
||||
string jsonText = ToJsonFunction(inputText);
|
||||
|
||||
SetClipboardContentAndHideWindow(jsonText);
|
||||
|
||||
@@ -306,61 +439,313 @@ namespace AdvancedPaste.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal string ToJsonFunction(string inputString, bool pasteAlways = false)
|
||||
{
|
||||
return JsonHelper.ToJsonFromXmlOrCsv(inputString);
|
||||
}
|
||||
|
||||
internal async void AudioToText()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
var fileContent = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
|
||||
string outputText = await AudioToTextFunction(fileContent);
|
||||
ClipboardHelper.SetClipboardTextContent(outputText);
|
||||
|
||||
SetClipboardContentAndHideWindow(outputText);
|
||||
|
||||
if (_userSettings.SendPasteKeyCombination)
|
||||
{
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<string> AudioToTextFunction(DataPackageView fileContent)
|
||||
{
|
||||
var fileList = await fileContent.GetStorageItemsAsync();
|
||||
var outputText = string.Empty;
|
||||
StorageFile file = null;
|
||||
if (fileList.Count > 0)
|
||||
{
|
||||
file = fileList[0] as StorageFile;
|
||||
|
||||
outputText = await aiLocalModelsHelper.DoWhisperInference(file);
|
||||
|
||||
return outputText;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Add error handling
|
||||
Console.WriteLine("Hit error");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<string> AudioToTextFunction(string fileName, int startSeconds, int durationSeconds)
|
||||
{
|
||||
// Get StorageFile from fileName
|
||||
var file = await StorageFile.GetFileFromPathAsync(fileName);
|
||||
|
||||
var outputText = await aiLocalModelsHelper.DoWhisperInference(file, startSeconds, durationSeconds);
|
||||
|
||||
return outputText;
|
||||
}
|
||||
|
||||
internal async Task<string> CustomWithAIFunction(string inputInstructions, string inputContent)
|
||||
{
|
||||
var aiOutput = await Task.Run(() => aiHelper.AIFormatString(inputInstructions, inputContent));
|
||||
|
||||
return aiOutput.Response;
|
||||
}
|
||||
|
||||
internal async void ToFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
// Determine the type of content in the clipboard
|
||||
string fileName = null;
|
||||
|
||||
if (ClipboardHasText)
|
||||
{
|
||||
string clipboardText = await ClipboardContent.GetTextAsync();
|
||||
fileName = await ToFileFunction(clipboardText);
|
||||
}
|
||||
else if (ClipboardHasImage)
|
||||
{
|
||||
SoftwareBitmap softwareBitmap = await ClipboardHelper.GetClipboardImageContent(ClipboardContent);
|
||||
fileName = await ToFileFunction(softwareBitmap);
|
||||
}
|
||||
|
||||
// Set the clipboard data
|
||||
_ = await ClipboardHelper.SetClipboardFile(fileName);
|
||||
HideWindow();
|
||||
|
||||
if (_userSettings.SendPasteKeyCombination)
|
||||
{
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<string> ToFileFunction(string inputContent)
|
||||
{
|
||||
// Create a local file in the temp directory
|
||||
string tempFileName = Path.Combine(Path.GetTempPath(), "clipboard.txt");
|
||||
|
||||
// Write the content to the file
|
||||
await File.WriteAllTextAsync(tempFileName, inputContent);
|
||||
|
||||
return tempFileName;
|
||||
}
|
||||
|
||||
internal async Task<string> ToFileFunction(SoftwareBitmap softwareBitmap)
|
||||
{
|
||||
// Create a local file in the temp directory
|
||||
string tempFileName = Path.Combine(Path.GetTempPath(), "clipboard.png");
|
||||
|
||||
using (var stream = new InMemoryRandomAccessStream())
|
||||
{
|
||||
// Encode the SoftwareBitmap to the stream
|
||||
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
|
||||
encoder.SetSoftwareBitmap(softwareBitmap);
|
||||
await encoder.FlushAsync();
|
||||
|
||||
// Set the stream position to the beginning
|
||||
stream.Seek(0);
|
||||
|
||||
// Create a new file in the temporary directory with a .png extension
|
||||
using (var fileStream = File.Create(tempFileName))
|
||||
{
|
||||
await stream.AsStream().CopyToAsync(fileStream);
|
||||
}
|
||||
}
|
||||
|
||||
return tempFileName;
|
||||
}
|
||||
|
||||
internal async Task<string> GenerateCustomFunction(string inputInstructions)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inputInstructions))
|
||||
// Get what operations are needed from the AI
|
||||
// For whatever operation is returned do that
|
||||
string aiOperationsOutput = await Task.Run(() => aiHelper.GetOperationsFromAI(inputInstructions, ClipboardHasText, ClipboardHasImage, ClipboardHasHtml, ClipboardHasFile, ClipboardHasAudio));
|
||||
|
||||
// Define in loop variables to hold values
|
||||
string currentClipboardText = await ClipboardHelper.GetClipboardTextContent(ClipboardContent);
|
||||
string currentClipboardHTML = await ClipboardHelper.GetClipboardHTMLContent(ClipboardContent);
|
||||
string currentFileName = await ClipboardHelper.GetClipboardFileName(ClipboardContent);
|
||||
SoftwareBitmap currentClipboardImage = null;
|
||||
|
||||
if (ClipboardHasImage)
|
||||
{
|
||||
return string.Empty;
|
||||
currentClipboardImage = await ClipboardHelper.GetClipboardImageContent(ClipboardContent);
|
||||
}
|
||||
|
||||
if (ClipboardData == null || !ClipboardData.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
Logger.LogWarning("Clipboard does not contain text data");
|
||||
return string.Empty;
|
||||
}
|
||||
ClipboardHelper.ClipboardContentFormats returnFormat = ClipboardHelper.ClipboardContentFormats.Invalid;
|
||||
|
||||
string currentClipboardText = await Task.Run(async () =>
|
||||
string[] lines = aiOperationsOutput.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
try
|
||||
foreach (string functionName in OptionsViewModel.FunctionNames)
|
||||
{
|
||||
string text = await ClipboardData.GetTextAsync() as string;
|
||||
return text;
|
||||
if (line.Contains(functionName + "("))
|
||||
{
|
||||
switch (functionName)
|
||||
{
|
||||
case "ToCustomWithAI":
|
||||
// Get the input instructions seen after 'CustomWithAI(' using regex to account for either the " or ' character
|
||||
string pattern = @"CustomWithAI\(['""](.+?)['""]";
|
||||
|
||||
string customInputInstructions = string.Empty;
|
||||
|
||||
Match match = Regex.Match(line, pattern);
|
||||
if (match.Success)
|
||||
{
|
||||
customInputInstructions = match.Groups[1].Value;
|
||||
}
|
||||
|
||||
string result = await CustomWithAIFunction(customInputInstructions, currentClipboardText);
|
||||
|
||||
currentClipboardHTML = result;
|
||||
currentClipboardText = ClipboardHelper.ConvertHTMLToPlainText(currentClipboardHTML);
|
||||
returnFormat = ClipboardHelper.ClipboardContentFormats.HTML;
|
||||
|
||||
break;
|
||||
case "ToJSON":
|
||||
break;
|
||||
case "ToPlainText":
|
||||
break;
|
||||
case "ToMarkdown":
|
||||
break;
|
||||
case "ToFile":
|
||||
if (currentClipboardText != null)
|
||||
{
|
||||
currentFileName = await ToFileFunction(currentClipboardText);
|
||||
}
|
||||
else if (currentClipboardHTML != null)
|
||||
{
|
||||
currentFileName = await ToFileFunction(currentClipboardHTML);
|
||||
}
|
||||
else if (currentClipboardImage != null)
|
||||
{
|
||||
currentFileName = await ToFileFunction(currentClipboardImage);
|
||||
}
|
||||
|
||||
returnFormat = ClipboardHelper.ClipboardContentFormats.File;
|
||||
|
||||
break;
|
||||
case "AudioToText":
|
||||
// Use regex and get the input instructions after AudioToText( and split them by the comma
|
||||
string audioToTextPattern = @"AudioToText\((.+?)\)";
|
||||
|
||||
string audioToTextFileName = string.Empty;
|
||||
int seekSeconds = 0;
|
||||
int maxDurationSeconds = 0;
|
||||
|
||||
Match audioToTextMatch = Regex.Match(line, audioToTextPattern);
|
||||
if (audioToTextMatch.Success)
|
||||
{
|
||||
audioToTextFileName = audioToTextMatch.Groups[1].Value.Split(',')[0];
|
||||
seekSeconds = int.Parse(audioToTextMatch.Groups[1].Value.Split(',')[1], CultureInfo.InvariantCulture);
|
||||
maxDurationSeconds = int.Parse(audioToTextMatch.Groups[1].Value.Split(',')[2], CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
currentClipboardText = await AudioToTextFunction(currentFileName, seekSeconds, maxDurationSeconds);
|
||||
returnFormat = ClipboardHelper.ClipboardContentFormats.Text;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
break; // No need to check other function names for this line
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Couldn't get text from the clipboard. Resume with empty text.
|
||||
}
|
||||
|
||||
var resultSavedClipboardItem = new SavedClipboardItem
|
||||
{
|
||||
Format = returnFormat,
|
||||
};
|
||||
|
||||
// DO return logic with enum
|
||||
switch (returnFormat)
|
||||
{
|
||||
case ClipboardHelper.ClipboardContentFormats.HTML:
|
||||
resultSavedClipboardItem.HTML = currentClipboardHTML;
|
||||
GeneratedResponses.Add(resultSavedClipboardItem);
|
||||
CurrentResponseIndex = GeneratedResponses.Count - 1;
|
||||
return currentClipboardHTML;
|
||||
|
||||
// Other formats not yet supported
|
||||
case ClipboardHelper.ClipboardContentFormats.Image:
|
||||
return "Image not implemented";
|
||||
case ClipboardHelper.ClipboardContentFormats.File:
|
||||
resultSavedClipboardItem.Filename = currentFileName;
|
||||
GeneratedResponses.Add(resultSavedClipboardItem);
|
||||
CurrentResponseIndex = GeneratedResponses.Count - 1;
|
||||
return "Paste as file.";
|
||||
case ClipboardHelper.ClipboardContentFormats.Audio:
|
||||
return "Audio not implemented";
|
||||
case ClipboardHelper.ClipboardContentFormats.Text:
|
||||
resultSavedClipboardItem.Text = currentClipboardText;
|
||||
GeneratedResponses.Add(resultSavedClipboardItem);
|
||||
CurrentResponseIndex = GeneratedResponses.Count - 1;
|
||||
return currentClipboardText;
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
});
|
||||
|
||||
if (string.IsNullOrWhiteSpace(currentClipboardText))
|
||||
{
|
||||
Logger.LogWarning("Clipboard has no usable text data");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var aiResponse = await Task.Run(() => aiHelper.AIFormatString(inputInstructions, currentClipboardText));
|
||||
|
||||
string aiOutput = aiResponse.Response;
|
||||
ApiRequestStatus = aiResponse.ApiRequestStatus;
|
||||
|
||||
GeneratedResponses.Add(aiOutput);
|
||||
CurrentResponseIndex = GeneratedResponses.Count - 1;
|
||||
return aiOutput;
|
||||
}
|
||||
|
||||
internal void PasteCustomFunction(string text)
|
||||
internal async Task<bool> PasteCustomFunction(SavedClipboardItem inItem)
|
||||
{
|
||||
Logger.LogTrace();
|
||||
|
||||
SetClipboardContentAndHideWindow(text);
|
||||
|
||||
if (_userSettings.SendPasteKeyCombination)
|
||||
try
|
||||
{
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
Logger.LogTrace();
|
||||
|
||||
switch (inItem.Format)
|
||||
{
|
||||
case ClipboardHelper.ClipboardContentFormats.HTML:
|
||||
ClipboardHelper.SetClipboardHTMLContent(inItem.HTML);
|
||||
break;
|
||||
case ClipboardHelper.ClipboardContentFormats.Image:
|
||||
break;
|
||||
case ClipboardHelper.ClipboardContentFormats.File:
|
||||
await ClipboardHelper.SetClipboardFile(inItem.Filename);
|
||||
break;
|
||||
case ClipboardHelper.ClipboardContentFormats.Audio:
|
||||
break;
|
||||
case ClipboardHelper.ClipboardContentFormats.Text:
|
||||
ClipboardHelper.SetClipboardTextContent(inItem.Text);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
HideWindow();
|
||||
|
||||
if (_userSettings.SendPasteKeyCombination)
|
||||
{
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal CustomQuery RecallPreviousCustomQuery()
|
||||
@@ -416,5 +801,11 @@ namespace AdvancedPaste.ViewModels
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal void FilterOptionsFromInput(string input)
|
||||
{
|
||||
// Generate event
|
||||
FormatsChanged?.Invoke(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
<value>New profile</value>
|
||||
</data>
|
||||
<data name="ProfilesDescriptionLbl.Text" xml:space="preserve">
|
||||
<value>Create profiles to quickly apply a set of preconfigured variables. Profile variables have precedence over User and System variables.</value>
|
||||
<value>You can create profiles to quickly apply a set of preconfigured 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>Applied variables list shows the current state of the environment, including Profile, User, and System variables.</value>
|
||||
<value>List of applied 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, edit, or remove User and System variables.</value>
|
||||
<value>Add, remove or edit USER and SYSTEM variables</value>
|
||||
</data>
|
||||
<data name="EditItem.Text" xml:space="preserve">
|
||||
<value>Edit</value>
|
||||
|
||||
@@ -47,7 +47,6 @@ 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="520"
|
||||
MinWidth="480"
|
||||
MinHeight="320"
|
||||
mc:Ignorable="d">
|
||||
<Window.SystemBackdrop>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.IO.Abstractions;
|
||||
using System.Threading;
|
||||
using HostsUILib.Helpers;
|
||||
using HostsUILib.Settings;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
@@ -44,8 +45,6 @@ namespace Hosts.Settings
|
||||
// Moved from Settings.UI.Library
|
||||
public HostsEncoding Encoding { get; set; }
|
||||
|
||||
public event EventHandler LoopbackDuplicatesChanged;
|
||||
|
||||
public UserSettings()
|
||||
{
|
||||
_settingsUtils = new SettingsUtils();
|
||||
@@ -59,6 +58,8 @@ namespace Hosts.Settings
|
||||
_watcher = Helper.GetFileWatcher(HostsModuleName, "settings.json", () => LoadSettingsFromJson());
|
||||
}
|
||||
|
||||
public event EventHandler LoopbackDuplicatesChanged;
|
||||
|
||||
private void LoadSettingsFromJson()
|
||||
{
|
||||
lock (_loadingSettingsLock)
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +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 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
|
||||
public interface IHostsService : IDisposable
|
||||
{
|
||||
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="*" MinWidth="150" />
|
||||
<ColumnDefinition Width="256" />
|
||||
<!-- Address -->
|
||||
<ColumnDefinition Width="*" MinWidth="120" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<!-- Comment -->
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- Status -->
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<!-- 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="Center"
|
||||
HorizontalAlignment="Right"
|
||||
GotFocus="Entries_GotFocus"
|
||||
IsOn="{x:Bind Active, Mode=TwoWay}"
|
||||
OffContent=""
|
||||
@@ -705,13 +705,10 @@
|
||||
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="NoWrap" />
|
||||
TextWrapping="Wrap" />
|
||||
</ContentDialog>
|
||||
|
||||
<TeachingTip
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
namespace HostsUILib.Settings
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ 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;
|
||||
@@ -22,14 +23,21 @@ using static HostsUILib.Settings.IUserSettings;
|
||||
|
||||
namespace HostsUILib.ViewModels
|
||||
{
|
||||
public partial class MainViewModel : ObservableObject
|
||||
public partial class MainViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
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;
|
||||
@@ -87,16 +95,10 @@ namespace HostsUILib.ViewModels
|
||||
|
||||
private OpenSettingsFunction _openSettingsFunction;
|
||||
|
||||
public MainViewModel(
|
||||
IHostsService hostService,
|
||||
IUserSettings userSettings,
|
||||
IDuplicateService duplicateService,
|
||||
ILogger logger,
|
||||
OpenSettingsFunction openSettingsFunction)
|
||||
public MainViewModel(IHostsService hostService, IUserSettings userSettings, ILogger logger, OpenSettingsFunction openSettingsFunction)
|
||||
{
|
||||
_hostsService = hostService;
|
||||
_userSettings = userSettings;
|
||||
_duplicateService = duplicateService;
|
||||
|
||||
_hostsService.FileChanged += (s, e) => _dispatcherQueue.TryEnqueue(() => FileChanged = true);
|
||||
_userSettings.LoopbackDuplicatesChanged += (s, e) => ReadHosts();
|
||||
@@ -109,7 +111,8 @@ namespace HostsUILib.ViewModels
|
||||
{
|
||||
entry.PropertyChanged += Entry_PropertyChanged;
|
||||
_entries.Add(entry);
|
||||
_duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
|
||||
|
||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||
}
|
||||
|
||||
public void Update(int index, Entry entry)
|
||||
@@ -123,8 +126,8 @@ namespace HostsUILib.ViewModels
|
||||
existingEntry.Hosts = entry.Hosts;
|
||||
existingEntry.Active = entry.Active;
|
||||
|
||||
_duplicateService.CheckDuplicates(oldAddress, oldHosts);
|
||||
_duplicateService.CheckDuplicates(entry.Address, entry.SplittedHosts);
|
||||
FindDuplicates(oldAddress, oldHosts);
|
||||
FindDuplicates(entry.Address, entry.SplittedHosts);
|
||||
}
|
||||
|
||||
public void DeleteSelected()
|
||||
@@ -132,7 +135,8 @@ namespace HostsUILib.ViewModels
|
||||
var address = Selected.Address;
|
||||
var hosts = Selected.SplittedHosts;
|
||||
_entries.Remove(Selected);
|
||||
_duplicateService.CheckDuplicates(address, hosts);
|
||||
|
||||
FindDuplicates(address, hosts);
|
||||
}
|
||||
|
||||
public void UpdateAdditionalLines(string lines)
|
||||
@@ -165,7 +169,8 @@ namespace HostsUILib.ViewModels
|
||||
var address = entry.Address;
|
||||
var hosts = entry.SplittedHosts;
|
||||
_entries.Remove(entry);
|
||||
_duplicateService.CheckDuplicates(address, hosts);
|
||||
|
||||
FindDuplicates(address, hosts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +213,9 @@ namespace HostsUILib.ViewModels
|
||||
});
|
||||
_readingHosts = false;
|
||||
|
||||
_duplicateService.Initialize(_entries);
|
||||
_tokenSource?.Cancel();
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
FindDuplicates(_tokenSource.Token);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -287,6 +294,12 @@ 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)
|
||||
@@ -313,6 +326,82 @@ 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;
|
||||
@@ -355,5 +444,17 @@ 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.240428000\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<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.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.240428000\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.240428000\build\native\Microsoft.WindowsAppSDK.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')" />
|
||||
</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.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'))" />
|
||||
<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'))" />
|
||||
</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.240428000" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.5.240311000" 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,7 +65,6 @@ 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;
|
||||
@@ -147,7 +146,6 @@ private:
|
||||
void OnMouseTimer();
|
||||
|
||||
void DetectShake();
|
||||
bool KeyboardInputCanActivate();
|
||||
|
||||
void StartSonar();
|
||||
void StopSonar();
|
||||
@@ -354,7 +352,7 @@ void SuperSonar<D>::OnSonarKeyboardInput(RAWINPUT const& input)
|
||||
break;
|
||||
|
||||
case SonarState::ControlUp1:
|
||||
if (pressed && KeyboardInputCanActivate())
|
||||
if (pressed)
|
||||
{
|
||||
auto now = GetTickCount64();
|
||||
auto doubleClickInterval = now - m_lastKeyTime;
|
||||
@@ -440,12 +438,6 @@ 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)
|
||||
{
|
||||
@@ -770,7 +762,6 @@ 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;
|
||||
@@ -800,7 +791,6 @@ 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,7 +18,6 @@ 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
|
||||
@@ -26,7 +25,6 @@ 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,7 +14,6 @@ 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";
|
||||
@@ -238,15 +237,6 @@ 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);
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,452 +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 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.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.
|
Before Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 215 KiB |
@@ -0,0 +1,229 @@
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MouseJumpUI.Common.Models.Drawing;
|
||||
using MouseJumpUI.Models.Drawing;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
|
||||
namespace MouseJumpUI.UnitTests.Models.Drawing;
|
||||
|
||||
[TestClass]
|
||||
public static class RectangleInfoTests
|
||||
@@ -23,30 +23,30 @@ public static class RectangleInfoTests
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public RectangleInfo Rectangle { get; }
|
||||
public RectangleInfo Rectangle { get; set; }
|
||||
|
||||
public PointInfo Point { get; }
|
||||
public PointInfo Point { get; set; }
|
||||
|
||||
public RectangleInfo ExpectedResult { get; }
|
||||
public RectangleInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// zero-sized
|
||||
yield return new object[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
|
||||
yield return new[] { new TestCase(new(0, 0, 0, 0), new(0, 0), new(0, 0, 0, 0)), };
|
||||
|
||||
// zero-origin
|
||||
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)), };
|
||||
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)), };
|
||||
|
||||
// non-zero origin
|
||||
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)), };
|
||||
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)), };
|
||||
|
||||
// negative result
|
||||
yield return new object[] { new TestCase(new(0, 0, 1000, 1200), new(300, 300), new(-200, -300, 1000, 1200)), };
|
||||
yield return new[] { 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; }
|
||||
public RectangleInfo Inner { get; set; }
|
||||
|
||||
public RectangleInfo Outer { get; }
|
||||
public RectangleInfo Outer { get; set; }
|
||||
|
||||
public RectangleInfo ExpectedResult { get; }
|
||||
public RectangleInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// already inside - obj fills bounds exactly
|
||||
yield return new object[]
|
||||
yield return new[]
|
||||
{
|
||||
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 object[]
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 0, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new object[]
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(100, 0, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new object[]
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(0, 100, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new object[]
|
||||
yield return new[]
|
||||
{
|
||||
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 object[]
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(-50, -50, 100, 100), new(0, 0, 200, 200), new(0, 0, 100, 100)),
|
||||
};
|
||||
yield return new object[]
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(250, -50, 100, 100), new(0, 0, 200, 200), new(100, 0, 100, 100)),
|
||||
};
|
||||
yield return new object[]
|
||||
yield return new[]
|
||||
{
|
||||
new TestCase(new(-50, 250, 100, 100), new(0, 0, 200, 200), new(0, 100, 100, 100)),
|
||||
};
|
||||
yield return new object[]
|
||||
yield return new[]
|
||||
{
|
||||
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.Common.Models.Drawing;
|
||||
using MouseJumpUI.Models.Drawing;
|
||||
|
||||
namespace MouseJumpUI.UnitTests.Common.Models.Drawing;
|
||||
namespace MouseJumpUI.UnitTests.Drawing;
|
||||
|
||||
[TestClass]
|
||||
public static class SizeInfoTests
|
||||
@@ -23,28 +23,28 @@ public static class SizeInfoTests
|
||||
this.ExpectedResult = expectedResult;
|
||||
}
|
||||
|
||||
public SizeInfo Obj { get; }
|
||||
public SizeInfo Obj { get; set; }
|
||||
|
||||
public SizeInfo Bounds { get; }
|
||||
public SizeInfo Bounds { get; set; }
|
||||
|
||||
public SizeInfo ExpectedResult { get; }
|
||||
public SizeInfo ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// identity tests
|
||||
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)), };
|
||||
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)), };
|
||||
|
||||
// general tests
|
||||
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)), };
|
||||
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)), };
|
||||
|
||||
// scale to fit width
|
||||
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), new(2048, 1536)), };
|
||||
|
||||
// scale to fit height
|
||||
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), new(2048, 1536)), };
|
||||
yield return new[] { 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; }
|
||||
public SizeInfo Obj { get; set; }
|
||||
|
||||
public SizeInfo Bounds { get; }
|
||||
public SizeInfo Bounds { get; set; }
|
||||
|
||||
public decimal ExpectedResult { get; }
|
||||
public decimal ExpectedResult { get; set; }
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetTestCases()
|
||||
{
|
||||
// identity tests
|
||||
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), };
|
||||
yield return new[] { new TestCase(new(512, 384), new(512, 384), 1), };
|
||||
yield return new[] { new TestCase(new(1024, 768), new(1024, 768), 1), };
|
||||
|
||||
// general tests
|
||||
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), };
|
||||
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), };
|
||||
|
||||
// scale to fit width
|
||||
yield return new object[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
|
||||
yield return new[] { new TestCase(new(512, 384), new(2048, 3072), 4), };
|
||||
|
||||
// scale to fit height
|
||||
yield return new object[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
|
||||
yield return new[] { new TestCase(new(512, 384), new(4096, 1536), 4), };
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -28,13 +28,6 @@
|
||||
</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>
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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;
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user