[New PowerToy] Add Screen Ruler module for measuring screen contents (#19701)

* [MeasureTool] initial commit

* [chore] clean up needless WindowsTargetPlatformVersion overrides from projects

* [MeasureTool] initial implementation

* Fix build errors

* Update vsconfig for needed Windows 10 SDK versions

* fix spellchecker

* another spellcheck fix

* more spellcheck errors

* Fix measurement being off by 1 on both ends

* UI fixes

* Add feet to crosses

* Remove anti-aliasing, as it's creating artifacts

* Use pixel tolerance from settings

* Tooltip updates

* Restore antialiasing to draw the tooltip

* remove comment for spell check

* Updated icons

* Icon updates

* Improve measurement accuracy and display

* Fix spellchecker

* Add less precise drawing on continuous warning

* Add setting for turning cross feet on

* Swap LMB/RMB for interaction

* Uncheck active tool's RadioButton when it exits

* activation hotkey toggles UI instead of just launching it

* track runner process and exit when it exits

* add proj ref

* toolbar is interactive during measurements

* always open toolbar on the main display

* refactor colors

* refactor edge detection & overlay ui

* refactor overlay ui even more

* simplify state structs

* multimonitor preparation: eliminate global state

* prepare for merge

* spelling

* proper thread termination + minor fixes

* multimonitor: launch tools on all monitors

* multimonitor support: track cursor position

* spell

* fix powertoys!

* ScreenSize -> Box

* add shadow effect for textbox

* spell

* fix debug mode

* dynamic text box size based on text layout metrics

* add mouse wheel to adjust pixel tolerance + per channel detection algorithm setting

* spelling

* fix per channel distance calculations

* update installer deps + spelling

* tool activation telemetry

* update assets and try to fix build

* use × instead of x

* allow multiple measurements with bounds tool with shift-click

* move #define DEBUG_OVERLAY in an appropriate space

* spell-checked

* update issue template + refactor text box drawing

* implement custom renderer and make × semiopaque

* spelling

* pass dpiScale to x renderer

* add sse2neon license

* update OOBE

* move license to NOTICE

* appropriate module preview image

* localization for AutomationPeer

* increase default pixel tolerance from 5 to 30

* add PowerToys.MeasureToolUI.exe to bugreport

* explicitly set texture dims

* clarify continuous capture description

* fix a real spelling error!

* cleanup

* clean up x2

* debug texture

* fix texture access

* fix saveasbitmap

* improve sum of all channel diffs method score calc

* optimize

* ContinuousCapture is enabled by default to avoid confusion

* build fix

* draw captured screen in a non continuous mode

* cast a spell...

* merge fix

* disable stroboscopic effect

* split global/perScreen measure state and minor improvements

* spelling

* fix comment

* primary monitor debug also active for the bounds tool

* dpi from rt for custom renderer

* add comment

* fix off by 1

* make backround convertion success for non continuous mode non-essential

* fix spelling

* overlay window covers taskbar

* fix CI

* revert taskbar covering

* fix CI

* fix ci again

* fix 2

* fix ci

* CI fix

* fix arm ci

* cleanup cursor convertion between coordinate spaces

* fix spelling

* Fix signing

* Fix MeasureToolUI version

* Fix core version

* fix race condition in system internals which happens during concurrent d3d/d2d resource creation

Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
This commit is contained in:
Andrey Nekrasov 2022-08-27 02:17:20 +03:00 committed by GitHub
parent 2274e0c67d
commit 78d65a87cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 5319 additions and 337 deletions

View File

@ -49,6 +49,7 @@ body:
- PowerAccent
- PowerRename
- PowerToys Run
- Screen ruler
- Shortcut Guide
- STL Thumbnail
- SVG Preview

View File

@ -40,6 +40,7 @@ body:
- PowerAccent
- PowerRename
- PowerToys Run
- Screen Ruler
- Shortcut Guide
- SVG Preview
- SVG Thumbnail

View File

@ -66,7 +66,6 @@ appdata
APPICON
appid
appium
APPLASTZONE
Applets
Applicationcan
applicationframehost
@ -121,7 +120,6 @@ AUTHN
AUTHZ
Autofill
autogenerate
autogenerated
AUTOHIDE
AUTOMATIONPROPERTIES
Autorun
@ -154,10 +152,13 @@ Bicubic
bigbar
bigobj
binlog
BITMAPFILEHEADER
bitmapimage
BITMAPINFO
BITMAPINFOHEADER
Bitmaps
bitmask
BITSPIXEL
bla
Blockquotes
blog
@ -266,6 +267,7 @@ CMINVOKECOMMANDINFOEX
CMock
CMONITORS
cmp
cmpgt
cmyk
cnt
Cocklebiddy
@ -309,6 +311,7 @@ CONFLICTINGMODIFIERSHORTCUT
CONOUT
Consolas
constexpr
consts
contentdialog
contentfiles
CONTEXTHELP
@ -368,13 +371,14 @@ customaction
CUSTOMACTIONTEST
cvd
CVirtual
cvtepu
cvtsi
cwchar
cwd
cxfksword
CXSMICON
CXVIRTUALSCREEN
cxxopts
Cxxx
cyberrex
Cyrl
CYSMICON
@ -383,6 +387,8 @@ cziplib
Dac
dacl
damienleroy
DAffine
DAFFINETRANSFORM
Danmarkshavn
DARKPURPLE
DARKTEAL
@ -396,11 +402,15 @@ Dbg
Dbghelp
DBLCLKS
DBLEPSILON
DCapture
DCBA
DCOM
dcomp
dcompi
DComposition
DDevice
ddf
DDxgi
Deact
debian
debugbreak
@ -463,6 +473,7 @@ dllexport
dllhost
dllmain
dlls
Dmap
DNLEN
Dns
doctype
@ -483,6 +494,7 @@ dreamsofameaningfullife
drivedetectionwarning
dshow
dst
DTo
dutil
DVASPECT
DVASPECTINFO
@ -528,6 +540,7 @@ ekus
elif
elseif
eltociear
emmintrin
Emoji
emptyrecyclebin
ENABLEDPOPUP
@ -544,6 +557,7 @@ enum
EOAC
EOL
epicgames
epu
Eqn
ERASEBKGND
EREOF
@ -618,8 +632,6 @@ finalizer
findfast
Firefox
FIXEDFILEINFO
FLASHZONES
FLASHZONESONQUICKSWITCH
flt
flyout
fmtlib
@ -641,7 +653,6 @@ Functiondiscoverykeys
Futuna
fwlink
fwrite
fxcop
FZE
gabime
GAC
@ -666,12 +677,15 @@ GETMINMAXINFO
GETSTATE
GETTEXT
GETTEXTLENGTH
GHND
github
githubusercontent
globals
GMEM
GNumber
google
GPTR
gpu
GSM
gtm
gui
@ -710,6 +724,7 @@ helptext
Heure
HEVC
hfile
HGFE
hglobal
hhk
HHmmss
@ -756,6 +771,7 @@ hotlight
hotspot
Hovd
HPAINTBUFFER
HPALETTE
HRAWINPUT
hread
HREDRAW
@ -770,6 +786,7 @@ hsl
hstring
hsv
htcfreek
HTCLIENT
HTHUMBNAIL
HTTRANSPARENT
HValue
@ -811,6 +828,7 @@ IDD
IDelayed
IDesktop
IDictionary
IDirect
IDirectory
IDispatch
IDispatcher
@ -819,6 +837,7 @@ idl
IDLIST
IDOn
IDR
IDrawing
IDrive
idx
IDXGI
@ -847,6 +866,7 @@ IFormat
IFormatter
ifstream
IGraph
IGraphics
iid
IImage
Iindex
@ -1100,7 +1120,6 @@ LMEM
LMENU
lnk
LOADLIBRARYASDATAFILE
LOADSTRING
LOBYTE
LOCALAPPDATA
LOCALDISPLAY
@ -1123,6 +1142,7 @@ lookbehind
lowlevel
LOWORD
lparam
LPBITMAPINFOHEADER
LPBYTE
LPCITEMIDLIST
LPCMINVOKECOMMANDINFO
@ -1237,6 +1257,7 @@ MINIMIZEEND
MINIMIZESTART
miniz
minlevel
minmax
MINORVERSION
Miracast
Mishkeegogamang
@ -1265,11 +1286,9 @@ MOUSEACTIVATE
MOUSEHWHEEL
MOUSEINPUT
MOUSEMOVE
MOUSESWITCH
MOUSEWHEEL
MOVESIZEEND
MOVESIZESTART
MOVEWINDOWS
mozilla
mpmc
MRM
@ -1366,6 +1385,7 @@ NOASYNC
NOCLOSEPROCESS
NOCOPYBITS
nodeca
nodiscard
nodoc
noexcept
NOINHERITLAYOUT
@ -1638,7 +1658,6 @@ QUERYENDSESSION
QUERYOPEN
QUEUESYNC
Quickime
QUICKLAYOUTSWITCH
QUNS
qwertyuiopasdfghjklzxcvbnm
qword
@ -1671,7 +1690,7 @@ rects
recyclebin
redirectedfrom
Redist
Redistributable
redistributable
reencode
reencoded
refactor
@ -1711,7 +1730,6 @@ Resizable
resizers
resmimetype
RESOURCEID
RESTORESIZE
RESTORETOMAXIMIZED
restrictedcapabilities
restrictederrorinfo
@ -1739,6 +1757,7 @@ roadmap
robmensching
Roboto
rohanrdy
Roolr
roslyn
Rothera
roundf
@ -1812,6 +1831,7 @@ SETTINGCHANGE
settingsheader
settingshotkeycontrol
SETWORKAREA
setzero
sfgao
SFGAOF
SFP
@ -1827,7 +1847,6 @@ SHELLEXECUTEINFOW
shellscalingapi
SHFILEINFO
SHGFI
SHIFTDRAG
Shl
shldisp
shlobj
@ -1916,6 +1935,7 @@ srme
srre
srw
srwlock
sse
ssf
ssh
sstream
@ -1959,6 +1979,7 @@ stoull
strcmp
streampos
strftime
strikethrough
Stringified
Stringify
STRINGIZE
@ -1979,6 +2000,7 @@ subkey
SUBLANG
submenu
subquery
subresource
substr
Sul
Superbar
@ -2031,6 +2053,7 @@ taskkill
tasklist
taskschd
tchar
Tcollab
tcscpy
TCustom
tdbuild
@ -2043,7 +2066,6 @@ Tenggara
testcase
testhost
testprocess
testzones
TEXCOORD
textblock
TEXTINCLUDE
@ -2112,6 +2134,7 @@ uefi
UHash
UIA
uid
UIEx
uint
uintptr
UIPI
@ -2129,6 +2152,7 @@ undef
UNDNAME
unescape
Unicast
UNICODETEXT
Unindent
Uninitialize
uninstall
@ -2177,16 +2201,20 @@ Uvs
uwp
uxtheme
UYVY
vabdq
validmodulename
Vanara
vcamp
vcdl
vcgtq
VCINSTALLDIR
vcm
Vcpkg
VCRT
vcvars
VDesktop
vdi
vdupq
vec
VERBSONLY
VERBW
@ -2196,6 +2224,8 @@ VERSIONINFO
Versioning
vformat
VFT
vget
vgetq
vid
VIDCAP
videoconference
@ -2214,15 +2244,21 @@ visualstudio
viter
VKey
VKTAB
vmovl
vorrq
VOS
vpaddlq
Vpn
vqsubq
VREDRAW
vreinterpretq
VSC
VSCBD
vscdb
vscode
vsconfig
VSCROLL
vsetq
vsonline
vstemplate
VSTHRD
@ -2241,11 +2277,13 @@ wchar
WClass
wcout
wcscat
wcschr
wcscmp
wcscpy
wcslen
wcsncmp
wcsnicmp
wcsstr
wdp
wdupenv
weakme
@ -2307,6 +2345,7 @@ winsdkver
winspool
WINTHRESHOLD
winui
winuiex
winxamlmanager
wistd
withinrafael
@ -2383,6 +2422,7 @@ XLoc
XNamespace
XOffset
xpath
XPixel
XResource
xsi
XStr
@ -2403,10 +2443,7 @@ ZEROINIT
ZIndex
zipfile
zonable
ZONECOLOR
ZONEHIGHLIGHTCOLOR
zoneset
ZONESETCHANGE
Zoneszonabletester
Zonev
zzz

View File

@ -102,6 +102,11 @@
"modules\\launcher\\Plugins\\WebSearch\\Community.PowerToys.Run.Plugin.WebSearch.dll",
"modules\\launcher\\Plugins\\WindowsTerminal\\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.dll",
"modules\\MeasureTool\\PowerToys.MeasureToolModuleInterface.dll",
"modules\\MeasureTool\\PowerToys.MeasureToolCore.dll",
"modules\\MeasureTool\\PowerToys.MeasureToolUI.dll",
"modules\\MeasureTool\\PowerToys.MeasureToolUI.exe",
"modules\\MouseUtils\\PowerToys.FindMyMouse.dll",
"modules\\MouseUtils\\PowerToys.MouseHighlighter.dll",
"modules\\MouseUtils\\PowerToys.MousePointerCrosshairs.dll",

View File

@ -279,6 +279,22 @@ jobs:
configuration: $(BuildConfiguration)
maximumCpuCount: true
- task: VSBuild@1
displayName: Publish Measure Tool UI for Packaging
inputs:
solution: 'src/modules/MeasureTool/MeasureToolUI/MeasureToolUI.csproj'
vsVersion: 17.0
# The arguments should be the same as the ones for Settings; make sure they are.
msbuildArgs: >-
/target:Publish
/p:Configuration=$(BuildConfiguration);Platform=$(BuildPlatform);AppxBundle=Never
/p:VCRTForwarders-IncludeDebugCRT=false
/p:PowerToysRoot=$(Build.SourcesDirectory)
/p:PublishProfile=InstallationPublishProfile.pubxml
platform: $(BuildPlatform)
configuration: $(BuildConfiguration)
maximumCpuCount: true
- task: VSBuild@1
displayName: Build PowerToysSetupCustomActions DLL # This dll needs to be build and signed before building the MSI.
inputs:

View File

@ -6,7 +6,8 @@
"Microsoft.VisualStudio.Workload.NativeDesktop",
"Microsoft.VisualStudio.Workload.ManagedDesktop",
"Microsoft.VisualStudio.Workload.Universal",
"Microsoft.VisualStudio.Component.Windows10SDK.18362",
"Microsoft.VisualStudio.Component.Windows10SDK.19041",
"Microsoft.VisualStudio.Component.Windows10SDK.20348",
"Microsoft.VisualStudio.ComponentGroup.UWP.VC",
"Microsoft.VisualStudio.Component.VC.Runtimes.x86.x64.Spectre",
"Microsoft.VisualStudio.Component.VC.ATL.Spectre"

View File

@ -12,14 +12,14 @@
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|ARM64">
<Configuration>Debug</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|ARM64">
<Configuration>Release</Configuration>
<Platform>ARM64</Platform>
</ProjectConfiguration>
</ItemGroup>
<!-- Props that should be disabled while building on CI server -->
@ -32,11 +32,12 @@
<!-- C++ source compile-specific things for all configurations -->
<PropertyGroup>
<VcpkgEnabled>false</VcpkgEnabled>
<ExternalIncludePath>$(MSBuildThisFileFullPath)\..\deps\;$(ExternalIncludePath)</ExternalIncludePath>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<WarningLevel>Level3</WarningLevel>
<DisableAnalyzeExternal >true</DisableAnalyzeExternal>
<ExternalWarningLevel>TurnOffAllWarnings</ExternalWarningLevel>
@ -83,8 +84,10 @@
</ItemDefinitionGroup>
<!-- Global props -->
<PropertyGroup Label="Globals" Condition="'$(OverrideWindowsTargetPlatformVersion)'!='True'">
<PropertyGroup Label="Globals"
Condition="'$(OverrideWindowsTargetPlatformVersion)'!='True'">
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<TargetPlatformVersion>10.0.19041.0</TargetPlatformVersion>
</PropertyGroup>
<!-- Props that are constant for both Debug and Release configurations -->
@ -92,14 +95,17 @@
<PlatformToolset Condition="'$(OverridePlatformToolset)'!='True'">v143</PlatformToolset>
<IntDir>$(SolutionDir)$(Platform)\$(Configuration)\obj\$(ProjectName)\</IntDir>
<CharacterSet>Unicode</CharacterSet>
<DesktopCompatible>true</DesktopCompatible>
</PropertyGroup>
<!-- Debug/Release props -->
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<PropertyGroup Condition="'$(Configuration)'=='Debug'"
Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<PropertyGroup Condition="'$(Configuration)'=='Release'"
Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>

View File

@ -6,6 +6,7 @@ This software incorporates material from third parties.
- ImageResizer
- PowerToys Run
- Installer/Runner
- Measure tool
## Utility: Color Picker
@ -274,3 +275,26 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
## Utility: Measure tool
### sse2neon
We adopted some functions from it.
**Source**: https://github.com/DLTcollab/sse2neon
sse2neon is freely redistributable under the MIT License.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -284,6 +284,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\process_path.h = src\common\utils\process_path.h
src\common\utils\registry.h = src\common\utils\registry.h
src\common\utils\resources.h = src\common\utils\resources.h
src\common\utils\serialized.h = src\common\utils\serialized.h
src\common\utils\string_utils.h = src\common\utils\string_utils.h
src\common\utils\timeutil.h = src\common\utils\timeutil.h
src\common\utils\UnhandledExceptionHandler.h = src\common\utils\UnhandledExceptionHandler.h
@ -433,6 +434,19 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerOCRModuleInterface", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.History", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.History\Microsoft.PowerToys.Run.Plugin.History.csproj", "{212AD910-8488-4036-BE20-326931B75FB2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MeasureTool", "MeasureTool", "{7AC943C9-52E8-44CF-9083-744D8049667B}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.MeasureToolCore", "src\modules\MeasureTool\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj", "{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}"
ProjectSection(ProjectDependencies) = postProject
{6955446D-23F7-4023-9BB3-8657F904AF99} = {6955446D-23F7-4023-9BB3-8657F904AF99}
{CABA8DFB-823B-4BF2-93AC-3F31984150D9} = {CABA8DFB-823B-4BF2-93AC-3F31984150D9}
{CC6E41AC-8174-4E8A-8D22-85DD7F4851DF} = {CC6E41AC-8174-4E8A-8D22-85DD7F4851DF}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MeasureToolModuleInterface", "src\modules\MeasureTool\MeasureToolModuleInterface\MeasureToolModuleInterface.vcxproj", "{92C39820-9F84-4529-BC7D-22AAE514D63B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MeasureToolUI", "src\modules\MeasureTool\MeasureToolUI\MeasureToolUI.csproj", "{515554D1-D004-4F7F-A107-2211FC0F6B2C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@ -1734,6 +1748,48 @@ Global
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x64.Build.0 = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x86.ActiveCfg = Release|x64
{212AD910-8488-4036-BE20-326931B75FB2}.Release|x86.Build.0 = Release|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|ARM64.ActiveCfg = Debug|arm64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|ARM64.Build.0 = Debug|arm64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x64.ActiveCfg = Debug|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x64.Build.0 = Debug|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x86.ActiveCfg = Debug|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Debug|x86.Build.0 = Debug|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|ARM64.ActiveCfg = Release|arm64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|ARM64.Build.0 = Release|arm64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x64.ActiveCfg = Release|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x64.Build.0 = Release|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x86.ActiveCfg = Release|x64
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A}.Release|x86.Build.0 = Release|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|ARM64.Build.0 = Debug|ARM64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x64.ActiveCfg = Debug|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x64.Build.0 = Debug|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x86.ActiveCfg = Debug|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Debug|x86.Build.0 = Debug|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|ARM64.ActiveCfg = Release|ARM64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|ARM64.Build.0 = Release|ARM64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x64.ActiveCfg = Release|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x64.Build.0 = Release|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x86.ActiveCfg = Release|x64
{92C39820-9F84-4529-BC7D-22AAE514D63B}.Release|x86.Build.0 = Release|x64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.ActiveCfg = Debug|arm64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.Build.0 = Debug|arm64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|ARM64.Deploy.0 = Debug|arm64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.ActiveCfg = Debug|x64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.Build.0 = Debug|x64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x64.Deploy.0 = Debug|x64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x86.ActiveCfg = Debug|x86
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x86.Build.0 = Debug|x86
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Debug|x86.Deploy.0 = Debug|x86
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.ActiveCfg = Release|arm64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.Build.0 = Release|arm64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|ARM64.Deploy.0 = Release|arm64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.ActiveCfg = Release|x64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.Build.0 = Release|x64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x64.Deploy.0 = Release|x64
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x86.ActiveCfg = Release|x86
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x86.Build.0 = Release|x86
{515554D1-D004-4F7F-A107-2211FC0F6B2C}.Release|x86.Deploy.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -1881,6 +1937,10 @@ Global
{25C91A4E-BA4E-467A-85CD-8B62545BF674} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5}
{6AB6A2D6-F859-4A82-9184-0BD29C9F07D1} = {A50C70A6-2DA0-4027-B90E-B1A40755A8A5}
{212AD910-8488-4036-BE20-326931B75FB2} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
{7AC943C9-52E8-44CF-9083-744D8049667B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{54A93AF7-60C7-4F6C-99D2-FBB1F75F853A} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{92C39820-9F84-4529-BC7D-22AAE514D63B} = {7AC943C9-52E8-44CF-9083-744D8049667B}
{515554D1-D004-4F7F-A107-2211FC0F6B2C} = {7AC943C9-52E8-44CF-9083-744D8049667B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@ -85,7 +85,7 @@ This is a lighter release, with a shorter development cycle and focused on stabi
- There are reports of users who are [unable to open the Settings window](https://github.com/microsoft/PowerToys/issues/18015). This is being caused by incompatibilities with some applications (RTSS RivaTuner Statistics Server and MSI AfterBurner are known examples of this). If you're affected by this, please check the linked issue to verify if any of the presented solutions works for you.
### General
- Upgraded the Windows App SDK runtimes to 1.1.2.
- Upgraded the Windows App SDK runtimes to 1.1.4.
- The new Windows 11 context menu entries are now correctly added to Windows 11 dev channel insider builds. (This was a hotfix for 0.60)
- The old context menu entries are shown alongside the new Windows 11 context menu entries to be compatible with software that overrides the Windows 11 context menu behavior. (This was a hotfix for 0.60)
- Consolidated C# language version across the solution. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!

View File

@ -14,6 +14,7 @@
<?define AwakeProjectName="Awake"?>
<?define MouseUtilsProjectName="MouseUtils"?>
<?define AlwaysOnTopProjectName="AlwaysOnTop"?>
<?define MeasureToolProjectName="MeasureTool"?>
<?define RepoDir="$(var.ProjectDir)..\..\" ?>
<?if $(var.Platform) = x64?>
@ -50,11 +51,11 @@
<?define SettingsV2Files=Ijwhost.dll;ColorCode.Core.dll;ColorCode.WinUI.dll;CommunityToolkit.Common.dll;CommunityToolkit.WinUI.dll;CommunityToolkit.WinUI.UI.Controls.Core.dll;CommunityToolkit.WinUI.UI.Controls.DataGrid.dll;CommunityToolkit.WinUI.UI.Controls.Input.dll;CommunityToolkit.WinUI.UI.Controls.Layout.dll;CommunityToolkit.WinUI.UI.Controls.Markdown.dll;CommunityToolkit.WinUI.UI.Controls.Media.dll;CommunityToolkit.WinUI.UI.Controls.Primitives.dll;CommunityToolkit.WinUI.UI.dll;icon.ico;Microsoft.Graphics.Canvas.Interop.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Windows.ApplicationModel.DynamicDependency.Projection.dll;Microsoft.Windows.ApplicationModel.Resources.Projection.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Power.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WinUI.dll;Microsoft.Xaml.Interactions.dll;Microsoft.Xaml.Interactivity.dll;PowerToys.Interop.dll;PowerToys.ManagedCommon.dll;System.Management.dll;PowerToys.ManagedTelemetry.dll;PowerToys.Settings.deps.json;PowerToys.Settings.dll;PowerToys.Settings.exe;PowerToys.Settings.runtimeconfig.json;PowerToys.Settings.UI.Lib.dll;resources.pri;System.IO.Abstractions.dll;System.Text.Json.dll;WinRT.Runtime.dll;Microsoft.Graphics.Canvas.dll;Microsoft.WindowsAppRuntime.Bootstrap.dll;CoreMessagingXP.dll;dcompi.dll;dwmcorei.dll;DwmSceneI.dll;DWriteCore.dll;marshal.dll;Microsoft.DirectManipulation.dll;Microsoft.InputStateManager.dll;Microsoft.Internal.FrameworkUdk.dll;Microsoft.UI.Composition.OSSupport.dll;Microsoft.UI.Input.dll;Microsoft.UI.Windowing.Core.dll;Microsoft.UI.Xaml.Controls.dll;Microsoft.UI.Xaml.Controls.pri;Microsoft.ui.xaml.dll;Microsoft.UI.Xaml.Internal.dll;Microsoft.UI.Xaml.Phone.dll;Microsoft.ui.xaml.resources.19h1.dll;Microsoft.ui.xaml.resources.common.dll;Microsoft.Web.WebView2.Core.dll;Microsoft.Windows.ApplicationModel.Resources.dll;Microsoft.Windows.AppNotifications.Projection.dll;Microsoft.Windows.PushNotifications.Projection.dll;Microsoft.Windows.System.Projection.dll;Microsoft.WindowsAppRuntime.dll;Microsoft.WindowsAppRuntime.Insights.Resource.dll;Microsoft.WindowsAppRuntime.Release.Net.dll;MRM.dll;PushNotificationsLongRunningTask.ProxyStub.dll;WindowsAppRuntime.png;WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll;WinUIEdit.dll;wuceffectsi.dll?>
<?define SettingsV2AssetsModulesFiles=ColorPicker.png;FancyZones.png;AlwaysOnTop.png;Awake.png;ImageResizer.png;KBM.png;MouseUtils.png;PowerAccent.png;PowerOCR.png;PowerLauncher.png;PowerPreview.png;PowerRename.png;PT.png;ShortcutGuide.png;VideoConference.png?>
<?define SettingsV2AssetsModulesFiles=ColorPicker.png;FancyZones.png;AlwaysOnTop.png;Awake.png;ImageResizer.png;KBM.png;MouseUtils.png;PowerAccent.png;PowerOCR.png;PowerLauncher.png;PowerPreview.png;PowerRename.png;PT.png;ScreenRuler.png;ShortcutGuide.png;VideoConference.png?>
<?define SettingsV2OOBEAssetsModulesFiles=ColorPicker.gif;AlwaysOnTop.png;Awake.png;FancyZones.gif;FileExplorer.png;ImageResizer.gif;KBM.gif;MouseUtils.gif;PowerAccent.gif;PowerOCR.gif;PowerRename.gif;Run.gif;OOBEShortcutGuide.png;VideoConferenceMute.png;OOBEPTHero.png?>
<?define SettingsV2OOBEAssetsModulesFiles=ColorPicker.gif;AlwaysOnTop.png;Awake.png;FancyZones.gif;FileExplorer.png;ImageResizer.gif;KBM.gif;MouseUtils.gif;PowerAccent.gif;PowerOCR.gif;PowerRename.gif;Run.gif;ScreenRuler.gif;OOBEShortcutGuide.png;VideoConferenceMute.png;OOBEPTHero.png?>
<?define SettingsV2OOBEAssetsFluentIconsFiles=ColorPicker.png;FancyZones.png;AlwaysOnTop.png;Awake.png;FileExplorerPreview.png;FindMyMouse.png;ImageResizer.png;KeyboardManager.png;MouseHighlighter.png;MouseCrosshairs.png;MouseUtils.png;PowerAccent.png;PowerOcr.png;PowerRename.png;PowerToys.png;PowerToysRun.png;Settings.png;ShortcutGuide.png;VideoConferenceMute.png?>
<?define SettingsV2OOBEAssetsFluentIconsFiles=ColorPicker.png;FancyZones.png;AlwaysOnTop.png;Awake.png;FileExplorerPreview.png;FindMyMouse.png;ImageResizer.png;KeyboardManager.png;MouseHighlighter.png;MouseCrosshairs.png;MouseUtils.png;PowerAccent.png;PowerOcr.png;PowerRename.png;PowerToys.png;PowerToysRun.png;ScreenRuler.png;Settings.png;ShortcutGuide.png;VideoConferenceMute.png?>
<?define SettingsV2MicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>
@ -118,6 +119,9 @@
<?define ImageResizerSparsePackageAssets=LargeTile.png;SmallTile.png;SplashScreen.png;Square150x150Logo.png;Square44x44Logo.png;storelogo.png;Wide310x150Logo.png?>
<?define MeasureToolFiles=CoreMessagingXP.dll;dcompi.dll;dwmcorei.dll;DwmSceneI.dll;DWriteCore.dll;marshal.dll;Microsoft.DirectManipulation.dll;Microsoft.InputStateManager.dll;Microsoft.InteractiveExperiences.Projection.dll;Microsoft.Internal.FrameworkUdk.dll;Microsoft.UI.Composition.OSSupport.dll;Microsoft.UI.Input.dll;Microsoft.UI.Windowing.Core.dll;Microsoft.UI.Xaml.Controls.dll;Microsoft.UI.Xaml.Controls.pri;Microsoft.ui.xaml.dll;Microsoft.UI.Xaml.Internal.dll;Microsoft.UI.Xaml.Phone.dll;Microsoft.ui.xaml.resources.19h1.dll;Microsoft.ui.xaml.resources.common.dll;Microsoft.Web.WebView2.Core.dll;Microsoft.Windows.ApplicationModel.Resources.dll;Microsoft.Windows.ApplicationModel.WindowsAppRuntime.Projection.dll;Microsoft.Windows.AppLifecycle.Projection.dll;Microsoft.Windows.AppNotifications.Projection.dll;Microsoft.Windows.PushNotifications.Projection.dll;Microsoft.Windows.SDK.NET.dll;Microsoft.Windows.System.Projection.dll;Microsoft.WindowsAppRuntime.Bootstrap.dll;Microsoft.WindowsAppRuntime.Bootstrap.Net.dll;Microsoft.WindowsAppRuntime.dll;Microsoft.WindowsAppRuntime.Insights.Resource.dll;Microsoft.WindowsAppRuntime.Release.Net.dll;Microsoft.WinUI.dll;MRM.dll;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;PowerToys.MeasureToolCore.dll;PowerToys.MeasureToolUI.deps.json;PowerToys.MeasureToolUI.dll;PowerToys.MeasureToolUI.exe;PowerToys.MeasureToolUI.runtimeconfig.json;PushNotificationsLongRunningTask.ProxyStub.dll;resources.pri;System.CodeDom.dll;System.Management.dll;WindowsAppRuntime.png;WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll;WinRT.Runtime.dll;WinUIEdit.dll;WinUIEx.dll;wuceffectsi.dll?>
<?define PowerRenameMicrosoftUIXamlAssetsInstallFiles=NoiseAsset_256x256_PNG.png?>
<?define PowerAccentFiles=ControlzEx.dll;GongSolutions.WPF.DragDrop.dll;Ijwhost.dll;MahApps.Metro.dll;Microsoft.Xaml.Behaviors.dll;PowerAccent.Core.dll;PowerAccent.deps.json;PowerAccent.dll;PowerAccent.exe;PowerAccent.runtimeconfig.json;PowerToys.PowerAccentModuleInterface.dll;PowerToys.Interop.dll;PowerToys.ManagedCommon.dll;PowerToys.ManagedTelemetry.dll;PowerToys.PowerAccent.deps.json;PowerToys.PowerAccent.dll;PowerToys.PowerAccent.exe;PowerToys.PowerAccent.runtimeconfig.json;PowerToys.Settings.UI.Lib.dll;System.IO.Abstractions.dll;System.Management.dll;System.Text.Json.dll;Vanara.Core.dll;Vanara.PInvoke.Gdi32.dll;Vanara.PInvoke.Kernel32.dll;Vanara.PInvoke.Shared.dll;Vanara.PInvoke.User32.dll?>
@ -461,6 +465,10 @@
<Directory Id="AlwaysOnTopInstallFolder" Name="$(var.AlwaysOnTopProjectName)">
</Directory>
<!-- MeasureTool -->
<Directory Id="MeasureToolInstallFolder" Name="$(var.MeasureToolProjectName)">
</Directory>
<!-- Launcher -->
<Directory Id="LauncherInstallFolder" Name="launcher">
<Directory Id="LauncherImagesFolder" Name="Images" />
@ -961,6 +969,20 @@
</DirectoryRef>
<!-- Measure Tool -->
<DirectoryRef Id="MeasureToolInstallFolder" FileSource="$(var.BinDir)modules\$(var.MeasureToolProjectName)">
<Component Id="Module_MeasureToolInterface" Win64="yes">
<File Source="$(var.BinDir)modules\$(var.MeasureToolProjectName)\PowerToys.MeasureToolModuleInterface.dll" />
</Component>
<?foreach File in $(var.MeasureToolFiles)?>
<Component Id="MEASURE_TOOL_$(var.File)" Win64="yes">
<File Id="MEASURE_TOOL_$(var.File)" Source="$(var.BinDir)modules\$(var.MeasureToolProjectName)\$(var.File)" />
</Component>
<?endforeach?>
</DirectoryRef>
<!-- SettingsV2 components -->
<DirectoryRef Id="SettingsV2InstallFolder" FileSource="$(var.BinDir)Settings\">
<?foreach File in $(var.SettingsV2Files)?>
@ -1103,6 +1125,11 @@
<ComponentRef Id="Module_MousePointerCrosshairs" />
<ComponentRef Id="Module_AlwaysOnTop"/>
<ComponentRef Id="Module_AlwaysOnTopInterface"/>
<ComponentRef Id="Module_MeasureToolInterface"/>
<?foreach File in $(var.MeasureToolFiles)?>
<ComponentRef Id="MEASURE_TOOL_$(var.File)" />
<?endforeach?>
<?foreach File in $(var.SettingsV2Files)?>
<ComponentRef Id="SV2C_$(var.File)" />
<?endforeach?>

View File

@ -18,3 +18,5 @@ msbuild !PTRoot!\src\modules\previewpane\MarkdownPreviewHandler\MarkdownPreviewH
msbuild !PTRoot!\src\modules\previewpane\SvgPreviewHandler\SvgPreviewHandler.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml
msbuild !PTRoot!\src\modules\previewpane\SvgThumbnailProvider\SvgThumbnailProvider.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml
msbuild !PTRoot!\src\modules\MeasureTool\MeasureToolUI\MeasureToolUI.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml

View File

@ -2,72 +2,53 @@
#include <algorithm>
namespace
Box MonitorInfo::GetScreenSize(const bool includeNonWorkingArea) const
{
// TODO: use compare
bool operator<(const RECT& lhs, const RECT& rhs)
{
auto lhs_tuple = std::make_tuple(lhs.left, lhs.right, lhs.top, lhs.bottom);
auto rhs_tuple = std::make_tuple(rhs.left, rhs.right, rhs.top, rhs.bottom);
return lhs_tuple < rhs_tuple;
}
return includeNonWorkingArea ? Box{ info.rcMonitor } : Box{ info.rcWork };
}
bool operator==(const ScreenSize& lhs, const ScreenSize& rhs)
bool MonitorInfo::IsPrimary() const
{
auto lhs_tuple = std::make_tuple(lhs.rect.left, lhs.rect.right, lhs.rect.top, lhs.rect.bottom);
auto rhs_tuple = std::make_tuple(rhs.rect.left, rhs.rect.right, rhs.rect.top, rhs.rect.bottom);
return lhs_tuple == rhs_tuple;
return static_cast<bool>(info.dwFlags & MONITORINFOF_PRIMARY);
}
MonitorInfo::MonitorInfo(HMONITOR h) :
handle{ h }
{
info.cbSize = sizeof(MONITORINFOEX);
GetMonitorInfoW(handle, &info);
}
static BOOL CALLBACK GetDisplaysEnumCb(HMONITOR monitor, HDC hdc, LPRECT rect, LPARAM data)
{
MONITORINFOEX monitorInfo;
monitorInfo.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(monitor, &monitorInfo))
{
reinterpret_cast<std::vector<MonitorInfo>*>(data)->emplace_back(monitor, monitorInfo.rcWork);
}
return true;
};
static BOOL CALLBACK GetDisplaysEnumCbWithNonWorkingArea(HMONITOR monitor, HDC hdc, LPRECT rect, LPARAM data)
{
MONITORINFOEX monitorInfo;
monitorInfo.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(monitor, &monitorInfo))
{
reinterpret_cast<std::vector<MonitorInfo>*>(data)->emplace_back(monitor, monitorInfo.rcMonitor);
}
auto* monitors = reinterpret_cast<std::vector<MonitorInfo>*>(data);
monitors->emplace_back(monitor);
return true;
};
std::vector<MonitorInfo> MonitorInfo::GetMonitors(bool includeNonWorkingArea)
{
std::vector<MonitorInfo> monitors;
EnumDisplayMonitors(NULL, NULL, includeNonWorkingArea ? GetDisplaysEnumCbWithNonWorkingArea : GetDisplaysEnumCb, reinterpret_cast<LPARAM>(&monitors));
std::sort(begin(monitors), end(monitors), [](const MonitorInfo& lhs, const MonitorInfo& rhs) {
return lhs.rect < rhs.rect;
EnumDisplayMonitors(nullptr, nullptr, GetDisplaysEnumCb, reinterpret_cast<LPARAM>(&monitors));
std::sort(begin(monitors), end(monitors), [=](const MonitorInfo& lhs, const MonitorInfo& rhs) {
const auto lhsSize = lhs.GetScreenSize(includeNonWorkingArea);
const auto rhsSize = rhs.GetScreenSize(includeNonWorkingArea);
return lhsSize < rhsSize;
});
return monitors;
}
static BOOL CALLBACK GetPrimaryDisplayEnumCb(HMONITOR monitor, HDC hdc, LPRECT rect, LPARAM data)
{
MONITORINFOEX monitorInfo;
monitorInfo.cbSize = sizeof(MONITORINFOEX);
if (GetMonitorInfo(monitor, &monitorInfo) && (monitorInfo.dwFlags & MONITORINFOF_PRIMARY))
{
reinterpret_cast<MonitorInfo*>(data)->handle = monitor;
reinterpret_cast<MonitorInfo*>(data)->rect = monitorInfo.rcWork;
}
return true;
};
MonitorInfo MonitorInfo::GetPrimaryMonitor()
{
MonitorInfo primary({}, {});
EnumDisplayMonitors(NULL, NULL, GetPrimaryDisplayEnumCb, reinterpret_cast<LPARAM>(&primary));
return primary;
auto monitors = MonitorInfo::GetMonitors(false);
if (monitors.size() > 1)
{
for (auto monitor : monitors)
{
if (monitor.IsPrimary())
return monitor;
}
}
return monitors[0];
}

View File

@ -1,12 +1,20 @@
#pragma once
#include <Windows.h>
#include <compare>
#include <optional>
#include <vector>
struct ScreenSize
// TODO: merge with FZ::Rect
struct Box
{
explicit ScreenSize(RECT rect) :
rect(rect) {}
RECT rect;
explicit Box(RECT rect = {}) :
rect(rect) {}
Box(const Box&) = default;
Box& operator=(const Box&) = default;
int left() const { return rect.left; }
int right() const { return rect.right; }
int top() const { return rect.top; }
@ -22,17 +30,31 @@ struct ScreenSize
POINT bottom_left() const { return { rect.left, rect.bottom }; };
POINT bottom_middle() const { return { rect.left + width() / 2, rect.bottom }; };
POINT bottom_right() const { return { rect.right, rect.bottom }; };
inline bool inside(const POINT point) const { return PtInRect(&rect, point); }
inline friend auto operator<=>(const Box& lhs, const Box& rhs)
{
auto lhs_tuple = std::make_tuple(lhs.rect.left, lhs.rect.right, lhs.rect.top, lhs.rect.bottom);
auto rhs_tuple = std::make_tuple(rhs.rect.left, rhs.rect.right, rhs.rect.top, rhs.rect.bottom);
return lhs_tuple <=> rhs_tuple;
}
};
struct MonitorInfo : ScreenSize
class MonitorInfo
{
explicit MonitorInfo(HMONITOR monitor, RECT rect) :
handle(monitor), ScreenSize(rect) {}
HMONITOR handle;
MONITORINFOEX info = {};
public:
explicit MonitorInfo(HMONITOR h);
inline HMONITOR GetHandle() const
{
return handle;
}
Box GetScreenSize(const bool includeNonWorkingArea) const;
bool IsPrimary() const;
// Returns monitor rects ordered from left to right
static std::vector<MonitorInfo> GetMonitors(bool includeNonWorkingArea);
static MonitorInfo GetPrimaryMonitor();
};
bool operator==(const ScreenSize& lhs, const ScreenSize& rhs);

View File

@ -0,0 +1,30 @@
#pragma once
#include <functional>
#include <shared_mutex>
template<typename StateT>
class Serialized
{
mutable std::shared_mutex m;
StateT s;
public:
void Read(std::function<void(const StateT&)> fn) const
{
std::shared_lock lock{ m };
fn(s);
}
void Access(std::function<void(StateT&)> fn)
{
std::unique_lock lock{ m };
fn(s);
}
void Reset()
{
std::unique_lock lock{ m };
s = {};
}
};

View File

@ -25,7 +25,7 @@ inline std::optional<std::wstring> get_last_error_message(const DWORD dw)
inline std::wstring get_last_error_or_default(const DWORD dw)
{
auto message = get_last_error_message(dw);
return message.has_value() ? message.value() : L"";
return message.has_value() ? *message : L"";
}
inline void show_last_error_message(const wchar_t* functionName, DWORD dw, const wchar_t* errorTitle)

View File

@ -8,24 +8,24 @@
#include <optional>
// Initializes and runs windows message loop
inline int run_message_loop(const bool until_idle = false, const std::optional<uint32_t> timeout_seconds = {})
inline int run_message_loop(const bool until_idle = false, const std::optional<uint32_t> timeout_ms = {})
{
MSG msg{};
bool stop = false;
UINT_PTR timerId = 0;
if (timeout_seconds.has_value())
if (timeout_ms.has_value())
{
timerId = SetTimer(nullptr, 0, *timeout_seconds * 1000, nullptr);
timerId = SetTimer(nullptr, 0, *timeout_ms, nullptr);
}
while (!stop && GetMessageW(&msg, nullptr, 0, 0))
while (!stop && (until_idle ? PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE) : GetMessageW(&msg, nullptr, 0, 0)))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
stop = until_idle && !PeekMessageW(&msg, nullptr, 0, 0, PM_NOREMOVE);
stop = stop || (msg.message == WM_TIMER && msg.wParam == timerId);
}
if (timeout_seconds.has_value())
if (timeout_ms.has_value())
{
KillTimer(nullptr, timerId);
}
@ -55,3 +55,24 @@ inline bool is_system_window(HWND hwnd, const char* class_name)
}
return false;
}
template<typename T>
inline T GetWindowCreateParam(LPARAM lparam)
{
static_assert(sizeof(T) <= sizeof(void*));
T data{ (T)(reinterpret_cast<CREATESTRUCT*>(lparam)->lpCreateParams) };
return data;
}
template<typename T>
inline void StoreWindowParam(HWND window, T data)
{
static_assert(sizeof(T) <= sizeof(void*));
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(data));
}
template<typename T>
inline T GetWindowParam(HWND window)
{
return (T)GetWindowLongPtrW(window, GWLP_USERDATA);
}

View File

@ -0,0 +1,73 @@
#include "pch.h"
#include "BGRATextureView.h"
#if defined(DEBUG_TEXTURE)
void BGRATextureView::SaveAsBitmap(const char* filename) const
{
wil::unique_hbitmap bitmap{ CreateBitmap(static_cast<int>(pitch), static_cast<int>(height), 1, 32, pixels) };
const HBITMAP hBitmap = bitmap.get();
DWORD dwPaletteSize = 0, dwBmBitsSize = 0, dwDIBSize = 0, dwWritten = 0;
LPBITMAPINFOHEADER lpBitmapInfo;
HANDLE hDib, hPal, hOldPal2 = NULL;
HDC hDC = CreateDC(TEXT("DISPLAY"), NULL, NULL, NULL);
const int iBits = GetDeviceCaps(hDC, BITSPIXEL) * GetDeviceCaps(hDC, PLANES);
DeleteDC(hDC);
WORD wBitCount = 24;
if (iBits <= 1)
wBitCount = 1;
else if (iBits <= 4)
wBitCount = 4;
else if (iBits <= 8)
wBitCount = 8;
BITMAP Bitmap0;
GetObject(hBitmap, sizeof(Bitmap0), (LPSTR)&Bitmap0);
BITMAPINFOHEADER bi = {};
bi.biSize = sizeof(BITMAPINFOHEADER);
bi.biWidth = Bitmap0.bmWidth;
bi.biHeight = -Bitmap0.bmHeight;
bi.biPlanes = 1;
bi.biBitCount = wBitCount;
bi.biCompression = BI_RGB;
bi.biClrUsed = 256;
dwBmBitsSize = ((Bitmap0.bmWidth * wBitCount + 31) & ~31) / 8 * Bitmap0.bmHeight;
hDib = GlobalAlloc(GHND, dwBmBitsSize + dwPaletteSize + sizeof(BITMAPINFOHEADER));
lpBitmapInfo = (LPBITMAPINFOHEADER)GlobalLock(hDib);
*lpBitmapInfo = bi;
hPal = GetStockObject(DEFAULT_PALETTE);
if (hPal)
{
hDC = GetDC(NULL);
hOldPal2 = SelectPalette(hDC, (HPALETTE)hPal, FALSE);
RealizePalette(hDC);
}
GetDIBits(hDC, hBitmap, 0, (UINT)Bitmap0.bmHeight, (LPSTR)lpBitmapInfo + sizeof(BITMAPINFOHEADER) + dwPaletteSize, (BITMAPINFO*)lpBitmapInfo, DIB_RGB_COLORS);
if (hOldPal2)
{
SelectPalette(hDC, (HPALETTE)hOldPal2, TRUE);
RealizePalette(hDC);
ReleaseDC(NULL, hDC);
}
wil::unique_handle fh{ CreateFileA(filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL) };
if (!fh)
return;
BITMAPFILEHEADER bitmapFileHeader = {};
bitmapFileHeader.bfType = 0x4D42; // "BM"
dwDIBSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwPaletteSize + dwBmBitsSize;
bitmapFileHeader.bfSize = dwDIBSize;
bitmapFileHeader.bfOffBits = (DWORD)sizeof(BITMAPFILEHEADER) + (DWORD)sizeof(BITMAPINFOHEADER) + dwPaletteSize;
WriteFile(fh.get(), (LPSTR)&bitmapFileHeader, sizeof(BITMAPFILEHEADER), &dwWritten, NULL);
WriteFile(fh.get(), (LPSTR)lpBitmapInfo, dwDIBSize, &dwWritten, NULL);
GlobalUnlock(hDib);
GlobalFree(hDib);
}
#endif

View File

@ -0,0 +1,127 @@
#pragma once
#include <cinttypes>
#include <wil/resource.h>
#ifdef _M_ARM64
#include <arm64_neon.h.>
#else
#include <emmintrin.h>
#endif
#include <cassert>
#include <limits>
//#define DEBUG_TEXTURE
#if defined(_M_ARM64)
// Adopted from https://github.com/DLTcollab/sse2neon/blob/master/sse2neon.h
using __m128i = int64x2_t;
inline __m128i _mm_cvtsi32_si128(int a)
{
return vreinterpretq_s64_s32(vsetq_lane_s32(a, vdupq_n_s32(0), 0));
}
inline __m128i _mm_or_si128(__m128i a, __m128i b)
{
return vreinterpretq_s64_s32(
vorrq_s32(vreinterpretq_s32_s64(a), vreinterpretq_s32_s64(b)));
}
inline __m128i _mm_subs_epu8(__m128i a, __m128i b)
{
return vreinterpretq_s64_u8(
vqsubq_u8(vreinterpretq_u8_s64(a), vreinterpretq_u8_s64(b)));
}
inline __m128i _mm_sad_epu8(__m128i a, __m128i b)
{
uint16x8_t t = vpaddlq_u8(vabdq_u8((uint8x16_t)a, (uint8x16_t)b));
return vreinterpretq_s64_u64(vpaddlq_u32(vpaddlq_u16(t)));
}
inline __m128i _mm_setzero_si128(void)
{
return vreinterpretq_s64_s32(vdupq_n_s32(0));
}
inline int _mm_cvtsi128_si32(__m128i a)
{
return vgetq_lane_s32(vreinterpretq_s32_s64(a), 0);
}
inline __m128i _mm_set1_epi16(short w)
{
return vreinterpretq_s64_s16(vdupq_n_s16(w));
}
inline __m128i _mm_cmpgt_epi16(__m128i a, __m128i b)
{
return vreinterpretq_s64_u16(
vcgtq_s16(vreinterpretq_s16_s64(a), vreinterpretq_s16_s64(b)));
}
inline __m128i _mm_cvtepu8_epi16(__m128i a)
{
uint8x16_t u8x16 = vreinterpretq_u8_s64(a); /* xxxx xxxx HGFE DCBA */
uint16x8_t u16x8 = vmovl_u8(vget_low_u8(u8x16)); /* 0H0G 0F0E 0D0C 0B0A */
return vreinterpretq_s64_u16(u16x8);
}
inline int64_t _mm_cvtsi128_si64(__m128i a)
{
return vgetq_lane_s64(a, 0);
}
#endif
inline __m128i distance_epu8(const __m128i a, __m128i b)
{
return _mm_or_si128(_mm_subs_epu8(a, b),
_mm_subs_epu8(b, a));
}
struct BGRATextureView
{
const uint32_t* pixels = nullptr;
size_t pitch = {};
size_t width = {};
size_t height = {};
BGRATextureView() = default;
BGRATextureView(BGRATextureView&& rhs) = default;
inline uint32_t GetPixel(const size_t x, const size_t y) const
{
assert(x < width && x >= 0);
assert(y < height && y >= 0);
return pixels[x + pitch * y];
}
template<bool perChannel>
static inline bool PixelsClose(const uint32_t pixel1, const uint32_t pixel2, uint8_t tolerance)
{
const __m128i rgba1 = _mm_cvtsi32_si128(pixel1);
const __m128i rgba2 = _mm_cvtsi32_si128(pixel2);
const __m128i distances = distance_epu8(rgba1, rgba2);
// Method 1: Test whether each channel distance is not greater than tolerance
if constexpr (perChannel)
{
const __m128i tolerances = _mm_set1_epi16(tolerance);
const auto gtResults128 = _mm_cmpgt_epi16(_mm_cvtepu8_epi16(distances), tolerances);
return _mm_cvtsi128_si64(gtResults128) == 0;
}
else
{
// Method 2: Test whether sum of all channel differences is smaller than tolerance
const int32_t score = _mm_cvtsi128_si32(_mm_sad_epu8(distances, _mm_setzero_si128())) & std::numeric_limits<uint8_t>::max();
return score <= tolerance;
}
}
#if defined(DEBUG_TEXTURE)
void SaveAsBitmap(const char* filename) const;
#endif
};

View File

@ -0,0 +1,161 @@
#include "pch.h"
#include "BoundsToolOverlayUI.h"
#include "CoordinateSystemConversion.h"
#include "Clipboard.h"
#include <common/utils/window.h>
LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
switch (message)
{
case WM_CREATE:
{
auto toolState = GetWindowCreateParam<BoundsToolState*>(lparam);
StoreWindowParam(window, toolState);
break;
}
case WM_ERASEBKGND:
return 1;
case WM_KEYUP:
if (wparam == VK_ESCAPE)
{
PostMessageW(window, WM_CLOSE, {}, {});
}
break;
case WM_LBUTTONDOWN:
{
auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState)
break;
const POINT cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace);
D2D_POINT_2F newRegionStart = { .x = static_cast<float>(cursorPos.x), .y = static_cast<float>(cursorPos.y) };
toolState->perScreen[window].currentRegionStart = newRegionStart;
break;
}
case WM_CURSOR_LEFT_MONITOR:
{
auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState)
break;
toolState->perScreen[window].currentRegionStart = std::nullopt;
break;
}
case WM_LBUTTONUP:
{
auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState || !toolState->perScreen[window].currentRegionStart)
break;
toolState->commonState->overlayBoxText.Read([](const OverlayBoxText& text) {
SetClipBoardToText(text.buffer);
});
if (const bool shiftPress = GetKeyState(VK_SHIFT) & 0x8000; shiftPress)
{
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, toolState->commonState->cursorPosSystemSpace);
D2D1_RECT_F rect;
std::tie(rect.left, rect.right) = std::minmax(static_cast<float>(cursorPos.x), toolState->perScreen[window].currentRegionStart->x);
std::tie(rect.top, rect.bottom) = std::minmax(static_cast<float>(cursorPos.y), toolState->perScreen[window].currentRegionStart->y);
toolState->perScreen[window].measurements.push_back(rect);
}
toolState->perScreen[window].currentRegionStart = std::nullopt;
break;
}
case WM_RBUTTONUP:
{
auto toolState = GetWindowParam<BoundsToolState*>(window);
if (!toolState)
break;
if (toolState->perScreen[window].currentRegionStart)
toolState->perScreen[window].currentRegionStart = std::nullopt;
else
{
if (toolState->perScreen[window].measurements.empty())
PostMessageW(window, WM_CLOSE, {}, {});
else
toolState->perScreen[window].measurements.clear();
}
break;
}
}
return DefWindowProcW(window, message, wparam, lparam);
}
namespace
{
void DrawMeasurement(const D2D1_RECT_F rect,
const bool alignTextBoxToCenter,
const CommonState& commonState,
HWND window,
const D2DState& d2dState)
{
const bool screenQuadrantAware = !alignTextBoxToCenter;
const auto prevMode = d2dState.rt->GetAntialiasMode();
d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
d2dState.rt->DrawRectangle(rect, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->SetAntialiasMode(prevMode);
OverlayBoxText text;
const auto width = std::abs(rect.right - rect.left + 1);
const auto height = std::abs(rect.top - rect.bottom + 1);
const uint32_t textLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f × %.0f",
width,
height);
std::optional<size_t> crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1;
commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
v = text;
});
float cornerX = rect.right;
float cornerY = rect.bottom;
if (alignTextBoxToCenter)
{
cornerX = rect.left + width / 2;
cornerY = rect.top + height / 2;
}
d2dState.DrawTextBox(text.buffer.data(),
textLen,
crossSymbolPos,
cornerX,
cornerY,
screenQuadrantAware,
window);
}
}
void DrawBoundsToolTick(const CommonState& commonState,
const BoundsToolState& toolState,
const HWND window,
const D2DState& d2dState)
{
const auto it = toolState.perScreen.find(window);
if (it == end(toolState.perScreen))
return;
d2dState.rt->Clear();
const auto& perScreen = it->second;
for (const auto& measure : perScreen.measurements)
DrawMeasurement(measure, true, commonState, window, d2dState);
if (!perScreen.currentRegionStart.has_value())
return;
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace);
const D2D1_RECT_F rect{ .left = perScreen.currentRegionStart->x,
.top = perScreen.currentRegionStart->y,
.right = static_cast<float>(cursorPos.x),
.bottom = static_cast<float>(cursorPos.y) };
DrawMeasurement(rect, false, commonState, window, d2dState);
}

