Compare commits

..

2 Commits

Author SHA1 Message Date
Stefan Markovic
9518dece7f Align target framework version 2024-05-22 11:58:06 +02:00
Craig Loewen
8dc95c991e Advanced Paste v2 improvements
---------

    Co-authored-by: Stefan Markovic <stefan@janeasystems.com>
    Co-authored-by: Niels Laute <niels.laute@live.nl>
    Co-authored-by: Jordi Adoumie <98557455+joadoumie@users.noreply.github.com>
    Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
    Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
2024-05-21 10:50:41 -04:00
309 changed files with 5060 additions and 7017 deletions

View File

@@ -9,7 +9,7 @@
]
},
"xamlstyler.console": {
"version": "3.2404.2",
"version": "3.2206.4",
"commands": [
"xstyler"
]

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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: |

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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>

View File

@@ -1333,7 +1333,7 @@ EXHIBIT A -Mozilla Public License.
- Microsoft.Windows.CsWinRT 2.0.4
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
- Microsoft.Windows.SDK.Contracts 10.0.19041.1
- Microsoft.WindowsAppSDK 1.5.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

View File

@@ -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
View File

@@ -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 wouldnt be nearly what it is today without your help filing bugs, updating documentation, guiding the design, or writing features. We want to say thank you and take time to recognize your work. Month by month, you directly help make PowerToys a better piece of software.

View File

@@ -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

View File

@@ -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).

View File

@@ -2,7 +2,7 @@
## Through runner
- The settings process communicates changes in the UI to most modules using the runner through delegates.
- More details on this are mentioned in [`runner-ipc.md`](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.

View File

@@ -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 |

View File

@@ -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>

View File

@@ -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>

View File

@@ -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")

View File

@@ -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());

View File

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

View File

@@ -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;
}
}
}

View File

@@ -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"]
]
}
};

View File

@@ -1,3 +0,0 @@
export const customTokenColors = [
{token: 'custom-gitignore.negation', foreground: 'c00ce0'}
];

View File

@@ -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']

View File

@@ -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>

View File

@@ -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());
}
}

View File

@@ -49,7 +49,6 @@ namespace winrt::PowerToys::GPOWrapper::implementation
static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue();
static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue();
static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue();
static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue();
};
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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";

View File

@@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.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>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (c) Microsoft Corporation.
Licensed under the MIT License. -->
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.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>

View File

@@ -0,0 +1 @@
*.onnx

View File

@@ -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.

View File

@@ -2,11 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using 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; }
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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,
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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"

View File

@@ -2,6 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.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)

View File

@@ -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"

View File

@@ -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()

View File

@@ -115,6 +115,7 @@
ItemClick="PasteOptionsListView_ItemClick"
ItemContainerTransitions="{x:Null}"
ItemsSource="{x:Bind pasteFormats, Mode=OneWay}"
ContainerContentChanging="PasteFormatListContentChanging"
SelectionMode="None"
TabIndex="1">
<ListView.ItemTemplate>

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.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));
});
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -9,7 +9,5 @@ namespace AdvancedPaste.Settings
public bool ShowCustomPreview { get; }
public bool SendPasteKeyCombination { get; }
public bool CloseAfterLosingFocus { get; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -13,5 +13,7 @@ namespace AdvancedPaste.Models
public string Name { get; set; }
public PasteFormats Format { get; set; }
public bool Enabled { get; set; }
}
}

View File

@@ -10,5 +10,7 @@ namespace AdvancedPaste.Models
Markdown,
Json,
Custom,
AudioToText,
File,
}
}

View File

@@ -127,7 +127,7 @@
<value>Clipboard data is not text</value>
</data>
<data name="OpenAINotConfigured" xml:space="preserve">
<value>To custom with AI 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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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>();

View File

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

View File

@@ -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)

View File

@@ -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;
}
}
}
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -412,23 +412,23 @@
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Entry">
<Grid
Margin="0"
AutomationProperties.Name="{x:Bind Address, Mode=OneWay}"
Background="Transparent"
ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" 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

View File

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

View File

@@ -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;
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.5.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>

View File

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

View File

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

View File

@@ -65,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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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}");
}
}
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -1,74 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MouseJumpUI.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

View File

@@ -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");
}
}
}

View File

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

View File

@@ -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)),
};

View File

@@ -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]

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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