View File

@ -0,0 +1,10 @@
#pragma once
#include "D2DState.h"
#include "ToolState.h"
void DrawBoundsToolTick(const CommonState& commonState,
const BoundsToolState& toolState,
const HWND overlayWindow,
const D2DState& d2dState);
LRESULT CALLBACK BoundsToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept;

View File

@ -0,0 +1,28 @@
#include "pch.h"
#include "Clipboard.h"
void SetClipBoardToText(const std::wstring_view text)
{
if (!OpenClipboard(nullptr))
{
return;
}
const wil::unique_hglobal handle{ GlobalAlloc(GMEM_MOVEABLE, static_cast<size_t>((text.length() + 1) * sizeof(wchar_t))) };
if (!handle)
{
CloseClipboard();
return;
}
if (auto* bufPtr = static_cast<wchar_t*>(GlobalLock(handle.get())); bufPtr != nullptr)
{
text.copy(bufPtr, text.length());
GlobalUnlock(handle.get());
}
EmptyClipboard();
SetClipboardData(CF_UNICODETEXT, handle.get());
CloseClipboard();
}

View File

@ -0,0 +1,5 @@
#pragma once
#include <string_view>
void SetClipBoardToText(const std::wstring_view text);

View File

@ -0,0 +1,26 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
namespace convert
{
// Converts a given point from multi-monitor coordinate system to the one relative to HWND
inline POINT FromSystemToRelative(HWND window, POINT p)
{
ScreenToClient(window, &p);
return p;
}
// Converts a given point from multi-monitor coordinate system to the one relative to HWND and also ready
// to be used in Direct2D calls with AA mode set to aliased
inline POINT FromSystemToRelativeForDirect2D(HWND window, POINT p)
{
ScreenToClient(window, &p);
// Submitting DrawLine calls to Direct2D with thickness == 1.f and AA mode set to aliased causes
// them to be drawn offset by [1,1] toward upper-left corner, so we must to compensate for that.
++p.x;
++p.y;
return p;
}
}

View File

@ -0,0 +1,170 @@
#include "pch.h"
#include "constants.h"
#include "D2DState.h"
#include <common/Display/dpi_aware.h>
#include <ToolState.h>
namespace
{
void DetermineScreenQuadrant(const HWND window, long x, long y, bool& inLeftHalf, bool& inTopHalf)
{
RECT windowRect{};
GetWindowRect(window, &windowRect);
const long w = windowRect.right - windowRect.left;
const long h = windowRect.bottom - windowRect.top;
inLeftHalf = x < w / 2;
inTopHalf = y < h / 2;
}
}
D2DState::D2DState(const HWND overlayWindow, std::vector<D2D1::ColorF> solidBrushesColors)
{
std::lock_guard guard{ gpuAccessLock };
RECT clientRect = {};
winrt::check_bool(GetClientRect(overlayWindow, &clientRect));
winrt::check_hresult(D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, &d2dFactory));
winrt::check_hresult(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory), writeFactory.put_unknown()));
// We should always use DPIAware::DEFAULT_DPI, since it's the correct thing to do in DPI-Aware mode
auto renderTargetProperties = D2D1::RenderTargetProperties(
D2D1_RENDER_TARGET_TYPE_DEFAULT,
D2D1::PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED),
DPIAware::DEFAULT_DPI,
DPIAware::DEFAULT_DPI,
D2D1_RENDER_TARGET_USAGE_NONE,
D2D1_FEATURE_LEVEL_DEFAULT);
auto renderTargetSize = D2D1::SizeU(clientRect.right - clientRect.left, clientRect.bottom - clientRect.top);
auto hwndRenderTargetProperties = D2D1::HwndRenderTargetProperties(overlayWindow, renderTargetSize);
winrt::check_hresult(d2dFactory->CreateHwndRenderTarget(renderTargetProperties, hwndRenderTargetProperties, &rt));
winrt::check_hresult(rt->CreateCompatibleRenderTarget(&bitmapRt));
unsigned dpi = DPIAware::DEFAULT_DPI;
DPIAware::GetScreenDPIForWindow(overlayWindow, dpi);
dpiScale = dpi / static_cast<float>(DPIAware::DEFAULT_DPI);
winrt::check_hresult(writeFactory->CreateTextFormat(L"Segoe UI Variable Text",
nullptr,
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
consts::FONT_SIZE * dpiScale,
L"en-US",
&textFormat));
winrt::check_hresult(textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER));
winrt::check_hresult(textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER));
winrt::check_hresult(textFormat->SetWordWrapping(DWRITE_WORD_WRAPPING_NO_WRAP));
solidBrushes.resize(solidBrushesColors.size());
for (size_t i = 0; i < solidBrushes.size(); ++i)
{
winrt::check_hresult(rt->CreateSolidColorBrush(solidBrushesColors[i], &solidBrushes[i]));
}
const auto deviceContext = rt.query<ID2D1DeviceContext>();
winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D1Shadow, &shadowEffect));
winrt::check_hresult(shadowEffect->SetValue(D2D1_SHADOW_PROP_BLUR_STANDARD_DEVIATION, consts::SHADOW_RADIUS));
winrt::check_hresult(shadowEffect->SetValue(D2D1_SHADOW_PROP_COLOR, D2D1::ColorF(0.f, 0.f, 0.f, consts::SHADOW_OPACITY)));
winrt::check_hresult(deviceContext->CreateEffect(CLSID_D2D12DAffineTransform, &affineTransformEffect));
affineTransformEffect->SetInputEffect(0, shadowEffect.get());
textRenderer = winrt::make_self<PerGlyphOpacityTextRender>(d2dFactory, rt, solidBrushes[Brush::foreground]);
}
void D2DState::DrawTextBox(const wchar_t* text,
const uint32_t textLen,
const std::optional<size_t> halfOpaqueSymbolPos,
const float centerX,
const float centerY,
const bool screenQuadrantAware,
const HWND window) const
{
wil::com_ptr<IDWriteTextLayout> textLayout;
winrt::check_hresult(writeFactory->CreateTextLayout(text,
textLen,
textFormat.get(),
std::numeric_limits<float>::max(),
std::numeric_limits<float>::max(),
&textLayout));
DWRITE_TEXT_METRICS textMetrics = {};
winrt::check_hresult(textLayout->GetMetrics(&textMetrics));
textMetrics.width *= consts::TEXT_BOX_MARGIN_COEFF;
textMetrics.height *= consts::TEXT_BOX_MARGIN_COEFF;
winrt::check_hresult(textLayout->SetMaxWidth(textMetrics.width));
winrt::check_hresult(textLayout->SetMaxHeight(textMetrics.height));
D2D1_RECT_F textRect{ .left = centerX - textMetrics.width / 2.f,
.top = centerY - textMetrics.height / 2.f,
.right = centerX + textMetrics.width / 2.f,
.bottom = centerY + textMetrics.height / 2.f };
if (screenQuadrantAware)
{
bool cursorInLeftScreenHalf = false;
bool cursorInTopScreenHalf = false;
DetermineScreenQuadrant(window,
static_cast<long>(centerX),
static_cast<long>(centerY),
cursorInLeftScreenHalf,
cursorInTopScreenHalf);
float textQuadrantOffsetX = textMetrics.width * dpiScale;
float textQuadrantOffsetY = textMetrics.height * dpiScale;
if (!cursorInLeftScreenHalf)
textQuadrantOffsetX *= -1.f;
if (!cursorInTopScreenHalf)
textQuadrantOffsetY *= -1.f;
textRect.left += textQuadrantOffsetX;
textRect.right += textQuadrantOffsetX;
textRect.top += textQuadrantOffsetY;
textRect.bottom += textQuadrantOffsetY;
}
// Draw shadow
bitmapRt->BeginDraw();
bitmapRt->Clear(D2D1::ColorF(0.f, 0.f, 0.f, 0.f));
D2D1_ROUNDED_RECT textBoxRect;
textBoxRect.radiusX = textBoxRect.radiusY = consts::TEXT_BOX_CORNER_RADIUS * dpiScale;
textBoxRect.rect.bottom = textRect.bottom;
textBoxRect.rect.top = textRect.top;
textBoxRect.rect.left = textRect.left;
textBoxRect.rect.right = textRect.right;
bitmapRt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get());
bitmapRt->EndDraw();
wil::com_ptr<ID2D1Bitmap> rtBitmap;
bitmapRt->GetBitmap(&rtBitmap);
shadowEffect->SetInput(0, rtBitmap.get());
const auto shadowMatrix = D2D1::Matrix3x2F::Translation(consts::SHADOW_OFFSET * dpiScale,
consts::SHADOW_OFFSET * dpiScale);
winrt::check_hresult(affineTransformEffect->SetValue(D2D1_2DAFFINETRANSFORM_PROP_TRANSFORM_MATRIX,
shadowMatrix));
auto deviceContext = rt.query<ID2D1DeviceContext>();
deviceContext->DrawImage(affineTransformEffect.get(), D2D1_INTERPOLATION_MODE_LINEAR);
// Draw text box border rectangle
rt->DrawRoundedRectangle(textBoxRect, solidBrushes[Brush::border].get());
const float TEXT_BOX_PADDING = 1.f * dpiScale;
textBoxRect.rect.bottom -= TEXT_BOX_PADDING;
textBoxRect.rect.top += TEXT_BOX_PADDING;
textBoxRect.rect.left += TEXT_BOX_PADDING;
textBoxRect.rect.right -= TEXT_BOX_PADDING;
// Draw text & its box
rt->FillRoundedRectangle(textBoxRect, solidBrushes[Brush::background].get());
if (halfOpaqueSymbolPos.has_value())
{
DWRITE_TEXT_RANGE textRange = { static_cast<uint32_t>(*halfOpaqueSymbolPos), 2 };
auto opacityEffect = winrt::make_self<OpacityEffect>();
opacityEffect->alpha = consts::CROSS_OPACITY;
winrt::check_hresult(textLayout->SetDrawingEffect(opacityEffect.get(), textRange));
}
winrt::check_hresult(textLayout->Draw(nullptr, textRenderer.get(), textRect.left, textRect.top));
}

View File

@ -0,0 +1,42 @@
#pragma once
#include <optional>
#include <vector>
#include <d2d1_3.h>
#include <wil/com.h>
#include <windef.h>
#include "PerGlyphOpacityTextRender.h"
enum Brush : size_t
{
line,
foreground,
background,
border
};
struct D2DState
{
wil::com_ptr<ID2D1Factory> d2dFactory;
wil::com_ptr<IDWriteFactory> writeFactory;
wil::com_ptr<ID2D1HwndRenderTarget> rt;
wil::com_ptr<ID2D1BitmapRenderTarget> bitmapRt;
wil::com_ptr<IDWriteTextFormat> textFormat;
winrt::com_ptr<PerGlyphOpacityTextRender> textRenderer;
std::vector<wil::com_ptr<ID2D1SolidColorBrush>> solidBrushes;
wil::com_ptr<ID2D1Effect> shadowEffect;
wil::com_ptr<ID2D1Effect> affineTransformEffect;
float dpiScale = 1.f;
D2DState(const HWND window, std::vector<D2D1::ColorF> solidBrushesColors);
void DrawTextBox(const wchar_t* text,
const uint32_t textLen,
const std::optional<size_t> halfOpaqueSymbolPos,
const float centerX,
const float centerY,
const bool screenQuadrantAware,
const HWND window) const;
};

View File

@ -0,0 +1,112 @@
#include "pch.h"
#include "constants.h"
#include "EdgeDetection.h"
template<bool PerChannel,
bool ContinuousCapture,
bool IsX,
bool Increment>
inline long FindEdge(const BGRATextureView& texture, const POINT centerPoint, const uint8_t tolerance)
{
using namespace consts;
long xOffset = 0;
long yOffset = 0;
// For continuous capture, we'll be a bit off center from the cursor so the cross we draw won't interfere with the measurement.
if constexpr (ContinuousCapture)
{
if constexpr (IsX)
{
xOffset = Increment ? CURSOR_OFFSET_AMOUNT_X : -CURSOR_OFFSET_AMOUNT_X;
yOffset = 1;
}
else
{
xOffset = 1;
yOffset = Increment ? CURSOR_OFFSET_AMOUNT_Y : -CURSOR_OFFSET_AMOUNT_Y;
}
}
const size_t maxDim = IsX ? texture.width : texture.height;
long x = std::clamp<long>(centerPoint.x + xOffset, 1, static_cast<long>(texture.width - 2));
long y = std::clamp<long>(centerPoint.y + yOffset, 1, static_cast<long>(texture.height - 2));
const uint32_t startPixel = texture.GetPixel(x, y);
while (true)
{
long oldX = x;
long oldY = y;
if constexpr (IsX)
{
if constexpr (Increment)
{
if (++x == maxDim)
break;
}
else
{
if (--x == 0)
break;
}
}
else
{
if constexpr (Increment)
{
if (++y == maxDim)
break;
}
else
{
if (--y == 0)
break;
}
}
const uint32_t nextPixel = texture.GetPixel(x, y);
if (!texture.PixelsClose<PerChannel>(startPixel, nextPixel, tolerance))
{
return IsX ? oldX : oldY;
}
}
return Increment ? static_cast<long>(IsX ? texture.width : texture.height) - 1 : 0;
}
template<bool PerChannel, bool ContinuousCapture>
inline RECT DetectEdgesInternal(const BGRATextureView& texture,
const POINT centerPoint,
const uint8_t tolerance)
{
return RECT{ .left = FindEdge<PerChannel,
ContinuousCapture,
true,
false>(texture, centerPoint, tolerance),
.top = FindEdge<PerChannel,
ContinuousCapture,
false,
false>(texture, centerPoint, tolerance),
.right = FindEdge<PerChannel,
ContinuousCapture,
true,
true>(texture, centerPoint, tolerance),
.bottom = FindEdge<PerChannel,
ContinuousCapture,
false,
true>(texture, centerPoint, tolerance) };
}
RECT DetectEdges(const BGRATextureView& texture,
const POINT centerPoint,
const bool perChannel,
const uint8_t tolerance,
const bool continuousCapture)
{
auto function = perChannel ? &DetectEdgesInternal<true, false> : DetectEdgesInternal<false, false>;
if (continuousCapture)
function = perChannel ? &DetectEdgesInternal<true, true> : &DetectEdgesInternal<false, true>;
return function(texture, centerPoint, tolerance);
}

View File

@ -0,0 +1,9 @@
#pragma once
#include "BGRATextureView.h"
RECT DetectEdges(const BGRATextureView& texture,
const POINT centerPoint,
const bool perChannel,
const uint8_t tolerance,
const bool continuousCapture);

View File

@ -0,0 +1,3 @@
EXPORTS
DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE

View File

@ -0,0 +1,300 @@
#include "pch.h"
#include "BGRATextureView.h"
#include "Clipboard.h"
#include "CoordinateSystemConversion.h"
#include "constants.h"
#include "MeasureToolOverlayUI.h"
#include <common/utils/window.h>
namespace
{
inline std::pair<D2D_POINT_2F, D2D_POINT_2F> ComputeCrossFeetLine(D2D_POINT_2F center, const bool horizontal)
{
D2D_POINT_2F start = center, end = center;
// Computing in this way to achieve pixel-perfect axial symmetry of aliased D2D lines
if (horizontal)
{
start.x -= consts::FEET_HALF_LENGTH + 1.f;
end.x += consts::FEET_HALF_LENGTH;
start.y += 1.f;
end.y += 1.f;
}
else
{
start.y -= consts::FEET_HALF_LENGTH + 1.f;
end.y += consts::FEET_HALF_LENGTH;
start.x += 1.f;
end.x += 1.f;
}
return { start, end };
}
}
winrt::com_ptr<ID2D1Bitmap> ConvertID3D11Texture2DToD2D1Bitmap(wil::com_ptr<ID2D1HwndRenderTarget> rt,
winrt::com_ptr<ID3D11Texture2D> texture)
{
std::lock_guard guard{ gpuAccessLock };
auto dxgiSurface = texture.try_as<IDXGISurface>();
if (!dxgiSurface)
return nullptr;
DXGI_MAPPED_RECT bitmap2Dmap = {};
HRESULT hr = dxgiSurface->Map(&bitmap2Dmap, DXGI_MAP_READ);
if (FAILED(hr))
{
return nullptr;
}
D2D1_BITMAP_PROPERTIES props = { .pixelFormat = rt->GetPixelFormat() };
rt->GetDpi(&props.dpiX, &props.dpiY);
const auto sizeF = rt->GetSize();
winrt::com_ptr<ID2D1Bitmap> bitmap;
if (FAILED(rt->CreateBitmap(D2D1::SizeU(static_cast<uint32_t>(sizeF.width),
static_cast<uint32_t>(sizeF.height)),
bitmap2Dmap.pBits,
bitmap2Dmap.Pitch,
props,
bitmap.put())))
return nullptr;
if (FAILED(dxgiSurface->Unmap()))
return nullptr;
return bitmap;
}
LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
switch (message)
{
case WM_CURSOR_LEFT_MONITOR:
{
if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
{
state->Access([&](MeasureToolState& s) {
s.perScreen[window].measuredEdges = {};
});
}
break;
}
case WM_NCHITTEST:
return HTCLIENT;
case WM_CREATE:
{
auto state = GetWindowCreateParam<Serialized<MeasureToolState>*>(lparam);
StoreWindowParam(window, state);
#if !defined(DEBUG_OVERLAY)
for (; ShowCursor(false) > 0;)
;
#endif
break;
}
case WM_ERASEBKGND:
return 1;
case WM_KEYUP:
if (wparam == VK_ESCAPE)
{
PostMessageW(window, WM_CLOSE, {}, {});
}
break;
case WM_RBUTTONUP:
PostMessageW(window, WM_CLOSE, {}, {});
break;
case WM_LBUTTONUP:
if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
{
state->Read([](const MeasureToolState& s) { s.commonState->overlayBoxText.Read([](const OverlayBoxText& text) {
SetClipBoardToText(text.buffer);
}); });
}
break;
case WM_MOUSEWHEEL:
if (auto state = GetWindowParam<Serialized<MeasureToolState>*>(window))
{
const int8_t step = static_cast<short>(HIWORD(wparam)) < 0 ? -consts::MOUSE_WHEEL_TOLERANCE_STEP : consts::MOUSE_WHEEL_TOLERANCE_STEP;
state->Access([step](MeasureToolState& s) {
int wideVal = s.global.pixelTolerance;
wideVal += step;
s.global.pixelTolerance = static_cast<uint8_t>(std::clamp(wideVal, 0, 255));
});
}
break;
}
return DefWindowProcW(window, message, wparam, lparam);
}
void DrawMeasureToolTick(const CommonState& commonState,
Serialized<MeasureToolState>& toolState,
HWND window,
D2DState& d2dState)
{
bool continuousCapture = {};
bool drawFeetOnCross = {};
bool drawHorizontalCrossLine = true;
bool drawVerticalCrossLine = true;
RECT measuredEdges{};
MeasureToolState::Mode mode = {};
winrt::com_ptr<ID2D1Bitmap> backgroundBitmap;
winrt::com_ptr<ID3D11Texture2D> backgroundTextureToConvert;
toolState.Read([&](const MeasureToolState& state) {
continuousCapture = state.global.continuousCapture;
drawFeetOnCross = state.global.drawFeetOnCross;
mode = state.global.mode;
if (auto it = state.perScreen.find(window); it != end(state.perScreen))
{
const auto& perScreen = it->second;
measuredEdges = perScreen.measuredEdges;
if (continuousCapture)
return;
if (perScreen.capturedScreenBitmap)
{
backgroundBitmap = perScreen.capturedScreenBitmap;
}
else if (perScreen.capturedScreenTexture)
{
backgroundTextureToConvert = perScreen.capturedScreenTexture;
}
}
});
switch (mode)
{
case MeasureToolState::Mode::Cross:
drawHorizontalCrossLine = true;
drawVerticalCrossLine = true;
break;
case MeasureToolState::Mode::Vertical:
drawHorizontalCrossLine = false;
drawVerticalCrossLine = true;
break;
case MeasureToolState::Mode::Horizontal:
drawHorizontalCrossLine = true;
drawVerticalCrossLine = false;
break;
}
if (!continuousCapture && !backgroundBitmap && backgroundTextureToConvert)
{
backgroundBitmap = ConvertID3D11Texture2DToD2D1Bitmap(d2dState.rt, backgroundTextureToConvert);
if (backgroundBitmap)
{
toolState.Access([&](MeasureToolState& state) {
state.perScreen[window].capturedScreenTexture = {};
state.perScreen[window].capturedScreenBitmap = backgroundBitmap;
});
}
}
if (continuousCapture || !backgroundBitmap)
d2dState.rt->Clear();
// Add 1px to each dim, since the range we obtain from measuredEdges is inclusive.
const float hMeasure = static_cast<float>(measuredEdges.right - measuredEdges.left + 1);
const float vMeasure = static_cast<float>(measuredEdges.bottom - measuredEdges.top + 1);
// Prevent drawing until we get the first capture
const bool hasMeasure = (measuredEdges.right != measuredEdges.left) && (measuredEdges.bottom != measuredEdges.top);
if (!hasMeasure)
{
return;
}
if (!continuousCapture && backgroundBitmap)
{
d2dState.rt->DrawBitmap(backgroundBitmap.get());
}
const auto previousAliasingMode = d2dState.rt->GetAntialiasMode();
// Anti-aliasing is creating artifacts. Aliasing is for drawing straight lines.
d2dState.rt->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
const auto cursorPos = convert::FromSystemToRelativeForDirect2D(window, commonState.cursorPosSystemSpace);
if (drawHorizontalCrossLine)
{
const D2D_POINT_2F hLineStart{ .x = static_cast<float>(measuredEdges.left), .y = static_cast<float>(cursorPos.y) };
D2D_POINT_2F hLineEnd{ .x = hLineStart.x + hMeasure, .y = hLineStart.y };
d2dState.rt->DrawLine(hLineStart, hLineEnd, d2dState.solidBrushes[Brush::line].get());
if (drawFeetOnCross && !continuousCapture)
{
// To fill all pixels which are close, we call DrawLine with end point one pixel too far, since
// it doesn't get filled, i.e. end point of the range is excluded. However, we want to draw cross
// feet *on* the last pixel row, so we must subtract 1px from the corresponding axis.
hLineEnd.x -= 1.f;
auto [left_start, left_end] = ComputeCrossFeetLine(hLineStart, false);
auto [right_start, right_end] = ComputeCrossFeetLine(hLineEnd, false);
d2dState.rt->DrawLine(left_start, left_end, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->DrawLine(right_start, right_end, d2dState.solidBrushes[Brush::line].get());
}
}
if (drawVerticalCrossLine)
{
const D2D_POINT_2F vLineStart{ .x = static_cast<float>(cursorPos.x), .y = static_cast<float>(measuredEdges.top) };
D2D_POINT_2F vLineEnd{ .x = vLineStart.x, .y = vLineStart.y + vMeasure };
d2dState.rt->DrawLine(vLineStart, vLineEnd, d2dState.solidBrushes[Brush::line].get());
if (drawFeetOnCross && !continuousCapture)
{
vLineEnd.y -= 1.f;
auto [top_start, top_end] = ComputeCrossFeetLine(vLineStart, true);
auto [bottom_start, bottom_end] = ComputeCrossFeetLine(vLineEnd, true);
d2dState.rt->DrawLine(top_start, top_end, d2dState.solidBrushes[Brush::line].get());
d2dState.rt->DrawLine(bottom_start, bottom_end, d2dState.solidBrushes[Brush::line].get());
}
}
// After drawing the lines, restore anti aliasing to draw the measurement tooltip.
d2dState.rt->SetAntialiasMode(previousAliasingMode);
uint32_t measureStringBufLen = 0;
OverlayBoxText text;
std::optional<size_t> crossSymbolPos;
switch (mode)
{
case MeasureToolState::Mode::Cross:
measureStringBufLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f × %.0f",
hMeasure,
vMeasure);
crossSymbolPos = wcschr(text.buffer.data(), L' ') - text.buffer.data() + 1;
break;
case MeasureToolState::Mode::Vertical:
measureStringBufLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f",
vMeasure);
break;
case MeasureToolState::Mode::Horizontal:
measureStringBufLen = swprintf_s(text.buffer.data(),
text.buffer.size(),
L"%.0f",
hMeasure);
break;
}
commonState.overlayBoxText.Access([&](OverlayBoxText& v) {
v = text;
});
d2dState.DrawTextBox(text.buffer.data(),
measureStringBufLen,
crossSymbolPos,
static_cast<float>(cursorPos.x),
static_cast<float>(cursorPos.y),
true,
window);
}

View File

@ -0,0 +1,12 @@
#pragma once
#include "D2DState.h"
#include "ToolState.h"
#include <common/utils/serialized.h>
void DrawMeasureToolTick(const CommonState& commonState,
Serialized<MeasureToolState>& toolState,
HWND overlayWindow,
D2DState& d2dState);
LRESULT CALLBACK MeasureToolWndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept;

View File

@ -0,0 +1,231 @@
#include "pch.h"
#include "BoundsToolOverlayUI.h"
#include "MeasureToolOverlayUI.h"
#include "OverlayUI.h"
#include <common/Display/dpi_aware.h>
#include <common/Display/monitors.h>
#include <common/logger/logger.h>
#include <common/Themes/windows_colors.h>
#include <common/utils/window.h>
namespace NonLocalizable
{
const wchar_t MeasureToolOverlayWindowName[] = L"PowerToys.MeasureToolOverlayWindow";
const wchar_t BoundsToolOverlayWindowName[] = L"PowerToys.BoundsToolOverlayWindow";
}
void CreateOverlayWindowClasses()
{
WNDCLASSEXW wcex{ .cbSize = sizeof(WNDCLASSEX), .hInstance = GetModuleHandleW(nullptr) };
wcex.lpfnWndProc = MeasureToolWndProc;
wcex.lpszClassName = NonLocalizable::MeasureToolOverlayWindowName;
RegisterClassExW(&wcex);
wcex.lpfnWndProc = BoundsToolWndProc;
wcex.lpszClassName = NonLocalizable::BoundsToolOverlayWindowName;
wcex.hCursor = LoadCursorW(nullptr, IDC_CROSS);
RegisterClassExW(&wcex);
}
HWND CreateOverlayUIWindow(const CommonState& commonState,
const MonitorInfo& monitor,
const wchar_t* windowClass,
void* extraParam)
{
static std::once_flag windowClassesCreatedFlag;
std::call_once(windowClassesCreatedFlag, CreateOverlayWindowClasses);
const auto screenArea = monitor.GetScreenSize(true);
HWND window{ CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
windowClass,
L"PowerToys.MeasureToolOverlay",
WS_POPUP,
screenArea.left(),
screenArea.top(),
screenArea.width(),
screenArea.height(),
HWND_DESKTOP,
nullptr,
GetModuleHandleW(nullptr),
extraParam) };
winrt::check_bool(window);
ShowWindow(window, SW_SHOWNORMAL);
#if !defined(DEBUG_OVERLAY)
SetWindowPos(window, HWND_TOPMOST, {}, {}, {}, {}, SWP_NOMOVE | SWP_NOSIZE);
#else
(void)window;
#endif
const int pos = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
if (wil::unique_hrgn hrgn{ CreateRectRgn(pos, 0, (pos + 1), 1) })
{
DWM_BLURBEHIND bh = { DWM_BB_ENABLE | DWM_BB_BLURREGION, TRUE, hrgn.get(), FALSE };
DwmEnableBlurBehindWindow(window, &bh);
}
RECT windowRect = {};
// Exclude toolbar from the window's region to be able to use toolbar during tool usage.
if (monitor.IsPrimary() && GetWindowRect(window, &windowRect))
{
// will be freed during SetWindowRgn call
const HRGN windowRegion{ CreateRectRgn(windowRect.left, windowRect.top, windowRect.right, windowRect.bottom) };
wil::unique_hrgn toolbarRegion{ CreateRectRgn(commonState.toolbarBoundingBox.left(),
commonState.toolbarBoundingBox.top(),
commonState.toolbarBoundingBox.right(),
commonState.toolbarBoundingBox.bottom()) };
const auto res = CombineRgn(windowRegion, windowRegion, toolbarRegion.get(), RGN_DIFF);
if (res != ERROR)
SetWindowRgn(window, windowRegion, true);
}
return window;
}
std::vector<D2D1::ColorF> AppendCommonOverlayUIColors(const D2D1::ColorF& lineColor)
{
D2D1::ColorF foreground = D2D1::ColorF::Black;
D2D1::ColorF background = D2D1::ColorF(0.96f, 0.96f, 0.96f, 1.0f);
D2D1::ColorF border = D2D1::ColorF(0.44f, 0.44f, 0.44f, 0.4f);
if (WindowsColors::is_dark_mode())
{
foreground = D2D1::ColorF::White;
background = D2D1::ColorF(0.17f, 0.17f, 0.17f, 1.0f);
border = D2D1::ColorF(0.44f, 0.44f, 0.44f, 0.4f);
}
return { lineColor, foreground, background, border };
}
void OverlayUIState::RunUILoop()
{
while (IsWindow(_window) && !_commonState.closeOnOtherMonitors)
{
const auto cursor = _commonState.cursorPosSystemSpace;
const bool cursorOnScreen = _monitorArea.inside(cursor);
const bool cursorOverToolbar = _commonState.toolbarBoundingBox.inside(cursor);
if (cursorOnScreen != _cursorOnScreen)
{
_cursorOnScreen = cursorOnScreen;
if (!cursorOnScreen)
{
if (_clearOnCursorLeavingScreen)
{
_d2dState.rt->BeginDraw();
_d2dState.rt->Clear();
_d2dState.rt->EndDraw();
}
PostMessageW(_window, WM_CURSOR_LEFT_MONITOR, {}, {});
}
}
if (cursorOnScreen)
{
_d2dState.rt->BeginDraw();
if (!cursorOverToolbar)
_tickFunc();
else
_d2dState.rt->Clear();
{
// TODO: use latch to wait until all threads are created their corresponding d2d textures
// in the non-continuous mode
// std::lock_guard guard{ gpuAccessLock };
_d2dState.rt->EndDraw();
}
}
run_message_loop(true, 1);
}
DestroyWindow(_window);
}
template<typename StateT, typename TickFuncT>
OverlayUIState::OverlayUIState(StateT& toolState,
TickFuncT tickFunc,
const CommonState& commonState,
HWND window) :
_window{ window },
_commonState{ commonState },
_d2dState{ window, AppendCommonOverlayUIColors(commonState.lineColor) },
_tickFunc{ [this, tickFunc, &toolState] {
tickFunc(_commonState, toolState, _window, _d2dState);
} }
{
}
OverlayUIState::~OverlayUIState()
{
PostMessageW(_window, WM_CLOSE, {}, {});
try
{
if (_uiThread.joinable())
_uiThread.join();
}
catch (...)
{
}
}
// Returning unique_ptr, since we need to pin ui state in memory
template<typename ToolT, typename TickFuncT>
inline std::unique_ptr<OverlayUIState> OverlayUIState::CreateInternal(ToolT& toolState,
TickFuncT tickFunc,
CommonState& commonState,
const wchar_t* toolWindowClassName,
void* windowParam,
const MonitorInfo& monitor,
const bool clearOnCursorLeavingScreen)
{
wil::shared_event uiCreatedEvent(wil::EventOptions::ManualReset);
std::unique_ptr<OverlayUIState> uiState;
auto threadHandle = SpawnLoggedThread(L"OverlayUI thread", [&] {
const HWND window = CreateOverlayUIWindow(commonState, monitor, toolWindowClassName, windowParam);
uiState = std::unique_ptr<OverlayUIState>{ new OverlayUIState{ toolState, tickFunc, commonState, window } };
uiState->_monitorArea = monitor.GetScreenSize(true);
uiState->_clearOnCursorLeavingScreen = clearOnCursorLeavingScreen;
// we must create window + d2d state in the same thread, then store thread handle in uiState, thus
// lifetime is ok here, since we join the thread in destructor
auto* state = uiState.get();
uiCreatedEvent.SetEvent();
state->RunUILoop();
commonState.closeOnOtherMonitors = true;
commonState.sessionCompletedCallback();
});
uiCreatedEvent.wait();
uiState->_uiThread = std::move(threadHandle);
return uiState;
}
std::unique_ptr<OverlayUIState> OverlayUIState::Create(Serialized<MeasureToolState>& toolState,
CommonState& commonState,
const MonitorInfo& monitor)
{
return OverlayUIState::CreateInternal(toolState,
DrawMeasureToolTick,
commonState,
NonLocalizable::MeasureToolOverlayWindowName,
&toolState,
monitor,
true);
}
std::unique_ptr<OverlayUIState> OverlayUIState::Create(BoundsToolState& toolState,
CommonState& commonState,
const MonitorInfo& monitor)
{
return OverlayUIState::CreateInternal(toolState,
DrawBoundsToolTick,
commonState,
NonLocalizable::BoundsToolOverlayWindowName,
&toolState,
monitor,
false);
}

View File

@ -0,0 +1,51 @@
#pragma once
#include "D2DState.h"
#include "ToolState.h"
#include <common/display/monitors.h>
#include <common/utils/serialized.h>
class OverlayUIState final
{
template<typename StateT, typename TickFuncT>
OverlayUIState(StateT& toolState,
TickFuncT tickFunc,
const CommonState& commonState,
HWND window);
Box _monitorArea;
HWND _window = {};
const CommonState& _commonState;
D2DState _d2dState;
std::function<void()> _tickFunc;
std::thread _uiThread;
bool _cursorOnScreen = true;
bool _clearOnCursorLeavingScreen = false;
template<typename ToolT, typename TickFuncT>
static std::unique_ptr<OverlayUIState> CreateInternal(ToolT& toolState,
TickFuncT tickFunc,
CommonState& commonState,
const wchar_t* toolWindowClassName,
void* windowParam,
const MonitorInfo& monitor,
const bool clearOnCursorLeavingScreen);
public:
OverlayUIState(OverlayUIState&&) noexcept = default;
~OverlayUIState();
static std::unique_ptr<OverlayUIState> Create(BoundsToolState& toolState,
CommonState& commonState,
const MonitorInfo& monitor);
static std::unique_ptr<OverlayUIState> Create(Serialized<MeasureToolState>& toolState,
CommonState& commonState,
const MonitorInfo& monitor);
inline HWND overlayWindowHandle() const
{
return _window;
}
void RunUILoop();
};

View File

@ -0,0 +1,141 @@
#include "pch.h"
#include "PerGlyphOpacityTextRender.h"
PerGlyphOpacityTextRender::PerGlyphOpacityTextRender(
wil::com_ptr<ID2D1Factory> pD2DFactory,
wil::com_ptr<ID2D1HwndRenderTarget> rt,
wil::com_ptr<ID2D1SolidColorBrush> baseBrush) :
_pD2DFactory{ pD2DFactory },
_rt{ rt },
_baseBrush{ baseBrush }
{
}
HRESULT __stdcall PerGlyphOpacityTextRender::DrawGlyphRun(void* /*clientDrawingContext*/,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/,
IUnknown* clientDrawingEffect) noexcept
{
HRESULT hr = S_OK;
if (!clientDrawingEffect)
{
_rt->DrawGlyphRun(D2D1_POINT_2F{ .x = baselineOriginX, .y = baselineOriginY }, glyphRun, _baseBrush.get(), measuringMode);
return hr;
}
// Create the path geometry.
wil::com_ptr<ID2D1PathGeometry> pathGeometry;
hr = _pD2DFactory->CreatePathGeometry(&pathGeometry);
// Write to the path geometry using the geometry sink.
ID2D1GeometrySink* pSink = nullptr;
if (SUCCEEDED(hr))
{
hr = pathGeometry->Open(&pSink);
}
// Get the glyph run outline geometries back from DirectWrite and place them within the
// geometry sink.
if (SUCCEEDED(hr))
{
hr = glyphRun->fontFace->GetGlyphRunOutline(
glyphRun->fontEmSize,
glyphRun->glyphIndices,
glyphRun->glyphAdvances,
glyphRun->glyphOffsets,
glyphRun->glyphCount,
glyphRun->isSideways,
glyphRun->bidiLevel % 2,
pSink);
}
// Close the geometry sink
if (SUCCEEDED(hr))
{
hr = pSink->Close();
}
// Initialize a matrix to translate the origin of the glyph run.
D2D1::Matrix3x2F const matrix = D2D1::Matrix3x2F(
1.0f, 0.0f, 0.0f, 1.0f, baselineOriginX, baselineOriginY);
// Create the transformed geometry
wil::com_ptr<ID2D1TransformedGeometry> pTransformedGeometry;
if (SUCCEEDED(hr))
{
hr = _pD2DFactory->CreateTransformedGeometry(pathGeometry.get(), &matrix, &pTransformedGeometry);
}
float prevOpacity = _baseBrush->GetOpacity();
OpacityEffect* opacityEffect = nullptr;
if (SUCCEEDED(hr))
{
hr = clientDrawingEffect->QueryInterface(__uuidof(IDrawingEffect), reinterpret_cast<void**>(&opacityEffect));
}
if (SUCCEEDED(hr))
{
_baseBrush->SetOpacity(opacityEffect->alpha);
}
if (SUCCEEDED(hr))
{
_rt->DrawGeometry(pTransformedGeometry.get(), _baseBrush.get());
_rt->FillGeometry(pTransformedGeometry.get(), _baseBrush.get());
_baseBrush->SetOpacity(prevOpacity);
}
return hr;
}
HRESULT __stdcall PerGlyphOpacityTextRender::DrawUnderline(void* /*clientDrawingContext*/,
FLOAT /*baselineOriginX*/,
FLOAT /*baselineOriginY*/,
_In_ const DWRITE_UNDERLINE* /*underline*/,
IUnknown* /*clientDrawingEffect*/) noexcept
{
return E_NOTIMPL;
}
HRESULT __stdcall PerGlyphOpacityTextRender::DrawStrikethrough(void* /*clientDrawingContext*/,
FLOAT /*baselineOriginX*/,
FLOAT /*baselineOriginY*/,
_In_ const DWRITE_STRIKETHROUGH* /*strikethrough*/,
IUnknown* /*clientDrawingEffect*/) noexcept
{
return E_NOTIMPL;
}
HRESULT __stdcall PerGlyphOpacityTextRender::DrawInlineObject(void* /*clientDrawingContext*/,
FLOAT /*originX*/,
FLOAT /*originY*/,
IDWriteInlineObject* /*inlineObject*/,
BOOL /*isSideways*/,
BOOL /*isRightToLeft*/,
IUnknown* /*clientDrawingEffect*/) noexcept
{
return E_NOTIMPL;
}
HRESULT __stdcall PerGlyphOpacityTextRender::IsPixelSnappingDisabled(void* /*clientDrawingContext*/, BOOL* isDisabled) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, isDisabled);
*isDisabled = false;
return S_OK;
}
HRESULT __stdcall PerGlyphOpacityTextRender::GetCurrentTransform(void* /*clientDrawingContext*/, DWRITE_MATRIX* transform) noexcept
{
_rt->GetTransform(reinterpret_cast<D2D1_MATRIX_3X2_F*>(transform));
return S_OK;
}
HRESULT __stdcall PerGlyphOpacityTextRender::GetPixelsPerDip(void* /*clientDrawingContext*/, FLOAT* pixelsPerDip) noexcept
{
_rt->GetDpi(pixelsPerDip, pixelsPerDip);
return S_OK;
}

View File

@ -0,0 +1,55 @@
#pragma once
#include <winrt/base.h>
#include <wil/resource.h>
#include <Windows.h>
#include <dwrite.h>
struct __declspec(uuid("{01557C9F-E3DD-4C28-AE64-E731EAB479CC}")) IDrawingEffect : IUnknown
{
};
struct OpacityEffect : winrt::implements<OpacityEffect, IDrawingEffect>
{
float alpha = 1.f;
};
struct PerGlyphOpacityTextRender : winrt::implements<PerGlyphOpacityTextRender, IDWriteTextRenderer>
{
wil::com_ptr<ID2D1Factory> _pD2DFactory;
wil::com_ptr<ID2D1HwndRenderTarget> _rt;
wil::com_ptr<ID2D1SolidColorBrush> _baseBrush;
PerGlyphOpacityTextRender(
wil::com_ptr<ID2D1Factory> pD2DFactory,
wil::com_ptr<ID2D1HwndRenderTarget> rt,
wil::com_ptr<ID2D1SolidColorBrush> baseBrush);
HRESULT __stdcall DrawGlyphRun(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
IUnknown* clientDrawingEffect) noexcept override;
HRESULT __stdcall DrawUnderline(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
_In_ const DWRITE_UNDERLINE* underline,
IUnknown* clientDrawingEffect) noexcept override;
HRESULT __stdcall DrawStrikethrough(void* clientDrawingContext,
FLOAT baselineOriginX,
FLOAT baselineOriginY,
_In_ const DWRITE_STRIKETHROUGH* strikethrough,
IUnknown* clientDrawingEffect) noexcept override;
HRESULT __stdcall DrawInlineObject(void* clientDrawingContext,
FLOAT originX,
FLOAT originY,
IDWriteInlineObject* inlineObject,
BOOL isSideways,
BOOL isRightToLeft,
IUnknown* clientDrawingEffect) noexcept override;
HRESULT __stdcall IsPixelSnappingDisabled(void* clientDrawingContext, BOOL* isDisabled) noexcept override;
HRESULT __stdcall GetCurrentTransform(void* clientDrawingContext, DWRITE_MATRIX* transform) noexcept override;
HRESULT __stdcall GetPixelsPerDip(void* clientDrawingContext, FLOAT* pixelsPerDip) noexcept override;
};

View File

@ -0,0 +1,156 @@
#include "pch.h"
#include <common/display/dpi_aware.h>
#include <common/display/monitors.h>
#include <common/utils/logger_helper.h>
#include <common/logger/logger.h>
#include "../MeasureToolModuleInterface/trace.h"
#include "constants.h"
#include "PowerToys.MeasureToolCore.h"
#include "Core.g.cpp"
#include "OverlayUI.h"
#include "ScreenCapturing.h"
//#define DEBUG_PRIMARY_MONITOR_ONLY
std::recursive_mutex gpuAccessLock;
namespace winrt::PowerToys::MeasureToolCore::implementation
{
void Core::MouseCaptureThread()
{
while (!_stopMouseCaptureThreadSignal.is_signaled())
{
static_assert(sizeof(_commonState.cursorPosSystemSpace) == sizeof(LONG64));
POINT cursorPos = {};
GetCursorPos(&cursorPos);
InterlockedExchange64(reinterpret_cast<LONG64*>(&_commonState.cursorPosSystemSpace), std::bit_cast<LONG64>(cursorPos));
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION);
}
}
Core::Core() :
_mouseCaptureThread{ [this] { MouseCaptureThread(); } },
_stopMouseCaptureThreadSignal{ wil::EventOptions::ManualReset }
{
Trace::RegisterProvider();
LoggerHelpers::init_logger(L"Measure Tool", L"Core", "Measure Tool");
}
Core::~Core()
{
_stopMouseCaptureThreadSignal.SetEvent();
_mouseCaptureThread.join();
ResetState();
Trace::UnregisterProvider();
}
void Core::ResetState()
{
_commonState.closeOnOtherMonitors = true;
_overlayUIStates.clear();
_boundsToolState = { .commonState = &_commonState };
for (auto& thread : _screenCaptureThreads)
{
if (thread.joinable())
{
thread.join();
}
}
_screenCaptureThreads.clear();
_measureToolState.Reset();
_measureToolState.Access([&](MeasureToolState& s) {
s.commonState = &_commonState;
});
_settings = Settings::LoadFromFile();
_commonState.lineColor.r = _settings.lineColor[0] / 255.f;
_commonState.lineColor.g = _settings.lineColor[1] / 255.f;
_commonState.lineColor.b = _settings.lineColor[2] / 255.f;
_commonState.closeOnOtherMonitors = false;
}
void Core::StartBoundsTool()
{
ResetState();
#if defined(DEBUG_PRIMARY_MONITOR_ONLY)
const auto& monitorInfo = MonitorInfo::GetPrimaryMonitor();
#else
for (const auto& monitorInfo : MonitorInfo::GetMonitors(true))
#endif
{
auto overlayUI = OverlayUIState::Create(_boundsToolState, _commonState, monitorInfo);
#if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
if (!overlayUI)
continue;
#endif
_overlayUIStates.push_back(std::move(overlayUI));
}
Trace::BoundsToolActivated();
}
void Core::StartMeasureTool(const bool horizontal, const bool vertical)
{
ResetState();
_measureToolState.Access([horizontal, vertical, this](MeasureToolState& state) {
if (horizontal)
state.global.mode = vertical ? MeasureToolState::Mode::Cross : MeasureToolState::Mode::Horizontal;
else
state.global.mode = MeasureToolState::Mode::Vertical;
state.global.continuousCapture = _settings.continuousCapture;
state.global.drawFeetOnCross = _settings.drawFeetOnCross;
state.global.pixelTolerance = _settings.pixelTolerance;
state.global.perColorChannelEdgeDetection = _settings.perColorChannelEdgeDetection;
});
#if defined(DEBUG_PRIMARY_MONITOR_ONLY)
const auto& monitorInfo = MonitorInfo::GetPrimaryMonitor();
#else
for (const auto& monitorInfo : MonitorInfo::GetMonitors(true))
#endif
{
auto overlayUI = OverlayUIState::Create(_measureToolState, _commonState, monitorInfo);
#if !defined(DEBUG_PRIMARY_MONITOR_ONLY)
if (!overlayUI)
continue;
#endif
_screenCaptureThreads.emplace_back(StartCapturingThread(_commonState,
_measureToolState,
overlayUI->overlayWindowHandle(),
monitorInfo));
_overlayUIStates.push_back(std::move(overlayUI));
}
Trace::MeasureToolActivated();
}
void MeasureToolCore::implementation::Core::SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger)
{
_commonState.sessionCompletedCallback = [trigger = std::move(sessionCompletedTrigger)] {
trigger();
};
}
void MeasureToolCore::implementation::Core::SetToolbarBoundingBox(const uint32_t fromX,
const uint32_t fromY,
const uint32_t toX,
const uint32_t toY)
{
_commonState.toolbarBoundingBox = Box{ RECT{ .left = static_cast<long>(fromX),
.top = static_cast<long>(fromY),
.right = static_cast<long>(toX),
.bottom = static_cast<long>(toY) } };
}
float MeasureToolCore::implementation::Core::GetDPIScaleForWindow(uint64_t windowHandle)
{
UINT dpi = DPIAware::DEFAULT_DPI;
DPIAware::GetScreenDPIForWindow(std::bit_cast<HWND>(windowHandle), dpi);
return static_cast<float>(dpi) / DPIAware::DEFAULT_DPI;
}
}

View File

@ -0,0 +1,41 @@
#pragma once
#include "Core.g.h"
#include "ToolState.h"
#include "OverlayUI.h"
#include "Settings.h"
#include <common/utils/serialized.h>
namespace winrt::PowerToys::MeasureToolCore::implementation
{
struct Core : CoreT<Core>
{
Core();
~Core();
void StartBoundsTool();
void StartMeasureTool(const bool horizontal, const bool vertical);
void SetToolCompletionEvent(ToolSessionCompleted sessionCompletedTrigger);
void SetToolbarBoundingBox(const uint32_t fromX, const uint32_t fromY, const uint32_t toX, const uint32_t toY);
void ResetState();
float GetDPIScaleForWindow(uint64_t windowHandle);
void MouseCaptureThread();
std::thread _mouseCaptureThread;
std::vector<std::thread> _screenCaptureThreads;
wil::shared_event _stopMouseCaptureThreadSignal;
std::vector<std::unique_ptr<OverlayUIState>> _overlayUIStates;
Serialized<MeasureToolState> _measureToolState;
BoundsToolState _boundsToolState;
CommonState _commonState;
Settings _settings;
};
}
namespace winrt::PowerToys::MeasureToolCore::factory_implementation
{
struct Core : CoreT<Core, implementation::Core>
{
};
}

View File

@ -0,0 +1,26 @@
namespace PowerToys
{
namespace MeasureToolCore
{
struct Point
{
Int32 X;
Int32 Y;
};
delegate void ToolSessionCompleted();
[default_interface]
runtimeclass Core
{
Core();
void SetToolCompletionEvent(event ToolSessionCompleted completionTrigger);
void StartMeasureTool(Boolean horizontal, Boolean vertical);
void StartBoundsTool();
void ResetState();
void SetToolbarBoundingBox(Int32 fromX, Int32 fromY, Int32 toX, Int32 toY);
Single GetDPIScaleForWindow(Int64 windowHandle);
}
}
}

View File

@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@ -0,0 +1,158 @@
<?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.1.4\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.1.4\build\native\Microsoft.WindowsAppSDK.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.props')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
<MinimalCoreWin>true</MinimalCoreWin>
<ProjectGuid>{54a93af7-60c7-4f6c-99d2-fbb1f75f853a}</ProjectGuid>
<ProjectName>PowerToys.MeasureToolCore</ProjectName>
<RootNamespace>PowerToys.MeasureToolCore</RootNamespace>
<!--
$(TargetName) should be same as $(RootNamespace) so that the produced binaries (.exe/.pri/etc.)
have a name that matches the .winmd
-->
<TargetName>PowerToys.MeasureToolCore</TargetName>
<DefaultLanguage>en-US</DefaultLanguage>
<MinimumVisualStudioVersion>16.0</MinimumVisualStudioVersion>
<AppContainerApplication>false</AppContainerApplication>
<AppxPackage>false</AppxPackage>
<ApplicationType>Windows Store</ApplicationType>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.20348.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.19041.0</WindowsTargetPlatformMinVersion>
<UseWinUI>true</UseWinUI>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<GenerateManifest>false</GenerateManifest>
<DesktopCompatible>true</DesktopCompatible>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\MeasureTool\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /bigobj</AdditionalOptions>
<AdditionalIncludeDirectories>$(SolutionDir)src\;..\..\..\common\Telemetry;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<ModuleDefinitionFile>MeasureTool.def</ModuleDefinitionFile>
<AdditionalDependencies>Shell32.lib;Shcore.lib;Dwmapi.lib;Gdi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<Manifest Include="app.manifest" />
</ItemGroup>
<ItemGroup>
<None Include="MeasureToolCore.def" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\MeasureToolModuleInterface\trace.h" />
<ClInclude Include="BoundsToolOverlayUI.h" />
<ClInclude Include="Clipboard.h" />
<ClInclude Include="constants.h" />
<ClInclude Include="D2DState.h" />
<ClInclude Include="MeasureToolOverlayUI.h" />
<ClInclude Include="PerGlyphOpacityTextRender.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="Settings.h" />
<ClInclude Include="PowerToys.MeasureToolCore.h">
<DependentUpon>PowerToys.MeasureToolCore.idl</DependentUpon>
</ClInclude>
<ClInclude Include="BGRATextureView.h" />
<ClInclude Include="EdgeDetection.h" />
<ClInclude Include="ToolState.h" />
<ClInclude Include="OverlayUI.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="ScreenCapturing.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\MeasureToolModuleInterface\trace.cpp" />
<ClCompile Include="BGRATextureView.cpp" />
<ClCompile Include="BoundsToolOverlayUI.cpp" />
<ClCompile Include="Clipboard.cpp" />
<ClCompile Include="D2DState.cpp" />
<ClCompile Include="EdgeDetection.cpp" />
<ClCompile Include="MeasureToolOverlayUI.cpp" />
<ClCompile Include="OverlayUI.cpp" />
<ClCompile Include="PerGlyphOpacityTextRender.cpp" />
<ClCompile Include="PowerToys.MeasureToolCore.cpp">
<DependentUpon>PowerToys.MeasureToolCore.idl</DependentUpon>
</ClCompile>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="ScreenCapturing.cpp" />
<ClCompile Include="Settings.cpp" />
</ItemGroup>
<ItemGroup>
<Midl Include="PowerToys.MeasureToolCore.idl">
<WarningLevel>2</WarningLevel>
<WarnAsError>true</WarnAsError>
</Midl>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Display\Display.vcxproj">
<Project>{caba8dfb-823b-4bf2-93ac-3f31984150d9}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\Themes\Themes.vcxproj">
<Project>{98537082-0fdb-40de-abd8-0dc5a4269bab}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerToys.MeasureToolCore.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.WindowsAppSDK.1.1.4\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.1.4\build\native\Microsoft.WindowsAppSDK.targets')" />
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.SDK.BuildTools.10.0.22000.197\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.1.4\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.1.4\build\native\Microsoft.WindowsAppSDK.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.WindowsAppSDK.1.1.4\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.WindowsAppSDK.1.1.4\build\native\Microsoft.WindowsAppSDK.targets'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.220201.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Midl Include="PowerToys.MeasureToolCore.idl" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp" />
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
<ClCompile Include="ScreenCapturing.cpp" />
<ClCompile Include="OverlayUI.cpp" />
<ClCompile Include="BGRATextureView.cpp" />
<ClCompile Include="EdgeDetection.cpp" />
<ClCompile Include="Settings.cpp" />
<ClCompile Include="D2DState.cpp" />
<ClCompile Include="BoundsToolOverlayUI.cpp" />
<ClCompile Include="MeasureToolOverlayUI.cpp" />
<ClCompile Include="..\MeasureToolModuleInterface\trace.cpp" />
<ClCompile Include="Clipboard.cpp" />
<ClCompile Include="PerGlyphOpacityTextRender.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="ScreenCapturing.h" />
<ClInclude Include="OverlayUI.h" />
<ClInclude Include="BGRATextureView.h" />
<ClInclude Include="EdgeDetection.h" />
<ClInclude Include="ToolState.h" />
<ClInclude Include="Settings.h" />
<ClInclude Include="BoundsToolOverlayUI.h" />
<ClInclude Include="D2DState.h" />
<ClInclude Include="MeasureToolOverlayUI.h" />
<ClInclude Include="constants.h" />
<ClInclude Include="..\MeasureToolModuleInterface\trace.h" />
<ClInclude Include="Clipboard.h" />
<ClInclude Include="PerGlyphOpacityTextRender.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<Filter Include="Assets">
<UniqueIdentifier>{e48dc53e-40b1-40cb-970a-f89935452892}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<Manifest Include="app.manifest" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="MeasureToolCore.def" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerToys.MeasureToolCore.rc" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,443 @@
#include "pch.h"
#include "BGRATextureView.h"
#include "constants.h"
#include "CoordinateSystemConversion.h"
#include "EdgeDetection.h"
#include "ScreenCapturing.h"
#include <common/Display/monitors.h>
//#define DEBUG_EDGES
class MappedTextureView
{
winrt::com_ptr<ID3D11DeviceContext> context;
winrt::com_ptr<ID3D11Texture2D> texture;
public:
BGRATextureView view;
MappedTextureView(winrt::com_ptr<ID3D11Texture2D> _texture,
winrt::com_ptr<ID3D11DeviceContext> _context,
const size_t textureWidth,
const size_t textureHeight) :
texture{ std::move(_texture) }, context{ std::move(_context) }
{
D3D11_TEXTURE2D_DESC desc;
texture->GetDesc(&desc);
D3D11_MAPPED_SUBRESOURCE resource = {};
winrt::check_hresult(context->Map(texture.get(), D3D11CalcSubresource(0, 0, 0), D3D11_MAP_READ, 0, &resource));
view.pixels = static_cast<const uint32_t*>(resource.pData);
view.pitch = resource.RowPitch / 4;
view.width = textureWidth;
view.height = textureHeight;
}
MappedTextureView(MappedTextureView&&) = default;
MappedTextureView& operator=(MappedTextureView&&) = default;
inline winrt::com_ptr<ID3D11Texture2D> GetTexture() const
{
return texture;
}
~MappedTextureView()
{
if (context && texture)
context->Unmap(texture.get(), D3D11CalcSubresource(0, 0, 0));
}
};
class D3DCaptureState final
{
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::IDirect3DDevice device;
winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::com_ptr<ID3D11DeviceContext> context;
winrt::SizeInt32 frameSize;
winrt::DirectXPixelFormat pixelFormat;
winrt::Direct3D11CaptureFramePool framePool;
winrt::GraphicsCaptureSession session;
std::function<void(MappedTextureView)> frameCallback;
Box monitorArea;
bool captureOutsideOfMonitor = false;
D3DCaptureState(winrt::com_ptr<ID3D11Device> d3dDevice,
winrt::IDirect3DDevice _device,
winrt::com_ptr<IDXGISwapChain1> _swapChain,
winrt::com_ptr<ID3D11DeviceContext> _context,
const winrt::GraphicsCaptureItem& item,
winrt::DirectXPixelFormat _pixelFormat,
Box monitorArea,
const bool captureOutsideOfMonitor);
winrt::com_ptr<ID3D11Texture2D> CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& texture);
void OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&);
void StartSessionInPreferredMode();
std::mutex destructorMutex;
public:
static std::unique_ptr<D3DCaptureState> Create(winrt::GraphicsCaptureItem item,
const winrt::DirectXPixelFormat pixelFormat,
Box monitorSize,
const bool captureOutsideOfMonitor);
~D3DCaptureState();
void StartCapture(std::function<void(MappedTextureView)> _frameCallback);
MappedTextureView CaptureSingleFrame();
void StopCapture();
};
D3DCaptureState::D3DCaptureState(winrt::com_ptr<ID3D11Device> _d3dDevice,
winrt::IDirect3DDevice _device,
winrt::com_ptr<IDXGISwapChain1> _swapChain,
winrt::com_ptr<ID3D11DeviceContext> _context,
const winrt::GraphicsCaptureItem& item,
winrt::DirectXPixelFormat _pixelFormat,
Box _monitorArea,
const bool _captureOutsideOfMonitor) :
d3dDevice{ std::move(_d3dDevice) },
device{ std::move(_device) },
swapChain{ std::move(_swapChain) },
context{ std::move(_context) },
frameSize{ item.Size() },
pixelFormat{ std::move(_pixelFormat) },
framePool{ winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(device, pixelFormat, 2, item.Size()) },
session{ framePool.CreateCaptureSession(item) },
monitorArea{ _monitorArea },
captureOutsideOfMonitor{ _captureOutsideOfMonitor }
{
framePool.FrameArrived({ this, &D3DCaptureState::OnFrameArrived });
}
winrt::com_ptr<ID3D11Texture2D> D3DCaptureState::CopyFrameToCPU(const winrt::com_ptr<ID3D11Texture2D>& frameTexture)
{
D3D11_TEXTURE2D_DESC desc = {};
frameTexture->GetDesc(&desc);
desc.Usage = D3D11_USAGE_STAGING;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
desc.MiscFlags = 0;
desc.BindFlags = 0;
winrt::com_ptr<ID3D11Texture2D> cpuTexture;
winrt::check_hresult(d3dDevice->CreateTexture2D(&desc, nullptr, cpuTexture.put()));
context->CopyResource(cpuTexture.get(), frameTexture.get());
return cpuTexture;
}
template<typename T>
auto GetDXGIInterfaceFromObject(winrt::IInspectable const& object)
{
auto access = object.as<Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>();
winrt::com_ptr<T> result;
winrt::check_hresult(access->GetInterface(winrt::guid_of<T>(), result.put_void()));
return result;
}
void D3DCaptureState::OnFrameArrived(const winrt::Direct3D11CaptureFramePool& sender, const winrt::IInspectable&)
{
// Prevent calling a callback on a partially destroyed state
std::unique_lock callbackLock{ destructorMutex };
bool resized = false;
POINT cursorPos = {};
GetCursorPos(&cursorPos);
auto frame = sender.TryGetNextFrame();
winrt::check_bool(frame);
if (monitorArea.inside(cursorPos) || captureOutsideOfMonitor)
{
winrt::com_ptr<ID3D11Texture2D> texture;
{
if (auto newFrameSize = frame.ContentSize(); newFrameSize != frameSize)
{
winrt::check_hresult(swapChain->ResizeBuffers(2,
static_cast<uint32_t>(newFrameSize.Height),
static_cast<uint32_t>(newFrameSize.Width),
static_cast<DXGI_FORMAT>(pixelFormat),
0));
frameSize = newFrameSize;
resized = true;
}
winrt::check_hresult(swapChain->GetBuffer(0, winrt::guid_of<ID3D11Texture2D>(), texture.put_void()));
auto gpuTexture = GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
texture = CopyFrameToCPU(gpuTexture);
MappedTextureView textureView{ texture, context, static_cast<size_t>(frameSize.Width), static_cast<size_t>(frameSize.Height) };
frameCallback(std::move(textureView));
}
}
frame.Close();
DXGI_PRESENT_PARAMETERS presentParameters = {};
swapChain->Present1(1, 0, &presentParameters);
if (resized)
{
framePool.Recreate(device, pixelFormat, 2, frameSize);
}
}
std::unique_ptr<D3DCaptureState> D3DCaptureState::Create(winrt::GraphicsCaptureItem item,
const winrt::DirectXPixelFormat pixelFormat,
Box monitorArea,
const bool captureOutsideOfMonitor)
{
std::lock_guard guard{ gpuAccessLock };
winrt::com_ptr<ID3D11Device> d3dDevice;
UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifndef NDEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
HRESULT hr =
D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_HARDWARE,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3dDevice.put(),
nullptr,
nullptr);
if (hr == DXGI_ERROR_UNSUPPORTED)
{
hr = D3D11CreateDevice(nullptr,
D3D_DRIVER_TYPE_WARP,
nullptr,
flags,
nullptr,
0,
D3D11_SDK_VERSION,
d3dDevice.put(),
nullptr,
nullptr);
}
winrt::check_hresult(hr);
auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
winrt::com_ptr<IInspectable> d3dDeviceInspectable;
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), d3dDeviceInspectable.put()));
const DXGI_SWAP_CHAIN_DESC1 desc = {
.Width = static_cast<uint32_t>(item.Size().Width),
.Height = static_cast<uint32_t>(item.Size().Height),
.Format = static_cast<DXGI_FORMAT>(pixelFormat),
.SampleDesc = { .Count = 1, .Quality = 0 },
.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
.BufferCount = 2,
.Scaling = DXGI_SCALING_STRETCH,
.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL,
.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED,
};
winrt::com_ptr<IDXGIAdapter> adapter;
winrt::check_hresult(dxgiDevice->GetParent(winrt::guid_of<IDXGIAdapter>(), adapter.put_void()));
winrt::com_ptr<IDXGIFactory2> factory;
winrt::check_hresult(adapter->GetParent(winrt::guid_of<IDXGIFactory2>(), factory.put_void()));
winrt::com_ptr<IDXGISwapChain1> swapChain;
winrt::check_hresult(factory->CreateSwapChainForComposition(d3dDevice.get(), &desc, nullptr, swapChain.put()));
winrt::com_ptr<ID3D11DeviceContext> context;
d3dDevice->GetImmediateContext(context.put());
winrt::check_bool(context);
// We must create the object in a heap, since we need to pin it in memory to receive callbacks
auto statePtr = new D3DCaptureState{ d3dDevice,
d3dDeviceInspectable.as<winrt::IDirect3DDevice>(),
std::move(swapChain),
std::move(context),
item,
pixelFormat,
monitorArea,
captureOutsideOfMonitor };
return std::unique_ptr<D3DCaptureState>{ statePtr };
}
D3DCaptureState::~D3DCaptureState()
{
std::unique_lock callbackLock{ destructorMutex };
StopCapture();
framePool.Close();
device.Close();
}
void D3DCaptureState::StartSessionInPreferredMode()
{
// Try disable border if possible (available on Windows ver >= 20348)
if (auto session3 = session.try_as<winrt::IGraphicsCaptureSession3>())
{
session3.IsBorderRequired(false);
}
session.IsCursorCaptureEnabled(false);
session.StartCapture();
}
void D3DCaptureState::StartCapture(std::function<void(MappedTextureView)> _frameCallback)
{
frameCallback = std::move(_frameCallback);
StartSessionInPreferredMode();
}
MappedTextureView D3DCaptureState::CaptureSingleFrame()
{
std::optional<MappedTextureView> result;
wil::shared_event frameArrivedEvent(wil::EventOptions::ManualReset);
frameCallback = [frameArrivedEvent, &result, this](MappedTextureView tex) {
if (result)
return;
StopCapture();
result.emplace(std::move(tex));
frameArrivedEvent.SetEvent();
};
std::lock_guard guard{ gpuAccessLock };
StartSessionInPreferredMode();
frameArrivedEvent.wait();
assert(result.has_value());
return std::move(*result);
}
void D3DCaptureState::StopCapture()
{
session.Close();
}
void UpdateCaptureState(const CommonState& commonState,
Serialized<MeasureToolState>& state,
HWND window,
const MappedTextureView& textureView,
const bool continuousCapture)
{
const auto cursorPos = convert::FromSystemToRelative(window, commonState.cursorPosSystemSpace);
const bool cursorInLeftScreenHalf = cursorPos.x < textureView.view.width / 2;
const bool cursorInTopScreenHalf = cursorPos.y < textureView.view.height / 2;
uint8_t pixelTolerance = {};
bool perColorChannelEdgeDetection = {};
state.Access([&](MeasureToolState& state) {
state.perScreen[window].cursorInLeftScreenHalf = cursorInLeftScreenHalf;
state.perScreen[window].cursorInTopScreenHalf = cursorInTopScreenHalf;
pixelTolerance = state.global.pixelTolerance;
perColorChannelEdgeDetection = state.global.perColorChannelEdgeDetection;
});
// Every one of 4 edges is a coordinate of the last similar pixel in a row
// Example: given a 5x5 green square on a blue background with its top-left pixel
// at 20x100, bounds should be [20,100]-[24,104]. We don't include [25,105] or
// [19,99], since those pixels are blue. Thus, square dims are equal to
// [24-20+1,104-100+1]=[5,5].
const RECT bounds = DetectEdges(textureView.view,
cursorPos,
perColorChannelEdgeDetection,
pixelTolerance,
continuousCapture);
#if defined(DEBUG_EDGES)
char buffer[256];
sprintf_s(buffer,
"Cursor: [%ld,%ld] Bounds: [%ld,%ld]-[%ld,%ld] Screen size: [%zu, %zu]\n",
cursorPos.x,
cursorPos.y,
bounds.left,
bounds.top,
bounds.right,
bounds.bottom,
textureView.view.width,
textureView.view.height);
OutputDebugStringA(buffer);
#endif
state.Access([&](MeasureToolState& state) {
state.perScreen[window].measuredEdges = bounds;
});
}
std::thread StartCapturingThread(const CommonState& commonState,
Serialized<MeasureToolState>& state,
HWND window,
MonitorInfo targetMonitor)
{
return SpawnLoggedThread(L"Screen Capture thread", [&state, &commonState, targetMonitor, window] {
auto captureInterop = winrt::get_activation_factory<
winrt::GraphicsCaptureItem,
IGraphicsCaptureItemInterop>();
winrt::GraphicsCaptureItem item = nullptr;
winrt::check_hresult(captureInterop->CreateForMonitor(
targetMonitor.GetHandle(),
winrt::guid_of<winrt::GraphicsCaptureItem>(),
winrt::put_abi(item)));
bool continuousCapture = {};
state.Read([&](const MeasureToolState& state) {
continuousCapture = state.global.continuousCapture;
});
const auto monitorArea = targetMonitor.GetScreenSize(true);
auto captureState = D3DCaptureState::Create(item,
winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized,
monitorArea,
!continuousCapture);
if (continuousCapture)
{
captureState->StartCapture([&, window](MappedTextureView textureView) {
UpdateCaptureState(commonState, state, window, textureView, continuousCapture);
});
while (IsWindow(window) && !commonState.closeOnOtherMonitors)
{
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION);
}
captureState->StopCapture();
}
else
{
const auto textureView = captureState->CaptureSingleFrame();
state.Access([&](MeasureToolState& s) {
s.perScreen[window].capturedScreenTexture = textureView.GetTexture();
});
while (IsWindow(window) && !commonState.closeOnOtherMonitors)
{
const auto now = std::chrono::high_resolution_clock::now();
if (monitorArea.inside(commonState.cursorPosSystemSpace))
{
#if defined(DEBUG_TEXTURE)
SYSTEMTIME lt{};
GetLocalTime(&lt);
char buf[256];
sprintf_s(buf, "frame-%02d-%02d-Monitor-%zu.bmp", lt.wHour, lt.wMinute, (uint64_t)window);
auto path = std::filesystem::temp_directory_path() / buf;
textureView.view.SaveAsBitmap(path.string().c_str());
#endif
UpdateCaptureState(commonState, state, window, textureView, continuousCapture);
}
const auto frameTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - now);
if (frameTime < consts::TARGET_FRAME_DURATION)
{
std::this_thread::sleep_for(consts::TARGET_FRAME_DURATION - frameTime);
}
}
}
});
}

View File

@ -0,0 +1,10 @@
#pragma once
#include "ToolState.h"
#include <common/utils/serialized.h>
std::thread StartCapturingThread(const CommonState& commonState,
Serialized<MeasureToolState>& state,
HWND targetWindow,
MonitorInfo targetMonitor);

View File

@ -0,0 +1,74 @@
#include "pch.h"
#include <common/SettingsAPI/settings_helpers.h>
#include <common/utils/color.h>
#include "Settings.h"
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_CONTINUOUS_CAPTURE[] = L"ContinuousCapture";
const wchar_t JSON_KEY_DRAW_FEET_ON_CROSS[] = L"DrawFeetOnCross";
const wchar_t JSON_KEY_PIXEL_TOLERANCE[] = L"PixelTolerance";
const wchar_t JSON_KEY_PER_COLOR_CHANNEL_EDGE_DETECTION[] = L"PerColorChannelEdgeDetection";
const wchar_t JSON_KEY_MEASURE_CROSS_COLOR[] = L"MeasureCrossColor";
}
Settings Settings::LoadFromFile()
{
Settings result;
try
{
auto props = PTSettingsHelper::load_module_settings(L"Measure Tool").GetNamedObject(JSON_KEY_PROPERTIES);
try
{
result.continuousCapture = props.GetNamedObject(JSON_KEY_CONTINUOUS_CAPTURE).GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
}
try
{
result.drawFeetOnCross = props.GetNamedObject(JSON_KEY_DRAW_FEET_ON_CROSS).GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
}
try
{
result.pixelTolerance = static_cast<uint8_t>(props.GetNamedObject(JSON_KEY_PIXEL_TOLERANCE).GetNamedNumber(JSON_KEY_VALUE));
}
catch (...)
{
}
try
{
const auto colorString = props.GetNamedObject(JSON_KEY_MEASURE_CROSS_COLOR).GetNamedString(JSON_KEY_VALUE);
checkValidRGB(colorString, &result.lineColor[0], &result.lineColor[1], &result.lineColor[2]);
}
catch (...)
{
}
try
{
result.perColorChannelEdgeDetection = props.GetNamedObject(JSON_KEY_PER_COLOR_CHANNEL_EDGE_DETECTION).GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
}
}
catch (...)
{
}
return result;
}

View File

@ -0,0 +1,15 @@
#pragma once
#include <array>
#include <cinttypes>
struct Settings
{
uint8_t pixelTolerance = 30;
bool continuousCapture = true;
bool drawFeetOnCross = true;
bool perColorChannelEdgeDetection = false;
std::array<uint8_t, 3> lineColor = {255, 69, 0};
static Settings LoadFromFile();
};

View File

@ -0,0 +1,83 @@
#pragma once
#include <array>
#include <functional>
#include <mutex>
#include <vector>
#include <thread>
#include <unordered_map>
#include <windef.h>
#include <d2d1helper.h>
#include <dCommon.h>
#include <common/Display/monitors.h>
#include <common/utils/serialized.h>
//#define DEBUG_OVERLAY
struct OverlayBoxText
{
std::array<wchar_t, 32> buffer = {};
};
struct CommonState
{
std::function<void()> sessionCompletedCallback;
D2D1::ColorF lineColor = D2D1::ColorF::OrangeRed;
Box toolbarBoundingBox;
mutable Serialized<OverlayBoxText> overlayBoxText;
POINT cursorPosSystemSpace = {}; // updated atomically
std::atomic_bool closeOnOtherMonitors = false;
};
struct BoundsToolState
{
struct PerScreen
{
std::optional<D2D_POINT_2F> currentRegionStart;
std::vector<D2D1_RECT_F> measurements;
};
std::unordered_map<HWND, PerScreen> perScreen;
CommonState* commonState = nullptr; // required for WndProc
};
struct MeasureToolState
{
enum class Mode
{
Horizontal,
Vertical,
Cross
};
struct Global
{
uint8_t pixelTolerance = 30;
bool continuousCapture = true;
bool drawFeetOnCross = true;
bool perColorChannelEdgeDetection = false;
Mode mode = Mode::Cross;
} global;
struct PerScreen
{
bool cursorInLeftScreenHalf = false;
bool cursorInTopScreenHalf = false;
RECT measuredEdges = {};
// While not in a continuous capturing mode, we need to draw captured backgrounds. These are passed
// directly from a capturing thread.
winrt::com_ptr<ID3D11Texture2D> capturedScreenTexture;
// After the drawing thread finds its capturedScreenTexture, it converts it to
// a Direct2D compatible bitmap and caches it here
winrt::com_ptr<ID2D1Bitmap> capturedScreenBitmap;
};
std::unordered_map<HWND, PerScreen> perScreen;
CommonState* commonState = nullptr; // required for WndProc
};
// Concurrently accessing Direct2D and Direct3D APIs make the driver go boom
extern std::recursive_mutex gpuAccessLock;

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MeasureTool.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@ -0,0 +1,22 @@
#pragma once
#include <chrono>
namespace consts
{
constexpr inline size_t TARGET_FRAME_RATE = 120;
constexpr inline auto TARGET_FRAME_DURATION = std::chrono::milliseconds{ 1000 } / TARGET_FRAME_RATE;
constexpr inline float FONT_SIZE = 14.f;
constexpr inline float TEXT_BOX_CORNER_RADIUS = 4.f;
constexpr inline float TEXT_BOX_MARGIN_COEFF = 1.25f;
constexpr inline float FEET_HALF_LENGTH = 2.f;
constexpr inline float SHADOW_OPACITY = .4f;
constexpr inline float SHADOW_RADIUS = 6.f;
constexpr inline float SHADOW_OFFSET = 5.f;
constexpr inline float CROSS_OPACITY = .25f;
constexpr inline int8_t MOUSE_WHEEL_TOLERANCE_STEP = 15;
/* Offset to not try not to use the cursor immediate pixels in measuring, but it seems only necessary for continuous mode. */
constexpr inline long CURSOR_OFFSET_AMOUNT_X = 4;
constexpr inline long CURSOR_OFFSET_AMOUNT_Y = 4;
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.220418.1" targetFramework="native" />
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.220201.1" targetFramework="native" />
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.22000.197" targetFramework="native" />
</packages>

View File

@ -0,0 +1 @@
#include "pch.h"

View File

@ -0,0 +1,103 @@
#pragma once
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <initguid.h>
#include <windows.h>
#include <unknwn.h>
#include <restrictederrorinfo.h>
#include <hstring.h>
#include <dxgi.h>
#include <d3d11.h>
#include <d3d11_4.h>
#include <dxgi1_6.h>
#include <d2d1_3.h>
#include <dwrite.h>
#include <dwmapi.h>
#include <windows.graphics.directX.direct3d11.interop.h>
#include <windows.graphics.capture.interop.h>
#include <windows.graphics.capture.h>
#include <thread>
#include <functional>
#include <cassert>
#include <cinttypes>
#include <iomanip>
#include <limits>
#include <sstream>
#include <filesystem>
#include <string_view>
#include <chrono>
#include <stdio.h>
#include <ProjectTelemetry.h>
// Undefine GetCurrentTime macro to prevent
// conflict with Storyboard::GetCurrentTime
#undef GetCurrentTime
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.ApplicationModel.Activation.h>
#include <winrt/Microsoft.UI.Composition.h>
#include <winrt/Microsoft.UI.Xaml.h>
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#include <winrt/Microsoft.UI.Xaml.Controls.Primitives.h>
#include <winrt/Microsoft.UI.Xaml.Data.h>
#include <winrt/Microsoft.UI.Xaml.Interop.h>
#include <winrt/Microsoft.UI.Xaml.Markup.h>
#include <winrt/Microsoft.UI.Xaml.Media.h>
#include <winrt/Microsoft.UI.Xaml.Navigation.h>
#include <winrt/Microsoft.UI.Xaml.Shapes.h>
#include <winrt/Microsoft.UI.Dispatching.h>
#include <winrt/Windows.Graphics.DirectX.h>
#include <winrt/Windows.Graphics.DirectX.Direct3d11.h>
#include <winrt/Windows.Graphics.Capture.h>
#include <inspectable.h>
#include <wil/cppwinrt_helpers.h>
#include <wil/resource.h>
#include <wil/com.h>
#include <common/logger/logger.h>
#include <common/utils/winapi_error.h>
namespace winrt
{
using namespace Windows::Foundation;
using namespace Windows::Foundation::Numerics;
using namespace Windows::Graphics;
using namespace Windows::Graphics::Capture;
using namespace Windows::Graphics::DirectX;
using namespace Windows::Graphics::DirectX::Direct3D11;
using namespace Microsoft::UI::Xaml;
using namespace Microsoft::UI::Xaml::Controls;
using namespace Microsoft::UI::Xaml::Navigation;
}
template<typename Func>
[[nodiscard]] std::thread SpawnLoggedThread(const wchar_t* description, Func&& f)
{
return std::thread{ [f = std::move(f), description = std::wstring{ description }] {
try
{
SetThreadDescription(GetCurrentThread(), description.c_str());
f();
}
catch (const std::exception& ex)
{
Logger::error(L"{}:", description);
Logger::error("{}", ex.what());
}
catch (winrt::hresult_error const& ex)
{
Logger::error(L"{}: {}", description, ex.message().c_str());
}
catch (...)
{
Logger::error(L"{} unknown error: {}", description, get_last_error_or_default(GetLastError()));
}
} };
}
#define WM_CURSOR_LEFT_MONITOR (WM_USER + 1)

View File

@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by PowerToys.MeasureToolCore.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys MeasureToolCore"
#define INTERNAL_NAME "PowerToys.MeasureToolCore"
#define ORIGINAL_FILENAME "PowerToys.MeasureToolCore.dll"
// Non-localizable
//////////////////////////////

View File

@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{92C39820-9F84-4529-BC7D-22AAE514D63B}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MeasureToolModuleInterface</RootNamespace>
<ProjectName>MeasureToolModuleInterface</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\MeasureTool\</OutDir>
<TargetName>PowerToys.MeasureToolModuleInterface</TargetName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>$(SolutionDir)src\;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(CIBuild)'!='true'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="MeasureTool.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="trace.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(CIBuild)'!='true'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="trace.cpp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="MeasureToolModuleInterface.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{875a08c6-f610-4667-bd0f-80171ed96072}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="MeasureTool.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Resource Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="MeasureToolModuleInterface.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,283 @@
#include "pch.h"
#include <interface/powertoy_module_interface.h>
#include <common/SettingsAPI/settings_objects.h>
#include "trace.h"
#include <common/utils/string_utils.h>
#include <common/utils/winapi_error.h>
#include <common/utils/logger_helper.h>
extern "C" IMAGE_DOS_HEADER __ImageBase;
HMODULE m_hModule;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
m_hModule = hModule;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
const static wchar_t* MODULE_NAME = L"Measure Tool";
const static wchar_t* MODULE_DESC = L"Measure your screen contents";
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_WIN[] = L"win";
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"ActivationShortcut";
}
class MeasureTool : public PowertoyModuleIface
{
private:
// The PowerToy state.
bool m_enabled = false;
Hotkey m_hotkey;
HANDLE m_hProcess;
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
}
catch (...)
{
Logger::error("Failed to initialize Measure Tool start shortcut");
}
}
else
{
Logger::info("MeasureTool settings are empty");
}
if (!m_hotkey.key)
{
Logger::info("MeasureTool is going to use default shortcut");
m_hotkey.win = true;
m_hotkey.alt = false;
m_hotkey.shift = true;
m_hotkey.ctrl = false;
m_hotkey.key = 'M';
}
}
bool is_process_running()
{
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void launch_process()
{
Logger::trace(L"Starting MeasureTool process");
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = L"modules\\MeasureTool\\PowerToys.MeasureToolUI.exe";
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
Logger::trace("Successfully started the Measure Tool process");
}
else
{
Logger::error(L"MeasureTool failed to start. {}", get_last_error_or_default(GetLastError()));
}
m_hProcess = sei.hProcess;
}
// Load the settings file.
void init_settings()
{
try
{
// Load and parse the settings file for this PowerToy.
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
parse_hotkey(settings);
}
catch (std::exception ex)
{
Logger::warn(L"An exception occurred while loading the settings file");
// Error while loading from the settings file. Let default values stay as they are.
}
}
void terminate_process()
{
TerminateProcess(m_hProcess, 1);
}
public:
MeasureTool()
{
LoggerHelpers::init_logger(L"Measure Tool", L"ModuleInterface", "Measure Tool");
init_settings();
}
~MeasureTool()
{
if (m_enabled)
{
terminate_process();
}
m_enabled = false;
}
// Destroy the powertoy and free memory
virtual void destroy() override
{
delete this;
}
// Return the localized display name of the powertoy
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
// Return the non localized key of the powertoy, this will be cached by the runner
virtual const wchar_t* get_key() override
{
return MODULE_NAME;
}
// Return JSON with the configuration options.
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
// Create a Settings object.
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC);
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors.
virtual void call_custom_action(const wchar_t*) override
{
}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_hotkey(values);
// If you don't need to do any custom processing of the settings, proceed
// to persists the values calling:
values.save_to_settings_file();
// Otherwise call a custom function to process the settings before saving them to disk:
// save_settings();
}
catch (std::exception ex)
{
// Improper JSON.
}
}
// Enable the powertoy
virtual void enable()
{
m_enabled = true;
Trace::EnableMeasureTool(true);
}
// Disable the powertoy
virtual void disable()
{
if (m_enabled)
{
terminate_process();
}
m_enabled = false;
Trace::EnableMeasureTool(false);
}
// Returns if the powertoys is enabled
virtual bool is_enabled() override
{
return m_enabled;
}
virtual bool on_hotkey(size_t hotkeyId) override
{
if (m_enabled)
{
Logger::trace(L"MeasureTool hotkey pressed");
if (is_process_running())
{
terminate_process();
}
else
{
launch_process();
}
return true;
}
return false;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
if (m_hotkey.key)
{
if (hotkeys && buffer_size >= 1)
{
hotkeys[0] = m_hotkey;
}
return 1;
}
else
{
return 0;
}
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new MeasureTool();
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.220418.1" targetFramework="native" />
</packages>

View File

@ -0,0 +1 @@
#include "pch.h"

View File

@ -0,0 +1,14 @@
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <strsafe.h>
#include <hIdUsage.h>
#include <shellapi.h>
#include <thread>
#include <winrt/Windows.Foundation.Collections.h>
#include <ProjectTelemetry.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/logger/logger.h>

View File

@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by MeasureTool.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys MeasureTool"
#define INTERNAL_NAME "PowerToys.MeasureTool"
#define ORIGINAL_FILENAME "PowerToys.MeasureTool.dll"
// Non-localizable
//////////////////////////////

View File

@ -0,0 +1,47 @@
#include "pch.h"
#include "trace.h"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
void Trace::RegisterProvider() noexcept
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider() noexcept
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::EnableMeasureTool(const bool enabled) noexcept
{
TraceLoggingWrite(
g_hProvider,
"MeasureTool_EnableMeasureTool",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}
void Trace::BoundsToolActivated() noexcept
{
TraceLoggingWrite(
g_hProvider,
"MeasureTool_BoundsToolActivated",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::MeasureToolActivated() noexcept
{
TraceLoggingWrite(
g_hProvider,
"MeasureTool_MeasureToolActivated",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}

View File

@ -0,0 +1,13 @@
#pragma once
class Trace
{
public:
static void RegisterProvider() noexcept;
static void UnregisterProvider() noexcept;
static void EnableMeasureTool(const bool enabled) noexcept;
static void BoundsToolActivated() noexcept;
static void MeasureToolActivated() noexcept;
};

View File

@ -0,0 +1,15 @@
<Application
x:Class="MeasureToolUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MeasureToolUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@ -0,0 +1,51 @@
// 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 ManagedCommon;
using Microsoft.UI.Xaml;
namespace MeasureToolUI
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
/// <summary>
/// Initializes a new instance of the <see cref="App"/> class.
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}
/// <summary>
/// Invoked when the application is launched normally by the end user. Other entry points
/// will be used such as when the application is launched to open a specific file.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
{
if (int.TryParse(cmdArgs[cmdArgs.Length - 1], out int powerToysRunnerPid))
{
RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () =>
{
Environment.Exit(0);
});
}
}
_window = new MainWindow();
_window.Activate();
}
private Window _window;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,298 @@
<winuiex:WindowEx
x:Class="MeasureToolUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:contract7NotPresent="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractNotPresent(Windows.Foundation.UniversalApiContract,7)"
xmlns:contract7Present="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract,7)"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:p="using:PowerToys.MeasureToolUI.Properties"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winuiex="using:WinUIEx"
IsAlwaysOnTop="True"
IsMaximizable="False"
IsMinimizable="False"
IsResizable="False"
IsShownInSwitchers="False"
IsTitleBarVisible="False"
mc:Ignorable="d">
<winuiex:WindowEx.Backdrop>
<winuiex:AcrylicSystemBackdrop
DarkFallbackColor="#1c1c1c"
DarkLuminosityOpacity="0.96"
DarkTintColor="#202020"
DarkTintOpacity="0.5"
LightFallbackColor="#EEEEEE"
LightLuminosityOpacity="0.85"
LightTintColor="#FCFCFC"
LightTintOpacity="0" />
</winuiex:WindowEx.Backdrop>
<Grid>
<Grid.Resources>
<SolidColorBrush x:Key="ToggleButtonBackground" Color="Transparent" />
<SolidColorBrush x:Key="ButtonBackground" Color="Transparent" />
<StaticResource x:Key="ButtonBackgroundPointerOver" ResourceKey="ControlSolidFillColorDefaultBrush" />
<Style BasedOn="{StaticResource DefaultButtonStyle}" TargetType="Button">
<Setter Property="MinHeight" Value="30" />
<Setter Property="MinWidth" Value="30" />
<Setter Property="Width" Value="30" />
<Setter Property="Height" Value="30" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontSize" Value="10" />
<Setter Property="Padding" Value="0" />
</Style>
<Style x:Key="ToggleButtonRadioButtonStyle" TargetType="ToggleButton">
<Setter Property="MinHeight" Value="30" />
<Setter Property="MinWidth" Value="30" />
<Setter Property="Width" Value="30" />
<Setter Property="Height" Value="30" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FontFamily" Value="{ThemeResource SymbolThemeFontFamily}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="{ThemeResource ToggleButtonBackground}" />
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
<Setter Property="Foreground" Value="{ThemeResource ToggleButtonForeground}" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
<Setter Property="FocusVisualMargin" Value="-3" />
<Setter Property="CornerRadius" Value="{StaticResource ControlCornerRadius}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
contract7NotPresent:CornerRadius="{ThemeResource ControlCornerRadius}"
contract7Present:BackgroundSizing="{TemplateBinding BackgroundSizing}"
contract7Present:CornerRadius="{TemplateBinding CornerRadius}"
AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}">
<contract7Present:ContentPresenter.BackgroundTransition>
<contract7Present:BrushTransition Duration="0:0:0.083" />
</contract7Present:ContentPresenter.BackgroundTransition>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ControlSolidFillColorDefaultBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundPointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundPressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Checked">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundChecked}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundChecked}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushChecked}" />
</ObjectAnimationUsingKeyFrames>
<contract7Present:ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BackgroundSizing">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonCheckedStateBackgroundSizing}" />
</contract7Present:ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedPointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushCheckedPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedPointerOver}" />
</ObjectAnimationUsingKeyFrames>
<contract7Present:ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BackgroundSizing">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonCheckedStateBackgroundSizing}" />
</contract7Present:ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedPressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedPressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushCheckedPressed}" />
</ObjectAnimationUsingKeyFrames>
<contract7Present:ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BackgroundSizing">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonCheckedStateBackgroundSizing}" />
</contract7Present:ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="CheckedDisabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushCheckedDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Indeterminate">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminate}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminatePointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminatePointerOver}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminatePressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminatePressed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="IndeterminateDisabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBackgroundIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonForegroundIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ToggleButtonBorderBrushIndeterminateDisabled}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Grid.Resources>
<StackPanel
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="8">
<ToggleButton
AutomationProperties.Name="{x:Bind p:Resources.Bounds}"
Click="BoundsTool_Click"
Content="&#xEF20;"
Style="{StaticResource ToggleButtonRadioButtonStyle}"
ToolTipService.ToolTip="{x:Bind p:Resources.Bounds}" />
<ToggleButton
AutomationProperties.Name="{x:Bind p:Resources.Spacing}"
Click="MeasureTool_Click"
Style="{StaticResource ToggleButtonRadioButtonStyle}"
ToolTipService.ToolTip="{x:Bind p:Resources.Spacing}">
<FontIcon Margin="1,0,0,0" Glyph="&#xE948;" />
</ToggleButton>
<ToggleButton
AutomationProperties.Name="{x:Bind p:Resources.HorizontalSpacing}"
Click="HorizontalMeasureTool_Click"
Style="{StaticResource ToggleButtonRadioButtonStyle}"
ToolTipService.ToolTip="{x:Bind p:Resources.HorizontalSpacing}">
<FontIcon Margin="1,0,0,0" Glyph="&#xE949;" />
</ToggleButton>
<ToggleButton
AutomationProperties.Name="{x:Bind p:Resources.VerticalSpacing}"
Click="VerticalMeasureTool_Click"
Style="{StaticResource ToggleButtonRadioButtonStyle}"
ToolTipService.ToolTip="{x:Bind p:Resources.VerticalSpacing}">
<FontIcon Glyph="&#xE949;" RenderTransformOrigin="0.5,0.5">
<FontIcon.RenderTransform>
<RotateTransform Angle="90" />
</FontIcon.RenderTransform>
</FontIcon>
</ToggleButton>
<AppBarSeparator Height="36" />
<Button
Click="ClosePanelTool_Click"
Content="&#xE8BB;"
ToolTipService.ToolTip="Close" />
</StackPanel>
</Grid>
</winuiex:WindowEx>

View File

@ -0,0 +1,141 @@
// 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 Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Windows.Graphics;
using WinUIEx;
namespace MeasureToolUI
{
using static NativeMethods;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainWindow : WindowEx
{
private const int WindowWidth = 216;
private const int WindowHeight = 50;
private PowerToys.MeasureToolCore.Core _coreLogic = new PowerToys.MeasureToolCore.Core();
private AppWindow _appWindow;
private PointInt32 _initialPosition;
protected override void OnPositionChanged(PointInt32 position)
{
_appWindow.Move(_initialPosition);
this.SetWindowSize(WindowWidth, WindowHeight);
}
public MainWindow()
{
InitializeComponent();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
_appWindow = AppWindow.GetFromWindowId(windowId);
var presenter = _appWindow.Presenter as OverlappedPresenter;
presenter.IsAlwaysOnTop = true;
this.SetIsAlwaysOnTop(true);
this.SetIsShownInSwitchers(false);
this.SetIsResizable(false);
this.SetIsMinimizable(false);
this.SetIsMaximizable(false);
IsTitleBarVisible = false;
DisplayArea displayArea = DisplayArea.GetFromWindowId(windowId, DisplayAreaFallback.Nearest);
float dpiScale = _coreLogic.GetDPIScaleForWindow((int)hwnd);
_initialPosition = new PointInt32(displayArea.WorkArea.X + (displayArea.WorkArea.Width / 2) - (int)(dpiScale * WindowWidth / 2), displayArea.WorkArea.Y + (int)(dpiScale * 12));
_coreLogic.SetToolbarBoundingBox(
_initialPosition.X,
_initialPosition.Y,
_initialPosition.X + (int)(dpiScale * WindowWidth),
_initialPosition.Y + (int)(dpiScale * WindowHeight));
OnPositionChanged(_initialPosition);
}
private void UpdateToolUsageCompletionEvent(object sender)
{
_coreLogic.SetToolCompletionEvent(new PowerToys.MeasureToolCore.ToolSessionCompleted(() =>
{
DispatcherQueue.TryEnqueue(() =>
{
((ToggleButton)sender).IsChecked = false;
});
}));
}
private void UncheckOtherButtons(ToggleButton button)
{
var panel = button.Parent as Panel;
foreach (var elem in panel.Children)
{
if (elem is ToggleButton otherButton)
{
if (!button.Equals(otherButton))
{
otherButton.IsChecked = false;
}
}
}
}
private void HandleToolClick(object toolButton, Action startToolAction)
{
ToggleButton button = toolButton as ToggleButton;
if (button == null)
{
return;
}
if (button.IsChecked.GetValueOrDefault())
{
UncheckOtherButtons(button);
_coreLogic.ResetState();
UpdateToolUsageCompletionEvent(toolButton);
startToolAction();
}
else
{
_coreLogic.ResetState();
}
}
private void BoundsTool_Click(object sender, RoutedEventArgs e)
{
HandleToolClick(sender, () => _coreLogic.StartBoundsTool());
}
private void MeasureTool_Click(object sender, RoutedEventArgs e)
{
HandleToolClick(sender, () => _coreLogic.StartMeasureTool(true, true));
}
private void HorizontalMeasureTool_Click(object sender, RoutedEventArgs e)
{
HandleToolClick(sender, () => _coreLogic.StartMeasureTool(true, false));
}
private void VerticalMeasureTool_Click(object sender, RoutedEventArgs e)
{
HandleToolClick(sender, () => _coreLogic.StartMeasureTool(false, true));
}
private void ClosePanelTool_Click(object sender, RoutedEventArgs e)
{
_coreLogic.ResetState();
this.Close();
}
}
}

View File

@ -0,0 +1,85 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\Version.props" />
<PropertyGroup>
<AssemblyTitle>PowerToys.MeasureTool</AssemblyTitle>
<AssemblyDescription>PowerToys MeasureTool</AssemblyDescription>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows10.0.20348.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\modules\MeasureTool</OutputPath>
<RootNamespace>PowerToys.MeasureToolUI</RootNamespace>
<AssemblyName>PowerToys.MeasureToolUI</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;arm64</Platforms>
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
<PublishProfile>win10-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<SupportedOSPlatformVersion>10.0.19041.0</SupportedOSPlatformVersion>
</PropertyGroup>
<!-- See https://docs.microsoft.com/en-us/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
<PropertyGroup>
<CsWinRTIncludes>PowerToys.MeasureToolCore</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>
<PropertyGroup>
<NoWarn>0436</NoWarn>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="1.6.4" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.1.4" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22000.194" />
<PackageReference Include="WinUIEx" Version="1.6.0" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<!-- Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored -->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnablePreviewMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\Icons\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\MeasureToolCore\PowerToys.MeasureToolCore.vcxproj" />
</ItemGroup>
</Project>

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.
using System;
using System.Runtime.InteropServices;
internal static class NativeMethods
{
[DllImport("user32.dll")]
internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags);
internal static readonly IntPtr HWND_TOPMOST = new System.IntPtr(-1);
internal const uint SWP_NOSIZE = 0x0001;
internal const uint SWP_NOMOVE = 0x0002;
internal const uint SWP_SHOWWINDOW = 0x0040;
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity
Name="a3e178b8-eac3-48e9-8ae0-1756eb4ff1ec"
Publisher="CN=Microsoft Corporation"
Version="1.0.0.0" />
<Properties>
<DisplayName>MeasureToolUI</DisplayName>
<PublisherDisplayName>nielslaute</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17134.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17134.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="MeasureToolUI"
Description="MeasureToolUI"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@ -0,0 +1,99 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PowerToys.MeasureToolUI.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PowerToys.MeasureToolUI.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Bounds.
/// </summary>
public static string Bounds {
get {
return ResourceManager.GetString("Bounds", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Horizontal spacing.
/// </summary>
public static string HorizontalSpacing {
get {
return ResourceManager.GetString("HorizontalSpacing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Spacing.
/// </summary>
public static string Spacing {
get {
return ResourceManager.GetString("Spacing", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Vertical spacing.
/// </summary>
public static string VerticalSpacing {
get {
return ResourceManager.GetString("VerticalSpacing", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Bounds" xml:space="preserve">
<value>Bounds</value>
</data>
<data name="Spacing" xml:space="preserve">
<value>Spacing</value>
</data>
<data name="HorizontalSpacing" xml:space="preserve">
<value>Horizontal spacing</value>
</data>
<data name="VerticalSpacing" xml:space="preserve">
<value>Vertical spacing</value>
</data>
</root>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MeasureToolUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@ -6,8 +6,6 @@
<ProjectGuid>{e94fd11c-0591-456f-899f-efc0ca548336}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>FindMyMouse</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<ProjectName>FindMyMouse</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />

View File

@ -334,13 +334,14 @@ void D2DOverlayWindow::show(HWND active_window, bool snappable)
}
monitors = MonitorInfo::GetMonitors(true);
// calculate the rect covering all the screens
total_screen = ScreenSize(monitors[0].rect);
total_screen = monitors[0].GetScreenSize(true);
for (auto& monitor : monitors)
{
total_screen.rect.left = std::min(total_screen.rect.left, monitor.rect.left);
total_screen.rect.top = std::min(total_screen.rect.top, monitor.rect.top);
total_screen.rect.right = std::max(total_screen.rect.right, monitor.rect.right);
total_screen.rect.bottom = std::max(total_screen.rect.bottom, monitor.rect.bottom);
const auto monitorSize = monitor.GetScreenSize(true);
total_screen.rect.left = std::min(total_screen.left(), monitorSize.left());
total_screen.rect.top = std::min(total_screen.top(), monitorSize.top());
total_screen.rect.right = std::max(total_screen.right(), monitorSize.right());
total_screen.rect.bottom = std::max(total_screen.bottom(), monitorSize.bottom());
}
// make sure top-right corner of all the monitor rects is (0,0)
monitor_dx = -total_screen.left();
@ -356,10 +357,10 @@ void D2DOverlayWindow::show(HWND active_window, bool snappable)
DwmRegisterThumbnail(hwnd, active_window, &thumbnail);
}
animation.reset();
auto primary_screen = MonitorInfo::GetPrimaryMonitor();
auto primary_size = MonitorInfo::GetPrimaryMonitor().GetScreenSize(false);
shown_start_time = std::chrono::steady_clock::now();
lock.unlock();
D2DWindow::show(primary_screen.left(), primary_screen.top(), primary_screen.width(), primary_screen.height());
D2DWindow::show(primary_size.left(), primary_size.top(), primary_size.width(), primary_size.height());
// Check if taskbar is auto-hidden. If so, don't display the number arrows
APPBARDATA param = {};
param.cbSize = sizeof(APPBARDATA);
@ -733,10 +734,11 @@ void D2DOverlayWindow::render(ID2D1DeviceContext5* d2d_dc)
for (auto& monitor : monitors)
{
D2D1_RECT_F monitor_rect;
monitor_rect.left = (float)((monitor.rect.left + monitor_dx) * rect_and_scale.scale + rect_and_scale.rect.left);
monitor_rect.top = (float)((monitor.rect.top + monitor_dy) * rect_and_scale.scale + rect_and_scale.rect.top);
monitor_rect.right = (float)((monitor.rect.right + monitor_dx) * rect_and_scale.scale + rect_and_scale.rect.left);
monitor_rect.bottom = (float)((monitor.rect.bottom + monitor_dy) * rect_and_scale.scale + rect_and_scale.rect.top);
const auto monitor_size = monitor.GetScreenSize(true);
monitor_rect.left = (float)((monitor_size.left() + monitor_dx) * rect_and_scale.scale + rect_and_scale.rect.left);
monitor_rect.top = (float)((monitor_size.top() + monitor_dy) * rect_and_scale.scale + rect_and_scale.rect.top);
monitor_rect.right = (float)((monitor_size.right() + monitor_dx) * rect_and_scale.scale + rect_and_scale.rect.left);
monitor_rect.bottom = (float)((monitor_size.bottom() + monitor_dy) * rect_and_scale.scale + rect_and_scale.rect.top);
d2d_dc->SetTransform(D2D1::Matrix3x2F::Identity());
d2d_dc->FillRectangle(monitor_rect, brush.get());
}

View File

@ -74,7 +74,7 @@ private:
bool running = true;
std::vector<AnimateKeys> key_animations;
std::vector<MonitorInfo> monitors;
ScreenSize total_screen;
Box total_screen;
int monitor_dx = 0, monitor_dy = 0;
D2DText text;
WindowsColors colors;

View File

@ -55,8 +55,7 @@
</Link>
</ItemDefinitionGroup>
<!-- Global props -->
<PropertyGroup Label="Globals" Condition="'$(OverrideWindowsTargetPlatformVersion)'!='True'">
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{1DC3BE92-CE89-43FB-8110-9C043A2FE7A2}</ProjectGuid>

View File

@ -55,8 +55,7 @@
</Link>
</ItemDefinitionGroup>
<!-- Global props -->
<PropertyGroup Label="Globals" Condition="'$(OverrideWindowsTargetPlatformVersion)'!='True'">
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{ff1d7936-842a-4bbb-8bea-e9fe796de700}</ProjectGuid>

View File

@ -63,9 +63,6 @@
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{8df78b53-200e-451f-9328-01eb907193ae}</ProjectGuid>
<RootNamespace>KeyboardManagerEditor</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<TargetPlatformVersion>10.0.19041.0</TargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<!-- Props that are constant for both Debug and Release configurations -->
<PropertyGroup Label="Configuration">

View File

@ -6,9 +6,6 @@
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{23d2070d-e4ad-4add-85a7-083d9c76ad49}</ProjectGuid>
<RootNamespace>KeyboardManagerEditorLibrary</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<TargetPlatformVersion>10.0.19041.0</TargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">

View File

@ -6,8 +6,6 @@
<ProjectGuid>{62173D9A-6724-4C00-A1C8-FB646480A9EC}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>KeyboardManagerEditorTest</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />

View File

@ -10,9 +10,6 @@
<ProjectGuid>{ba661f5b-1d5a-4ffc-9bf1-fc39df280bdd}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>KeyboardManagerEngine</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<TargetPlatformVersion>10.0.19041.0</TargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">

View File

@ -6,9 +6,6 @@
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{e496b7fc-1e99-4bab-849b-0e8367040b02}</ProjectGuid>
<RootNamespace>KeyboardManagerEngineLibrary</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<TargetPlatformVersion>10.0.19041.0</TargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">

View File

@ -6,8 +6,6 @@
<ProjectGuid>{7f4b3a60-bc27-45a7-8000-68b0b6ea7466}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>KeyboardManagerEngineTest</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>KeyboardManagerEngineTest</ProjectName>
</PropertyGroup>

View File

@ -5,8 +5,6 @@
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{8AFFA899-0B73-49EC-8C50-0FADDA57B2FC}</ProjectGuid>
<RootNamespace>KeyboardManagerCommon</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">

View File

@ -6,8 +6,6 @@
<ProjectGuid>{89f34af7-1c34-4a72-aa6e-534bcf972bd9}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>KeyboardManager</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
<ProjectName>KeyboardManager</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />

View File

@ -232,38 +232,39 @@ void Toolbar::show(std::wstring position, std::wstring monitorString)
for (auto& monitorInfo : monitorInfos)
{
const auto screenSize = monitorInfo.GetScreenSize(false);
int positionX = 0;
int positionY = 0;
if (position == L"Top left corner")
{
positionX = monitorInfo.left() + BORDER_OFFSET;
positionY = monitorInfo.top() + BORDER_OFFSET;
positionX = screenSize.left() + BORDER_OFFSET;
positionY = screenSize.top() + BORDER_OFFSET;
}
else if (position == L"Top center")
{
positionX = monitorInfo.middle().x - overlayWidth / 2;
positionY = monitorInfo.top() + BORDER_OFFSET;
positionX = screenSize.middle().x - overlayWidth / 2;
positionY = screenSize.top() + BORDER_OFFSET;
}
else if (position == L"Bottom left corner")
{
positionX = monitorInfo.left() + BORDER_OFFSET;
positionY = monitorInfo.bottom() - overlayHeight - BORDER_OFFSET;
positionX = screenSize.left() + BORDER_OFFSET;
positionY = screenSize.bottom() - overlayHeight - BORDER_OFFSET;
}
else if (position == L"Bottom center")
{
positionX = monitorInfo.middle().x - overlayWidth / 2;
positionY = monitorInfo.bottom() - overlayHeight - BORDER_OFFSET;
positionX = screenSize.middle().x - overlayWidth / 2;
positionY = screenSize.bottom() - overlayHeight - BORDER_OFFSET;
}
else if (position == L"Bottom right corner")
{
positionX = monitorInfo.right() - overlayWidth - BORDER_OFFSET;
positionY = monitorInfo.bottom() - overlayHeight - BORDER_OFFSET;
positionX = screenSize.right() - overlayWidth - BORDER_OFFSET;
positionY = screenSize.bottom() - overlayHeight - BORDER_OFFSET;
}
else //"Top right corner" or non-present
{
positionX = monitorInfo.right() - overlayWidth - BORDER_OFFSET;
positionY = monitorInfo.top() + TOP_RIGHT_BORDER_OFFSET;
positionX = screenSize.right() - overlayWidth - BORDER_OFFSET;
positionY = screenSize.top() + TOP_RIGHT_BORDER_OFFSET;
}
HWND hwnd;

View File

@ -32,8 +32,6 @@
<Keyword>Win32Proj</Keyword>
<RootNamespace>VideoConferenceProxyFilter</RootNamespace>
<ProjectName>VideoConferenceProxyFilter</ProjectName>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">

View File

@ -37,8 +37,6 @@
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{459e0768-7ebd-4c41-bba1-6db3b3815e0a}</ProjectGuid>
<RootNamespace>VideoConferenceShared</RootNamespace>
<OverrideWindowsTargetPlatformVersion>true</OverrideWindowsTargetPlatformVersion>
<WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup>

View File

@ -155,6 +155,8 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
L"modules/MouseUtils/PowerToys.MousePointerCrosshairs.dll",
L"modules/PowerAccent/PowerToys.PowerAccentModuleInterface.dll",
L"modules/PowerOCR/PowerToys.PowerOCRModuleInterface.dll",
L"modules/MeasureTool/PowerToys.MeasureToolModuleInterface.dll",
};
const auto VCM_PATH = L"modules/VideoConference/PowerToys.VideoConferenceModule.dll";
if (const auto mf = LoadLibraryA("mf.dll"))

View File

@ -271,6 +271,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
}
}
private bool measureTool = true;
[JsonPropertyName("Measure Tool")]
public bool MeasureTool
{
get => measureTool;
set
{
if (measureTool != value)
{
LogTelemetryEvent(value);
measureTool = value;
}
}
}
public string ToJsonString()
{
return JsonSerializer.Serialize(this);

View File

@ -0,0 +1,41 @@
// 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.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Enumerations;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MeasureToolProperties
{
public MeasureToolProperties()
{
ActivationShortcut = new HotkeySettings(true, false, false, true, 0x4D);
PixelTolerance = new IntProperty(30);
ContinuousCapture = true;
DrawFeetOnCross = true;
PerColorChannelEdgeDetection = false;
MeasureCrossColor = new StringProperty("#FF4500");
}
public HotkeySettings ActivationShortcut { get; set; }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool ContinuousCapture { get; set; }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool DrawFeetOnCross { get; set; }
[JsonConverter(typeof(BoolPropertyJsonConverter))]
public bool PerColorChannelEdgeDetection { get; set; }
public IntProperty PixelTolerance { get; set; }
public StringProperty MeasureCrossColor { get; set; }
public override string ToString() => JsonSerializer.Serialize(this);
}
}

View File

@ -0,0 +1,33 @@
// 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.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library
{
public class MeasureToolSettings : BasePTModuleSettings, ISettingsConfig
{
public const string ModuleName = "Measure Tool";
[JsonPropertyName("properties")]
public MeasureToolProperties Properties { get; set; }
public MeasureToolSettings()
{
Properties = new MeasureToolProperties();
Version = "1";
Name = ModuleName;
}
public string GetModuleName()
=> Name;
// This can be utilized in the future if the settings.json file is to be modified/deleted.
public bool UpgradeSettingsConfiguration()
=> false;
}
}

View File

@ -0,0 +1,170 @@
// 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.Runtime.CompilerServices;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
{
public class MeasureToolViewModel : Observable
{
private ISettingsUtils SettingsUtils { get; set; }
private GeneralSettings GeneralSettingsConfig { get; set; }
private MeasureToolSettings Settings { get; set; }
public MeasureToolViewModel(ISettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, ISettingsRepository<MeasureToolSettings> measureToolSettingsRepository, Func<string, int> ipcMSGCallBackFunc)
{
SettingsUtils = settingsUtils;
if (settingsRepository == null)
{
throw new ArgumentNullException(nameof(settingsRepository));
}
GeneralSettingsConfig = settingsRepository.SettingsConfig;
if (measureToolSettingsRepository == null)
{
throw new ArgumentNullException(nameof(measureToolSettingsRepository));
}
Settings = measureToolSettingsRepository.SettingsConfig;
SendConfigMSG = ipcMSGCallBackFunc;
}
public bool IsEnabled
{
get => GeneralSettingsConfig.Enabled.MeasureTool;
set
{
if (GeneralSettingsConfig.Enabled.MeasureTool != value)
{
GeneralSettingsConfig.Enabled.MeasureTool = value;
OnPropertyChanged(nameof(IsEnabled));
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
SendConfigMSG(outgoing.ToString());
NotifyPropertyChanged();
}
}
}
public bool ContinuousCapture
{
get
{
return Settings.Properties.ContinuousCapture;
}
set
{
if (Settings.Properties.ContinuousCapture != value)
{
Settings.Properties.ContinuousCapture = value;
NotifyPropertyChanged();
}
}
}
public bool DrawFeetOnCross
{
get
{
return Settings.Properties.DrawFeetOnCross;
}
set
{
if (Settings.Properties.DrawFeetOnCross != value)
{
Settings.Properties.DrawFeetOnCross = value;
NotifyPropertyChanged();
}
}
}
public string CrossColor
{
get
{
return Settings.Properties.MeasureCrossColor.Value;
}
set
{
value = (value != null) ? SettingsUtilities.ToRGBHex(value) : "#FF4500";
if (!value.Equals(Settings.Properties.MeasureCrossColor.Value, StringComparison.OrdinalIgnoreCase))
{
Settings.Properties.MeasureCrossColor.Value = value;
NotifyPropertyChanged();
}
}
}
public bool PerColorChannelEdgeDetection
{
get
{
return Settings.Properties.PerColorChannelEdgeDetection;
}
set
{
if (Settings.Properties.PerColorChannelEdgeDetection != value)
{
Settings.Properties.PerColorChannelEdgeDetection = value;
NotifyPropertyChanged();
}
}
}
public int PixelTolerance
{
get
{
return Settings.Properties.PixelTolerance.Value;
}
set
{
if (Settings.Properties.PixelTolerance.Value != value)
{
Settings.Properties.PixelTolerance.Value = value;
NotifyPropertyChanged();
}
}
}
public HotkeySettings ActivationShortcut
{
get
{
return Settings.Properties.ActivationShortcut;
}
set
{
if (Settings.Properties.ActivationShortcut != value)
{
Settings.Properties.ActivationShortcut = value;
NotifyPropertyChanged();
}
}
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
OnPropertyChanged(propertyName);
SettingsUtils.SaveSettings(Settings.ToJsonString(), MeasureToolSettings.ModuleName);
}
private Func<string, int> SendConfigMSG { get; }
}
}

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