mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-05 03:47:01 +01:00
Compare commits
47 Commits
dev/vanzue
...
crutkas-pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8038df1b62 | ||
|
|
0de60445ea | ||
|
|
47d4a65223 | ||
|
|
1b72c0b969 | ||
|
|
9160c82fc2 | ||
|
|
452e0dcf51 | ||
|
|
2c9a9e9fca | ||
|
|
09c8c1d79a | ||
|
|
95c8a83f79 | ||
|
|
2830ea919c | ||
|
|
725ad21952 | ||
|
|
ebc3a139c5 | ||
|
|
28dba2633e | ||
|
|
9fbd3de3a2 | ||
|
|
4a0d9912ae | ||
|
|
15c79a0176 | ||
|
|
97d46efec2 | ||
|
|
46242b384e | ||
|
|
84be261581 | ||
|
|
5a8095b704 | ||
|
|
417c1a6b98 | ||
|
|
2593149d22 | ||
|
|
0b50c38fe1 | ||
|
|
840808b465 | ||
|
|
b50df36b70 | ||
|
|
b94593ef73 | ||
|
|
7a01d56179 | ||
|
|
130e9a0a68 | ||
|
|
34c37f2d38 | ||
|
|
47aed03c03 | ||
|
|
6423c7693d | ||
|
|
3e14d50f65 | ||
|
|
db7c9e180e | ||
|
|
bcc3ded280 | ||
|
|
24a3cdd486 | ||
|
|
1884e6abc1 | ||
|
|
ad4b553bb1 | ||
|
|
193d9aacbe | ||
|
|
483e773299 | ||
|
|
dd4c7ba57e | ||
|
|
29534601be | ||
|
|
74225b01ce | ||
|
|
2f001e8150 | ||
|
|
a983a773f3 | ||
|
|
02eb55e3b9 | ||
|
|
afaf904016 | ||
|
|
4425e3ad28 |
108
.github/actions/spell-check/expect.txt
vendored
108
.github/actions/spell-check/expect.txt
vendored
@@ -2,8 +2,8 @@ AAAAs
|
||||
abcdefghjkmnpqrstuvxyz
|
||||
abgr
|
||||
ABlocked
|
||||
ABOUTBOX
|
||||
ABORTIFHUNG
|
||||
ABOUTBOX
|
||||
Abug
|
||||
Acceleratorkeys
|
||||
ACCEPTFILES
|
||||
@@ -97,8 +97,8 @@ ASSOCSTR
|
||||
ASYNCWINDOWPLACEMENT
|
||||
ASYNCWINDOWPOS
|
||||
atl
|
||||
ATX
|
||||
ATRIOX
|
||||
ATX
|
||||
aumid
|
||||
authenticode
|
||||
AUTOBUDDY
|
||||
@@ -117,10 +117,10 @@ azman
|
||||
azureaiinference
|
||||
azureinference
|
||||
azureopenai
|
||||
backticks
|
||||
bbwe
|
||||
BCIE
|
||||
bck
|
||||
backticks
|
||||
BESTEFFORT
|
||||
bezelled
|
||||
bhid
|
||||
@@ -148,8 +148,8 @@ bmi
|
||||
BNumber
|
||||
BODGY
|
||||
BOklab
|
||||
Bootstrappers
|
||||
BOOTSTRAPPERINSTALLFOLDER
|
||||
Bootstrappers
|
||||
BOTTOMALIGN
|
||||
boxmodel
|
||||
BPBF
|
||||
@@ -176,17 +176,16 @@ BYPOSITION
|
||||
CALCRECT
|
||||
CALG
|
||||
callbackptr
|
||||
cabstr
|
||||
calpwstr
|
||||
caub
|
||||
Cangjie
|
||||
CANRENAME
|
||||
Carlseibert
|
||||
Canvascustomlayout
|
||||
CAPTUREBLT
|
||||
CAPTURECHANGED
|
||||
CARETBLINKING
|
||||
Carlseibert
|
||||
CAtl
|
||||
caub
|
||||
CBN
|
||||
cch
|
||||
CCHDEVICENAME
|
||||
@@ -206,11 +205,9 @@ changecursor
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
claude
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
@@ -264,7 +261,6 @@ CONFIGW
|
||||
CONFLICTINGMODIFIERKEY
|
||||
CONFLICTINGMODIFIERSHORTCUT
|
||||
CONOUT
|
||||
coreclr
|
||||
constexpr
|
||||
contentdialog
|
||||
contentfiles
|
||||
@@ -276,6 +272,7 @@ copiedcolorrepresentation
|
||||
coppied
|
||||
copyable
|
||||
COPYPEN
|
||||
coreclr
|
||||
COREWINDOW
|
||||
Corpor
|
||||
cotaskmem
|
||||
@@ -284,18 +281,18 @@ countof
|
||||
covrun
|
||||
cpcontrols
|
||||
cph
|
||||
cppcoreguidelines
|
||||
cplusplus
|
||||
CPower
|
||||
cppcoreguidelines
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
createdump
|
||||
creativecommons
|
||||
CREATEPROCESS
|
||||
CREATESCHEDULEDTASK
|
||||
CREATESTRUCT
|
||||
CREATEWINDOWFAILED
|
||||
creativecommons
|
||||
CRECT
|
||||
CRH
|
||||
critsec
|
||||
@@ -331,7 +328,6 @@ CYSCREEN
|
||||
CYSMICON
|
||||
CYVIRTUALSCREEN
|
||||
Czechia
|
||||
cziplib
|
||||
Dac
|
||||
dacl
|
||||
DAffine
|
||||
@@ -355,9 +351,7 @@ Deact
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
dfx
|
||||
Deduplicator
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
DEFAULTCOLOR
|
||||
DEFAULTFLAGS
|
||||
@@ -404,7 +398,6 @@ DISPLAYFREQUENCY
|
||||
displayname
|
||||
DISPLAYORIENTATION
|
||||
divyan
|
||||
djwsxzxb
|
||||
Dlg
|
||||
DLGFRAME
|
||||
DLGMODALFRAME
|
||||
@@ -417,7 +410,6 @@ DONTVALIDATEPATH
|
||||
dotnet
|
||||
downsampled
|
||||
downsampling
|
||||
Downsampled
|
||||
downscale
|
||||
DPICHANGED
|
||||
DPIs
|
||||
@@ -531,7 +523,6 @@ EXTRINSICPROPERTIES
|
||||
eyetracker
|
||||
FANCYZONESDRAWLAYOUTTEST
|
||||
FANCYZONESEDITOR
|
||||
FNumber
|
||||
FARPROC
|
||||
fdx
|
||||
fesf
|
||||
@@ -563,8 +554,8 @@ FIXEDSYS
|
||||
flac
|
||||
flyouts
|
||||
FMask
|
||||
foundrylocal
|
||||
fmtid
|
||||
FNumber
|
||||
FOF
|
||||
FOFX
|
||||
FOLDERID
|
||||
@@ -575,6 +566,7 @@ FORCEMINIMIZE
|
||||
FORMATDLGORD
|
||||
formatetc
|
||||
FORPARSING
|
||||
foundrylocal
|
||||
FRAMECHANGED
|
||||
frm
|
||||
FROMTOUCH
|
||||
@@ -593,13 +585,13 @@ gdi
|
||||
gdiplus
|
||||
GDIPVER
|
||||
GDISCALED
|
||||
geolocator
|
||||
GETCLIENTAREAANIMATION
|
||||
GETCURSEL
|
||||
GETDESKWALLPAPER
|
||||
GETDLGCODE
|
||||
GETDPISCALEDSIZE
|
||||
getfilesiginforedist
|
||||
geolocator
|
||||
GETHOTKEY
|
||||
GETICON
|
||||
GETLBTEXT
|
||||
@@ -610,11 +602,12 @@ GETSCREENSAVERRUNNING
|
||||
GETSECKEY
|
||||
GETSTICKYKEYS
|
||||
GETTEXTLENGTH
|
||||
GIFs
|
||||
gitmodules
|
||||
GHND
|
||||
gitmodules
|
||||
GMEM
|
||||
GNumber
|
||||
googleai
|
||||
googlegemini
|
||||
gpedit
|
||||
gpo
|
||||
GPOCA
|
||||
@@ -631,8 +624,6 @@ GValue
|
||||
gwl
|
||||
GWLP
|
||||
GWLSTYLE
|
||||
googleai
|
||||
googlegemini
|
||||
hangeul
|
||||
Hanzi
|
||||
Hardlines
|
||||
@@ -743,9 +734,7 @@ IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
idl
|
||||
IIM
|
||||
idlist
|
||||
ifd
|
||||
IDOK
|
||||
IDOn
|
||||
IDR
|
||||
@@ -754,15 +743,16 @@ ietf
|
||||
IEXPLORE
|
||||
IFACEMETHOD
|
||||
IFACEMETHODIMP
|
||||
ifd
|
||||
IGNOREUNKNOWN
|
||||
IGo
|
||||
iid
|
||||
IIM
|
||||
Iindex
|
||||
Ijwhost
|
||||
ILD
|
||||
IMAGEHLP
|
||||
IMAGERESIZERCONTEXTMENU
|
||||
IPTC
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
@@ -798,7 +788,6 @@ INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
|
||||
INSTALLLOCATION
|
||||
INSTALLMESSAGE
|
||||
INSTALLPROPERTY
|
||||
installscopeperuser
|
||||
INSTALLSTARTMENUSHORTCUT
|
||||
INSTALLSTATE
|
||||
Inste
|
||||
@@ -811,6 +800,7 @@ invokecommand
|
||||
ipcmanager
|
||||
IPREVIEW
|
||||
ipreviewhandlervisualssetfont
|
||||
IPTC
|
||||
irow
|
||||
irprops
|
||||
isbi
|
||||
@@ -854,15 +844,14 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
ksa
|
||||
kvp
|
||||
Kybd
|
||||
LARGEICON
|
||||
lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
LAYOUTRTL
|
||||
LCh
|
||||
lbl
|
||||
LCh
|
||||
lcid
|
||||
LCIDTo
|
||||
lcl
|
||||
@@ -878,10 +867,10 @@ LExit
|
||||
lhwnd
|
||||
LIBFUZZER
|
||||
LIBID
|
||||
lightswitch
|
||||
LIMITSIZE
|
||||
LIMITTEXT
|
||||
lindex
|
||||
lightswitch
|
||||
linkid
|
||||
LINKOVERLAY
|
||||
LINQTo
|
||||
@@ -892,6 +881,7 @@ LLKH
|
||||
llkhf
|
||||
LMEM
|
||||
LMENU
|
||||
lng
|
||||
LOADFROMFILE
|
||||
LOBYTE
|
||||
localappdata
|
||||
@@ -901,17 +891,14 @@ LOCATIONCHANGE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
logon
|
||||
lon
|
||||
LOGMSG
|
||||
logon
|
||||
LOGPIXELSX
|
||||
LOGPIXELSY
|
||||
lng
|
||||
lon
|
||||
longdate
|
||||
LONGNAMES
|
||||
lowlevel
|
||||
lquadrant
|
||||
LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
@@ -945,6 +932,7 @@ lpv
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
lquadrant
|
||||
LReader
|
||||
LRESULT
|
||||
LSTATUS
|
||||
@@ -971,6 +959,7 @@ MAKELONG
|
||||
MAKELPARAM
|
||||
makepri
|
||||
MAKEWPARAM
|
||||
Malware
|
||||
manifestdependency
|
||||
MAPPEDTOSAMEKEY
|
||||
MAPTOSAMESHORTCUT
|
||||
@@ -993,8 +982,8 @@ MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metadatas
|
||||
metadatamatters
|
||||
Metadatas
|
||||
metafile
|
||||
mfc
|
||||
Mgmt
|
||||
@@ -1040,9 +1029,6 @@ mousepointer
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
muxx
|
||||
muxxc
|
||||
muxxh
|
||||
MRM
|
||||
MRT
|
||||
mru
|
||||
@@ -1075,6 +1061,9 @@ MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
muxc
|
||||
muxx
|
||||
muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
MVVMTK
|
||||
@@ -1157,7 +1146,6 @@ nonstd
|
||||
NOOWNERZORDER
|
||||
NOPARENTNOTIFY
|
||||
NOPREFIX
|
||||
NPU
|
||||
NOREDIRECTIONBITMAP
|
||||
NOREDRAW
|
||||
NOREMOVE
|
||||
@@ -1186,6 +1174,7 @@ nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
NPU
|
||||
NResize
|
||||
NTAPI
|
||||
ntdll
|
||||
@@ -1210,16 +1199,15 @@ oldpath
|
||||
oldtheme
|
||||
oleaut
|
||||
OLECHAR
|
||||
ollama
|
||||
onebranch
|
||||
onnx
|
||||
OOBEUI
|
||||
openas
|
||||
opencode
|
||||
OPENFILENAME
|
||||
opensource
|
||||
openxmlformats
|
||||
ollama
|
||||
Olllama
|
||||
onnx
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
@@ -1292,6 +1280,7 @@ pguid
|
||||
phbm
|
||||
phbmp
|
||||
phicon
|
||||
Photoshop
|
||||
phwnd
|
||||
pici
|
||||
pidl
|
||||
@@ -1314,7 +1303,6 @@ pnid
|
||||
PNMLINK
|
||||
Poc
|
||||
Podcasts
|
||||
Photoshop
|
||||
POINTERID
|
||||
POINTERUPDATE
|
||||
Pokedex
|
||||
@@ -1409,10 +1397,9 @@ pwsz
|
||||
pwtd
|
||||
QDC
|
||||
qit
|
||||
QNN
|
||||
Qualcomm
|
||||
QITAB
|
||||
QITABENT
|
||||
QNN
|
||||
qoi
|
||||
Quarternary
|
||||
QUERYENDSESSION
|
||||
@@ -1422,8 +1409,8 @@ quickaccent
|
||||
QUNS
|
||||
RAII
|
||||
RAlt
|
||||
RAquadrant
|
||||
randi
|
||||
RAquadrant
|
||||
rasterization
|
||||
Rasterize
|
||||
RAWINPUTDEVICE
|
||||
@@ -1450,9 +1437,7 @@ regfile
|
||||
REGISTERCLASSFAILED
|
||||
REGISTRYHEADER
|
||||
REGISTRYPREVIEWEXT
|
||||
registryroot
|
||||
regkey
|
||||
regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
releaseblog
|
||||
@@ -1505,7 +1490,6 @@ rstringalpha
|
||||
rstringdigit
|
||||
rtb
|
||||
RTLREADING
|
||||
rtm
|
||||
runas
|
||||
rundll
|
||||
rungameid
|
||||
@@ -1562,8 +1546,8 @@ SETRULES
|
||||
SETSCREENSAVEACTIVE
|
||||
SETSTICKYKEYS
|
||||
SETTEXT
|
||||
settingscard
|
||||
SETTINGCHANGE
|
||||
settingscard
|
||||
SETTINGSCHANGED
|
||||
settingsheader
|
||||
settingshotkeycontrol
|
||||
@@ -1708,6 +1692,7 @@ stringtable
|
||||
stringval
|
||||
Strm
|
||||
strret
|
||||
STRSAFE
|
||||
stscanf
|
||||
sttngs
|
||||
Stubless
|
||||
@@ -1719,7 +1704,6 @@ sublang
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
suntimes
|
||||
sut
|
||||
svchost
|
||||
SVGIn
|
||||
@@ -1753,7 +1737,6 @@ SYSTEMMODAL
|
||||
SYSTEMTIME
|
||||
TARG
|
||||
TARGETAPPHEADER
|
||||
TARGETDIR
|
||||
targetentrypoint
|
||||
TARGETHEADER
|
||||
targetver
|
||||
@@ -1783,10 +1766,10 @@ textextractor
|
||||
TEXTINCLUDE
|
||||
tfopen
|
||||
tgz
|
||||
THEMECHANGED
|
||||
themeresources
|
||||
THH
|
||||
THICKFRAME
|
||||
THEMECHANGED
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
TILEDWINDOW
|
||||
@@ -1883,7 +1866,6 @@ USEINSTALLERFORTEST
|
||||
USESHOWWINDOW
|
||||
USESTDHANDLES
|
||||
USRDLL
|
||||
utm
|
||||
UType
|
||||
uuidv
|
||||
uwp
|
||||
@@ -1956,11 +1938,11 @@ Wca
|
||||
WCE
|
||||
wcex
|
||||
WClass
|
||||
WCRAPI
|
||||
wcsicmp
|
||||
wcsncpy
|
||||
wcsnicmp
|
||||
WCT
|
||||
WCRAPI
|
||||
WDA
|
||||
wdm
|
||||
wdp
|
||||
@@ -1988,6 +1970,7 @@ WINDOWPLACEMENT
|
||||
WINDOWPOSCHANGED
|
||||
WINDOWPOSCHANGING
|
||||
WINDOWSBUILDNUMBER
|
||||
windowsml
|
||||
windowssearch
|
||||
windowssettings
|
||||
WINDOWSTYLES
|
||||
@@ -2003,9 +1986,8 @@ Winhook
|
||||
WINL
|
||||
winlogon
|
||||
winmd
|
||||
WINNT
|
||||
windowsml
|
||||
winml
|
||||
WINNT
|
||||
winres
|
||||
winrt
|
||||
winsdk
|
||||
@@ -2067,20 +2049,21 @@ WTSAT
|
||||
Wubi
|
||||
WUX
|
||||
Wwanpp
|
||||
xap
|
||||
XAxis
|
||||
XButton
|
||||
xclip
|
||||
xcopy
|
||||
xap
|
||||
XDeployment
|
||||
XDimension
|
||||
xdf
|
||||
XDimension
|
||||
XDocument
|
||||
XElement
|
||||
xfd
|
||||
XFile
|
||||
XIncrement
|
||||
XLoc
|
||||
xmp
|
||||
XNamespace
|
||||
Xoshiro
|
||||
XPels
|
||||
@@ -2091,23 +2074,22 @@ xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
xmp
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
xxxxxx
|
||||
YAxis
|
||||
ycombinator
|
||||
YIncrement
|
||||
YDimension
|
||||
YIncrement
|
||||
yinle
|
||||
yinyue
|
||||
YPels
|
||||
YPos
|
||||
YResolution
|
||||
YSpeed
|
||||
YTimer
|
||||
YStr
|
||||
YTimer
|
||||
YVIRTUALSCREEN
|
||||
ZEROINIT
|
||||
zonability
|
||||
|
||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -5,6 +5,7 @@
|
||||
## PR Checklist
|
||||
|
||||
- [ ] Closes: #xxx
|
||||
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) -->
|
||||
- [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
|
||||
- [ ] **Tests:** Added/updated and all pass
|
||||
- [ ] **Localization:** All end-user-facing strings can be localized
|
||||
|
||||
2
.github/workflows/msstore-submissions.yml
vendored
2
.github/workflows/msstore-submissions.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
echo powerToysInstallerArm64Url=$(jq -n "$powerToysSetup" | jq -r '[.[]|select(.name | contains("arm64"))][0].browser_download_url') >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup .NET 9.0
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -4,6 +4,3 @@
|
||||
[submodule "deps/expected-lite"]
|
||||
path = deps/expected-lite
|
||||
url = https://github.com/martinmoene/expected-lite.git
|
||||
[submodule "deps/cziplib"]
|
||||
path = deps/cziplib
|
||||
url = https://github.com/kuba--/zip.git
|
||||
|
||||
@@ -291,6 +291,7 @@
|
||||
"Mono.Cecil.Rocks.dll",
|
||||
"Newtonsoft.Json.dll",
|
||||
"CommunityToolkit.WinUI.Controls.TitleBar.dll",
|
||||
"CommunityToolkit.WinUI.Controls.OpacityMaskView.dll",
|
||||
|
||||
"NLog.dll",
|
||||
"HtmlAgilityPack.dll",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<PropertyGroup Condition="'$(SkipCppCodeAnalysis)' == ''">
|
||||
<RunCodeAnalysis>true</RunCodeAnalysis>
|
||||
<CodeAnalysisRuleSet>$(MsbuildThisFileDirectory)\CppRuleSet.ruleset</CodeAnalysisRuleSet>
|
||||
<CAExcludePath>$(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(CAExcludePath)</CAExcludePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- C++ source compile-specific things for all configurations -->
|
||||
@@ -34,7 +35,7 @@
|
||||
<PreferredToolArchitecture Condition="'$(PROCESSOR_ARCHITECTURE)' == 'ARM64' or '$(PROCESSOR_ARCHITEW6432)' == 'ARM64'">arm64</PreferredToolArchitecture>
|
||||
<VcpkgEnabled>false</VcpkgEnabled>
|
||||
<ReplaceWildcardsInProjectItems>true</ReplaceWildcardsInProjectItems>
|
||||
<ExternalIncludePath>$(MSBuildThisFileFullPath)\..\deps\;$(MSBuildThisFileFullPath)\..\packages\;$(ExternalIncludePath)</ExternalIncludePath>
|
||||
<ExternalIncludePath>$(MSBuildThisFileDirectory)deps;$(MSBuildThisFileDirectory)packages;$(ExternalIncludePath)</ExternalIncludePath>
|
||||
<!-- Enable control flow guard for C++ projects that don't consume any C++ files -->
|
||||
<!-- This covers the case where a .dll exports a .lib, but doesn't have any ClCompile entries. -->
|
||||
<LinkControlFlowGuard>Guard</LinkControlFlowGuard>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<PackageVersion Include="AdaptiveCards.ObjectModel.WinUI3" Version="2.0.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Rendering.WinUI3" Version="2.1.0-beta" />
|
||||
<PackageVersion Include="AdaptiveCards.Templating" Version="2.0.5" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" Version="0.1.251101-build.2372" />
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
|
||||
@@ -1498,6 +1498,7 @@ SOFTWARE.
|
||||
- CoenM.ImageSharp.ImageHash
|
||||
- CommunityToolkit.Common
|
||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||
- CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView
|
||||
- CommunityToolkit.Mvvm
|
||||
- CommunityToolkit.WinUI.Animations
|
||||
- CommunityToolkit.WinUI.Collections
|
||||
|
||||
240
README.md
240
README.md
@@ -51,19 +51,20 @@ But to get started quickly, choose one of the installation methods below:
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.95%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysUserSetup-0.95.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.95.1/PowerToysSetup-0.95.1-arm64.exe
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
[github-current-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.96%22
|
||||
[ptUserX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-x64.exe
|
||||
[ptUserArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysUserSetup-0.96.1-arm64.exe
|
||||
[ptMachineX64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-x64.exe
|
||||
[ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-arm64.exe
|
||||
|
||||
| Description | Filename |
|
||||
|----------------|----------|
|
||||
| Per user - x64 | [PowerToysUserSetup-0.95.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.95.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.95.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.95.1-arm64.exe][ptMachineArm64] |
|
||||
| Per user - x64 | [PowerToysUserSetup-0.96.1-x64.exe][ptUserX64] |
|
||||
| Per user - ARM64 | [PowerToysUserSetup-0.96.1-arm64.exe][ptUserArm64] |
|
||||
| Machine wide - x64 | [PowerToysSetup-0.96.1-x64.exe][ptMachineX64] |
|
||||
| Machine wide - ARM64 | [PowerToysSetup-0.96.1-arm64.exe][ptMachineArm64] |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
@@ -102,156 +103,131 @@ There are [community driven install methods](./doc/unofficialInstallMethods.md)
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
**Version 0.95 (October 2025)**
|
||||
**Version 0.96 (November 2025)**
|
||||
|
||||
For an in-depth look at the latest changes, visit the [Windows Command Line blog](https://aka.ms/powertoys-releaseblog).
|
||||
|
||||
**✨ Highlights**
|
||||
- **NEW:** The **Light Switch** utility in PowerToys allows you to automatically switch between light and dark themes in Windows based on the time of day.
|
||||
- Command Palette delivered major search performance gains (new fuzzy matcher and smarter fallbacks) improving relevance and speed.
|
||||
- Peek can now be activated using just the Spacebar!
|
||||
- Find My Mouse added transparent spotlight with independent backdrop opacity, boosting focus and accessibility.
|
||||
- Settings now lets you delete shortcuts entirely and ignore conflicts.
|
||||
- Mouse Pointer Crosshairs gained orientation options (vertical / horizontal / both) for customizable accessibility. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- PowerRename fixed enumeration counter skipping ensuring reliable batch renames. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- ZoomIt restored legacy draw and snipping behaviors, and fixed recording issues, improving reliability. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
- Advanced Paste now supports multiple online and on-device AI model providers: Azure OpenAI, OpenAI, Google Gemini, Mistral, Foundry Local and Ollama.
|
||||
- Command Palette received extensive improvements including file search filters, better clipboard history metadata, context-menu styling, and dozens of bug fixes and enhancements.
|
||||
- PowerRename can now extract and use photo metadata (EXIF, XMP) in renaming patterns like `%Camera`, `%Lens`, and `%ExposureTime`.
|
||||
|
||||
### Advanced Paste
|
||||
- Advanced Paste now lets you connect to multiple AI providers instead of being limited to a single OpenAI provider. See [Advanced Paste documentation](https://learn.microsoft.com/windows/powertoys/advanced-paste) for usage.
|
||||
|
||||
### Awake
|
||||
- The Awake countdown timer now stays accurate over long periods. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed Awake context menu positioning. The fix removed the conversion of the mouse cursor from screen to client-window coordinates, instead using the raw screen coordinates returned by GetCursorPos; the context menu now appears at the correct screen position. Thanks [@lzandman](https://github.com/lzandman)!
|
||||
|
||||
### Command Palette
|
||||
- Applied conditional margin for icon-only tags to tighten layout. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Improved the reliability of accessing Command Palette settings through PowerToys Settings and executing other x-cmdpal:// protocol commands. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Enabled AOT by default for improved performance while simplifying publish configs.
|
||||
- Replaced service state color dots with play/pause/stop icons for enhanced accessibility. Thanks [@samrueby](https://github.com/samrueby)
|
||||
- Fixed filter dropdown sync and crash by binding SelectedValue and raising UI-thread notifications. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured long links wrap correctly in details view.
|
||||
- Removed animation and enforced minimum width on filter dropdown for clarity. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored focus to More button after ESC closes context menu, improving keyboard flow. Thanks [@chatasweetie](https://github.com/chatasweetie)
|
||||
- Marked main and toast windows as tool windows to keep them out of Alt+Tab while preserving style. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed AOT template and theming issues for filter separators. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Introduced grid layouts (small, medium, gallery) for richer page presentation.
|
||||
- Materialized result lists to avoid rescoring overhead.
|
||||
- Disabled problematic selection TextToSuggest behind environment flag.
|
||||
- Major search performance improvements (new fuzzy matcher, smarter fallbacks, fewer exceptions).
|
||||
- Added context menu "Show Details" command when details pane is hidden.
|
||||
- Reduced window flicker by avoiding unnecessary cloaking. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Restored EmptyContent rendering for blank states. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)
|
||||
- Saved new state even if prior app state file was corrupt (better resilience). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Migrated settings window to WinUI TitleBar control. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Prevented crash on duplicate keybindings and simplified matching. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hotkeys now always respect the “Ignore shortcut in fullscreen” setting. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Hid search box on content pages, improving focus and accessibility, and added Home title. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Blocked Ctrl+I from inserting stray tabs in search box.
|
||||
- Logged HRESULT codes in error logs for deeper diagnostics. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Advanced font and emoji icon classification and alignment improvements. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Ensured that fallback command icons are visible on the extension settings page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed breadcrumb margin misalignment (visual polish). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Truncated overly long command labels with ellipsis to prevent overflow.
|
||||
- Added a setting to configure the page transition animation.
|
||||
- Collection of small improvements and nits for Run Commands.
|
||||
- Improved bookmarks performance and experience. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Ctrl+O shortcut in Clipboard History to open links directly.
|
||||
- Resolved conflict with external software that blocked Command Palette from hiding.
|
||||
- Updated context menu items to reflect name and icon changes, and ensured application icons are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added Alt+Home shortcut to return immediately to the Command Palette home page. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed a crash when displaying code blocks in markdown on detail or content pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Fixed an issue where the search bar icon and title were not updated when rapidly switching pages. Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Improved the appearance of the search box in the context menu.
|
||||
|
||||
- The search field in context menus now matches the look of the Command Palette, with a smoke backdrop and improved padding.
|
||||
- Fallback items such as math calculations or the Run command now appear in results more quickly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured the command bar updates correctly after navigating to another page and commands are displayed correctly. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- The Command Palette settings page has been reorganized. Activation-key options are grouped under an expander and extension settings are framed for improved readability.
|
||||
- When you modify a command, its alias, hotkey, and tags now update in the top-level list, keeping the displayed information in sync. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Press `Ctrl + ,` to open Command Palette settings from anywhere. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- You can use `Page Up` and `Page Down` to navigate the list while focus is in the search box. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- Fixed an issue where the search box could disappear when navigating pages. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured search text is selected when *Go home when activated* and *Highlight search on activate* are both enabled. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed an issue where Command Palette window occasionally appeared on the taskbar under certain Windows settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured that labels and icons of list items and menu items update when they change. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed visibility of list filters when navigating to a content page. Thanks [@DevLGuilherme](https://github.com/DevLGuilherme)!
|
||||
- Added search to the extension list and a link to extensions on the Microsoft Store. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added options to open the Command Palette window at its last position or re-center it.
|
||||
- The Command Palette now remembers its window size after restarting.
|
||||
- Added a global error handler that logs fatal errors and provides feedback when unexpected failures force Command Palette to close. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed forms and extension settings not showing on some machines due to a missing VC++ runtime.
|
||||
- Restored ranking of fallback commands for built-in extensions (Sleep, Shutdown, Windows settings, Web search, etc.). Thanks [@jiripolasek](https://github.com/jiripolasek).
|
||||
- Improved and unified labels and texts across the application!
|
||||
- Maintainance: Resolved numerous build warnings in Command Palette projects; no user-visible impact. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Maintainance: Fixed a logging issue so exception messages are properly recorded instead of placeholder text, improving troubleshooting. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Command Palette Extensions
|
||||
- Replaced localized WebSearch setting keys with stable literals and numeric history count. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled advanced markdown tables and emphasis extensions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added setting to choose Clipboard History primary action (Paste vs Copy). Thanks [@jiripolasek](https://github.com/jiripolasek)
|
||||
- Added actionable empty-state hints for File Search (search PC / open indexing settings). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Ensured all WinGet extension assets copy reliably to output. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Improved Run command line parsing for paths with spaces; sped up related tests.
|
||||
- Updated WebSearch extension icon set for enhanced clarity and contrast. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal profile sort order setting including MRU tracking. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Uninstall Application command (UWP direct, Win32 via Settings). Thanks [@mKpwnz](https://github.com/mKpwnz)!
|
||||
- Deferred WinGet details loading and added timing logs.
|
||||
- Removed LINQ from All Apps extension for performance.
|
||||
- Added standardized key chord system + shortcuts to File Search commands. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added Terminal channel filter & remembered selection option. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Enabled loading local/data/app images in markdown with sizing hints. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added external extension reload via x-cmdpal://reload (configurable). Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Instant WebSearch history updates with in-memory store & events. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Added keep-after-paste option and safe delete with confirmation for Clipboard History. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Environment Variables
|
||||
- Replaced custom window chrome with WinUI TitleBar for cleaner, maintainable Environment Variables UI.
|
||||
|
||||
### File Locksmith
|
||||
- Adopted WinUI TitleBar to simplify window chrome while preserving appearance.
|
||||
- Bookmarks: Added hints about bookmark placeholders to the Add/Edit Bookmark form. — Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Bookmarks: Improved migration of bookmarks from older versions and fixed an issue where aliases or keyboard shortcuts could be lost after restart. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Clipboard history: Items shown in Command Palette’s clipboard history now include helpful metadata. For example, image items show dimensions, text files show names and sizes, web links include page titles, and text entries display word counts. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- File search: Added filter buttons to show *all items*, *files only*, or *folders only*. Selecting a filter adds `kind:folders` or `kind:not folders` to narrow results.
|
||||
- System commands: Replaced the `:red_circle:` placeholder with an actual red-circle emoji so the correct icon appears in the UI. Thanks [@samrueby](https://github.com/samrueby)!
|
||||
- WinGet: Search performance feels more responsive because typed input is now processed via a task queue rather than complex cancellation tokens!
|
||||
- Window Walker: UWP apps no longer show a "not responding" label when suspended. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Window Walker: Now displays the actual icon of each window rather than using the process icon, improving recognition of PWAs and Python GUIs. Thanks [@Lee-WonJun](https://github.com/Lee-WonJun)!
|
||||
- Windows Terminal profiles: Fixed a rare crash in the Windows Terminal extension when the `LOCALAPPDATA` environment variable was missing. The path is now retrieved via a reliable API. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
### Find My Mouse
|
||||
- Added transparent spotlight support with separate backdrop opacity; migrated to Windows App SDK composition APIs.
|
||||
- Activating Find My Mouse no longer makes the cursor change to the busy (hourglass) icon or steals focus from your active application.
|
||||
|
||||
### Hosts File Editor
|
||||
- Migrated to native WinUI TitleBar for cleaner, maintainable window chrome.
|
||||
- Added customizable backup settings allowing users to configure backup frequency, location, and auto-deletion policies. Thanks [@davidegiacometti](https://github.com/davidegiacometti)!
|
||||
|
||||
### Image Resizer
|
||||
- Fixed settings consistency during batch resize operations by capturing settings once before processing. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Light Switch
|
||||
- Introduced as a brand-new PowerToy module.
|
||||
- Automatically switches between light and dark themes.
|
||||
- Supports time-based scheduling or location-based sunrise/sunset switching.
|
||||
- Supports using a keyboard shortcut to force a change.
|
||||
- Supports filtering changes for Apps and/or System Theme.
|
||||
- Introduced new UI to allow users to manually enter their latitude and longitude in Sunrise to Sunset mode.
|
||||
- Refactored service with cleaner state management for stability.
|
||||
- Removed logs from every tick, only logging key events to largely reduce log size.
|
||||
|
||||
### Mouse Pointer Crosshairs
|
||||
- Added Esc key to cancel active gliding cursor sequence. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Added orientation option (vertical / horizontal / both) for crosshairs customization. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
- Enabled switching between Mouse Pointer Crosshairs and Gliding Cursor modes. Thanks [@mikehall-ms](https://github.com/mikehall-ms)!
|
||||
|
||||
### Mouse Without Borders
|
||||
- Continued Common class refactor (part 5/7) by extracting clipboard and init/cleanup logic into focused classes. Thanks [@mikeclayton](https://github.com/mikeclayton)!
|
||||
|
||||
- Fix connection failures caused by conflicting MachineId across machines. Thanks [@noraa-junker](https://github.com/noraa-junker) for troubleshooting!
|
||||
- Added horizontal scrolling support. Thanks [@MasonBergstrom](https://github.com/MasonBergstrom)!
|
||||
|
||||
### Peek
|
||||
- Added the option to activate Peek with just the Spacebar.
|
||||
- Fixed media files remaining locked after preview window closes. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Added a command-line interface for file previewing. See the [Peek documentation](https://learn.microsoft.com/windows/powertoys/peek) for usage. Thanks [@prochan2](https://github.com/prochan2)!
|
||||
|
||||
### PowerRename
|
||||
- Fixed enumeration counter skipping when regex replacement equals original filename (counters now advance reliably). Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- PowerRename no longer crashes due to a missing resources file.
|
||||
- Added photo metadata extraction support using EXIF and XMP for pattern-based renaming with camera info, GPS coordinates, and date taken. See [PowerRename Documentation](https://learn.microsoft.com/en-us/windows/powertoys/powerrename).
|
||||
|
||||
### Quick Accent
|
||||
- Expanded Welsh layout with acute, grave, and dieresis variants for vowels (consistent ordering). Thanks [@PesBandi](https://github.com/PesBandi)!
|
||||
### PowerToys Run
|
||||
- Added retry logic with exponential backoff to handle DWM composition errors during theme changes. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Updated OneNote icons to reflect new Microsoft 365 design. Thanks [@trevorNgo](https://github.com/trevorNgo)!
|
||||
|
||||
### Registry Preview
|
||||
- Migrated to native TitleBar and AppWindow APIs for cleaner window chrome.
|
||||
### Quick Accent
|
||||
- Added diameter symbol (⌀) for Shift+O in Special Characters mode, thanks to [@anselumjuju](https://github.com/anselumjuju)!
|
||||
|
||||
### Screen Ruler
|
||||
- Fixed ARM64 crash by aligning cursor position structure to 8-byte boundary.
|
||||
### Zoomit
|
||||
- Smoothed out zoom-animation in ZoomIt by coalescing mouse-move and timer events, thanks to [@foxmsft](https://github.com/foxmsft)!
|
||||
- Enabled GIF support for ZoomIt, thanks to [@MarioHewardt](https://github.com/MarioHewardt)!
|
||||
- Fixed spelling mistakes, and refactored some literal strings to string constants, thanks to [@lzandman](https://github.com/lzandman)!
|
||||
- Fixed inaccurate "actual size" screenshots in ZoomIt and resolves a GDI handle leak, improving capture fidelity and long-session stability. thanks to [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### Settings
|
||||
- Added ability to ignore specific hotkey conflicts to reduce noise.
|
||||
- Stopped creating backup directory during dry-run status checks (cleaner first-run).
|
||||
- Standardized casing and localization for ZoomIt and modules header.
|
||||
- Improved search results page accessibility and conditional module grouping.
|
||||
- Fixed title bar overlapping issue at smaller window sizes.
|
||||
- Refined shortcut control visual design with improved consistency and spacing.
|
||||
- Added dashboard utilities sorting by name or status.
|
||||
- Made update notification InfoBar in flyout clickable for direct navigation to update page.
|
||||
- Expanded installation instructions by default in README.
|
||||
- Improved accessibility for shortcut conflict button with static resource-based automation properties.
|
||||
- Added ScrollViewer to Command Palette page in PowerToys Settings. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Fixed module list glitches and Sort Status checkmark issue. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
|
||||
### ZoomIt
|
||||
- Updated resource file to reflect standalone v9.01 and current copyright year. Thanks [@foxmsft](https://github.com/foxmsft)!
|
||||
- Restored legacy draw/snipping behaviors and fixed recording race conditions. Thanks [@chakrik73](https://github.com/chakrik73)!
|
||||
- Added smooth image option for improved zoom quality using GDI+ for static zoom and Magnifier API for live zoom. Thanks [@markrussinovich](https://github.com/markrussinovich)!
|
||||
|
||||
### Documentation
|
||||
- New Microsoft Learn documentation for the Light Switch module.
|
||||
- New dev docs for the Light Switch module.
|
||||
|
||||
### Development (Area-Build & Area-Tests)
|
||||
- Allowed debug launches to continue when modules fail to load, speeding developer iteration.
|
||||
- Fixed spell checker dictionary entry (advapi) to eliminate false error.
|
||||
- Added VS Code development guide and launch configs to streamline cross-editor workflows.
|
||||
- Upgraded Windows App SDK and related dependencies to 1.8 for newer platform features.
|
||||
- Rewrote YAML comment to resolve new spell checker forbidden pattern. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Corrected solution structure by returning misplaced Common project, reducing build confusion.
|
||||
- Modernized build scripts with shared helpers and VS environment autodetection for simpler CLI builds.
|
||||
- Standardized build scripts and platform detection to improve reliability and reuse.
|
||||
- Added missing Command Palette version bump to align module release cadence.
|
||||
- Added EXECUTEDEFAULT term to dictionary to prevent regression build failures. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced nightly pre-warm pipeline and configurable MSBuild cache mode to improve CI performance.
|
||||
- Resolved CI forbidden pattern spelling complaint to keep pipelines green.
|
||||
- Added AI contributor instruction set to clarify code area expectations.
|
||||
- Added accessibility IDs to settings and FancyZones toggles, stabilizing UI tests.
|
||||
- Added automatic log collection on UI test failures to speed root cause analysis.
|
||||
- Stabilized Mouse Utils tests by switching to AccessibilityId selectors.
|
||||
- Added Screen Ruler UI test coverage to validate core measurement workflows.
|
||||
### Development
|
||||
- Fixed accessibility by associating controls with labels for screen readers.
|
||||
- Added accessible name to Shortcut Conflicts button for screen readers.
|
||||
- Excluded TitleBars from tab navigation across multiple utilities. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Migrated build infrastructure from Windows Server 2019 to Server 2022 with improved failure logging and predictable NuGet package paths.
|
||||
- Configured build agents to use larger P: drive for release builds to address disk space constraints.
|
||||
- Enhanced DSC v3 support by organizing resource manifests in a dedicated subfolder with PATH configuration.
|
||||
- Reduced installer bundle size by 6-7MB through centralized Hybrid CRT configuration across all C++ projects.
|
||||
- Updated .NET packages to version 9.0.10 for security fixes. Thanks [@snickler](https://github.com/snickler)!
|
||||
- Fixed spell check dictionary entries for consistency.
|
||||
- Restored accidentally deleted NuGet configuration file for Command Palette extensions.
|
||||
- Fixed package identity build by updating AppxManifest entry points to use PowerShell Core.
|
||||
- Optimized CI pipeline by replacing file copy operations with hard links and moves, reducing build time and disk usage by 10-15GB.
|
||||
- Updated Copilot guidance and PR prompt workflow.
|
||||
- Included high-volume bugs in issue template header. Thanks [@daverayment](https://github.com/daverayment)!
|
||||
- Fixed incorrect HRESULT logging for inner exceptions. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
- Introduced shared sparse package identity for PowerToys Win32 components to enable access to Windows platform APIs.
|
||||
- Consolidated installer builds to produce both machine and user installers simultaneously, reducing build time and complexity.
|
||||
- Migrated exclusively to WiX v5 installer infrastructure, removing legacy WiX v3 support.
|
||||
- Temporarily removed PowerToys installer path from PATH environment variable to prevent application crashes.
|
||||
- Added complete OCR UI test coverage with automated tests for activation, settings, language selection, and text extraction.
|
||||
- Fixed test input for drive path normalization in bookmark resolver unit tests.
|
||||
- Fixed Peek UI tests by restoring Ctrl+Space activation shortcut for test scenarios.
|
||||
- Hided apps in PowerToys.SpareApps package from Start Menu. Thanks [@jiripolasek](https://github.com/jiripolasek)!
|
||||
|
||||
## 🛣️ Roadmap
|
||||
We are planning some nice new features and improvements for the next releases – a revamped Keyboard Manager UI, custom endpoint and local model support for Advanced Paste, Command Palette improvements and a brand-new Shortcut Guide experience! Stay tuned for [v0.96][github-next-release-work]!
|
||||
|
||||
1
deps/cziplib
vendored
1
deps/cziplib
vendored
Submodule deps/cziplib deleted from 81314fff0a
@@ -33,9 +33,12 @@ The **Light Switch** module lets users automatically transition between light an
|
||||
|
||||
> **Note:** Using the shortcut overrides the current schedule until the next transition event.
|
||||
|
||||
* **LightSwitchService**
|
||||
Reads settings and applies theming. Runs a check every minute to ensure the state is correct.
|
||||
|
||||
* **LightSwitchService.cpp**
|
||||
is the heart beat of the module. Controls ticking every minute and depending on user actions (manual override, settings changing, etc) triggers the state manager to perform the corresponding operation.
|
||||
|
||||
* **LightSwitchStateManager.cpp**
|
||||
handles updating the state based on the signals sent by LightSwitchService.
|
||||
|
||||
* **SettingsXAML/LightSwitch**
|
||||
Provides the settings UI for configuring schedules, syncing location, and customizing shortcuts.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <ProjectTelemetry.h>
|
||||
#include <spdlog/sinks/base_sink.h>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
|
||||
#include "../../src/common/logger/logger.h"
|
||||
#include "../../src/common/utils/gpo.h"
|
||||
@@ -856,14 +857,69 @@ UINT __stdcall UnsetAdvancedPasteAPIKeyCA(MSIHANDLE hInstall)
|
||||
|
||||
try
|
||||
{
|
||||
winrt::Windows::Security::Credentials::PasswordVault vault;
|
||||
winrt::Windows::Security::Credentials::PasswordCredential cred;
|
||||
|
||||
hr = WcaInitialize(hInstall, "UnsetAdvancedPasteAPIKey");
|
||||
ExitOnFailure(hr, "Failed to initialize");
|
||||
|
||||
cred = vault.Retrieve(L"https://platform.openai.com/api-keys", L"PowerToys_AdvancedPaste_OpenAIKey");
|
||||
vault.Remove(cred);
|
||||
winrt::Windows::Security::Credentials::PasswordVault vault;
|
||||
|
||||
auto hasPrefix = [](std::wstring_view value, wchar_t const* prefix) {
|
||||
std::wstring_view prefixView{ prefix };
|
||||
return value.compare(0, prefixView.size(), prefixView) == 0;
|
||||
};
|
||||
|
||||
const wchar_t* resourcePrefixes[] = {
|
||||
L"https://platform.openai.com/api-keys",
|
||||
L"https://azure.microsoft.com/products/ai-services/openai-service",
|
||||
L"https://azure.microsoft.com/products/ai-services/ai-inference",
|
||||
L"https://console.mistral.ai/account/api-keys",
|
||||
L"https://ai.google.dev/",
|
||||
};
|
||||
|
||||
const wchar_t* usernamePrefixes[] = {
|
||||
L"PowerToys_AdvancedPaste_",
|
||||
};
|
||||
|
||||
auto credentials = vault.RetrieveAll();
|
||||
for (auto const& credential : credentials)
|
||||
{
|
||||
bool shouldRemove = false;
|
||||
|
||||
std::wstring resource{ credential.Resource() };
|
||||
for (auto const prefix : resourcePrefixes)
|
||||
{
|
||||
if (hasPrefix(resource, prefix))
|
||||
{
|
||||
shouldRemove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRemove)
|
||||
{
|
||||
std::wstring username{ credential.UserName() };
|
||||
for (auto const prefix : usernamePrefixes)
|
||||
{
|
||||
if (hasPrefix(username, prefix))
|
||||
{
|
||||
shouldRemove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldRemove)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
vault.Remove(credential);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
Description="PowerToys OCR Module"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.SettingsUI" Executable="WinUI3Apps\PowerToys.Settings.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
@@ -51,7 +52,8 @@
|
||||
Description="PowerToys Settings UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
<Application Id="PowerToys.ImageResizerUI" Executable="WinUI3Apps\PowerToys.ImageResizer.exe" EntryPoint="Windows.FullTrustApplication">
|
||||
@@ -60,7 +62,8 @@
|
||||
Description="PowerToys Image Resizer UI"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Images\Square150x150Logo.png"
|
||||
Square44x44Logo="Images\Square44x44Logo.png">
|
||||
Square44x44Logo="Images\Square44x44Logo.png"
|
||||
AppListEntry="none">
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
@@ -10,6 +10,23 @@ namespace LanguageModelProvider.FoundryLocal;
|
||||
internal sealed class FoundryClient
|
||||
{
|
||||
public static async Task<FoundryClient?> CreateAsync()
|
||||
{
|
||||
// First attempt with current environment
|
||||
var client = await TryCreateClientAsync().ConfigureAwait(false);
|
||||
if (client != null)
|
||||
{
|
||||
return client;
|
||||
}
|
||||
|
||||
// If failed, refresh PATH from registry and retry once
|
||||
// This handles cases where PowerToys was launched by MSI installer.
|
||||
Logger.LogInfo("[FoundryClient] First attempt failed, refreshing PATH and retrying");
|
||||
RefreshEnvironmentPath();
|
||||
|
||||
return await TryCreateClientAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<FoundryClient?> TryCreateClientAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -169,41 +186,23 @@ internal sealed class FoundryClient
|
||||
|
||||
public async Task<bool> EnsureModelLoaded(string modelId)
|
||||
{
|
||||
try
|
||||
Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
|
||||
|
||||
// Check if already loaded
|
||||
if (await IsModelLoaded(modelId).ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] EnsureModelLoaded called with: {modelId}");
|
||||
|
||||
// Check if already loaded
|
||||
if (await IsModelLoaded(modelId).ConfigureAwait(false))
|
||||
{
|
||||
Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if model exists in cache
|
||||
var cachedModels = await ListCachedModels().ConfigureAwait(false);
|
||||
Logger.LogInfo($"[FoundryClient] Cached models: {string.Join(", ", cachedModels.Select(m => m.Name))}");
|
||||
|
||||
if (!cachedModels.Any(m => m.Name == modelId))
|
||||
{
|
||||
Logger.LogWarning($"[FoundryClient] Model not found in cache: {modelId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load the model
|
||||
Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
|
||||
await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
|
||||
|
||||
// Verify it's loaded
|
||||
var loaded = await IsModelLoaded(modelId).ConfigureAwait(false);
|
||||
Logger.LogInfo($"[FoundryClient] Model load result: {loaded}");
|
||||
return loaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryClient] EnsureModelLoaded exception: {ex.Message}");
|
||||
return false;
|
||||
Logger.LogInfo($"[FoundryClient] Model already loaded: {modelId}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load the model
|
||||
Logger.LogInfo($"[FoundryClient] Loading model: {modelId}");
|
||||
await _foundryManager.LoadModelAsync(modelId).ConfigureAwait(false);
|
||||
|
||||
// Verify it's loaded
|
||||
var loaded = await IsModelLoaded(modelId).ConfigureAwait(false);
|
||||
Logger.LogInfo($"[FoundryClient] Model load result: {loaded}");
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public async Task EnsureRunning()
|
||||
@@ -213,4 +212,68 @@ internal sealed class FoundryClient
|
||||
await _foundryManager.StartServiceAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the PATH environment variable from the system registry.
|
||||
/// This is necessary when tools are installed while PowerToys is running,
|
||||
/// as the installer updates the system PATH but running processes don't see the change.
|
||||
/// </summary>
|
||||
private static void RefreshEnvironmentPath()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Refreshing PATH environment variable from system");
|
||||
|
||||
var currentPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Process) ?? string.Empty;
|
||||
var machinePath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.Machine) ?? string.Empty;
|
||||
var userPath = Environment.GetEnvironmentVariable("PATH", EnvironmentVariableTarget.User) ?? string.Empty;
|
||||
|
||||
var pathsToAdd = new List<string>();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(currentPath))
|
||||
{
|
||||
pathsToAdd.AddRange(currentPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userPath))
|
||||
{
|
||||
var userPaths = userPath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var path in userPaths)
|
||||
{
|
||||
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
pathsToAdd.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(machinePath))
|
||||
{
|
||||
var machinePaths = machinePath.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var path in machinePaths)
|
||||
{
|
||||
if (!pathsToAdd.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
pathsToAdd.Add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newPath = string.Join(Path.PathSeparator.ToString(), pathsToAdd);
|
||||
|
||||
if (currentPath != newPath)
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] Updating process PATH with latest system values");
|
||||
Environment.SetEnvironmentVariable("PATH", newPath, EnvironmentVariableTarget.Process);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogInfo("[FoundryClient] PATH is already up to date");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryClient] Failed to refresh PATH: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ namespace LanguageModelProvider;
|
||||
|
||||
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
{
|
||||
private IEnumerable<ModelDetails>? _downloadedModels;
|
||||
private FoundryClient? _foundryClient;
|
||||
private IEnumerable<FoundryCatalogModel>? _catalogModels;
|
||||
private string? _serviceUrl;
|
||||
|
||||
public static FoundryLocalModelProvider Instance { get; } = new();
|
||||
@@ -24,22 +24,8 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
|
||||
public IChatClient? GetIChatClient(string modelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Failed to initialize: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_serviceUrl) || _foundryClient == null)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Service URL or manager is null");
|
||||
return null;
|
||||
}
|
||||
Logger.LogInfo($"[FoundryLocal] GetIChatClient called with url: {modelId}");
|
||||
InitializeAsync().GetAwaiter().GetResult();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(modelId))
|
||||
{
|
||||
@@ -47,39 +33,38 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the model is loaded before returning chat client
|
||||
try
|
||||
// Check if model is in catalog
|
||||
var isInCatalog = _catalogModels?.Any(m => m.Name == modelId) ?? false;
|
||||
if (!isInCatalog)
|
||||
{
|
||||
var isLoaded = _foundryClient.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
if (!isLoaded)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"[FoundryLocal] Model is loaded: {modelId}");
|
||||
var errorMessage = $"{modelId} is not supported in Foundry Local. Please configure supported models in Settings.";
|
||||
Logger.LogError($"[FoundryLocal] {errorMessage}");
|
||||
throw new InvalidOperationException(errorMessage);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
// Ensure the model is loaded before returning chat client
|
||||
var isLoaded = _foundryClient!.EnsureModelLoaded(modelId).GetAwaiter().GetResult();
|
||||
if (!isLoaded)
|
||||
{
|
||||
Logger.LogError($"[FoundryLocal] Exception ensuring model loaded: {ex.Message}");
|
||||
return null;
|
||||
Logger.LogError($"[FoundryLocal] Failed to load model: {modelId}");
|
||||
throw new InvalidOperationException($"Failed to load the model '{modelId}'.");
|
||||
}
|
||||
|
||||
// Use ServiceUri instead of Endpoint since Endpoint already includes /v1
|
||||
var baseUri = _foundryClient.GetServiceUri();
|
||||
if (baseUri == null)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Service URI is null");
|
||||
return null;
|
||||
const string message = "Foundry Local service URL is not available. Please make sure Foundry Local is installed and running.";
|
||||
Logger.LogError($"[FoundryLocal] {message}");
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
var endpointUri = new Uri($"{baseUri.ToString().TrimEnd('/')}/v1");
|
||||
Logger.LogInfo($"[FoundryLocal] Creating OpenAI client with endpoint: {endpointUri}");
|
||||
Logger.LogInfo($"[FoundryLocal] Model ID for chat client: {modelId}");
|
||||
|
||||
return new OpenAIClient(
|
||||
new ApiKeyCredential("none"),
|
||||
new OpenAIClientOptions { Endpoint = endpointUri })
|
||||
new OpenAIClientOptions { Endpoint = endpointUri, NetworkTimeout = TimeSpan.FromMinutes(5) })
|
||||
.GetChatClient(modelId)
|
||||
.AsIChatClient();
|
||||
}
|
||||
@@ -105,49 +90,16 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
|
||||
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (ignoreCached)
|
||||
{
|
||||
Logger.LogInfo("[FoundryLocal] Ignoring cached models, resetting");
|
||||
Reset();
|
||||
}
|
||||
|
||||
await InitializeAsync(cancelationToken);
|
||||
|
||||
Logger.LogInfo($"[FoundryLocal] Returning {_downloadedModels?.Count() ?? 0} downloaded models");
|
||||
return _downloadedModels ?? [];
|
||||
}
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
_downloadedModels = null;
|
||||
_ = InitializeAsync();
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (_foundryClient != null && _downloadedModels != null && _downloadedModels.Any())
|
||||
{
|
||||
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo("[FoundryLocal] Initializing provider");
|
||||
_foundryClient ??= await FoundryClient.CreateAsync();
|
||||
|
||||
if (_foundryClient == null)
|
||||
{
|
||||
Logger.LogError("[FoundryLocal] Failed to create Foundry client");
|
||||
return;
|
||||
return Array.Empty<ModelDetails>();
|
||||
}
|
||||
|
||||
_serviceUrl ??= await _foundryClient.GetServiceUrl();
|
||||
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
|
||||
|
||||
var cachedModels = await _foundryClient.ListCachedModels();
|
||||
Logger.LogInfo($"[FoundryLocal] Found {cachedModels.Count} cached models");
|
||||
|
||||
List<ModelDetails> downloadedModels = [];
|
||||
|
||||
foreach (var model in cachedModels)
|
||||
@@ -160,13 +112,37 @@ public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||
Url = $"fl://{model.Name}",
|
||||
Description = $"{model.Name} running locally with Foundry Local",
|
||||
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||
SupportedOnQualcomm = true,
|
||||
ProviderModelDetails = model,
|
||||
});
|
||||
}
|
||||
|
||||
_downloadedModels = downloadedModels;
|
||||
Logger.LogInfo($"[FoundryLocal] Initialization complete. Total downloaded models: {downloadedModels.Count}");
|
||||
return downloadedModels;
|
||||
}
|
||||
|
||||
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||
{
|
||||
if (_foundryClient != null && _catalogModels != null && _catalogModels.Any())
|
||||
{
|
||||
await _foundryClient.EnsureRunning().ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInfo("[FoundryLocal] Initializing provider");
|
||||
_foundryClient ??= await FoundryClient.CreateAsync();
|
||||
|
||||
if (_foundryClient == null)
|
||||
{
|
||||
const string message = "Foundry Local client could not be created. Please make sure Foundry Local is installed and running.";
|
||||
Logger.LogError($"[FoundryLocal] {message}");
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
_serviceUrl ??= await _foundryClient.GetServiceUrl();
|
||||
Logger.LogInfo($"[FoundryLocal] Service URL: {_serviceUrl}");
|
||||
|
||||
var catalogModels = await _foundryClient.ListCatalogModels();
|
||||
Logger.LogInfo($"[FoundryLocal] Found {catalogModels.Count} catalog models");
|
||||
_catalogModels = catalogModels;
|
||||
}
|
||||
|
||||
public async Task<bool> IsAvailable()
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface ILanguageModelProvider
|
||||
|
||||
string ProviderDescription { get; }
|
||||
|
||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
|
||||
Task<IEnumerable<ModelDetails>> GetModelsAsync(CancellationToken cancelationToken = default);
|
||||
|
||||
IChatClient? GetIChatClient(string modelId);
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ public class ModelDetails
|
||||
|
||||
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
|
||||
|
||||
public bool SupportedOnQualcomm { get; set; }
|
||||
|
||||
public string License { get; set; } = string.Empty;
|
||||
|
||||
public object? ProviderModelDetails { get; set; }
|
||||
|
||||
@@ -558,7 +558,7 @@
|
||||
<TextBlock
|
||||
x:Uid="AIProvidersFlyoutHeader"
|
||||
Grid.Row="0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
<ListView
|
||||
x:Name="AIProviderListView"
|
||||
Grid.Row="1"
|
||||
|
||||
@@ -299,47 +299,49 @@
|
||||
</StackPanel>
|
||||
</controls:PromptBox.Footer>
|
||||
</controls:PromptBox>
|
||||
<Grid Grid.Row="2" RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="1" />
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
<ScrollViewer Grid.Row="2">
|
||||
<Grid RowSpacing="4">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{x:Bind ViewModel.StandardPasteFormats.Count, Mode=OneWay, Converter={StaticResource standardPasteFormatsToHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" MinHeight="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource customActionsToMinHeightConverter}}" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<ListView
|
||||
x:Name="PasteOptionsListView"
|
||||
Grid.Row="0"
|
||||
VerticalAlignment="Bottom"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.StandardPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="1" />
|
||||
<Rectangle
|
||||
Grid.Row="1"
|
||||
Height="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
Visibility="{x:Bind ViewModel.CustomActionPasteFormats.Count, Mode=OneWay, Converter={StaticResource countToVisibilityConverter}}" />
|
||||
|
||||
<ListView
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
ScrollViewer.VerticalScrollMode="Auto"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
</Grid>
|
||||
<ListView
|
||||
x:Name="CustomActionsListView"
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Top"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="PasteFormat_ItemClick"
|
||||
ItemContainerTransitions="{x:Null}"
|
||||
ItemTemplateSelector="{StaticResource PasteFormatTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.CustomActionPasteFormats, Mode=OneWay}"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollMode="Disabled"
|
||||
SelectionMode="None"
|
||||
TabIndex="2" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
|
||||
@@ -168,6 +168,15 @@ namespace AdvancedPaste.Settings
|
||||
}
|
||||
|
||||
var properties = settings.Properties;
|
||||
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
|
||||
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
|
||||
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
|
||||
|
||||
if (legacyCredential is null)
|
||||
{
|
||||
return legacyAdvancedAIConsumed;
|
||||
}
|
||||
|
||||
var configuration = properties.PasteAIConfiguration;
|
||||
|
||||
if (configuration is null)
|
||||
@@ -176,30 +185,11 @@ namespace AdvancedPaste.Settings
|
||||
properties.PasteAIConfiguration = configuration;
|
||||
}
|
||||
|
||||
bool hasLegacyProviders = configuration.LegacyProviderConfigurations is { Count: > 0 };
|
||||
bool legacyAdvancedAIConsumed = properties.TryConsumeLegacyAdvancedAIEnabled(out var advancedFlag);
|
||||
bool legacyAdvancedAIEnabled = legacyAdvancedAIConsumed && advancedFlag;
|
||||
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
|
||||
|
||||
if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool configurationUpdated = false;
|
||||
|
||||
if (hasLegacyProviders)
|
||||
{
|
||||
configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration);
|
||||
}
|
||||
|
||||
PasteAIProviderDefinition openAIProvider = null;
|
||||
if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed)
|
||||
{
|
||||
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
|
||||
openAIProvider = ensureResult.Provider;
|
||||
configurationUpdated |= ensureResult.Updated;
|
||||
}
|
||||
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
|
||||
PasteAIProviderDefinition openAIProvider = ensureResult.Provider;
|
||||
configurationUpdated |= ensureResult.Updated;
|
||||
|
||||
if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
|
||||
{
|
||||
@@ -207,13 +197,13 @@ namespace AdvancedPaste.Settings
|
||||
configurationUpdated = true;
|
||||
}
|
||||
|
||||
if (legacyCredential is not null && openAIProvider is not null)
|
||||
if (openAIProvider is not null)
|
||||
{
|
||||
StoreMigratedOpenAICredential(openAIProvider.Id, openAIProvider.ServiceType, legacyCredential.Password);
|
||||
RemoveLegacyOpenAICredential();
|
||||
}
|
||||
|
||||
bool shouldEnableAI = legacyCredential is not null;
|
||||
const bool shouldEnableAI = true;
|
||||
bool enabledUpdated = false;
|
||||
if (properties.IsAIEnabled != shouldEnableAI)
|
||||
{
|
||||
|
||||
@@ -215,7 +215,6 @@ public sealed class AdvancedAIKernelService : KernelServiceBase
|
||||
return new OpenAIPromptExecutionSettings
|
||||
{
|
||||
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
|
||||
Temperature = 0.01,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AdvancedPaste.Helpers;
|
||||
using AdvancedPaste.Models;
|
||||
using LanguageModelProvider;
|
||||
using Microsoft.Extensions.AI;
|
||||
@@ -33,10 +34,6 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public string ProviderName => AIServiceType.FoundryLocal.ToNormalizedKey();
|
||||
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(_config?.Model) ? "Foundry Local" : _config.Model;
|
||||
|
||||
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -76,13 +73,20 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var chatClient = _modelProvider.GetIChatClient(modelReference);
|
||||
if (chatClient is null)
|
||||
|
||||
IChatClient chatClient;
|
||||
try
|
||||
{
|
||||
chatClient = _modelProvider.GetIChatClient(modelReference);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
// GetIChatClient throws InvalidOperationException for user-facing errors
|
||||
var errorMessage = string.Format(System.Globalization.CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("FoundryLocal_UnableToLoadModel"), modelReference);
|
||||
throw new PasteActionException(
|
||||
$"Unable to load Foundry Local model: {modelReference}",
|
||||
new InvalidOperationException("Chat client resolution failed"),
|
||||
aiServiceMessage: "The model may not be downloaded or the Foundry Local service may not be running. Please check the model status in settings.");
|
||||
errorMessage,
|
||||
ex,
|
||||
aiServiceMessage: ex.Message);
|
||||
}
|
||||
|
||||
var userMessageContent = $"""
|
||||
@@ -142,6 +146,7 @@ public sealed class FoundryLocalPasteProvider : IPasteAIProvider
|
||||
var options = new ChatOptions
|
||||
{
|
||||
ModelId = modelReference,
|
||||
MaxOutputTokens = 2048,
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(systemPrompt))
|
||||
|
||||
@@ -157,8 +157,6 @@ namespace AdvancedPaste.Services.CustomActions
|
||||
{
|
||||
AIServiceType.OpenAI or AIServiceType.AzureOpenAI => new OpenAIPromptExecutionSettings
|
||||
{
|
||||
Temperature = 0.01,
|
||||
MaxTokens = 2000,
|
||||
FunctionChoiceBehavior = null,
|
||||
},
|
||||
_ => new PromptExecutionSettings(),
|
||||
|
||||
@@ -160,10 +160,10 @@
|
||||
<value>Active provider: {0}</value>
|
||||
</data>
|
||||
<data name="AIProvidersFlyoutHeader.Text" xml:space="preserve">
|
||||
<value>AI providers</value>
|
||||
<value>Configured models</value>
|
||||
</data>
|
||||
<data name="AIProvidersEmptyText.Text" xml:space="preserve">
|
||||
<value>No AI providers configured</value>
|
||||
<value>No models configured</value>
|
||||
</data>
|
||||
<data name="AIProvidersManageButtonContent.Content" xml:space="preserve">
|
||||
<value>Configure models in Settings</value>
|
||||
@@ -364,8 +364,12 @@
|
||||
<data name="CustomEndpointWarning" xml:space="preserve">
|
||||
<value>You are using a custom endpoint. Verify all answers.</value>
|
||||
</data>
|
||||
<data name="LocalModelBadge" xml:space="preserve">
|
||||
<data name="LocalModelBadge.Text" xml:space="preserve">
|
||||
<value>Local</value>
|
||||
<comment>Badge label displayed next to local AI model providers (e.g., Ollama, Foundry Local) to indicate the model runs locally</comment>
|
||||
</data>
|
||||
<data name="FoundryLocal_UnableToLoadModel" xml:space="preserve">
|
||||
<value>Unable to load Foundry Local model: {0}</value>
|
||||
<comment>{0} is the model identifier. Do not translate {0}.</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -271,7 +271,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
||||
|
||||
if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2))
|
||||
{
|
||||
Logger::info(L"[LightSwitchService] Settings file changed event detected.");
|
||||
ResetEvent(hSettingsChanged);
|
||||
LightSwitchSettings::instance().LoadSettings();
|
||||
stateManager.OnSettingsChanged();
|
||||
|
||||
@@ -17,12 +17,10 @@ LightSwitchStateManager::LightSwitchStateManager()
|
||||
void LightSwitchStateManager::OnSettingsChanged()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
Logger::info(L"[LightSwitchStateManager] Settings changed event received");
|
||||
|
||||
// If manual override was active, clear it so new settings take effect
|
||||
if (_state.isManualOverride)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Clearing manual override due to settings update.");
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
|
||||
@@ -33,7 +31,6 @@ void LightSwitchStateManager::OnSettingsChanged()
|
||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
Logger::debug(L"[LightSwitchStateManager] Tick received: {}", currentMinutes);
|
||||
EvaluateAndApplyIfNeeded();
|
||||
}
|
||||
|
||||
@@ -51,7 +48,7 @@ void LightSwitchStateManager::OnManualOverride()
|
||||
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::info(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced internal theme state to current system theme ({}) and apps theme ({}).",
|
||||
(_state.isSystemLightActive ? L"light" : L"dark"),
|
||||
(_state.isAppsLightActive ? L"light" : L"dark"));
|
||||
}
|
||||
@@ -79,9 +76,9 @@ void LightSwitchStateManager::SyncInitialThemeState()
|
||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
Logger::info(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||
_state.isSystemLightActive ? L"light" : L"dark");
|
||||
Logger::info(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
}
|
||||
|
||||
@@ -127,7 +124,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
// Early exit: OFF mode just pauses activity
|
||||
if (_currentSettings.scheduleMode == ScheduleMode::Off)
|
||||
{
|
||||
Logger::debug(L"[LightSwitchStateManager] Mode is OFF — pausing service logic.");
|
||||
_state.lastTickMinutes = now;
|
||||
return;
|
||||
}
|
||||
@@ -145,7 +141,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
if (newDay || modeChangedToSun)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Recalculating sun times (mode/day change).");
|
||||
auto [newLightTime, newDarkTime] = update_sun_times(_currentSettings);
|
||||
_state.lastEvaluatedDay = st.wDay;
|
||||
_state.effectiveLightMinutes = newLightTime + _currentSettings.sunrise_offset;
|
||||
@@ -188,12 +183,10 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
if (crossedBoundary)
|
||||
{
|
||||
Logger::info(L"[LightSwitchStateManager] Manual override cleared after crossing boundary.");
|
||||
_state.isManualOverride = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::debug(L"[LightSwitchStateManager] Manual override active — skipping auto apply.");
|
||||
_state.lastTickMinutes = now;
|
||||
return;
|
||||
}
|
||||
@@ -206,7 +199,7 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
bool appsNeedsToChange = _currentSettings.changeApps && (_state.isAppsLightActive != shouldBeLight);
|
||||
bool systemNeedsToChange = _currentSettings.changeSystem && (_state.isSystemLightActive != shouldBeLight);
|
||||
|
||||
Logger::debug(
|
||||
/* Logger::debug(
|
||||
L"[LightSwitchStateManager] now = {:02d}:{:02d}, light boundary = {:02d}:{:02d} ({}), dark boundary = {:02d}:{:02d} ({})",
|
||||
now / 60,
|
||||
now % 60,
|
||||
@@ -215,12 +208,12 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
_state.effectiveLightMinutes,
|
||||
_state.effectiveDarkMinutes / 60,
|
||||
_state.effectiveDarkMinutes % 60,
|
||||
_state.effectiveDarkMinutes);
|
||||
_state.effectiveDarkMinutes); */
|
||||
|
||||
Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
|
||||
/* Logger::debug("should be light = {}, apps needs change = {}, system needs change = {}",
|
||||
shouldBeLight ? "true" : "false",
|
||||
appsNeedsToChange ? "true" : "false",
|
||||
systemNeedsToChange ? "true" : "false");
|
||||
systemNeedsToChange ? "true" : "false"); */
|
||||
|
||||
// Only apply theme if there's a change or no override active
|
||||
if (!_state.isManualOverride && (appsNeedsToChange || systemNeedsToChange))
|
||||
@@ -230,10 +223,6 @@ void LightSwitchStateManager::EvaluateAndApplyIfNeeded()
|
||||
|
||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||
|
||||
Logger::debug(L"[LightSwitchStateManager] Synced post-apply theme state — System: {}, Apps: {}",
|
||||
_state.isSystemLightActive ? L"light" : L"dark",
|
||||
_state.isAppsLightActive ? L"light" : L"dark");
|
||||
}
|
||||
|
||||
_state.lastTickMinutes = now;
|
||||
|
||||
@@ -39,6 +39,10 @@ type_pEnableThemeDialogTexture pEnableThemeDialogTexture;
|
||||
#define WIN7_VERSION 0x106
|
||||
#define WIN10_VERSION 0x206
|
||||
|
||||
// Default recording format frame rates
|
||||
#define RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE 15
|
||||
#define RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE 30
|
||||
|
||||
// Time that we'll cache live zoom window to avoid flicker
|
||||
// of live zooming on Vista/ws2k8
|
||||
#define LIVEZOOM_WINDOW_TIMEOUT 2*3600*1000
|
||||
|
||||
@@ -121,7 +121,7 @@ FONT 8, "MS Shell Dlg", 0, 0, 0x0
|
||||
BEGIN
|
||||
DEFPUSHBUTTON "OK",IDOK,166,306,50,14
|
||||
PUSHBUTTON "Cancel",IDCANCEL,223,306,50,14
|
||||
LTEXT "ZoomIt v9.20",IDC_VERSION,42,7,73,10
|
||||
LTEXT "ZoomIt v9.21",IDC_VERSION,42,7,73,10
|
||||
LTEXT "Copyright © 2006-2025 Mark Russinovich",IDC_COPYRIGHT,42,17,231,8
|
||||
CONTROL "<a HREF=""https://www.sysinternals.com"">Sysinternals - www.sysinternals.com</a>",IDC_LINK,
|
||||
"SysLink",WS_TABSTOP,42,26,150,9
|
||||
|
||||
@@ -44,11 +44,11 @@ LOGFONT g_LogFont;
|
||||
BOOLEAN g_DemoTypeUserDriven = false;
|
||||
TCHAR g_DemoTypeFile[MAX_PATH] = {0};
|
||||
DWORD g_DemoTypeSpeedSlider = static_cast<int>(((MIN_TYPING_SPEED - MAX_TYPING_SPEED) / 2) + MAX_TYPING_SPEED);
|
||||
DWORD g_RecordFrameRate = 30;
|
||||
DWORD g_RecordFrameRate = 30; // We default to 30 here, but g_RecordFrameRate can be different depending on recording format and gets set accordingly
|
||||
DWORD g_RecordScaling = 100;
|
||||
DWORD g_RecordScalingGIF = 50;
|
||||
DWORD g_RecordScalingMP4 = 100;
|
||||
RecordingFormat g_RecordingFormat = RecordingFormat::GIF;
|
||||
RecordingFormat g_RecordingFormat = RecordingFormat::MP4;
|
||||
BOOLEAN g_CaptureAudio = FALSE;
|
||||
TCHAR g_MicrophoneDeviceId[MAX_PATH] = {0};
|
||||
|
||||
@@ -87,8 +87,7 @@ REG_SETTING RegSettings[] = {
|
||||
{ L"SnapToGrid", SETTING_TYPE_BOOLEAN, 0, &g_SnapToGrid, static_cast<DOUBLE>(g_SnapToGrid) },
|
||||
{ L"ZoominSliderLevel", SETTING_TYPE_DWORD, 0, &g_SliderZoomLevel, static_cast<DOUBLE>(g_SliderZoomLevel) },
|
||||
{ L"Font", SETTING_TYPE_BINARY, sizeof g_LogFont, &g_LogFont, static_cast<DOUBLE>(0) },
|
||||
{ L"RecordFrameRate", SETTING_TYPE_DWORD, 0, &g_RecordFrameRate, static_cast<DOUBLE>(g_RecordFrameRate) },
|
||||
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(0) },
|
||||
{ L"RecordingFormat", SETTING_TYPE_DWORD, 0, &g_RecordingFormat, static_cast<DOUBLE>(g_RecordingFormat) },
|
||||
{ L"RecordScalingGIF", SETTING_TYPE_DWORD, 0, &g_RecordScalingGIF, static_cast<DOUBLE>(g_RecordScalingGIF) },
|
||||
{ L"RecordScalingMP4", SETTING_TYPE_DWORD, 0, &g_RecordScalingMP4, static_cast<DOUBLE>(g_RecordScalingMP4) },
|
||||
{ L"CaptureAudio", SETTING_TYPE_BOOLEAN, 0, &g_CaptureAudio, static_cast<DOUBLE>(g_CaptureAudio) },
|
||||
|
||||
@@ -168,6 +168,7 @@ BOOL g_RecordToggle = FALSE;
|
||||
BOOL g_RecordCropping = FALSE;
|
||||
SelectRectangle g_SelectRectangle;
|
||||
std::wstring g_RecordingSaveLocation;
|
||||
std::wstring g_RecordingSaveLocationGIF;
|
||||
winrt::IDirect3DDevice g_RecordDevice{ nullptr };
|
||||
std::shared_ptr<VideoRecordingSession> g_RecordingSession = nullptr;
|
||||
std::shared_ptr<GifRecordingSession> g_GifRecordingSession = nullptr;
|
||||
@@ -2173,7 +2174,10 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
CheckDlgButton( g_OptionsTabs[RECORD_PAGE].hPage, IDC_CAPTURE_AUDIO,
|
||||
g_CaptureAudio ? BST_CHECKED: BST_UNCHECKED );
|
||||
|
||||
for (int i = 0; i < _countof(g_FramerateOptions); i++) {
|
||||
//
|
||||
// The framerate drop down list is not used in the current version (might be added in the future)
|
||||
//
|
||||
/*for (int i = 0; i < _countof(g_FramerateOptions); i++) {
|
||||
|
||||
_stprintf(text, L"%d", g_FramerateOptions[i]);
|
||||
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_ADDSTRING),
|
||||
@@ -2182,7 +2186,7 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
|
||||
SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), CB_SETCURSEL, static_cast<WPARAM>(i), static_cast<LPARAM>(0));
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// Add the recording format to the combo box and set the current selection
|
||||
size_t selection = 0;
|
||||
@@ -2345,17 +2349,8 @@ INT_PTR CALLBACK OptionsProc( HWND hDlg, UINT message,
|
||||
text[2] = 0;
|
||||
newTimeout = _tstoi( text );
|
||||
|
||||
if( g_RecordingFormat == RecordingFormat::GIF )
|
||||
{
|
||||
// Hardcode lower frame rate for GIFs
|
||||
g_RecordFrameRate = 15;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_RecordFrameRate = g_FramerateOptions[SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FRAME_RATE), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0))];
|
||||
}
|
||||
|
||||
g_RecordingFormat = static_cast<RecordingFormat>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_FORMAT), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)));
|
||||
g_RecordFrameRate = (g_RecordingFormat == RecordingFormat::GIF) ? RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE : RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
|
||||
g_RecordScaling = static_cast<int>(SendMessage(GetDlgItem(g_OptionsTabs[RECORD_PAGE].hPage, IDC_RECORD_SCALING), static_cast<UINT>(CB_GETCURSEL), static_cast<WPARAM>(0), static_cast<LPARAM>(0)) * 10 + 10);
|
||||
|
||||
// Get the selected microphone
|
||||
@@ -3536,7 +3531,16 @@ void StopRecording()
|
||||
//----------------------------------------------------------------------------
|
||||
auto GetUniqueRecordingFilename()
|
||||
{
|
||||
std::filesystem::path path{ g_RecordingSaveLocation };
|
||||
std::filesystem::path path;
|
||||
|
||||
if (g_RecordingFormat == RecordingFormat::GIF)
|
||||
{
|
||||
path = g_RecordingSaveLocationGIF;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = g_RecordingSaveLocation;
|
||||
}
|
||||
|
||||
// Chop off index if it's there
|
||||
auto base = std::regex_replace( path.stem().wstring(), std::wregex( L" [(][0-9]+[)]$" ), L"" );
|
||||
@@ -3591,6 +3595,7 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
auto stream = co_await file.OpenAsync( winrt::FileAccessMode::ReadWrite );
|
||||
|
||||
// Create the appropriate recording session based on format
|
||||
OutputDebugStringW((L"Starting recording session. Framerate: " + std::to_wstring(g_RecordFrameRate) + L" scaling: " + std::to_wstring(g_RecordScaling) + L" Format: " + (g_RecordingFormat == RecordingFormat::GIF ? L"GIF" : L"MP4") + L"\n").c_str());
|
||||
if (g_RecordingFormat == RecordingFormat::GIF)
|
||||
{
|
||||
g_GifRecordingSession = GifRecordingSession::Create(
|
||||
@@ -3657,18 +3662,44 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
saveDialog->SetFileTypes( _countof( fileTypes ), fileTypes );
|
||||
}
|
||||
|
||||
if( g_RecordingSaveLocation.size() == 0) {
|
||||
// Peek the folder Windows has chosen to display
|
||||
static std::filesystem::path lastSaveFolder;
|
||||
wil::unique_cotaskmem_string chosenFolderPath;
|
||||
wil::com_ptr<IShellItem> currentSelectedFolder;
|
||||
bool bFolderChanged = false;
|
||||
if (SUCCEEDED(saveDialog->GetFolder(currentSelectedFolder.put())))
|
||||
{
|
||||
if (SUCCEEDED(currentSelectedFolder->GetDisplayName(SIGDN_FILESYSPATH, chosenFolderPath.put())))
|
||||
{
|
||||
if (lastSaveFolder != chosenFolderPath.get())
|
||||
{
|
||||
lastSaveFolder = chosenFolderPath.get() ? chosenFolderPath.get() : std::filesystem::path{};
|
||||
bFolderChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( (g_RecordingFormat == RecordingFormat::GIF && g_RecordingSaveLocationGIF.size() == 0) || (g_RecordingFormat == RecordingFormat::MP4 && g_RecordingSaveLocation.size() == 0) || (bFolderChanged)) {
|
||||
|
||||
wil::com_ptr<IShellItem> shellItem;
|
||||
wil::unique_cotaskmem_string folderPath;
|
||||
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put())))
|
||||
g_RecordingSaveLocation = folderPath.get();
|
||||
if (SUCCEEDED(saveDialog->GetFolder(shellItem.put())) && SUCCEEDED(shellItem->GetDisplayName(SIGDN_FILESYSPATH, folderPath.put()))) {
|
||||
if (g_RecordingFormat == RecordingFormat::GIF) {
|
||||
g_RecordingSaveLocationGIF = folderPath.get();
|
||||
std::filesystem::path currentPath{ g_RecordingSaveLocationGIF };
|
||||
g_RecordingSaveLocationGIF = currentPath / DEFAULT_GIF_RECORDING_FILE;
|
||||
}
|
||||
else {
|
||||
g_RecordingSaveLocation = folderPath.get();
|
||||
if (g_RecordingFormat == RecordingFormat::MP4) {
|
||||
std::filesystem::path currentPath{ g_RecordingSaveLocation };
|
||||
g_RecordingSaveLocation = currentPath / DEFAULT_RECORDING_FILE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always use appropriate default filename based on current format
|
||||
std::filesystem::path currentPath{ g_RecordingSaveLocation };
|
||||
const wchar_t* defaultFile = (g_RecordingFormat == RecordingFormat::GIF) ? DEFAULT_GIF_RECORDING_FILE : DEFAULT_RECORDING_FILE;
|
||||
g_RecordingSaveLocation = currentPath.parent_path() / defaultFile;
|
||||
auto suggestedName = GetUniqueRecordingFilename();
|
||||
saveDialog->SetFileName( suggestedName.c_str() );
|
||||
|
||||
@@ -3696,9 +3727,15 @@ winrt::fire_and_forget StartRecordingAsync( HWND hWnd, LPRECT rcCrop, HWND hWndR
|
||||
}
|
||||
else {
|
||||
|
||||
co_await file.MoveAndReplaceAsync( destFile );
|
||||
g_RecordingSaveLocation = file.Path();
|
||||
SaveToClipboard(g_RecordingSaveLocation.c_str(), hWnd);
|
||||
co_await file.MoveAndReplaceAsync(destFile);
|
||||
if (g_RecordingFormat == RecordingFormat::GIF) {
|
||||
g_RecordingSaveLocationGIF = file.Path();
|
||||
SaveToClipboard(g_RecordingSaveLocationGIF.c_str(), hWnd);
|
||||
}
|
||||
else {
|
||||
g_RecordingSaveLocation = file.Path();
|
||||
SaveToClipboard(g_RecordingSaveLocation.c_str(), hWnd);
|
||||
}
|
||||
}
|
||||
g_bSaveInProgress = false;
|
||||
|
||||
@@ -4039,8 +4076,10 @@ LRESULT APIENTRY MainWndProc(
|
||||
// Set g_RecordScaling based on the current recording format
|
||||
if (g_RecordingFormat == RecordingFormat::GIF) {
|
||||
g_RecordScaling = g_RecordScalingGIF;
|
||||
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
|
||||
} else {
|
||||
g_RecordScaling = g_RecordScalingMP4;
|
||||
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
|
||||
}
|
||||
|
||||
// to support migrating from
|
||||
@@ -6332,6 +6371,17 @@ LRESULT APIENTRY MainWndProc(
|
||||
{
|
||||
// Reload the settings. This message is called from PowerToys after a setting is changed by the user.
|
||||
reg.ReadRegSettings(RegSettings);
|
||||
|
||||
if (g_RecordingFormat == RecordingFormat::GIF)
|
||||
{
|
||||
g_RecordScaling = g_RecordScalingGIF;
|
||||
g_RecordFrameRate = RECORDING_FORMAT_GIF_DEFAULT_FRAMERATE;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_RecordScaling = g_RecordScalingMP4;
|
||||
g_RecordFrameRate = RECORDING_FORMAT_MP4_DEFAULT_FRAMERATE;
|
||||
}
|
||||
|
||||
// Apply tray icon setting
|
||||
EnableDisableTrayIcon(hWnd, g_ShowTrayIcon);
|
||||
|
||||
@@ -350,7 +350,7 @@ namespace Awake.Core
|
||||
TrayHelper.TimedIcon,
|
||||
TrayIconAction.Update);
|
||||
},
|
||||
_ => HandleTimerCompletion("timed"),
|
||||
() => HandleTimerCompletion("timed"),
|
||||
_tokenSource.Token);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
|
||||
public string Title { get => string.IsNullOrEmpty(field) ? Name : field; protected set; } = string.Empty;
|
||||
|
||||
public string Id { get; protected set; } = string.Empty;
|
||||
|
||||
// This property maps to `IPage.IsLoading`, but we want to expose our own
|
||||
// `IsLoading` property as a combo of this value and `IsInitialized`
|
||||
public bool ModelIsLoading { get; protected set; } = true;
|
||||
@@ -142,6 +144,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
Id = page.Id;
|
||||
Name = page.Name;
|
||||
ModelIsLoading = page.IsLoading;
|
||||
Title = page.Title;
|
||||
|
||||
@@ -6,7 +6,9 @@ using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
@@ -15,6 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class SettingsModel : ObservableObject
|
||||
{
|
||||
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
|
||||
|
||||
[JsonIgnore]
|
||||
public static readonly string FilePath;
|
||||
|
||||
@@ -30,8 +34,6 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public bool ShowAppDetails { get; set; }
|
||||
|
||||
public bool HotkeyGoesHome { get; set; }
|
||||
|
||||
public bool BackspaceGoesBack { get; set; }
|
||||
|
||||
public bool SingleClickActivates { get; set; }
|
||||
@@ -56,6 +58,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public WindowPosition? LastWindowPosition { get; set; }
|
||||
|
||||
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -98,12 +102,29 @@ public partial class SettingsModel : ObservableObject
|
||||
{
|
||||
// Read the JSON content from the file
|
||||
var jsonContent = File.ReadAllText(FilePath);
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel) ?? new();
|
||||
|
||||
var loaded = JsonSerializer.Deserialize<SettingsModel>(jsonContent, JsonSerializationContext.Default.SettingsModel);
|
||||
var migratedAny = false;
|
||||
try
|
||||
{
|
||||
if (JsonNode.Parse(jsonContent) is JsonObject root)
|
||||
{
|
||||
migratedAny |= ApplyMigrations(root, loaded);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Migration check failed: {ex}");
|
||||
}
|
||||
|
||||
Debug.WriteLine(loaded is not null ? "Loaded settings file" : "Failed to parse");
|
||||
Debug.WriteLine("Loaded settings file");
|
||||
|
||||
return loaded ?? new();
|
||||
if (migratedAny)
|
||||
{
|
||||
SaveSettings(loaded);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -113,6 +134,51 @@ public partial class SettingsModel : ObservableObject
|
||||
return new();
|
||||
}
|
||||
|
||||
private static bool ApplyMigrations(JsonObject root, SettingsModel model)
|
||||
{
|
||||
var migrated = false;
|
||||
|
||||
// Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)
|
||||
// The old 'HotkeyGoesHome' boolean indicated whether the "go home" action should happen immediately (true) or never (false).
|
||||
// The new 'AutoGoHomeInterval' uses a TimeSpan: 'TimeSpan.Zero' means immediate, 'Timeout.InfiniteTimeSpan' means never.
|
||||
migrated |= TryMigrate(
|
||||
"Migration #1: HotkeyGoesHome (bool) -> AutoGoHomeInterval (TimeSpan)",
|
||||
root,
|
||||
model,
|
||||
nameof(AutoGoHomeInterval),
|
||||
DeprecatedHotkeyGoesHomeKey,
|
||||
(settingsModel, goesHome) => settingsModel.AutoGoHomeInterval = goesHome ? TimeSpan.Zero : Timeout.InfiniteTimeSpan,
|
||||
JsonSerializationContext.Default.Boolean);
|
||||
|
||||
return migrated;
|
||||
}
|
||||
|
||||
private static bool TryMigrate<T>(string migrationName, JsonObject root, SettingsModel model, string newKey, string oldKey, Action<SettingsModel, T> apply, JsonTypeInfo<T> jsonTypeInfo)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If new key already present, skip migration
|
||||
if (root.ContainsKey(newKey) && root[newKey] is not null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If old key present, try to deserialize and apply
|
||||
if (root.TryGetPropertyValue(oldKey, out var oldNode) && oldNode is not null)
|
||||
{
|
||||
var value = oldNode.Deserialize<T>(jsonTypeInfo);
|
||||
apply(model, value!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Error during migration {migrationName}.", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void SaveSettings(SettingsModel model)
|
||||
{
|
||||
if (string.IsNullOrEmpty(FilePath))
|
||||
@@ -139,6 +205,9 @@ public partial class SettingsModel : ObservableObject
|
||||
savedSettings[item.Key] = item.Value?.DeepClone();
|
||||
}
|
||||
|
||||
// Remove deprecated keys
|
||||
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
|
||||
|
||||
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
|
||||
File.WriteAllText(FilePath, serialized);
|
||||
|
||||
|
||||
@@ -11,6 +11,19 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private static readonly List<TimeSpan> AutoGoHomeIntervals =
|
||||
[
|
||||
Timeout.InfiniteTimeSpan,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(20),
|
||||
TimeSpan.FromSeconds(30),
|
||||
TimeSpan.FromSeconds(60),
|
||||
TimeSpan.FromSeconds(90),
|
||||
TimeSpan.FromSeconds(120),
|
||||
TimeSpan.FromSeconds(180),
|
||||
];
|
||||
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
@@ -58,16 +71,6 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public bool HotkeyGoesHome
|
||||
{
|
||||
get => _settings.HotkeyGoesHome;
|
||||
set
|
||||
{
|
||||
_settings.HotkeyGoesHome = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool BackspaceGoesBack
|
||||
{
|
||||
get => _settings.BackspaceGoesBack;
|
||||
@@ -138,6 +141,25 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public int AutoGoBackIntervalIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
var index = AutoGoHomeIntervals.IndexOf(_settings.AutoGoHomeInterval);
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value >= 0 && value < AutoGoHomeIntervals.Count)
|
||||
{
|
||||
_settings.AutoGoHomeInterval = AutoGoHomeIntervals[value];
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
|
||||
|
||||
public SettingsExtensionsViewModel Extensions { get; }
|
||||
|
||||
@@ -2,21 +2,52 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Graphics;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed class WindowPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets left position in device pixels.
|
||||
/// </summary>
|
||||
public int X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets top position in device pixels.
|
||||
/// </summary>
|
||||
public int Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width in device pixels.
|
||||
/// </summary>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height in device pixels.
|
||||
/// </summary>
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets width of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets height of the screen in device pixels where the window is located.
|
||||
/// </summary>
|
||||
public int ScreenHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets DPI (dots per inch) of the display where the window is located.
|
||||
/// </summary>
|
||||
public int Dpi { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts the window position properties to a <see cref="RectInt32"/> structure representing the physical window rectangle.
|
||||
/// </summary>
|
||||
public RectInt32 ToPhysicalWindowRectangle()
|
||||
{
|
||||
return new RectInt32(X, Y, Width, Height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@ public class OpenPage : EventBase, IEvent
|
||||
{
|
||||
public int PageDepth { get; set; }
|
||||
|
||||
public OpenPage(int pageDepth)
|
||||
public string Id { get; set; }
|
||||
|
||||
public OpenPage(int pageDepth, string id)
|
||||
{
|
||||
PageDepth = pageDepth;
|
||||
Id = id;
|
||||
|
||||
EventName = "CmdPal_OpenPage";
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Input;
|
||||
@@ -33,6 +34,8 @@ using Windows.UI.WindowManagement;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.Graphics.Gdi;
|
||||
using Windows.Win32.UI.HiDpi;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
@@ -48,10 +51,14 @@ public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<QuitMessage>,
|
||||
IDisposable
|
||||
{
|
||||
private const int DefaultWidth = 800;
|
||||
private const int DefaultHeight = 480;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Stylistically, window messages are WM_")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", Justification = "Stylistically, window messages are WM_")]
|
||||
private readonly uint WM_TASKBAR_RESTART;
|
||||
private readonly HWND _hwnd;
|
||||
private readonly DispatcherTimer _autoGoHomeTimer;
|
||||
private readonly WNDPROC? _hotkeyWndProc;
|
||||
private readonly WNDPROC? _originalWndProc;
|
||||
private readonly List<TopLevelHotkey> _hotkeys = [];
|
||||
@@ -62,6 +69,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
|
||||
|
||||
private WindowPosition _currentWindowPosition = new();
|
||||
|
||||
@@ -69,6 +77,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_autoGoHomeTimer = new DispatcherTimer();
|
||||
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
|
||||
|
||||
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
|
||||
|
||||
unsafe
|
||||
@@ -135,6 +146,15 @@ public sealed partial class MainWindow : WindowEx,
|
||||
HideWindow();
|
||||
}
|
||||
|
||||
private void OnAutoGoHomeTimerOnTick(object? s, object e)
|
||||
{
|
||||
_autoGoHomeTimer.Stop();
|
||||
|
||||
// BEAR LOADING: Focus Search must be suppressed here; otherwise it may steal focus (for example, from the system tray icon)
|
||||
// and prevent the user from opening its context menu.
|
||||
WeakReferenceMessenger.Default.Send(new GoHomeMessage(WithAnimation: false, FocusSearch: false));
|
||||
}
|
||||
|
||||
private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
|
||||
{
|
||||
if (e.Key == VirtualKey.GoBack)
|
||||
@@ -173,22 +193,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
return;
|
||||
}
|
||||
|
||||
AppWindow.Resize(new SizeInt32 { Width = savedPosition.Width, Height = savedPosition.Height });
|
||||
|
||||
var savedRect = new RectInt32(savedPosition.X, savedPosition.Y, savedPosition.Width, savedPosition.Height);
|
||||
var displayArea = DisplayArea.GetFromRect(savedRect, DisplayAreaFallback.Nearest);
|
||||
var workArea = displayArea.WorkArea;
|
||||
|
||||
var maxX = workArea.X + Math.Max(0, workArea.Width - savedPosition.Width);
|
||||
var maxY = workArea.Y + Math.Max(0, workArea.Height - savedPosition.Height);
|
||||
|
||||
var targetPoint = new PointInt32
|
||||
{
|
||||
X = Math.Clamp(savedPosition.X, workArea.X, maxX),
|
||||
Y = Math.Clamp(savedPosition.Y, workArea.Y, maxY),
|
||||
};
|
||||
|
||||
AppWindow.Move(targetPoint);
|
||||
var newRect = EnsureWindowIsVisible(savedPosition.ToPhysicalWindowRectangle(), new SizeInt32(savedPosition.ScreenWidth, savedPosition.ScreenHeight), savedPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
|
||||
private void PositionCentered(DisplayArea displayArea)
|
||||
@@ -207,12 +213,16 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void UpdateWindowPositionInMemory()
|
||||
{
|
||||
var displayArea = DisplayArea.GetFromWindowId(AppWindow.Id, DisplayAreaFallback.Nearest) ?? DisplayArea.Primary;
|
||||
_currentWindowPosition = new WindowPosition
|
||||
{
|
||||
X = AppWindow.Position.X,
|
||||
Y = AppWindow.Position.Y,
|
||||
Width = AppWindow.Size.Width,
|
||||
Height = AppWindow.Size.Height,
|
||||
Dpi = (int)this.GetDpiForWindow(),
|
||||
ScreenWidth = displayArea.WorkArea.Width,
|
||||
ScreenHeight = displayArea.WorkArea.Height,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -224,6 +234,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
|
||||
|
||||
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
|
||||
|
||||
_autoGoHomeInterval = settings.AutoGoHomeInterval;
|
||||
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
|
||||
}
|
||||
|
||||
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
|
||||
@@ -283,6 +296,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
{
|
||||
StopAutoGoHome();
|
||||
|
||||
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
|
||||
|
||||
// Remember, IsIconic == "minimized", which is entirely different state
|
||||
@@ -300,8 +315,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
AppWindow.Resize(new SizeInt32 { Width = _currentWindowPosition.Width, Height = _currentWindowPosition.Height });
|
||||
AppWindow.Move(new PointInt32 { X = _currentWindowPosition.X, Y = _currentWindowPosition.Y });
|
||||
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -330,6 +345,114 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PInvoke.SetWindowPos(hwnd, HWND.HWND_TOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the window rectangle is visible on-screen.
|
||||
/// </summary>
|
||||
/// <param name="windowRect">The window rectangle in physical pixels.</param>
|
||||
/// <param name="originalScreen">The desktop area the window was positioned on.</param>
|
||||
/// <param name="originalDpi">The window's original DPI.</param>
|
||||
/// <returns>
|
||||
/// A window rectangle in physical pixels, moved to the nearest display and resized
|
||||
/// if the DPI has changed.
|
||||
/// </returns>
|
||||
private static RectInt32 EnsureWindowIsVisible(RectInt32 windowRect, SizeInt32 originalScreen, int originalDpi)
|
||||
{
|
||||
var displayArea = DisplayArea.GetFromRect(windowRect, DisplayAreaFallback.Nearest);
|
||||
if (displayArea is null)
|
||||
{
|
||||
return windowRect;
|
||||
}
|
||||
|
||||
var workArea = displayArea.WorkArea;
|
||||
if (workArea.Width <= 0 || workArea.Height <= 0)
|
||||
{
|
||||
// Fallback, nothing reasonable to do
|
||||
return windowRect;
|
||||
}
|
||||
|
||||
var effectiveDpi = GetEffectiveDpiFromDisplayId(displayArea);
|
||||
if (originalDpi <= 0)
|
||||
{
|
||||
originalDpi = effectiveDpi; // use current DPI as baseline (no scaling adjustment needed)
|
||||
}
|
||||
|
||||
var hasInvalidSize = windowRect.Width <= 0 || windowRect.Height <= 0;
|
||||
if (hasInvalidSize)
|
||||
{
|
||||
windowRect = new RectInt32(windowRect.X, windowRect.Y, DefaultWidth, DefaultHeight);
|
||||
}
|
||||
|
||||
// If we have a DPI change, scale the window rectangle accordingly
|
||||
if (effectiveDpi != originalDpi)
|
||||
{
|
||||
var scalingFactor = effectiveDpi / (double)originalDpi;
|
||||
windowRect = new RectInt32(
|
||||
(int)Math.Round(windowRect.X * scalingFactor),
|
||||
(int)Math.Round(windowRect.Y * scalingFactor),
|
||||
(int)Math.Round(windowRect.Width * scalingFactor),
|
||||
(int)Math.Round(windowRect.Height * scalingFactor));
|
||||
}
|
||||
|
||||
var targetWidth = Math.Min(windowRect.Width, workArea.Width);
|
||||
var targetHeight = Math.Min(windowRect.Height, workArea.Height);
|
||||
|
||||
// Ensure at least some minimum visible area (e.g., 100 pixels)
|
||||
// This helps prevent the window from being entirely offscreen, regardless of display scaling.
|
||||
const int minimumVisibleSize = 100;
|
||||
var isOffscreen =
|
||||
windowRect.X + minimumVisibleSize > workArea.X + workArea.Width ||
|
||||
windowRect.X + windowRect.Width - minimumVisibleSize < workArea.X ||
|
||||
windowRect.Y + minimumVisibleSize > workArea.Y + workArea.Height ||
|
||||
windowRect.Y + windowRect.Height - minimumVisibleSize < workArea.Y;
|
||||
|
||||
// if the work area size has changed, re-center the window
|
||||
var workAreaSizeChanged =
|
||||
originalScreen.Width != workArea.Width ||
|
||||
originalScreen.Height != workArea.Height;
|
||||
|
||||
int targetX;
|
||||
int targetY;
|
||||
var recenter = isOffscreen || workAreaSizeChanged || hasInvalidSize;
|
||||
if (recenter)
|
||||
{
|
||||
targetX = workArea.X + ((workArea.Width - targetWidth) / 2);
|
||||
targetY = workArea.Y + ((workArea.Height - targetHeight) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetX = windowRect.X;
|
||||
targetY = windowRect.Y;
|
||||
}
|
||||
|
||||
return new RectInt32(targetX, targetY, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
private static int GetEffectiveDpiFromDisplayId(DisplayArea displayArea)
|
||||
{
|
||||
var effectiveDpi = 96;
|
||||
|
||||
var hMonitor = (HMONITOR)Win32Interop.GetMonitorFromDisplayId(displayArea.DisplayId);
|
||||
if (!hMonitor.IsNull)
|
||||
{
|
||||
var hr = PInvoke.GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
|
||||
if (hr == 0)
|
||||
{
|
||||
effectiveDpi = (int)dpiX;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"GetDpiForMonitor failed with HRESULT: 0x{hr.Value:X8} on display {displayArea.DisplayId}");
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveDpi <= 0)
|
||||
{
|
||||
effectiveDpi = 96;
|
||||
}
|
||||
|
||||
return effectiveDpi;
|
||||
}
|
||||
|
||||
private DisplayArea GetScreen(HWND currentHwnd, MonitorBehavior target)
|
||||
{
|
||||
// Leaving a note here, in case we ever need it:
|
||||
@@ -429,6 +552,25 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// If the window was not cloaked, then leave it hidden.
|
||||
// Sure, it's not ideal, but at least it's not visible.
|
||||
}
|
||||
|
||||
// Start auto-go-home timer
|
||||
RestartAutoGoHome();
|
||||
}
|
||||
|
||||
private void StopAutoGoHome()
|
||||
{
|
||||
_autoGoHomeTimer.Stop();
|
||||
}
|
||||
|
||||
private void RestartAutoGoHome()
|
||||
{
|
||||
if (_autoGoHomeInterval == Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_autoGoHomeTimer.Stop();
|
||||
_autoGoHomeTimer.Start();
|
||||
}
|
||||
|
||||
private bool Cloak()
|
||||
@@ -479,6 +621,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
Y = _currentWindowPosition.Y,
|
||||
Width = _currentWindowPosition.Width,
|
||||
Height = _currentWindowPosition.Height,
|
||||
Dpi = _currentWindowPosition.Dpi,
|
||||
ScreenWidth = _currentWindowPosition.ScreenWidth,
|
||||
ScreenHeight = _currentWindowPosition.ScreenHeight,
|
||||
};
|
||||
|
||||
SettingsModel.SaveSettings(settings);
|
||||
|
||||
@@ -21,7 +21,6 @@ using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Input;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
@@ -160,7 +159,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
new AsyncNavigationRequest(message.Page, message.CancellationToken),
|
||||
message.WithAnimation ? DefaultPageAnimation : _noAnimation);
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth));
|
||||
PowerToysTelemetry.Log.WriteEvent(new OpenPage(RootFrame.BackStackDepth, message.Page.Id));
|
||||
|
||||
if (!ViewModel.IsNested)
|
||||
{
|
||||
@@ -346,7 +345,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
// Depending on the settings, either
|
||||
// * Go home, or
|
||||
// * Select the search text (if we should remain open on this page)
|
||||
if (settings.HotkeyGoesHome)
|
||||
if (settings.AutoGoHomeInterval == TimeSpan.Zero)
|
||||
{
|
||||
GoHome(false);
|
||||
}
|
||||
@@ -655,15 +654,15 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
e.Handled = true;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
break;
|
||||
}
|
||||
{
|
||||
// The CommandBar is responsible for handling all the item keybindings,
|
||||
// since the bound context item may need to then show another
|
||||
// context menu
|
||||
TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
e.Handled = msg.Handled;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
"nativeDebugging": false,
|
||||
"doNotLaunchApp": false
|
||||
},
|
||||
"Microsoft.CmdPal.UI (Package) + Native debugging": {
|
||||
"commandName": "MsixPackage",
|
||||
"nativeDebugging": true,
|
||||
"doNotLaunchApp": false
|
||||
},
|
||||
"Microsoft.CmdPal.UI (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
}
|
||||
|
||||
@@ -51,8 +51,18 @@
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" />
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind viewModel.AutoGoBackIntervalIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Never" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_Immediately" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After10Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After20Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After30Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After60Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After90Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After120Seconds" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AutoGoHome_Item_After180Seconds" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />
|
||||
|
||||
@@ -344,12 +344,6 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_IgnoreShortcutWhenFullscreen_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_GoHome_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Go home when activated</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_GoHome_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Automatically opens the home page upon activation</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Highlight search on activate</value>
|
||||
</data>
|
||||
@@ -523,4 +517,37 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve">
|
||||
<value>Command Palette - Fatal error</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_Never.Content" xml:space="preserve">
|
||||
<value>Never</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_Immediately.Content" xml:space="preserve">
|
||||
<value>Immediately</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After10Seconds.Content" xml:space="preserve">
|
||||
<value>10 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After20Seconds.Content" xml:space="preserve">
|
||||
<value>20 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After30Seconds.Content" xml:space="preserve">
|
||||
<value>30 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After60Seconds.Content" xml:space="preserve">
|
||||
<value>60 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After90Seconds.Content" xml:space="preserve">
|
||||
<value>90 seconds</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After120Seconds.Content" xml:space="preserve">
|
||||
<value>2 minutes</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_Item_After180Seconds.Content" xml:space="preserve">
|
||||
<value>3 minutes</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Automatically return home</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_AutoGoHome_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Automatically returns to home page after a period of inactivity when Command Palette is closed</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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 Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
||||
|
||||
public class MockBrowserInfoService : IBrowserInfoService
|
||||
{
|
||||
public BrowserInfo GetDefaultBrowser() => new() { Name = "mocked browser", Path = "C:\\mockery\\mock.exe" };
|
||||
}
|
||||
@@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
// Setup
|
||||
var settings = new MockSettingsInterface();
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText(string.Empty, query);
|
||||
@@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
};
|
||||
|
||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText("abcdef", string.Empty);
|
||||
@@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
};
|
||||
|
||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)));
|
||||
|
||||
@@ -123,8 +126,9 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
};
|
||||
|
||||
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings);
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
// Act
|
||||
page.UpdateSearchText("abcdef", string.Empty);
|
||||
|
||||
@@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
// Setup
|
||||
var settings = new MockSettingsInterface(historyItemCount: 5);
|
||||
var page = new WebSearchListPage(settings);
|
||||
var browserInfoService = new MockBrowserInfoService();
|
||||
|
||||
var page = new WebSearchListPage(settings, browserInfoService);
|
||||
|
||||
var eventRaised = false;
|
||||
|
||||
|
||||
@@ -2,32 +2,28 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
|
||||
internal sealed partial class OpenURLCommand : InvokableCommand
|
||||
{
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public string Url { get; internal set; } = string.Empty;
|
||||
|
||||
internal OpenURLCommand(string url)
|
||||
internal OpenURLCommand(string url, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
_browserInfoService = browserInfoService;
|
||||
Url = url;
|
||||
BrowserInfo.UpdateIfTimePassed();
|
||||
Icon = Icons.WebSearch;
|
||||
Name = string.Empty;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}"))
|
||||
{
|
||||
// TODO GH# 138 --> actually display feedback from the extension somewhere.
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
// TODO GH# 138 --> actually display feedback from the extension somewhere.
|
||||
return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,31 +4,31 @@
|
||||
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
|
||||
internal sealed partial class SearchWebCommand : InvokableCommand
|
||||
{
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public string Arguments { get; internal set; } = string.Empty;
|
||||
public string Arguments { get; internal set; }
|
||||
|
||||
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager)
|
||||
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
Arguments = arguments;
|
||||
BrowserInfo.UpdateIfTimePassed();
|
||||
Icon = Icons.WebSearch;
|
||||
Name = Properties.Resources.open_in_default_browser;
|
||||
Name = Resources.open_in_default_browser;
|
||||
_settingsManager = settingsManager;
|
||||
_browserInfoService = browserInfoService;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"? {Arguments}"))
|
||||
if (!_browserInfoService.Open($"? {Arguments}"))
|
||||
{
|
||||
// TODO GH# 138 --> actually display feedback from the extension somewhere.
|
||||
return CommandResult.KeepOpen();
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
|
||||
@@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
|
||||
private readonly SearchWebCommand _executeItem;
|
||||
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
||||
private static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle);
|
||||
private string _title;
|
||||
|
||||
public FallbackExecuteSearchItem(SettingsManager settings)
|
||||
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public FallbackExecuteSearchItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
|
||||
: base(new SearchWebCommand(string.Empty, settings, browserInfoService) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
|
||||
{
|
||||
_executeItem = (SearchWebCommand)this.Command!;
|
||||
_executeItem = (SearchWebCommand)Command!;
|
||||
_browserInfoService = browserInfoService;
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
_executeItem.Name = string.Empty;
|
||||
_title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
|
||||
Icon = Icons.WebSearch;
|
||||
}
|
||||
|
||||
private static string UpdateBrowserName(IBrowserInfoService browserInfoService)
|
||||
{
|
||||
var browserName = browserInfoService.GetDefaultBrowser()?.Name;
|
||||
return string.IsNullOrWhiteSpace(browserName)
|
||||
? Resources.open_in_default_browser
|
||||
: string.Format(CultureInfo.CurrentCulture, PluginOpen, browserName);
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
_executeItem.Arguments = query;
|
||||
var isEmpty = string.IsNullOrEmpty(query);
|
||||
_executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser;
|
||||
Title = isEmpty ? string.Empty : _title;
|
||||
_executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
|
||||
Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,26 @@ using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch;
|
||||
|
||||
internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
{
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
private readonly OpenURLCommand _executeItem;
|
||||
private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url);
|
||||
private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser);
|
||||
|
||||
public FallbackOpenURLItem(SettingsManager settings)
|
||||
: base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title)
|
||||
public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
|
||||
: base(new OpenURLCommand(string.Empty, browserInfoService), Resources.open_url_fallback_title)
|
||||
{
|
||||
_executeItem = (OpenURLCommand)this.Command!;
|
||||
ArgumentNullException.ThrowIfNull(browserInfoService);
|
||||
|
||||
_browserInfoService = browserInfoService;
|
||||
_executeItem = (OpenURLCommand)Command!;
|
||||
Title = string.Empty;
|
||||
_executeItem.Name = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
@@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
return;
|
||||
}
|
||||
|
||||
var success = Uri.TryCreate(query, UriKind.Absolute, out var uri);
|
||||
var success = Uri.TryCreate(query, UriKind.Absolute, out _);
|
||||
|
||||
// if url not contain schema, add http:// by default.
|
||||
if (!success)
|
||||
@@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
|
||||
}
|
||||
|
||||
_executeItem.Url = query;
|
||||
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Properties.Resources.open_in_default_browser;
|
||||
_executeItem.Name = string.IsNullOrEmpty(query) ? string.Empty : Resources.open_in_default_browser;
|
||||
|
||||
Title = string.Format(CultureInfo.CurrentCulture, PluginOpenURL, query);
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
|
||||
|
||||
var browserName = _browserInfoService.GetDefaultBrowser()?.Name;
|
||||
Subtitle = string.IsNullOrWhiteSpace(browserName) ? Resources.open_in_default_browser : string.Format(CultureInfo.CurrentCulture, PluginOpenUrlInBrowser, browserName);
|
||||
}
|
||||
|
||||
public static bool IsValidUrl(string url)
|
||||
private static bool IsValidUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
public record BrowserInfo
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
|
||||
public required string Name { get; init; }
|
||||
|
||||
public string? ArgumentsPattern { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IBrowserInfoService"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="IBrowserInfoService"/>
|
||||
internal static class BrowserInfoServiceExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Opens the specified URL in the system's default web browser.
|
||||
/// </summary>
|
||||
/// <param name="browserInfoService">The browser information service used to resolve the system's default browser.</param>
|
||||
/// <param name="url">The URL to open.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if a default browser is found and the URL launch command is issued successfully;
|
||||
/// otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Returns <see langword="false"/> if the default browser cannot be determined.
|
||||
/// </remarks>
|
||||
public static bool Open(this IBrowserInfoService browserInfoService, string url)
|
||||
{
|
||||
var defaultBrowser = browserInfoService.GetDefaultBrowser();
|
||||
return defaultBrowser != null && ShellHelpers.OpenCommandInShell(defaultBrowser.Path, defaultBrowser.ArgumentsPattern, url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
/// <summary>
|
||||
/// Service to get information about the default browser.
|
||||
/// </summary>
|
||||
internal class DefaultBrowserInfoService : IBrowserInfoService
|
||||
{
|
||||
private static readonly IDefaultBrowserProvider[] Providers =
|
||||
[
|
||||
new ShellAssociationProvider(),
|
||||
new LegacyRegistryAssociationProvider(),
|
||||
new FallbackMsEdgeBrowserProvider(),
|
||||
];
|
||||
|
||||
private readonly Lock _updateLock = new();
|
||||
|
||||
private readonly Dictionary<Type, string> _lastLoggedErrors = [];
|
||||
|
||||
private const long UpdateTimeout = 3000;
|
||||
private long _lastUpdateTickCount = -UpdateTimeout;
|
||||
|
||||
private BrowserInfo? _defaultBrowser;
|
||||
|
||||
public BrowserInfo? GetDefaultBrowser()
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateIfTimePassed();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// exception is already logged at this point
|
||||
}
|
||||
|
||||
return _defaultBrowser;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates only if at least more than 3000ms has passed since the last update, to avoid multiple calls to <see cref="UpdateCore"/>.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
private void UpdateIfTimePassed()
|
||||
{
|
||||
lock (_updateLock)
|
||||
{
|
||||
var curTickCount = Environment.TickCount64;
|
||||
if (curTickCount - _lastUpdateTickCount < UpdateTimeout && _defaultBrowser != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var newDefaultBrowser = UpdateCore();
|
||||
_defaultBrowser = newDefaultBrowser;
|
||||
_lastUpdateTickCount = curTickCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
private BrowserInfo UpdateCore()
|
||||
{
|
||||
foreach (var provider in Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = provider.GetDefaultBrowserInfo();
|
||||
#if DEBUG
|
||||
result = result with { Name = result.Name + " (" + provider.GetType().Name + ")" };
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// since we run this fairly often, avoid logging the same error multiple times
|
||||
var lastLoggedError = _lastLoggedErrors.GetValueOrDefault(provider.GetType());
|
||||
var error = ex.ToString();
|
||||
if (error != lastLoggedError)
|
||||
{
|
||||
_lastLoggedErrors[provider.GetType()] = error;
|
||||
Logger.LogError($"Exception when retrieving browser using provider {provider.GetType()}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unable to determine default browser");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to retrieve information about the system's default web browser.
|
||||
/// </summary>
|
||||
public interface IBrowserInfoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets information about the system's default web browser.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
BrowserInfo? GetDefaultBrowser();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
internal record AssociatedApp(string? Command, string? FriendlyName);
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Windows.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for providers that determine the default browser via application associations.
|
||||
/// </summary>
|
||||
internal abstract class AssociationProviderBase : IDefaultBrowserProvider
|
||||
{
|
||||
protected abstract AssociatedApp? FindAssociation();
|
||||
|
||||
public BrowserInfo GetDefaultBrowserInfo()
|
||||
{
|
||||
var appAssociation = FindAssociation();
|
||||
if (appAssociation is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(appAssociation), "Could not determine default browser application.");
|
||||
}
|
||||
|
||||
var commandPattern = appAssociation.Command;
|
||||
var appAndArgs = SplitAppAndArgs(commandPattern);
|
||||
|
||||
if (string.IsNullOrEmpty(appAndArgs.Path))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(appAndArgs.Path), "Default browser program path could not be determined.");
|
||||
}
|
||||
|
||||
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
|
||||
if (!Path.Exists(appAndArgs.Path) && !Uri.TryCreate(appAndArgs.Path, UriKind.Absolute, out _))
|
||||
{
|
||||
throw new ArgumentException($"Command validation failed: {commandPattern}", nameof(commandPattern));
|
||||
}
|
||||
|
||||
return new BrowserInfo
|
||||
{
|
||||
Path = appAndArgs.Path,
|
||||
Name = appAssociation.FriendlyName ?? Path.GetFileNameWithoutExtension(appAndArgs.Path),
|
||||
ArgumentsPattern = appAndArgs.Arguments,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string? Path, string? Arguments) SplitAppAndArgs(string? commandPattern)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commandPattern))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(commandPattern), "Default browser program command is not specified.");
|
||||
}
|
||||
|
||||
commandPattern = GetIndirectString(commandPattern);
|
||||
|
||||
// HACK: for firefox installed through Microsoft store
|
||||
// When installed through Microsoft Firefox the commandPattern does not have
|
||||
// quotes for the path. As the Program Files does have a space
|
||||
// the extracted path would be invalid, here we add the quotes to fix it
|
||||
const string FirefoxExecutableName = "firefox.exe";
|
||||
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") &&
|
||||
!commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) +
|
||||
FirefoxExecutableName.Length;
|
||||
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
|
||||
commandPattern = commandPattern.Insert(0, "\"");
|
||||
}
|
||||
|
||||
if (commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
|
||||
if (endQuoteIndex != -1)
|
||||
{
|
||||
return (commandPattern[1..endQuoteIndex], commandPattern[(endQuoteIndex + 1)..].Trim());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spaceIndex = commandPattern.IndexOf(' ');
|
||||
if (spaceIndex != -1)
|
||||
{
|
||||
return (commandPattern[..spaceIndex], commandPattern[(spaceIndex + 1)..].Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
protected static string GetIndirectString(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str) || str[0] != '@')
|
||||
{
|
||||
return str;
|
||||
}
|
||||
|
||||
const int initialCapacity = 128;
|
||||
const int maxCapacity = 8192; // Reasonable upper limit
|
||||
int hresult;
|
||||
|
||||
unsafe
|
||||
{
|
||||
// Try with stack allocation first for common cases
|
||||
var stackBuffer = stackalloc char[initialCapacity];
|
||||
|
||||
fixed (char* pszSource = str)
|
||||
{
|
||||
hresult = PInvoke.SHLoadIndirectString(
|
||||
pszSource,
|
||||
stackBuffer,
|
||||
initialCapacity,
|
||||
null);
|
||||
|
||||
// S_OK (0) means success
|
||||
if (hresult == 0)
|
||||
{
|
||||
return new string(stackBuffer);
|
||||
}
|
||||
|
||||
// STRSAFE_E_INSUFFICIENT_BUFFER (0x8007007A) means buffer too small
|
||||
// Try with progressively larger heap buffers
|
||||
if (unchecked((uint)hresult) == 0x8007007A)
|
||||
{
|
||||
for (var capacity = initialCapacity * 2; capacity <= maxCapacity; capacity *= 2)
|
||||
{
|
||||
var heapBuffer = new char[capacity];
|
||||
fixed (char* pBuffer = heapBuffer)
|
||||
{
|
||||
hresult = PInvoke.SHLoadIndirectString(
|
||||
pszSource,
|
||||
pBuffer,
|
||||
(uint)capacity,
|
||||
null);
|
||||
|
||||
if (hresult == 0)
|
||||
{
|
||||
return new string(pBuffer);
|
||||
}
|
||||
|
||||
if (unchecked((uint)hresult) != 0x8007007A)
|
||||
{
|
||||
break; // Different error, stop retrying
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Could not load indirect string. HRESULT: 0x{unchecked((uint)hresult):X8}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.IO;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a fallback implementation of the default browser provider that returns information for Microsoft Edge.
|
||||
/// </summary>
|
||||
/// <remarks>This class is used when no other default browser provider is available. It supplies the path,
|
||||
/// arguments pattern, and name for Microsoft Edge as the default browser information.</remarks>
|
||||
internal sealed class FallbackMsEdgeBrowserProvider : IDefaultBrowserProvider
|
||||
{
|
||||
private const string MsEdgeArgumentsPattern = "--single-argument %1";
|
||||
|
||||
private const string MsEdgeName = "Microsoft Edge";
|
||||
|
||||
private static string MsEdgePath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
|
||||
@"Microsoft\Edge\Application\msedge.exe");
|
||||
|
||||
public BrowserInfo GetDefaultBrowserInfo() => new()
|
||||
{
|
||||
Path = MsEdgePath,
|
||||
ArgumentsPattern = MsEdgeArgumentsPattern,
|
||||
Name = MsEdgeName,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves information about the default browser.
|
||||
/// </summary>
|
||||
internal interface IDefaultBrowserProvider
|
||||
{
|
||||
BrowserInfo GetDefaultBrowserInfo();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the default web browser by reading registry keys. This is a legacy method and may not work on all systems.
|
||||
/// </summary>
|
||||
internal sealed class LegacyRegistryAssociationProvider : AssociationProviderBase
|
||||
{
|
||||
protected override AssociatedApp? FindAssociation()
|
||||
{
|
||||
var progId = GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
|
||||
"ProgId")
|
||||
?? GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
|
||||
"ProgId");
|
||||
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
|
||||
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
|
||||
|
||||
if (appName is not null)
|
||||
{
|
||||
appName = GetIndirectString(appName);
|
||||
appName = appName
|
||||
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
|
||||
.TrimEnd();
|
||||
}
|
||||
|
||||
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
|
||||
|
||||
return commandPattern is null ? null : new AssociatedApp(commandPattern, appName);
|
||||
|
||||
static string? GetRegistryValue(string registryLocation, string? valueName)
|
||||
{
|
||||
return Registry.GetValue(registryLocation, valueName, null) as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the default web browser using the system shell functions.
|
||||
/// </summary>
|
||||
internal sealed class ShellAssociationProvider : AssociationProviderBase
|
||||
{
|
||||
private static readonly string[] Protocols = ["https", "http"];
|
||||
|
||||
protected override AssociatedApp FindAssociation()
|
||||
{
|
||||
foreach (var protocol in Protocols)
|
||||
{
|
||||
var command = AssocQueryStringSafe(NativeMethods.AssocStr.Command, protocol);
|
||||
if (string.IsNullOrWhiteSpace(command))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var appName = AssocQueryStringSafe(NativeMethods.AssocStr.FriendlyAppName, protocol);
|
||||
|
||||
return new AssociatedApp(command, appName);
|
||||
}
|
||||
|
||||
return new AssociatedApp(null, null);
|
||||
}
|
||||
|
||||
private static unsafe string? AssocQueryStringSafe(NativeMethods.AssocStr what, string protocol)
|
||||
{
|
||||
uint cch = 0;
|
||||
|
||||
// First call: get required length (incl. null)
|
||||
_ = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, null, ref cch);
|
||||
if (cch == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Small buffers on stack; large on heap
|
||||
var span = cch <= 512 ? stackalloc char[(int)cch] : new char[(int)cch];
|
||||
|
||||
fixed (char* p = span)
|
||||
{
|
||||
var hr = NativeMethods.AssocQueryStringW(NativeMethods.AssocF.IsProtocol, what, protocol, null, p, ref cch);
|
||||
if (hr != 0 || cch == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// cch includes the null terminator; slice it off
|
||||
var len = (int)cch - 1;
|
||||
if (len < 0)
|
||||
{
|
||||
len = 0;
|
||||
}
|
||||
|
||||
return new string(span[..len]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Contains information (e.g. path to executable, name...) about the default browser.
|
||||
/// </summary>
|
||||
public static class DefaultBrowserInfo
|
||||
{
|
||||
private static readonly Lock _updateLock = new();
|
||||
|
||||
/// <summary>Gets the path to the MS Edge browser executable.</summary>
|
||||
public static string MSEdgePath => System.IO.Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
|
||||
@"Microsoft\Edge\Application\msedge.exe");
|
||||
|
||||
/// <summary>Gets the command line pattern of the MS Edge.</summary>
|
||||
public const string MSEdgeArgumentsPattern = "--single-argument %1";
|
||||
|
||||
public const string MSEdgeName = "Microsoft Edge";
|
||||
|
||||
/// <summary>Gets the path to default browser's executable.</summary>
|
||||
public static string? Path { get; private set; }
|
||||
|
||||
/// <summary>Gets <see cref="Path"/> since the icon is embedded in the executable.</summary>
|
||||
public static string? IconPath => Path;
|
||||
|
||||
/// <summary>Gets the user-friendly name of the default browser.</summary>
|
||||
public static string? Name { get; private set; }
|
||||
|
||||
/// <summary>Gets the command line pattern of the default browser.</summary>
|
||||
public static string? ArgumentsPattern { get; private set; }
|
||||
|
||||
public static bool IsDefaultBrowserSet => !string.IsNullOrEmpty(Path);
|
||||
|
||||
public const long UpdateTimeout = 300;
|
||||
|
||||
private static long _lastUpdateTickCount = -UpdateTimeout;
|
||||
|
||||
private static bool _updatedOnce;
|
||||
private static bool _errorLogged;
|
||||
|
||||
/// <summary>
|
||||
/// Updates only if at least more than 300ms has passed since the last update, to avoid multiple calls to <see cref="Update"/>.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
public static void UpdateIfTimePassed()
|
||||
{
|
||||
var curTickCount = Environment.TickCount64;
|
||||
if (curTickCount - _lastUpdateTickCount >= UpdateTimeout)
|
||||
{
|
||||
_lastUpdateTickCount = curTickCount;
|
||||
Update();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consider using <see cref="UpdateIfTimePassed"/> to avoid updating multiple times.
|
||||
/// (because of multiple plugins calling update at the same time.)
|
||||
/// </summary>
|
||||
public static void Update()
|
||||
{
|
||||
lock (_updateLock)
|
||||
{
|
||||
if (!_updatedOnce)
|
||||
{
|
||||
// Log.Info("I've tried updating the chosen Web Browser info at least once.", typeof(DefaultBrowserInfo));
|
||||
_updatedOnce = true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var progId = GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoiceLatest\ProgId",
|
||||
"ProgId")
|
||||
?? GetRegistryValue(
|
||||
@"HKEY_CURRENT_USER\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice",
|
||||
"ProgId");
|
||||
var appName = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\Application", "ApplicationName")
|
||||
?? GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}", "FriendlyTypeName");
|
||||
|
||||
if (appName is not null)
|
||||
{
|
||||
// Handle indirect strings:
|
||||
if (appName.StartsWith('@'))
|
||||
{
|
||||
appName = GetIndirectString(appName);
|
||||
}
|
||||
|
||||
appName = appName
|
||||
.Replace("URL", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("HTML", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Document", null, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("Web", null, StringComparison.OrdinalIgnoreCase)
|
||||
.TrimEnd();
|
||||
}
|
||||
|
||||
Name = appName;
|
||||
|
||||
var commandPattern = GetRegistryValue($@"HKEY_CLASSES_ROOT\{progId}\shell\open\command", null);
|
||||
|
||||
if (string.IsNullOrEmpty(commandPattern))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(commandPattern),
|
||||
"Default browser program command is not specified.");
|
||||
}
|
||||
|
||||
if (commandPattern.StartsWith('@'))
|
||||
{
|
||||
commandPattern = GetIndirectString(commandPattern);
|
||||
}
|
||||
|
||||
// HACK: for firefox installed through Microsoft store
|
||||
// When installed through Microsoft Firefox the commandPattern does not have
|
||||
// quotes for the path. As the Program Files does have a space
|
||||
// the extracted path would be invalid, here we add the quotes to fix it
|
||||
const string FirefoxExecutableName = "firefox.exe";
|
||||
if (commandPattern.Contains(FirefoxExecutableName) && commandPattern.Contains(@"\WindowsApps\") && (!commandPattern.StartsWith('\"')))
|
||||
{
|
||||
var pathEndIndex = commandPattern.IndexOf(FirefoxExecutableName, StringComparison.Ordinal) + FirefoxExecutableName.Length;
|
||||
commandPattern = commandPattern.Insert(pathEndIndex, "\"");
|
||||
commandPattern = commandPattern.Insert(0, "\"");
|
||||
}
|
||||
|
||||
if (commandPattern.StartsWith('\"'))
|
||||
{
|
||||
var endQuoteIndex = commandPattern.IndexOf('\"', 1);
|
||||
if (endQuoteIndex != -1)
|
||||
{
|
||||
Path = commandPattern.Substring(1, endQuoteIndex - 1);
|
||||
ArgumentsPattern = commandPattern.Substring(endQuoteIndex + 1).Trim();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var spaceIndex = commandPattern.IndexOf(' ');
|
||||
if (spaceIndex != -1)
|
||||
{
|
||||
Path = commandPattern.Substring(0, spaceIndex);
|
||||
ArgumentsPattern = commandPattern.Substring(spaceIndex + 1).Trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Packaged applications could be an URI. Example: shell:AppsFolder\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe!App
|
||||
if (!System.IO.Path.Exists(Path) && !Uri.TryCreate(Path, UriKind.Absolute, out _))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"Command validation failed: {commandPattern}",
|
||||
nameof(commandPattern));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Path))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(Path),
|
||||
"Default browser program path could not be determined.");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fallback to MS Edge
|
||||
Path = MSEdgePath;
|
||||
Name = MSEdgeName;
|
||||
ArgumentsPattern = MSEdgeArgumentsPattern;
|
||||
|
||||
if (!_errorLogged)
|
||||
{
|
||||
// Log.Exception("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.", e, typeof(DefaultBrowserInfo));
|
||||
Logger.LogError("Exception when retrieving browser path/name. Path and Name are set to use Microsoft Edge.");
|
||||
_errorLogged = true;
|
||||
}
|
||||
}
|
||||
|
||||
string? GetRegistryValue(string registryLocation, string? valueName)
|
||||
{
|
||||
return Microsoft.Win32.Registry.GetValue(registryLocation, valueName, null) as string;
|
||||
}
|
||||
|
||||
string GetIndirectString(string str)
|
||||
{
|
||||
var stringBuilder = new StringBuilder(128);
|
||||
unsafe
|
||||
{
|
||||
var buffer = stackalloc char[128];
|
||||
var capacity = 128;
|
||||
var firstChar = str[0];
|
||||
var strPtr = &firstChar;
|
||||
|
||||
// S_OK == 0
|
||||
fixed (char* pszSourceLocal = str)
|
||||
{
|
||||
if (global::Windows.Win32.PInvoke.SHLoadIndirectString(
|
||||
pszSourceLocal,
|
||||
buffer,
|
||||
(uint)capacity,
|
||||
default) == 0)
|
||||
{
|
||||
return new string(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentNullException(nameof(str), "Could not load indirect string.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
internal static partial class NativeMethods
|
||||
{
|
||||
[LibraryImport("shlwapi.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = false)]
|
||||
internal static unsafe partial int AssocQueryStringW(
|
||||
AssocF flags,
|
||||
AssocStr str,
|
||||
string pszAssoc,
|
||||
string? pszExtra,
|
||||
char* pszOut,
|
||||
ref uint pcchOut);
|
||||
|
||||
[Flags]
|
||||
public enum AssocF : uint
|
||||
{
|
||||
None = 0,
|
||||
IsProtocol = 0x00001000,
|
||||
}
|
||||
|
||||
public enum AssocStr
|
||||
{
|
||||
Command = 1,
|
||||
Executable,
|
||||
FriendlyDocName,
|
||||
FriendlyAppName,
|
||||
NoOpen,
|
||||
ShellNewValue,
|
||||
DDECommand,
|
||||
DDEIfExec,
|
||||
DDEApplication,
|
||||
DDETopic,
|
||||
InfoTip,
|
||||
QuickTip,
|
||||
TileInfo,
|
||||
ContentType,
|
||||
DefaultIcon,
|
||||
ShellExtension,
|
||||
DropTarget,
|
||||
DelegateExecute,
|
||||
SupportedUriProtocols,
|
||||
ProgId,
|
||||
AppId,
|
||||
AppPublisher,
|
||||
AppIconReference, // sometimes present, but DefaultIcon is most common
|
||||
}
|
||||
}
|
||||
@@ -9,23 +9,24 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||
|
||||
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
{
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
private readonly Lock _sync = new();
|
||||
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
|
||||
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
||||
private IListItem[] _allItems = [];
|
||||
private List<ListItem> _historyItems = [];
|
||||
|
||||
public WebSearchListPage(ISettingsInterface settingsManager)
|
||||
public WebSearchListPage(ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settingsManager);
|
||||
|
||||
@@ -35,6 +36,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
Id = "com.microsoft.cmdpal.websearch";
|
||||
|
||||
_settingsManager = settingsManager;
|
||||
_browserInfoService = browserInfoService;
|
||||
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
|
||||
|
||||
// It just looks viewer to have string twice on the page, and default placeholder is good enough
|
||||
@@ -43,8 +45,8 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
EmptyContent = new CommandItem(new NoOpCommand())
|
||||
{
|
||||
Icon = Icon,
|
||||
Title = Properties.Resources.plugin_description,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||
Title = Resources.plugin_description,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
|
||||
};
|
||||
|
||||
UpdateHistory();
|
||||
@@ -67,7 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
for (var index = items.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var historyItem = items[index];
|
||||
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
|
||||
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager, _browserInfoService))
|
||||
{
|
||||
Icon = Icons.History,
|
||||
Title = historyItem.SearchString,
|
||||
@@ -82,7 +84,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager)
|
||||
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
@@ -95,10 +97,10 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
var searchTerm = query;
|
||||
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
|
||||
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager, browserInfoService))
|
||||
{
|
||||
Title = searchTerm,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
|
||||
Icon = Icons.Search,
|
||||
};
|
||||
results.Add(result);
|
||||
@@ -117,7 +119,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
historySnapshot = _historyItems;
|
||||
}
|
||||
|
||||
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager);
|
||||
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _browserInfoService);
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
|
||||
// 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.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
@@ -69,6 +69,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to default browser.
|
||||
/// </summary>
|
||||
public static string default_browser {
|
||||
get {
|
||||
return ResourceManager.GetString("default_browser", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Web Search.
|
||||
/// </summary>
|
||||
|
||||
@@ -184,4 +184,7 @@
|
||||
<data name="open_url_fallback_title" xml:space="preserve">
|
||||
<value>Open URL</value>
|
||||
</data>
|
||||
<data name="default_browser" xml:space="preserve">
|
||||
<value>default browser</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -19,6 +20,7 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
|
||||
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
|
||||
private readonly ICommandItem[] _topLevelItems;
|
||||
private readonly IFallbackCommandItem[] _fallbackCommands;
|
||||
private readonly IBrowserInfoService _browserInfoService = new DefaultBrowserInfoService();
|
||||
|
||||
public WebSearchCommandsProvider()
|
||||
{
|
||||
@@ -27,10 +29,10 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
|
||||
Icon = Icons.WebSearch;
|
||||
Settings = _settingsManager.Settings;
|
||||
|
||||
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
|
||||
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
|
||||
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager, _browserInfoService);
|
||||
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager, _browserInfoService);
|
||||
|
||||
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
|
||||
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager, _browserInfoService)
|
||||
{
|
||||
MoreCommands =
|
||||
[
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -15,13 +16,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch;
|
||||
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
|
||||
{
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly IBrowserInfoService _browserInfoService;
|
||||
|
||||
public WebSearchTopLevelCommandItem(SettingsManager settingsManager)
|
||||
: base(new WebSearchListPage(settingsManager))
|
||||
public WebSearchTopLevelCommandItem(SettingsManager settingsManager, IBrowserInfoService browserInfoService)
|
||||
: base(new WebSearchListPage(settingsManager, browserInfoService))
|
||||
{
|
||||
Icon = Icons.WebSearch;
|
||||
SetDefaultTitle();
|
||||
_settingsManager = settingsManager;
|
||||
_browserInfoService = browserInfoService;
|
||||
}
|
||||
|
||||
private void SetDefaultTitle() => Title = Resources.command_item_title;
|
||||
@@ -37,12 +40,12 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
SetDefaultTitle();
|
||||
ReplaceCommand(new WebSearchListPage(_settingsManager));
|
||||
ReplaceCommand(new WebSearchListPage(_settingsManager, _browserInfoService));
|
||||
}
|
||||
else
|
||||
{
|
||||
Title = query;
|
||||
ReplaceCommand(new SearchWebCommand(query, _settingsManager));
|
||||
ReplaceCommand(new SearchWebCommand(query, _settingsManager, _browserInfoService));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
[JsonSerializable(typeof(List<Choice>))]
|
||||
[JsonSerializable(typeof(List<ChoiceSetSetting>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>), TypeInfoPropertyName = "Dictionary")]
|
||||
[JsonSerializable(typeof(List<Dictionary<string, object>>))]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)]
|
||||
internal sealed partial class JsonSerializationContext : JsonSerializerContext
|
||||
{
|
||||
|
||||
@@ -26,15 +26,69 @@ public sealed class ToggleSetting : Setting<bool>
|
||||
|
||||
public override Dictionary<string, object> ToDictionary()
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
var items = new List<Dictionary<string, object>>();
|
||||
|
||||
if (!string.IsNullOrEmpty(Label))
|
||||
{
|
||||
{ "type", "Input.Toggle" },
|
||||
{ "title", Label },
|
||||
{ "id", Key },
|
||||
{ "label", Description },
|
||||
{ "value", JsonSerializer.Serialize(Value, JsonSerializationContext.Default.Boolean) },
|
||||
{ "isRequired", IsRequired },
|
||||
{ "errorMessage", ErrorMessage },
|
||||
items.Add(
|
||||
new()
|
||||
{
|
||||
{ "type", "TextBlock" },
|
||||
{ "text", Label },
|
||||
{ "wrap", true },
|
||||
});
|
||||
}
|
||||
|
||||
if (!(string.IsNullOrEmpty(Description) || string.Equals(Description, Label, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
items.Add(
|
||||
new()
|
||||
{
|
||||
{ "type", "TextBlock" },
|
||||
{ "text", Description },
|
||||
{ "isSubtle", true },
|
||||
{ "size", "Small" },
|
||||
{ "spacing", "Small" },
|
||||
{ "wrap", true },
|
||||
});
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
{ "type", "ColumnSet" },
|
||||
{
|
||||
"columns", new List<Dictionary<string, object>>
|
||||
{
|
||||
new()
|
||||
{
|
||||
{ "type", "Column" },
|
||||
{ "width", "20px" },
|
||||
{
|
||||
"items", new List<Dictionary<string, object>>
|
||||
{
|
||||
new()
|
||||
{
|
||||
{ "type", "Input.Toggle" },
|
||||
{ "title", " " },
|
||||
{ "id", Key },
|
||||
{ "value", JsonSerializer.Serialize(Value, JsonSerializationContext.Default.Boolean) },
|
||||
{ "isRequired", IsRequired },
|
||||
{ "errorMessage", ErrorMessage },
|
||||
},
|
||||
}
|
||||
},
|
||||
{ "verticalContentAlignment", "Center" },
|
||||
},
|
||||
new()
|
||||
{
|
||||
{ "type", "Column" },
|
||||
{ "width", "stretch" },
|
||||
{ "items", items },
|
||||
{ "verticalContentAlignment", "Center" },
|
||||
},
|
||||
}
|
||||
},
|
||||
{ "spacing", "Medium" },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- <PropertyGroup>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
</PropertyGroup> -->
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
|
||||
@@ -161,7 +161,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow
|
||||
L"PowerToys.MouseJump.dll",
|
||||
L"PowerToys.AlwaysOnTopModuleInterface.dll",
|
||||
L"PowerToys.MousePointerCrosshairs.dll",
|
||||
// L"PowerToys.CursorWrap.dll",
|
||||
L"PowerToys.CursorWrap.dll",
|
||||
L"PowerToys.PowerAccentModuleInterface.dll",
|
||||
L"PowerToys.PowerOCRModuleInterface.dll",
|
||||
L"PowerToys.AdvancedPasteModuleInterface.dll",
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores provider-specific configuration overrides so each AI service can keep distinct settings.
|
||||
/// </summary>
|
||||
public class AIProviderConfigurationSnapshot
|
||||
{
|
||||
[JsonPropertyName("model-name")]
|
||||
public string ModelName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("endpoint-url")]
|
||||
public string EndpointUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("api-version")]
|
||||
public string ApiVersion { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("deployment-name")]
|
||||
public string DeploymentName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("model-path")]
|
||||
public string ModelPath { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("system-prompt")]
|
||||
public string SystemPrompt { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("moderation-enabled")]
|
||||
public bool ModerationEnabled { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ public static class AIServiceTypeRegistry
|
||||
{
|
||||
ServiceType = AIServiceType.AzureAIInference,
|
||||
DisplayName = "Azure AI Inference",
|
||||
IconPath = "ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg", // No icon for Azure AI Inference, use Foundry Local temporarily
|
||||
IconPath = "ms-appx:///Assets/Settings/Icons/Models/Azure.svg",
|
||||
IsOnlineService = true,
|
||||
LegalDescription = "AdvancedPaste_AzureAIInference_LegalDescription",
|
||||
TermsLabel = "AdvancedPaste_AzureAIInference_TermsLabel",
|
||||
|
||||
@@ -13,56 +13,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
/// </summary>
|
||||
public static class AdvancedPasteMigrationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Moves legacy provider configuration snapshots into the strongly-typed providers collection.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance to migrate.</param>
|
||||
/// <returns>True if the configuration was modified.</returns>
|
||||
public static bool MigrateLegacyProviderConfigurations(PasteAIConfiguration configuration)
|
||||
{
|
||||
if (configuration is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
configuration.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
|
||||
|
||||
bool configurationUpdated = false;
|
||||
|
||||
if (configuration.LegacyProviderConfigurations is { Count: > 0 })
|
||||
{
|
||||
foreach (var entry in configuration.LegacyProviderConfigurations)
|
||||
{
|
||||
var result = EnsureProvider(configuration, entry.Key, entry.Value);
|
||||
configurationUpdated |= result.Updated;
|
||||
}
|
||||
|
||||
configuration.LegacyProviderConfigurations = null;
|
||||
}
|
||||
|
||||
configurationUpdated |= EnsureActiveProviderIsValid(configuration);
|
||||
|
||||
return configurationUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures an OpenAI provider exists in the configuration, creating one if necessary.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance.</param>
|
||||
/// <returns>The ensured provider and a flag indicating whether changes were made.</returns>
|
||||
public static (PasteAIProviderDefinition Provider, bool Updated) EnsureOpenAIProvider(PasteAIConfiguration configuration)
|
||||
{
|
||||
return EnsureProvider(configuration, AIServiceType.OpenAI.ToConfigurationString(), null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a provider for the supplied service type exists, optionally applying a legacy snapshot.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration instance.</param>
|
||||
/// <param name="serviceTypeKey">The persisted service type key.</param>
|
||||
/// <param name="snapshot">An optional snapshot containing legacy values.</param>
|
||||
/// <returns>The ensured provider and whether the configuration was updated.</returns>
|
||||
public static (PasteAIProviderDefinition Provider, bool Updated) EnsureProvider(PasteAIConfiguration configuration, string serviceTypeKey, AIProviderConfigurationSnapshot snapshot)
|
||||
{
|
||||
if (configuration is null)
|
||||
{
|
||||
@@ -71,101 +27,45 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
configuration.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
|
||||
|
||||
var normalizedServiceType = NormalizeServiceType(serviceTypeKey);
|
||||
var existingProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.ServiceType, normalizedServiceType, StringComparison.OrdinalIgnoreCase));
|
||||
bool configurationUpdated = false;
|
||||
const string serviceTypeKey = "OpenAI";
|
||||
var existingProvider = configuration.Providers.FirstOrDefault(provider => string.Equals(provider.ServiceType, serviceTypeKey, StringComparison.OrdinalIgnoreCase));
|
||||
bool updated = false;
|
||||
|
||||
if (existingProvider is null)
|
||||
{
|
||||
existingProvider = CreateProvider(normalizedServiceType, snapshot);
|
||||
existingProvider = CreateProvider(serviceTypeKey);
|
||||
configuration.Providers.Add(existingProvider);
|
||||
configurationUpdated = true;
|
||||
}
|
||||
else if (snapshot is not null)
|
||||
{
|
||||
configurationUpdated |= ApplySnapshot(existingProvider, snapshot);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
configurationUpdated |= EnsureActiveProviderIsValid(configuration, existingProvider);
|
||||
updated |= EnsureActiveProviderIsValid(configuration, existingProvider);
|
||||
|
||||
return (existingProvider, configurationUpdated);
|
||||
return (existingProvider, updated);
|
||||
}
|
||||
|
||||
private static string NormalizeServiceType(string serviceTypeKey)
|
||||
{
|
||||
var serviceType = serviceTypeKey.ToAIServiceType();
|
||||
return serviceType.ToConfigurationString();
|
||||
}
|
||||
|
||||
private static PasteAIProviderDefinition CreateProvider(string serviceTypeKey, AIProviderConfigurationSnapshot snapshot)
|
||||
/// <summary>
|
||||
/// Creates a provider with default values for the requested service type.
|
||||
/// </summary>
|
||||
private static PasteAIProviderDefinition CreateProvider(string serviceTypeKey)
|
||||
{
|
||||
var serviceType = serviceTypeKey.ToAIServiceType();
|
||||
var metadata = AIServiceTypeRegistry.GetMetadata(serviceType);
|
||||
var provider = new PasteAIProviderDefinition
|
||||
{
|
||||
ServiceType = serviceTypeKey,
|
||||
ModelName = !string.IsNullOrWhiteSpace(snapshot?.ModelName) ? snapshot.ModelName : PasteAIProviderDefaults.GetDefaultModelName(serviceType),
|
||||
EndpointUrl = snapshot?.EndpointUrl ?? string.Empty,
|
||||
ApiVersion = snapshot?.ApiVersion ?? string.Empty,
|
||||
DeploymentName = snapshot?.DeploymentName ?? string.Empty,
|
||||
ModelPath = snapshot?.ModelPath ?? string.Empty,
|
||||
SystemPrompt = snapshot?.SystemPrompt ?? string.Empty,
|
||||
ModerationEnabled = snapshot?.ModerationEnabled ?? true,
|
||||
ModelName = PasteAIProviderDefaults.GetDefaultModelName(serviceType),
|
||||
EndpointUrl = string.Empty,
|
||||
ApiVersion = string.Empty,
|
||||
DeploymentName = string.Empty,
|
||||
ModelPath = string.Empty,
|
||||
SystemPrompt = string.Empty,
|
||||
ModerationEnabled = serviceType == AIServiceType.OpenAI,
|
||||
IsLocalModel = metadata.IsLocalModel,
|
||||
};
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
private static bool ApplySnapshot(PasteAIProviderDefinition provider, AIProviderConfigurationSnapshot snapshot)
|
||||
{
|
||||
bool updated = false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.ModelName) && !string.Equals(provider.ModelName, snapshot.ModelName, StringComparison.Ordinal))
|
||||
{
|
||||
provider.ModelName = snapshot.ModelName;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.EndpointUrl) && !string.Equals(provider.EndpointUrl, snapshot.EndpointUrl, StringComparison.Ordinal))
|
||||
{
|
||||
provider.EndpointUrl = snapshot.EndpointUrl;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.ApiVersion) && !string.Equals(provider.ApiVersion, snapshot.ApiVersion, StringComparison.Ordinal))
|
||||
{
|
||||
provider.ApiVersion = snapshot.ApiVersion;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.DeploymentName) && !string.Equals(provider.DeploymentName, snapshot.DeploymentName, StringComparison.Ordinal))
|
||||
{
|
||||
provider.DeploymentName = snapshot.DeploymentName;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.ModelPath) && !string.Equals(provider.ModelPath, snapshot.ModelPath, StringComparison.Ordinal))
|
||||
{
|
||||
provider.ModelPath = snapshot.ModelPath;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(snapshot.SystemPrompt) && !string.Equals(provider.SystemPrompt, snapshot.SystemPrompt, StringComparison.Ordinal))
|
||||
{
|
||||
provider.SystemPrompt = snapshot.SystemPrompt;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (provider.ModerationEnabled != snapshot.ModerationEnabled)
|
||||
{
|
||||
provider.ModerationEnabled = snapshot.ModerationEnabled;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private static bool EnsureActiveProviderIsValid(PasteAIConfiguration configuration, PasteAIProviderDefinition preferredProvider = null)
|
||||
{
|
||||
if (configuration?.Providers is null || configuration.Providers.Count == 0)
|
||||
|
||||
@@ -20,8 +20,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
private string _activeProviderId = string.Empty;
|
||||
private ObservableCollection<PasteAIProviderDefinition> _providers = new();
|
||||
private bool _useSharedCredentials = true;
|
||||
private Dictionary<string, AIProviderConfigurationSnapshot> _legacyProviderConfigurations;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
@@ -39,21 +37,6 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
set => SetProperty(ref _providers, value ?? new ObservableCollection<PasteAIProviderDefinition>());
|
||||
}
|
||||
|
||||
[JsonPropertyName("use-shared-credentials")]
|
||||
public bool UseSharedCredentials
|
||||
{
|
||||
get => _useSharedCredentials;
|
||||
set => SetProperty(ref _useSharedCredentials, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName("provider-configurations")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public Dictionary<string, AIProviderConfigurationSnapshot> LegacyProviderConfigurations
|
||||
{
|
||||
get => _legacyProviderConfigurations;
|
||||
set => _legacyProviderConfigurations = value;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public PasteAIProviderDefinition ActiveProvider
|
||||
{
|
||||
|
||||
@@ -1,59 +1,34 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2092_1741)">
|
||||
<mask id="mask0_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="17" height="16">
|
||||
<path d="M16.5 0H0.5V16H16.5V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_2092_1741)">
|
||||
<mask id="mask1_2092_1741" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-1" y="-2" width="19" height="20">
|
||||
<path d="M17.8337 -1.33337H-0.833008V17.3333H17.8337V-1.33337Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_2092_1741)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1137 0.315668C11.57 0.315668 11.9744 0.657891 12.1196 1.15567C12.2648 1.65345 13.1152 4.73345 13.1152 4.73345V10.852H10.0352L10.0974 0.305298H11.1137V0.315668Z" fill="url(#paint0_linear_2092_1741)"/>
|
||||
<path d="M15.6352 5.09586C15.6352 4.87808 15.4589 4.71216 15.2515 4.71216H13.4366C12.1611 4.71216 11.124 5.7492 11.124 7.02472V10.8618H13.3226C14.5982 10.8618 15.6352 9.82472 15.6352 8.54919V5.09586Z" fill="url(#paint1_linear_2092_1741)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1133 0.315674C10.7607 0.315674 10.4807 0.595674 10.4807 0.948265L10.4185 12.5942C10.4185 14.2949 9.0392 15.6742 7.33847 15.6742H1.74885C1.47921 15.6742 1.30292 15.4149 1.38589 15.1661L5.86589 2.37938C6.30144 1.14531 7.46293 0.315674 8.7696 0.315674H11.1237H11.1133Z" fill="url(#paint2_linear_2092_1741)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.2663 0H0.231885C0.103572 0 0 0.10358 0 0.231885V2.55072C0 2.67904 0.103572 2.78261 0.231885 2.78261H12.5217C12.9059 2.78261 13.2174 3.09411 13.2174 3.47826V3.18995C13.2174 1.53971 11.9861 0 10.2663 0Z" fill="url(#paint0_linear_178_3940)"/>
|
||||
<path d="M12.2334 0.81543C12.8633 1.44693 13.2174 2.29872 13.2174 3.19069V15.7689C13.2174 15.8972 13.3209 16.0007 13.4492 16.0007H15.7681C15.8964 16.0007 16 15.8972 16 15.7689V5.73524C16 4.99707 15.707 4.28983 15.1853 3.76732L12.2334 0.81543Z" fill="url(#paint1_linear_178_3940)"/>
|
||||
<path d="M6.78804 3.47852H0.231885C0.103572 3.47852 0 3.58209 0 3.71039V6.02921C0 6.1575 0.103572 6.26112 0.231885 6.26112H9.04346C9.42759 6.26112 9.7391 6.57263 9.7391 6.95676V6.6685C9.7391 5.01822 8.50778 3.47852 6.78804 3.47852Z" fill="url(#paint2_linear_178_3940)"/>
|
||||
<path d="M8.75537 4.29297C9.38531 4.92446 9.73928 5.77628 9.73928 6.6682V15.7681C9.73928 15.8964 9.8429 16 9.97119 16H12.29C12.4183 16 12.5219 15.8964 12.5219 15.7681V9.21281C12.5219 8.47462 12.229 7.76735 11.7072 7.24482L8.75537 4.29297Z" fill="url(#paint3_linear_178_3940)"/>
|
||||
<path d="M3.30975 6.95703H0.231885C0.103572 6.95703 0 7.06056 0 7.18886V9.50771C0 9.63609 0.103572 9.73962 0.231885 9.73962H5.56521C5.94936 9.73962 6.26087 10.0511 6.26087 10.4353V10.147C6.26087 8.49675 5.02956 6.95703 3.30975 6.95703Z" fill="url(#paint4_linear_178_3940)"/>
|
||||
<path d="M5.27686 7.77148C5.90677 8.40302 6.26083 9.25477 6.26083 10.1468V15.7684C6.26083 15.8967 6.36436 16.0003 6.49274 16.0003H8.8115C8.93988 16.0003 9.04341 15.8967 9.04341 15.7684V12.6913C9.04341 11.9531 8.75051 11.2459 8.22874 10.7234L5.27686 7.77148Z" fill="url(#paint5_linear_178_3940)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2092_1741" x1="12.3996" y1="11.0801" x2="9.80702" y2="0.699373" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#712575"/>
|
||||
<stop offset="0.09" stop-color="#9A2884"/>
|
||||
<stop offset="0.18" stop-color="#BF2C92"/>
|
||||
<stop offset="0.27" stop-color="#DA2E9C"/>
|
||||
<stop offset="0.34" stop-color="#EB30A2"/>
|
||||
<stop offset="0.4" stop-color="#F131A5"/>
|
||||
<stop offset="0.5" stop-color="#EC30A3"/>
|
||||
<stop offset="0.61" stop-color="#DF2F9E"/>
|
||||
<stop offset="0.72" stop-color="#C92D96"/>
|
||||
<stop offset="0.83" stop-color="#AA2A8A"/>
|
||||
<stop offset="0.95" stop-color="#83267C"/>
|
||||
<stop offset="1" stop-color="#712575"/>
|
||||
<linearGradient id="paint0_linear_178_3940" x1="13.2174" y1="3.15349" x2="0" y2="3.15349" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2C08AC"/>
|
||||
<stop offset="0.8" stop-color="#4F42FD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_2092_1741" x1="13.3848" y1="0.532897" x2="13.3848" y2="15.1759" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.08" stop-color="#B17BD5"/>
|
||||
<stop offset="0.19" stop-color="#8778DB"/>
|
||||
<stop offset="0.3" stop-color="#6276E1"/>
|
||||
<stop offset="0.41" stop-color="#4574E5"/>
|
||||
<stop offset="0.54" stop-color="#2E72E8"/>
|
||||
<stop offset="0.67" stop-color="#1D71EB"/>
|
||||
<stop offset="0.81" stop-color="#1471EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
<linearGradient id="paint1_linear_178_3940" x1="14.2303" y1="0.81543" x2="23.44" y2="11.9747" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.3" stop-color="#7274FF"/>
|
||||
<stop offset="1" stop-color="#4F42FD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_2092_1741" x1="12.5029" y1="0.865306" x2="2.79625" y2="16.4313" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#DA7ED0"/>
|
||||
<stop offset="0.05" stop-color="#B77BD4"/>
|
||||
<stop offset="0.11" stop-color="#9079DA"/>
|
||||
<stop offset="0.18" stop-color="#6E77DF"/>
|
||||
<stop offset="0.25" stop-color="#5175E3"/>
|
||||
<stop offset="0.33" stop-color="#3973E7"/>
|
||||
<stop offset="0.42" stop-color="#2772E9"/>
|
||||
<stop offset="0.54" stop-color="#1A71EB"/>
|
||||
<stop offset="0.68" stop-color="#1371EC"/>
|
||||
<stop offset="1" stop-color="#1171ED"/>
|
||||
<linearGradient id="paint2_linear_178_3940" x1="9.7391" y1="6.63202" x2="0" y2="6.63202" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2C08AC"/>
|
||||
<stop offset="0.8" stop-color="#4F42FD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_178_3940" x1="10.7523" y1="4.29297" x2="17.3026" y2="14.5881" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.3" stop-color="#7274FF"/>
|
||||
<stop offset="1" stop-color="#4F42FD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_178_3940" x1="6.26087" y1="9.91172" x2="0" y2="9.91172" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2C08AC"/>
|
||||
<stop offset="0.8" stop-color="#4F42FD"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_178_3940" x1="7.2738" y1="7.77148" x2="11.0624" y2="16.243" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.3" stop-color="#7274FF"/>
|
||||
<stop offset="1" stop-color="#4F42FD"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2092_1741">
|
||||
<rect width="16" height="16" fill="white" transform="translate(0.5)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 318 KiB |
@@ -28,6 +28,8 @@
|
||||
<None Remove="Assets\Settings\Icons\Models\WindowsML.svg" />
|
||||
<None Remove="Assets\Settings\Modules\APDialog.dark.png" />
|
||||
<None Remove="Assets\Settings\Modules\APDialog.light.png" />
|
||||
<None Remove="Assets\Settings\Modules\CmdPal_Background.png" />
|
||||
<None Remove="Assets\Settings\Modules\CmdPal_Hero.png" />
|
||||
<None Remove="Assets\Settings\Modules\LightSwitch.png" />
|
||||
<None Remove="SettingsXAML\Controls\Dashboard\CheckUpdateControl.xaml" />
|
||||
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
|
||||
@@ -71,6 +73,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
|
||||
@@ -30,6 +30,24 @@
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<InfoBar
|
||||
x:Uid="AdvancedPaste_FL_PreviewMessage"
|
||||
Grid.Row="1"
|
||||
Padding="4"
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="Foundry Local is still in Public Preview">
|
||||
<InfoBar.ActionButton>
|
||||
<HyperlinkButton
|
||||
x:Uid="AdvancedPaste_FL_LearnMoreFoundryLocal"
|
||||
Content="Learn more"
|
||||
NavigateUri="https://learn.microsoft.com/azure/ai-foundry/foundry-local/what-is-foundry-local" />
|
||||
</InfoBar.ActionButton>
|
||||
</InfoBar>
|
||||
<StackPanel
|
||||
x:Name="LoadingPanel"
|
||||
HorizontalAlignment="Center"
|
||||
@@ -69,12 +87,12 @@
|
||||
FontSize="24"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
x:Uid="AdvancedPaste_FL_NoModelsDownloaded."
|
||||
x:Uid="AdvancedPaste_FL_NoModelsDownloaded"
|
||||
HorizontalAlignment="Center"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
TextAlignment="Center" />
|
||||
<TextBlock
|
||||
x:Uid="AdvancedPaste_FL_RunFoundryLocalText.Text"
|
||||
x:Uid="AdvancedPaste_FL_RunFoundryLocalText"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
@@ -106,7 +124,7 @@
|
||||
<ComboBox.Header>
|
||||
<TextBlock>
|
||||
<Run x:Uid="AdvancedPaste_FL_LocalModel" /><LineBreak /><Run
|
||||
x:Uid="AdvancedPaste_FL_UseCLIToDownloadModels"
|
||||
x:Uid="AdvancedPaste_FL_UseCliToDownloadModels"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</TextBlock>
|
||||
@@ -152,7 +170,7 @@
|
||||
Spacing="8">
|
||||
<Image Width="36" Source="ms-appx:///Assets/Settings/Icons/Models/FoundryLocal.svg" />
|
||||
<TextBlock
|
||||
x:Uid="AdvancedPaste_FL_FLNotavailableYet"
|
||||
x:Uid="AdvancedPaste_FL_FLNotAvailableYet"
|
||||
HorizontalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
TextAlignment="Center"
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
||||
|
||||
public delegate void DownloadRequestedEventHandler(object sender, object payload);
|
||||
|
||||
public delegate void LoadRequestedEventHandler(object sender, FoundryLoadRequestedEventArgs args);
|
||||
public delegate void LoadRequestedEventHandler(object sender);
|
||||
|
||||
public event ModelSelectionChangedEventHandler SelectionChanged;
|
||||
|
||||
@@ -94,7 +94,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
||||
|
||||
public bool HasDownloadableModels => DownloadableModels?.Cast<object>().Any() ?? false;
|
||||
|
||||
public void RequestLoad(bool refresh)
|
||||
public void RequestLoad()
|
||||
{
|
||||
if (IsLoading)
|
||||
{
|
||||
@@ -107,7 +107,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
||||
|
||||
IsAvailable = false;
|
||||
StatusText = "Loading Foundry Local status...";
|
||||
LoadRequested?.Invoke(this, new FoundryLoadRequestedEventArgs(refresh));
|
||||
LoadRequested?.Invoke(this);
|
||||
}
|
||||
|
||||
private static void OnCachedModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
@@ -310,7 +310,7 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
||||
|
||||
private void RefreshModelsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RequestLoad(refresh: true);
|
||||
RequestLoad();
|
||||
}
|
||||
|
||||
private void UpdateVisualStates()
|
||||
@@ -444,14 +444,4 @@ public sealed partial class FoundryLocalModelPicker : UserControl
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(license) ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public sealed class FoundryLoadRequestedEventArgs : EventArgs
|
||||
{
|
||||
public FoundryLoadRequestedEventArgs(bool refresh)
|
||||
{
|
||||
Refresh = refresh;
|
||||
}
|
||||
|
||||
public bool Refresh { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
{
|
||||
ViewModel.RefreshEnabledState();
|
||||
UpdatePasteAIUIVisibility();
|
||||
_ = UpdateFoundryLocalUIAsync(refreshFoundry: true);
|
||||
_ = UpdateFoundryLocalUIAsync();
|
||||
}
|
||||
|
||||
private void EnableAdvancedPasteAI() => ViewModel.EnableAI();
|
||||
@@ -384,7 +384,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
}
|
||||
}
|
||||
|
||||
private Task UpdateFoundryLocalUIAsync(bool refreshFoundry = false)
|
||||
private Task UpdateFoundryLocalUIAsync()
|
||||
{
|
||||
string selectedType = ViewModel?.PasteAIProviderDraft?.ServiceType ?? string.Empty;
|
||||
bool isFoundryLocal = string.Equals(selectedType, "FoundryLocal", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -419,12 +419,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
PasteAIProviderConfigurationDialog.IsPrimaryButtonEnabled = false;
|
||||
}
|
||||
|
||||
FoundryLocalPicker?.RequestLoad(refreshFoundry);
|
||||
FoundryLocalPicker?.RequestLoad();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LoadFoundryLocalModelsAsync(bool refresh = false)
|
||||
private async Task LoadFoundryLocalModelsAsync()
|
||||
{
|
||||
if (FoundryLocalPanel is null)
|
||||
{
|
||||
@@ -456,9 +456,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<ModelDetails> cachedModelsEnumerable = refresh
|
||||
? await provider.GetModelsAsync(ignoreCached: true, cancelationToken: cancellationToken)
|
||||
: await provider.GetModelsAsync(cancelationToken: cancellationToken);
|
||||
IEnumerable<ModelDetails> cachedModelsEnumerable = await provider.GetModelsAsync(cancelationToken: cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -467,9 +465,12 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
var cachedModels = cachedModelsEnumerable?.ToList() ?? new List<ModelDetails>();
|
||||
|
||||
UpdateFoundryCollections(cachedModels);
|
||||
ShowFoundryAvailableState();
|
||||
RestoreFoundrySelection(cachedModels);
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
UpdateFoundryCollections(cachedModels);
|
||||
ShowFoundryAvailableState();
|
||||
RestoreFoundrySelection(cachedModels);
|
||||
});
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -478,12 +479,18 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorMessage = $"Unable to load Foundry Local models. {ex.Message}";
|
||||
ShowFoundryUnavailableState(errorMessage);
|
||||
System.Diagnostics.Debug.WriteLine($"[AdvancedPastePage] Failed to load Foundry Local models: {ex}");
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
ShowFoundryUnavailableState(errorMessage);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
UpdateFoundrySaveButtonState();
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
UpdateFoundrySaveButtonState();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,9 +679,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
UpdateFoundrySaveButtonState();
|
||||
}
|
||||
|
||||
private async void FoundryLocalPicker_LoadRequested(object sender, FoundryLocalModelPicker.FoundryLoadRequestedEventArgs args)
|
||||
private async void FoundryLocalPicker_LoadRequested(object sender)
|
||||
{
|
||||
await LoadFoundryLocalModelsAsync(args?.Refresh ?? false);
|
||||
await LoadFoundryLocalModelsAsync();
|
||||
}
|
||||
|
||||
private sealed class FoundryDownloadableModel : INotifyPropertyChanged
|
||||
@@ -1089,7 +1096,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
PasteAIProviderConfigurationDialog.Title = $"{displayName} provider configuration";
|
||||
}
|
||||
|
||||
await UpdateFoundryLocalUIAsync(refreshFoundry: true);
|
||||
await UpdateFoundryLocalUIAsync();
|
||||
UpdatePasteAIUIVisibility();
|
||||
RefreshDialogBindings();
|
||||
|
||||
@@ -1118,7 +1125,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
: $"{titlePrefix} provider configuration";
|
||||
|
||||
UpdatePasteAIUIVisibility();
|
||||
await UpdateFoundryLocalUIAsync(refreshFoundry: false);
|
||||
await UpdateFoundryLocalUIAsync();
|
||||
RefreshDialogBindings();
|
||||
PasteAIApiKeyPasswordBox.Password = ViewModel.GetPasteAIApiKey(provider.Id, provider.ServiceType);
|
||||
await PasteAIProviderConfigurationDialog.ShowAsync();
|
||||
|
||||
@@ -10,40 +10,202 @@
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
AutomationProperties.LandmarkType="Main"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<ScrollViewer AutomationProperties.AutomationId="PageScrollViewer">
|
||||
<Grid
|
||||
MaxWidth="1000"
|
||||
Padding="16,0,16,0"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16"
|
||||
RowSpacing="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<tkcontrols:OpacityMaskView Margin="-16,0,-16,0" HorizontalAlignment="Stretch">
|
||||
<tkcontrols:OpacityMaskView.OpacityMask>
|
||||
<Rectangle>
|
||||
<Rectangle.Fill>
|
||||
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
|
||||
<GradientStop Offset="0.50" Color="Black" />
|
||||
<GradientStop Offset="0.75" Color="#80000000" />
|
||||
<GradientStop Offset="0.95" Color="Transparent" />
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.Fill>
|
||||
</Rectangle>
|
||||
</tkcontrols:OpacityMaskView.OpacityMask>
|
||||
<Grid Height="560">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Image
|
||||
Grid.RowSpan="3"
|
||||
HorizontalAlignment="Stretch"
|
||||
Source="/Assets/Settings/Modules/CmdPal_Background.png"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock
|
||||
Margin="0,24,0,12"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="36"
|
||||
FontWeight="Bold"
|
||||
Text="Command Palette">
|
||||
<TextBlock.Foreground>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Offset="0.0" Color="#FFB9EBFF" />
|
||||
<GradientStop Offset="0.49" Color="#FF86CBFF" />
|
||||
<GradientStop Offset="1.0" Color="#FFA1E7FF" />
|
||||
</LinearGradientBrush>
|
||||
</TextBlock.Foreground>
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="White"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap">
|
||||
<Run x:Uid="CmdPal_Description" />
|
||||
<Hyperlink NavigateUri="">
|
||||
<Run x:Uid="LearnMore_CmdPal.Text" Foreground="White" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<Image
|
||||
Grid.Row="2"
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Source="/Assets/Settings/Modules/CmdPal_Hero.png"
|
||||
Stretch="Uniform" />
|
||||
</Grid>
|
||||
</tkcontrols:OpacityMaskView>
|
||||
|
||||
<controls:SettingsPageControl x:Uid="CmdPal" ModuleImageSource="ms-appx:///Assets/Settings/Modules/CmdPal.png">
|
||||
<controls:SettingsPageControl.ModuleContent>
|
||||
<StackPanel ChildrenTransitions="{StaticResource SettingsCardsAnimations}" Orientation="Vertical">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="CmdPalEnableCmdPal"
|
||||
x:Uid="CmdPal_Enable_CmdPal"
|
||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CmdPal.png}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:GPOInfoControl>
|
||||
<controls:SettingsGroup x:Uid="CmdPal_Activation_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="CmdPalActivationShortcut"
|
||||
x:Uid="CmdPal_ActivationShortcut"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<controls:ShortcutControl
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
HotkeySettings="{x:Bind Path=ViewModel.Hotkey, Mode=OneWay}"
|
||||
IsEnabled="False" />
|
||||
<tkcontrols:SettingsCard.Description>
|
||||
<HyperlinkButton
|
||||
x:Name="CmdPalSettingsDeeplink"
|
||||
x:Uid="CmdPal_DeeplinkContent"
|
||||
Click="CmdPalSettingsDeeplink_Click" />
|
||||
</tkcontrols:SettingsCard.Description>
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:SettingsGroup>
|
||||
</StackPanel>
|
||||
</controls:SettingsPageControl.ModuleContent>
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Margin="0,-12,0,24"
|
||||
ColumnSpacing="32"
|
||||
RowSpacing="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap">
|
||||
<Run x:Uid="CmdPal_ExtensibleHeader" FontWeight="SemiBold" /> <LineBreak />
|
||||
<Run
|
||||
x:Uid="CmdPal_ExtensibleDescription"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</TextBlock>
|
||||
|
||||
<controls:SettingsPageControl.PrimaryLinks>
|
||||
<controls:PageLink x:Uid="LearnMore_CmdPal" Link="https://aka.ms/PowerToysOverview_CmdPal" />
|
||||
</controls:SettingsPageControl.PrimaryLinks>
|
||||
</controls:SettingsPageControl>
|
||||
<FontIcon
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap">
|
||||
<Run x:Uid="CmdPal_FastHeader" FontWeight="SemiBold" /> <LineBreak />
|
||||
<Run
|
||||
x:Uid="CmdPal_FastDescription"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</TextBlock>
|
||||
|
||||
<FontIcon
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap">
|
||||
<Run x:Uid="CmdPal_ModernHeader" FontWeight="SemiBold" /> <LineBreak />
|
||||
<Run
|
||||
x:Uid="CmdPal_ModernDescription"
|
||||
FontSize="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,8,0,0"
|
||||
Orientation="Vertical"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
Name="CmdPalEnableCmdPal"
|
||||
x:Uid="CmdPal_Enable_CmdPal"
|
||||
HorizontalAlignment="Stretch"
|
||||
HeaderIcon="{ui:BitmapIcon Source=/Assets/Settings/Icons/CmdPal.png}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.IsEnabled, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
</controls:GPOInfoControl>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="CmdPal_Launch"
|
||||
Grid.Row="3"
|
||||
ActionIcon="{ui:FontIcon Glyph=}"
|
||||
Click="LaunchCard_Click"
|
||||
Header="Launch Command Palette"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"
|
||||
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">
|
||||
<ItemsControl
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
IsTabStop="False"
|
||||
ItemsSource="{x:Bind Path=ViewModel.Hotkey.GetKeysList(), Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<controls:KeyVisual
|
||||
Padding="8,8,8,8"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Content="{Binding}"
|
||||
Style="{StaticResource AccentKeyVisualStyle}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard
|
||||
x:Uid="CmdPal_Settings"
|
||||
Grid.Row="4"
|
||||
ActionIcon="{ui:FontIcon Glyph=}"
|
||||
Click="SettingsCard_Click"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsClickEnabled="True"
|
||||
IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</local:NavigablePage>
|
||||
|
||||
@@ -63,12 +63,20 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
}
|
||||
}
|
||||
|
||||
private void CmdPalSettingsDeeplink_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
private void SettingsCard_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
// Launch CmdPal settings window as normal user using explorer
|
||||
string launchPath = "explorer.exe";
|
||||
string launchArgs = "x-cmdpal://settings";
|
||||
LaunchApp(launchPath, launchArgs);
|
||||
}
|
||||
|
||||
private void LaunchCard_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
|
||||
{
|
||||
// Launch CmdPal window as normal user using explorer
|
||||
string launchPath = "explorer.exe";
|
||||
string launchArgs = "x-cmdpal:";
|
||||
LaunchApp(launchPath, launchArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,6 @@
|
||||
|
||||
<panels:MouseJumpPanel x:Name="MouseUtils_MouseJump_Panel" x:Uid="MouseUtils_MouseJump_Panel" />
|
||||
|
||||
<!--
|
||||
<controls:SettingsGroup x:Uid="MouseUtils_CursorWrap" AutomationProperties.AutomationId="MouseUtils_CursorWrapTestId">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsCursorWrapEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
@@ -301,7 +300,7 @@
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>-->
|
||||
</controls:SettingsGroup>
|
||||
<controls:SettingsGroup x:Uid="MouseUtils_MousePointerCrosshairs" AutomationProperties.AutomationId="MouseUtils_MousePointerCrosshairsTestId">
|
||||
<controls:GPOInfoControl ShowWarning="{x:Bind ViewModel.IsMousePointerCrosshairsEnabledGpoConfigured, Mode=OneWay}">
|
||||
<tkcontrols:SettingsCard
|
||||
|
||||
@@ -285,6 +285,9 @@
|
||||
AutomationProperties.AutomationId="InputOutputNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/InputOutput.png}"
|
||||
SelectsOnInvoked="False">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
<NavigationViewItem.MenuItems>
|
||||
<NavigationViewItem
|
||||
x:Name="KeyboardManagerNavigationItem"
|
||||
@@ -299,7 +302,11 @@
|
||||
x:Uid="Shell_MouseUtilities"
|
||||
helpers:NavHelper.NavigateTo="views:MouseUtilsPage"
|
||||
AutomationProperties.AutomationId="MouseUtilitiesNavItem"
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseUtils.png}" />
|
||||
Icon="{ui:BitmapIcon Source=/Assets/Settings/Icons/MouseUtils.png}">
|
||||
<NavigationViewItem.InfoBadge>
|
||||
<InfoBadge Style="{StaticResource NewInfoBadge}" />
|
||||
</NavigationViewItem.InfoBadge>
|
||||
</NavigationViewItem>
|
||||
<NavigationViewItem
|
||||
x:Name="MouseWithoutBordersNavigationItem"
|
||||
x:Uid="Shell_MouseWithoutBorders"
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
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
|
||||
|
||||
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>
|
||||
@@ -26,36 +26,36 @@
|
||||
<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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
@@ -2703,7 +2703,7 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<value>Mouse Pointer Crosshairs</value>
|
||||
<comment>Mouse as in the hardware peripheral.</comment>
|
||||
</data>
|
||||
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Description" xml:space="preserve">
|
||||
<data name="Oobe_MouseUtils_MousePointerCrosshairs_Description.Text" xml:space="preserve">
|
||||
<value>Draw crosshairs centered around the mouse pointer.</value>
|
||||
<comment>Mouse as in the hardware peripheral.</comment>
|
||||
</data>
|
||||
@@ -2827,7 +2827,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<comment>Refers to the utility name</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_FindMyMouse.Description" xml:space="preserve">
|
||||
<value>Find My Mouse highlights the position of the cursor when pressing the Ctrl key twice, using a custom shortcut or when shaking the mouse.</value>
|
||||
<value>Highlight the position of the cursor when pressing the Ctrl key twice, using a custom shortcut or when shaking the mouse.</value>
|
||||
<comment>"Ctrl" is a keyboard key. "Find My Mouse" is the name of the utility</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_Enable_FindMyMouse.Header" xml:space="preserve">
|
||||
@@ -2916,7 +2916,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<comment>Refers to the utility name</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MouseHighlighter.Description" xml:space="preserve">
|
||||
<value>Mouse Highlighter mode will highlight mouse clicks.</value>
|
||||
<value>Highlight mouse clicks.</value>
|
||||
<comment>"Mouse Highlighter" is the name of the utility. Mouse is the hardware mouse.</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_Enable_MouseHighlighter.Header" xml:space="preserve">
|
||||
@@ -2961,7 +2961,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<comment>Refers to the utility name</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_MousePointerCrosshairs.Description" xml:space="preserve">
|
||||
<value>Mouse Pointer Crosshairs draws crosshairs centered on the mouse pointer.</value>
|
||||
<value>Draw crosshairs centered on the mouse pointer.</value>
|
||||
<comment>"Mouse Pointer Crosshairs" is the name of the utility. Mouse is the hardware mouse.</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_Enable_MousePointerCrosshairs.Header" xml:space="preserve">
|
||||
@@ -3410,7 +3410,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>An AI powered tool to put your clipboard content into any format you need, focused towards developer workflows.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAISettingsCardDescription.Text" xml:space="preserve">
|
||||
<value>Transform your clipboard content with the power of AI. An cloud or local endpoint is required.</value>
|
||||
<value>Transform your clipboard content with the power of AI. A cloud or local endpoint is required.</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_EnableAISettingsCardDescriptionLearnMore.Content" xml:space="preserve">
|
||||
<value>Learn more</value>
|
||||
@@ -5159,25 +5159,12 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="Shell_TopLevelSystemTools.Content" xml:space="preserve">
|
||||
<value>System Tools</value>
|
||||
</data>
|
||||
<data name="CmdPal.ModuleTitle" xml:space="preserve">
|
||||
<value>Command Palette</value>
|
||||
</data>
|
||||
<data name="CmdPal_ShortDescription" xml:space="preserve">
|
||||
<value>A better quick launcher</value>
|
||||
</data>
|
||||
<data name="CmdPal_ActivationDescription" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
</data>
|
||||
<data name="CmdPal_Enable_CmdPal.Header" xml:space="preserve">
|
||||
<value>Enable Command Palette</value>
|
||||
<comment>"Command Palette" is the name of the utility.</comment>
|
||||
</data>
|
||||
<data name="CmdPal.ModuleDescription" xml:space="preserve">
|
||||
<value>A fully extensible quick launcher with a richer display and additional capabilities without sacrificing performance.</value>
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="LearnMore_CmdPal.Text" xml:space="preserve">
|
||||
<value>Learn more about Command Palette</value>
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Shell_CmdPal.Content" xml:space="preserve">
|
||||
<value>Command Palette</value>
|
||||
@@ -5185,11 +5172,11 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
</data>
|
||||
<data name="Oobe_CmdPal.Description" xml:space="preserve">
|
||||
<value>A fully extensible quick launcher with a richer display and additional capabilities without sacrificing performance.</value>
|
||||
<comment>"Command Palette" is a product name</comment>
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Oobe_CmdPal.Title" xml:space="preserve">
|
||||
<value>Command Palette</value>
|
||||
<comment>"Command Palette" is a product name</comment>
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Oobe_CmdPal_HowToUse.Text" xml:space="preserve">
|
||||
<value>and start typing!</value>
|
||||
@@ -5222,14 +5209,8 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="RetryLabel.Text" xml:space="preserve">
|
||||
<value>Retry</value>
|
||||
</data>
|
||||
<data name="CmdPal_Activation_GroupSettings.Header" xml:space="preserve">
|
||||
<value>Activation</value>
|
||||
</data>
|
||||
<data name="CmdPal_ActivationShortcut.Header" xml:space="preserve">
|
||||
<value>Activation shortcut</value>
|
||||
</data>
|
||||
<data name="CmdPal_DeeplinkContent.Content" xml:space="preserve">
|
||||
<value>Open Command Palette settings to customize the activation shortcut</value>
|
||||
<data name="CmdPal_Settings.Header" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="Help_chromaCIE" xml:space="preserve">
|
||||
<value>chroma (CIE LCh)</value>
|
||||
@@ -5693,14 +5674,14 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<value>Foundry Local model</value>
|
||||
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_FL_UseCLIToDownloadModels.Text" xml:space="preserve">
|
||||
<data name="AdvancedPaste_FL_UseCliToDownloadModels.Text" xml:space="preserve">
|
||||
<value>Use the Foundry Local CLI to download models that run locally on-device. They'll appear here.</value>
|
||||
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
||||
</data>
|
||||
<data name="AdvancedPaste_FL_RefreshModelList.Text" xml:space="preserve">
|
||||
<value>Refresh model list</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_FL_FLNotavailableYet.Text" xml:space="preserve">
|
||||
<data name="AdvancedPaste_FL_FLNotAvailableYet.Text" xml:space="preserve">
|
||||
<value>Foundry Local is not available on this device yet.</value>
|
||||
<comment>Do not localize "Foundry Local", it's a product name</comment>
|
||||
</data>
|
||||
@@ -5753,4 +5734,51 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="AdvancedPaste_EnableClipboardPreview.Description" xml:space="preserve">
|
||||
<value>Display a preview of the current clipboard content</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="AdvancedPaste_FL_LearnMoreFoundryLocal.Content" xml:space="preserve">
|
||||
<value>Learn more</value>
|
||||
</data>
|
||||
<data name="AdvancedPaste_FL_PreviewMessage.Message" xml:space="preserve">
|
||||
<value>Foundry Local is still in public preview</value>
|
||||
<comment>Do not loc "Foundry Local"</comment>
|
||||
</data>
|
||||
<data name="CmdPal_Settings.Description" xml:space="preserve">
|
||||
<value>Configure the activation shortcut, extensions, behavior and much more</value>
|
||||
</data>
|
||||
<data name="CmdPal_Launch.Header" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="CmdPal_ShortDescription" xml:space="preserve">
|
||||
<value>A better quick launcher</value>
|
||||
</data>
|
||||
<data name="CmdPal_Description.Text" xml:space="preserve">
|
||||
<value>Find files, launch apps, and do so much more with the most extensible quick launcher.</value>
|
||||
</data>
|
||||
<data name="CmdPal.ModuleTitle" xml:space="preserve">
|
||||
<value>Command Palette</value>
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="CmdPal_ActivationDescription" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
<comment>Command Palette is a product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="CmdPal_ExtensibleDescription.Text" xml:space="preserve">
|
||||
<value>Powerful extensions help you do more</value>
|
||||
</data>
|
||||
<data name="CmdPal_ExtensibleHeader.Text" xml:space="preserve">
|
||||
<value>Extensible</value>
|
||||
</data>
|
||||
<data name="CmdPal_FastDescription.Text" xml:space="preserve">
|
||||
<value>Find files and launch apps in an instant</value>
|
||||
</data>
|
||||
<data name="CmdPal_FastHeader.Text" xml:space="preserve">
|
||||
<value>Fast</value>
|
||||
</data>
|
||||
<data name="CmdPal_ModernHeader.Text" xml:space="preserve">
|
||||
<value>Beautiful</value>
|
||||
</data>
|
||||
<data name="CmdPal_ModernDescription.Text" xml:space="preserve">
|
||||
<value>A modern UI built with Fluent Design</value>
|
||||
<comment>Fluent Design is a product name, do not loc</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -191,6 +191,18 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
|
||||
|
||||
if (legacyCredential is null)
|
||||
{
|
||||
if (legacyAdvancedAIConsumed)
|
||||
{
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var configuration = properties.PasteAIConfiguration;
|
||||
if (configuration is null)
|
||||
{
|
||||
@@ -198,28 +210,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
properties.PasteAIConfiguration = configuration;
|
||||
}
|
||||
|
||||
bool hasLegacyProviders = configuration.LegacyProviderConfigurations is { Count: > 0 };
|
||||
PasswordCredential legacyCredential = TryGetLegacyOpenAICredential();
|
||||
|
||||
if (!hasLegacyProviders && legacyCredential is null && !legacyAdvancedAIConsumed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool configurationUpdated = false;
|
||||
|
||||
if (hasLegacyProviders)
|
||||
{
|
||||
configurationUpdated |= AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(configuration);
|
||||
}
|
||||
|
||||
PasteAIProviderDefinition openAIProvider = null;
|
||||
if (legacyCredential is not null || hasLegacyProviders || legacyAdvancedAIConsumed)
|
||||
{
|
||||
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
|
||||
openAIProvider = ensureResult.Provider;
|
||||
configurationUpdated |= ensureResult.Updated;
|
||||
}
|
||||
var ensureResult = AdvancedPasteMigrationHelper.EnsureOpenAIProvider(configuration);
|
||||
PasteAIProviderDefinition openAIProvider = ensureResult.Provider;
|
||||
configurationUpdated |= ensureResult.Updated;
|
||||
|
||||
if (legacyAdvancedAIConsumed && openAIProvider is not null && openAIProvider.EnableAdvancedAI != legacyAdvancedAIEnabled)
|
||||
{
|
||||
@@ -233,7 +228,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
RemoveLegacyOpenAICredential();
|
||||
}
|
||||
|
||||
bool shouldEnableAI = legacyCredential is not null;
|
||||
const bool shouldEnableAI = true;
|
||||
bool enabledChanged = false;
|
||||
if (properties.IsAIEnabled != shouldEnableAI)
|
||||
{
|
||||
@@ -1248,11 +1243,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return true;
|
||||
}
|
||||
|
||||
if (current.UseSharedCredentials != incoming.UseSharedCredentials)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var currentProviders = current.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
|
||||
var incomingProviders = incoming.Providers ?? new ObservableCollection<PasteAIProviderDefinition>();
|
||||
|
||||
@@ -1408,8 +1398,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal)
|
||||
|| string.Equals(e.PropertyName, nameof(PasteAIConfiguration.UseSharedCredentials), StringComparison.Ordinal))
|
||||
if (string.Equals(e.PropertyName, nameof(PasteAIConfiguration.ActiveProviderId), StringComparison.Ordinal))
|
||||
{
|
||||
SaveAndNotifySettings();
|
||||
}
|
||||
@@ -1426,15 +1415,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
pasteConfig.Providers ??= new ObservableCollection<PasteAIProviderDefinition>();
|
||||
|
||||
bool configurationUpdated = AdvancedPasteMigrationHelper.MigrateLegacyProviderConfigurations(pasteConfig);
|
||||
|
||||
SubscribeToPasteAIProviders(pasteConfig);
|
||||
|
||||
if (configurationUpdated)
|
||||
{
|
||||
SaveAndNotifySettings();
|
||||
OnPropertyChanged(nameof(PasteAIConfiguration));
|
||||
}
|
||||
}
|
||||
|
||||
private static string RetrieveCredentialValue(string credentialResource, string credentialUserName)
|
||||
|
||||
@@ -40,6 +40,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public ObservableCollection<DashboardListItem> ActionModules { get; set; } = new ObservableCollection<DashboardListItem>();
|
||||
|
||||
// Master list of module items that is sorted and projected into AllModules.
|
||||
private List<DashboardListItem> _moduleItems = new List<DashboardListItem>();
|
||||
|
||||
// Flag to prevent circular updates when a UI toggle triggers settings changes.
|
||||
private bool _isUpdatingFromUI;
|
||||
|
||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||
|
||||
public AllHotkeyConflictsData AllHotkeyConflictsData
|
||||
@@ -74,7 +80,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
generalSettingsConfig.DashboardSortOrder = value;
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(generalSettingsConfig);
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
RefreshModuleList();
|
||||
SortModuleList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,8 +102,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
RefreshModuleList();
|
||||
GetShortcutModules();
|
||||
BuildModuleList();
|
||||
SortModuleList();
|
||||
RefreshShortcutModules();
|
||||
}
|
||||
|
||||
protected override void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
||||
@@ -129,20 +136,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
|
||||
}
|
||||
|
||||
private void RefreshModuleList()
|
||||
/// <summary>
|
||||
/// Builds the master list of module items. Called once during initialization.
|
||||
/// Each module item contains its configuration, enabled state, and GPO lock status.
|
||||
/// </summary>
|
||||
private void BuildModuleList()
|
||||
{
|
||||
AllModules.Clear();
|
||||
|
||||
var moduleItems = new List<DashboardListItem>();
|
||||
_moduleItems.Clear();
|
||||
|
||||
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
|
||||
{
|
||||
// Hide CursorWrap from Dashboard
|
||||
if (moduleType == ModuleType.CursorWrap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(moduleType);
|
||||
var newItem = new DashboardListItem()
|
||||
{
|
||||
@@ -151,52 +154,147 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
IsEnabled = gpo == GpoRuleConfigured.Enabled || (gpo != GpoRuleConfigured.Disabled && ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, moduleType)),
|
||||
IsLocked = gpo == GpoRuleConfigured.Enabled || gpo == GpoRuleConfigured.Disabled,
|
||||
Icon = ModuleHelper.GetModuleTypeFluentIconName(moduleType),
|
||||
|
||||
// IsNew = moduleType == ModuleType.CursorWrap,
|
||||
IsNew = moduleType == ModuleType.CursorWrap,
|
||||
DashboardModuleItems = GetModuleItems(moduleType),
|
||||
};
|
||||
newItem.EnabledChangedCallback = EnabledChangedOnUI;
|
||||
moduleItems.Add(newItem);
|
||||
}
|
||||
|
||||
// Sort based on current sort order
|
||||
var sortedItems = DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
||||
_ => moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
||||
};
|
||||
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
AllModules.Add(item);
|
||||
_moduleItems.Add(newItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts the module list according to the current sort order and updates the AllModules collection.
|
||||
/// On first call, populates AllModules. On subsequent calls, uses Move() to reorder items in-place
|
||||
/// to avoid destroying and recreating UI elements.
|
||||
/// </summary>
|
||||
private void SortModuleList()
|
||||
{
|
||||
var sortedItems = (DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => _moduleItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label),
|
||||
_ => _moduleItems.OrderBy(x => x.Label), // Default alphabetical
|
||||
}).ToList();
|
||||
|
||||
// If AllModules is empty (first load), just populate it.
|
||||
if (AllModules.Count == 0)
|
||||
{
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
AllModules.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update the collection in place using Move to avoid UI glitches.
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
{
|
||||
var currentItem = sortedItems[i];
|
||||
var currentIndex = AllModules.IndexOf(currentItem);
|
||||
|
||||
if (currentIndex != -1 && currentIndex != i)
|
||||
{
|
||||
AllModules.Move(currentIndex, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify that DashboardSortOrder changed so the menu updates its checked state.
|
||||
OnPropertyChanged(nameof(DashboardSortOrder));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes module enabled/locked states by re-reading GPO configuration. Only
|
||||
/// updates properties that have actually changed to minimize UI notifications
|
||||
/// then re-sorts the list according to the current sort order.
|
||||
/// </summary>
|
||||
private void RefreshModuleList()
|
||||
{
|
||||
foreach (var item in _moduleItems)
|
||||
{
|
||||
GpoRuleConfigured gpo = ModuleHelper.GetModuleGpoConfiguration(item.Tag);
|
||||
|
||||
// GPO can force-enable (Enabled) or force-disable (Disabled) a module.
|
||||
// If Enabled: module is on and the user cannot disable it.
|
||||
// If Disabled: module is off and the user cannot enable it.
|
||||
// Otherwise, the setting is unlocked and the user can enable/disable it.
|
||||
bool newEnabledState = gpo == GpoRuleConfigured.Enabled || (gpo != GpoRuleConfigured.Disabled && ModuleHelper.GetIsModuleEnabled(generalSettingsConfig, item.Tag));
|
||||
|
||||
// Lock the toggle when GPO is controlling the module.
|
||||
bool newLockedState = gpo == GpoRuleConfigured.Enabled || gpo == GpoRuleConfigured.Disabled;
|
||||
|
||||
// Only update if there's an actual change to minimize UI notifications.
|
||||
if (item.IsEnabled != newEnabledState)
|
||||
{
|
||||
item.IsEnabled = newEnabledState;
|
||||
}
|
||||
|
||||
if (item.IsLocked != newLockedState)
|
||||
{
|
||||
item.IsLocked = newLockedState;
|
||||
}
|
||||
}
|
||||
|
||||
SortModuleList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked when a user toggles a module's enabled state in the UI.
|
||||
/// Sets the _isUpdatingFromUI flag to prevent circular updates, then updates
|
||||
/// settings, re-sorts if needed, and refreshes dependent collections.
|
||||
/// </summary>
|
||||
private void EnabledChangedOnUI(DashboardListItem dashboardListItem)
|
||||
{
|
||||
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
|
||||
|
||||
if (dashboardListItem.Tag == ModuleType.NewPlus && dashboardListItem.IsEnabled == true)
|
||||
_isUpdatingFromUI = true;
|
||||
try
|
||||
{
|
||||
var settingsUtils = new SettingsUtils();
|
||||
var settings = NewPlusViewModel.LoadSettings(settingsUtils);
|
||||
NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value);
|
||||
}
|
||||
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
|
||||
|
||||
// Request updated conflicts after module state change
|
||||
RequestConflictData();
|
||||
if (dashboardListItem.Tag == ModuleType.NewPlus && dashboardListItem.IsEnabled == true)
|
||||
{
|
||||
var settingsUtils = new SettingsUtils();
|
||||
var settings = NewPlusViewModel.LoadSettings(settingsUtils);
|
||||
NewPlusViewModel.CopyTemplateExamples(settings.Properties.TemplateLocation.Value);
|
||||
}
|
||||
|
||||
// Re-sort only required if sorting by enabled status.
|
||||
if (DashboardSortOrder == DashboardSortOrder.ByStatus)
|
||||
{
|
||||
SortModuleList();
|
||||
}
|
||||
|
||||
// Always refresh shortcuts/actions to reflect enabled state changes.
|
||||
RefreshShortcutModules();
|
||||
|
||||
// Request updated conflicts after module state change.
|
||||
RequestConflictData();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingFromUI = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Callback invoked when module enabled state changes from other parts of the
|
||||
/// settings UI. Ignores the notification if it was triggered by a UI toggle
|
||||
/// we're already handling, to prevent circular updates.
|
||||
/// </summary>
|
||||
public void ModuleEnabledChangedOnSettingsPage()
|
||||
{
|
||||
// Ignore if this was triggered by a UI change that we're already handling.
|
||||
if (_isUpdatingFromUI)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RefreshModuleList();
|
||||
GetShortcutModules();
|
||||
RefreshShortcutModules();
|
||||
|
||||
OnPropertyChanged(nameof(ShortcutModules));
|
||||
|
||||
// Request updated conflicts after module state change
|
||||
// Request updated conflicts after module state change.
|
||||
RequestConflictData();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -205,7 +303,11 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void GetShortcutModules()
|
||||
/// <summary>
|
||||
/// Rebuilds ShortcutModules and ActionModules collections by filtering AllModules
|
||||
/// to only include enabled modules and their respective shortcut/action items.
|
||||
/// </summary>
|
||||
private void RefreshShortcutModules()
|
||||
{
|
||||
ShortcutModules.Clear();
|
||||
ActionModules.Clear();
|
||||
|
||||
@@ -36,12 +36,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
|
||||
{
|
||||
// Hide CursorWrap from All Apps flyout
|
||||
if (moduleType == ModuleType.CursorWrap)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddFlyoutMenuItem(moduleType);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<AdditionalIncludeDirectories>../../../src/</AdditionalIncludeDirectories>
|
||||
<TreatAngleIncludeAsExternal>true</TreatAngleIncludeAsExternal>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
@@ -37,10 +38,6 @@
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\..\..\deps\cziplib\src\zip.c">
|
||||
<!-- Disabling warnings for external code -->
|
||||
<DisableSpecificWarnings>4706;26451;4267;4244;%(DisableSpecificWarnings)</DisableSpecificWarnings>
|
||||
</ClCompile>
|
||||
<ClCompile Include="EventViewer.cpp" />
|
||||
<ClCompile Include="InstallationFolder.cpp" />
|
||||
<ClCompile Include="Package.cpp" />
|
||||
@@ -61,8 +58,6 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\..\deps\cziplib\src\miniz.h" />
|
||||
<ClInclude Include="..\..\..\deps\cziplib\src\zip.h" />
|
||||
<ClInclude Include="EventViewer.h" />
|
||||
<ClInclude Include="InstallationFolder.h" />
|
||||
<ClInclude Include="Package.h" />
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<ClCompile Include="ZipTools\ZipFolder.cpp">
|
||||
<Filter>ZipTools</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="..\..\..\deps\cziplib\src\zip.c" />
|
||||
<ClCompile Include="ReportMonitorInfo.cpp" />
|
||||
<ClCompile Include="RegistryUtils.cpp" />
|
||||
<ClCompile Include="EventViewer.cpp" />
|
||||
@@ -28,8 +27,6 @@
|
||||
<Filter>ZipTools</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\..\..\common\utils\json.h" />
|
||||
<ClInclude Include="..\..\..\deps\cziplib\src\miniz.h" />
|
||||
<ClInclude Include="..\..\..\deps\cziplib\src\zip.h" />
|
||||
<ClInclude Include="ReportMonitorInfo.h" />
|
||||
<ClInclude Include="RegistryUtils.h" />
|
||||
<ClInclude Include="EventViewer.h" />
|
||||
|
||||
@@ -1,50 +1,53 @@
|
||||
#include "ZipFolder.h"
|
||||
#include "..\..\..\..\deps\cziplib\src\zip.h"
|
||||
#include <common/utils/timeutil.h>
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
|
||||
#include <format>
|
||||
#include <wil/stl.h>
|
||||
#include <wil/win32_helpers.h>
|
||||
|
||||
void ZipFolder(std::filesystem::path zipPath, std::filesystem::path folderPath)
|
||||
{
|
||||
std::string reportFilename{ "PowerToysReport_" };
|
||||
reportFilename += timeutil::format_as_local("%F-%H-%M-%S", timeutil::now());
|
||||
reportFilename += ".zip";
|
||||
const auto reportFilename{
|
||||
std::format("PowerToysReport_{0}.zip",
|
||||
timeutil::format_as_local("%F-%H-%M-%S", timeutil::now()))
|
||||
};
|
||||
const auto finalReportFullPath{ zipPath / reportFilename };
|
||||
|
||||
auto tmpZipPath = std::filesystem::temp_directory_path();
|
||||
tmpZipPath /= reportFilename;
|
||||
const auto tempReportFilename{ reportFilename + ".tmp" };
|
||||
const auto tempReportFullPath{ zipPath / tempReportFilename };
|
||||
|
||||
struct zip_t* zip = zip_open(tmpZipPath.string().c_str(), ZIP_DEFAULT_COMPRESSION_LEVEL, 'w');
|
||||
if (!zip)
|
||||
// tar -c --format=zip -f "ReportFile.zip" *
|
||||
const auto executable{ wil::ExpandEnvironmentStringsW<std::wstring>(LR"(%WINDIR%\System32\tar.exe)") };
|
||||
auto commandline{ std::format(LR"("{0}" -c --format=zip -f "{1}" *)", executable, tempReportFullPath.wstring()) };
|
||||
|
||||
const auto folderPathAsString{ folderPath.lexically_normal().wstring() };
|
||||
|
||||
wil::unique_process_information pi;
|
||||
STARTUPINFOW si{ .cb = sizeof(STARTUPINFOW) };
|
||||
if (!CreateProcessW(executable.c_str(),
|
||||
commandline.data() /* must be mutable */,
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
DETACHED_PROCESS,
|
||||
nullptr,
|
||||
folderPathAsString.c_str(),
|
||||
&si,
|
||||
&pi))
|
||||
{
|
||||
printf("Cannot open zip.");
|
||||
throw -1;
|
||||
}
|
||||
|
||||
using recursive_directory_iterator = std::filesystem::recursive_directory_iterator;
|
||||
const size_t rootSize = folderPath.wstring().size();
|
||||
for (const auto& dirEntry : recursive_directory_iterator(folderPath))
|
||||
{
|
||||
if (dirEntry.is_regular_file())
|
||||
{
|
||||
auto path = dirEntry.path().string();
|
||||
auto relativePath = path.substr(rootSize, path.size());
|
||||
zip_entry_open(zip, relativePath.c_str());
|
||||
zip_entry_fwrite(zip, path.c_str());
|
||||
zip_entry_close(zip);
|
||||
}
|
||||
}
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
|
||||
zip_close(zip);
|
||||
|
||||
std::error_code err;
|
||||
std::filesystem::copy(tmpZipPath, zipPath, err);
|
||||
std::error_code err{};
|
||||
std::filesystem::rename(tempReportFullPath, finalReportFullPath, err);
|
||||
if (err.value() != 0)
|
||||
{
|
||||
wprintf_s(L"Failed to copy %s. Error code: %d\n", tmpZipPath.c_str(), err.value());
|
||||
wprintf_s(L"Failed to rename %s. Error code: %d\n", tempReportFullPath.native().c_str(), err.value());
|
||||
}
|
||||
|
||||
err = {};
|
||||
std::filesystem::remove_all(tmpZipPath, err);
|
||||
if (err.value() != 0)
|
||||
{
|
||||
wprintf_s(L"Failed to delete %s. Error code: %d\n", tmpZipPath.c_str(), err.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
tools/module_loader/ModuleLoader.manifest
Normal file
37
tools/module_loader/ModuleLoader.manifest
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="Microsoft.PowerToys.ModuleLoader"
|
||||
type="win32"
|
||||
/>
|
||||
<description>PowerToys Module Loader - Standalone module testing utility</description>
|
||||
|
||||
<!-- Per-Monitor DPI Awareness V2 -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Request administrator execution level if needed -->
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<!-- Windows 10+ compatibility -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9b}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
205
tools/module_loader/ModuleLoader.vcxproj
Normal file
205
tools/module_loader/ModuleLoader.vcxproj
Normal file
@@ -0,0 +1,205 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}</ProjectGuid>
|
||||
<RootNamespace>ModuleLoader</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<ProjectName>ModuleLoader</ProjectName>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'" Label="Configuration">
|
||||
<ConfigurationType>Application</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" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>$(Platform)\$(Configuration)\</IntDir>
|
||||
<TargetName>ModuleLoader</TargetName>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|ARM64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|ARM64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)src;$(SolutionDir)src\modules\interface;$(SolutionDir)src;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
<PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
<TreatWarningAsError>false</TreatWarningAsError>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
<AdditionalDependencies>kernel32.lib;user32.lib;Shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
|
||||
<AdditionalManifestDependencies>type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'</AdditionalManifestDependencies>
|
||||
</Link>
|
||||
<Manifest>
|
||||
<AdditionalManifestFiles>$(ProjectDir)ModuleLoader.manifest</AdditionalManifestFiles>
|
||||
</Manifest>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\main.cpp" />
|
||||
<ClCompile Include="src\ModuleLoader.cpp" />
|
||||
<ClCompile Include="src\SettingsLoader.cpp" />
|
||||
<ClCompile Include="src\HotkeyManager.cpp" />
|
||||
<ClCompile Include="src\ConsoleHost.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\ModuleLoader.h" />
|
||||
<ClInclude Include="src\SettingsLoader.h" />
|
||||
<ClInclude Include="src\HotkeyManager.h" />
|
||||
<ClInclude Include="src\ConsoleHost.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
51
tools/module_loader/ModuleLoader.vcxproj.filters
Normal file
51
tools/module_loader/ModuleLoader.vcxproj.filters
Normal file
@@ -0,0 +1,51 @@
|
||||
<?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++;cppm;ixx;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>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="src\main.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\ModuleLoader.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\SettingsLoader.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\HotkeyManager.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\ConsoleHost.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="src\ModuleLoader.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\SettingsLoader.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\HotkeyManager.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\ConsoleHost.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="README.md" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
483
tools/module_loader/SHARING.md
Normal file
483
tools/module_loader/SHARING.md
Normal file
@@ -0,0 +1,483 @@
|
||||
# Sharing ModuleLoader and Modules
|
||||
|
||||
This guide explains how to share the ModuleLoader tool and PowerToy modules with others for testing purposes.
|
||||
|
||||
## Overview
|
||||
|
||||
The ModuleLoader is designed to be a **portable, standalone testing tool** that can be shared with module developers and testers. It has minimal dependencies and can work with any compatible PowerToy module DLL.
|
||||
|
||||
---
|
||||
|
||||
## What You Need to Share
|
||||
|
||||
### For Testing a Module (e.g., CursorWrap)
|
||||
|
||||
#### **Minimum Package** (Recommended for Quick Testing)
|
||||
|
||||
1. **ModuleLoader.exe** - The standalone loader application
|
||||
- Location: `x64\Debug\ModuleLoader.exe` or `x64\Release\ModuleLoader.exe`
|
||||
- No additional DLLs required (uses only Windows system libraries)
|
||||
|
||||
2. **The Module DLL** - The PowerToy module to test
|
||||
- Example: `CursorWrap.dll` from `x64\Debug\` or `x64\Release\`
|
||||
- Location varies by module (see module-specific locations below)
|
||||
|
||||
3. **settings.json** - Module configuration (place in same folder as the DLL)
|
||||
- **NEW**: Settings can be placed alongside the module DLL for portable testing
|
||||
- Location: Same directory as the module DLL (e.g., `settings.json` next to `CursorWrap.dll`)
|
||||
- Falls back to: `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json` if not found locally
|
||||
|
||||
#### **Complete Standalone Package** (For Users Without PowerToys Installed)
|
||||
|
||||
1. **ModuleLoader.exe**
|
||||
2. **Module DLL**
|
||||
3. **Sample settings.json** - Pre-configured settings file
|
||||
4. **Installation instructions** - See "Standalone Package Setup" section below
|
||||
|
||||
---
|
||||
|
||||
### Debug Builds
|
||||
If you build the module in Debug configuration:
|
||||
- The module will output debug messages via `OutputDebugString()`
|
||||
- View these with [DebugView](https://learn.microsoft.com/sysinternals/downloads/debugview) or Visual Studio Output window
|
||||
- Example: CursorWrap outputs detailed topology and cursor wrapping debug info
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Module-Specific File Locations
|
||||
|
||||
### CursorWrap
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\CursorWrap.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\settings.json
|
||||
|
||||
Size: ~100KB
|
||||
```
|
||||
|
||||
### MouseHighlighter
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\MouseHighlighter.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseHighlighter\settings.json
|
||||
|
||||
Size: ~150KB
|
||||
```
|
||||
|
||||
### FindMyMouse
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\FindMyMouse.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\FindMyMouse\settings.json
|
||||
|
||||
Size: ~120KB
|
||||
```
|
||||
|
||||
### MousePointerCrosshairs
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\MousePointerCrosshairs.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\MousePointerCrosshairs\settings.json
|
||||
|
||||
Size: ~140KB
|
||||
```
|
||||
|
||||
### MouseJump
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\MouseJump.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\MouseJump\settings.json
|
||||
|
||||
Note: MouseJump is a UI-based module and may not work fully with ModuleLoader
|
||||
Size: ~200KB
|
||||
```
|
||||
|
||||
### AlwaysOnTop
|
||||
```
|
||||
Files to share:
|
||||
- x64\Debug\AlwaysOnTop.dll (or Release)
|
||||
- %LOCALAPPDATA%\Microsoft\PowerToys\AlwaysOnTop\settings.json
|
||||
|
||||
Size: ~100KB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency Analysis
|
||||
|
||||
### ModuleLoader.exe Dependencies
|
||||
**Windows System Libraries Only** (automatically available on all Windows systems):
|
||||
- `KERNEL32.dll` - Core Windows API
|
||||
- `USER32.dll` - User interface functions
|
||||
- `SHELL32.dll` - Shell functions
|
||||
- `ole32.dll` - COM library
|
||||
|
||||
**No PowerToys dependencies required!** The ModuleLoader is completely standalone.
|
||||
|
||||
### Module DLL Dependencies (Typical)
|
||||
Most PowerToy modules depend on:
|
||||
- Windows system DLLs (automatically available)
|
||||
- PowerToys common libraries (if any, they're typically statically linked)
|
||||
- **Module settings** - Must be present in `%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\`
|
||||
|
||||
**Important**: Modules are generally **self-contained** and statically link most dependencies. You typically only need the module DLL itself.
|
||||
|
||||
---
|
||||
|
||||
## Creating a Standalone Package
|
||||
|
||||
### Step 1: Prepare the Files
|
||||
|
||||
Create a folder structure like this:
|
||||
```
|
||||
ModuleLoaderPackage\
|
||||
??? ModuleLoader.exe
|
||||
??? CursorWrap.dll (or other module)
|
||||
??? settings.json (module settings - placed locally!)
|
||||
```
|
||||
|
||||
**NEW Simplified Structure**: You can now place `settings.json` directly alongside the module DLL! The ModuleLoader will check this location first before looking in the standard PowerToys settings directories.
|
||||
|
||||
### Step 2: Extract Settings from Your Machine
|
||||
|
||||
```powershell
|
||||
# Copy settings from your development machine
|
||||
$moduleName = "CursorWrap" # Change as needed
|
||||
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json"
|
||||
Copy-Item $settingsPath ".\settings\$moduleName\settings.json"
|
||||
```
|
||||
|
||||
### Step 3: Create Installation Instructions (README.txt)
|
||||
|
||||
```text
|
||||
PowerToys Module Testing Package
|
||||
=================================
|
||||
|
||||
This package contains the ModuleLoader tool for testing PowerToy modules.
|
||||
|
||||
Contents:
|
||||
- ModuleLoader.exe : Standalone module loader
|
||||
- modules\*.dll : PowerToy module(s) to test
|
||||
- settings\*\*.json : Module configuration files
|
||||
|
||||
Setup (First Time):
|
||||
-------------------
|
||||
1. Create settings directory:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||
|
||||
2. Copy settings:
|
||||
Copy the entire "settings\<ModuleName>" folder to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||
|
||||
Example for CursorWrap:
|
||||
Copy "settings\CursorWrap" to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\CursorWrap\
|
||||
|
||||
Usage:
|
||||
------
|
||||
ModuleLoader.exe modules\CursorWrap.dll
|
||||
|
||||
The tool will:
|
||||
- Load the module DLL
|
||||
- Read settings from %LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\
|
||||
- Register hotkeys
|
||||
- Enable the module
|
||||
|
||||
Press Ctrl+C to exit.
|
||||
Press the module's hotkey to toggle functionality.
|
||||
|
||||
Requirements:
|
||||
-------------
|
||||
- Windows 10 1803 or later
|
||||
- No PowerToys installation required!
|
||||
|
||||
Troubleshooting:
|
||||
----------------
|
||||
If you see "Settings file not found":
|
||||
1. Make sure you copied the settings folder correctly
|
||||
2. Check that the path is:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\settings.json
|
||||
3. You can also run PowerToys once to generate default settings
|
||||
|
||||
Debug Logs:
|
||||
-----------
|
||||
Module logs are written to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\<ModuleName>\Logs\
|
||||
|
||||
For debug builds, use DebugView to see real-time output.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Distribution Methods
|
||||
|
||||
### Method 1: ZIP Archive
|
||||
```powershell
|
||||
# Create a complete package
|
||||
$moduleName = "CursorWrap"
|
||||
$packageName = "ModuleLoader-$moduleName-Package"
|
||||
|
||||
# Collect files
|
||||
New-Item $packageName -ItemType Directory
|
||||
Copy-Item "x64\Debug\ModuleLoader.exe" "$packageName\"
|
||||
New-Item "$packageName\modules" -ItemType Directory
|
||||
Copy-Item "x64\Debug\$moduleName.dll" "$packageName\modules\"
|
||||
New-Item "$packageName\settings\$moduleName" -ItemType Directory -Force
|
||||
Copy-Item "$env:LOCALAPPDATA\Microsoft\PowerToys\$moduleName\settings.json" "$packageName\settings\$moduleName\"
|
||||
|
||||
# Create README
|
||||
@"
|
||||
See README in the tools\module_loader folder for instructions
|
||||
"@ | Out-File "$packageName\README.txt"
|
||||
|
||||
# Zip it
|
||||
Compress-Archive -Path $packageName -DestinationPath "$packageName.zip"
|
||||
```
|
||||
|
||||
### Method 2: Direct Share (Advanced Users)
|
||||
For developers who already have PowerToys installed:
|
||||
```powershell
|
||||
# Just share the executables
|
||||
Copy-Item "x64\Debug\ModuleLoader.exe" "\\ShareLocation\"
|
||||
Copy-Item "x64\Debug\CursorWrap.dll" "\\ShareLocation\"
|
||||
```
|
||||
|
||||
They can run: `ModuleLoader.exe CursorWrap.dll`
|
||||
(Settings will be loaded from their existing PowerToys installation)
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Notes
|
||||
|
||||
### x64 vs ARM64
|
||||
|
||||
**Important**: Match architectures!
|
||||
- `x64\Debug\ModuleLoader.exe` ? Only works with `x64` module DLLs
|
||||
- `ARM64\Debug\ModuleLoader.exe` ? Only works with `ARM64` module DLLs
|
||||
|
||||
**Distribution Tip**: Provide both architectures if targeting multiple platforms:
|
||||
```
|
||||
ModuleLoaderPackage\
|
||||
??? x64\
|
||||
? ??? ModuleLoader.exe
|
||||
? ??? modules\CursorWrap.dll
|
||||
??? ARM64\
|
||||
? ??? ModuleLoader.exe
|
||||
? ??? modules\CursorWrap.dll
|
||||
??? settings\...
|
||||
```
|
||||
|
||||
### Debug vs Release
|
||||
|
||||
**Debug builds**:
|
||||
- Larger file size
|
||||
- Include debug symbols
|
||||
- Verbose logging via `OutputDebugString()`
|
||||
- Recommended for testing/development
|
||||
|
||||
**Release builds**:
|
||||
- Smaller file size
|
||||
- Optimized performance
|
||||
- Minimal logging
|
||||
- Recommended for end-user testing
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
Before sharing a module package:
|
||||
|
||||
- [ ] ModuleLoader.exe is included
|
||||
- [ ] Module DLL is included (matching architecture)
|
||||
- [ ] Sample settings.json is included
|
||||
- [ ] README/instructions are included
|
||||
- [ ] Tested on a clean machine (no PowerToys installed)
|
||||
- [ ] Verified hotkeys work
|
||||
- [ ] Verified Ctrl+C exits cleanly
|
||||
- [ ] Confirmed settings path in documentation
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Portable Package Script
|
||||
|
||||
Here's a complete PowerShell script to create a fully portable package:
|
||||
|
||||
```powershell
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$ModuleName,
|
||||
|
||||
[ValidateSet("Debug", "Release")]
|
||||
[string]$Configuration = "Debug",
|
||||
|
||||
[ValidateSet("x64", "ARM64")]
|
||||
[string]$Platform = "x64"
|
||||
)
|
||||
|
||||
$packageName = "ModuleLoader-$ModuleName-$Platform-$Configuration"
|
||||
$packagePath = ".\$packageName"
|
||||
|
||||
Write-Host "Creating portable package: $packageName" -ForegroundColor Green
|
||||
|
||||
# Create structure
|
||||
New-Item $packagePath -ItemType Directory -Force | Out-Null
|
||||
New-Item "$packagePath\modules" -ItemType Directory -Force | Out-Null
|
||||
New-Item "$packagePath\settings\$ModuleName" -ItemType Directory -Force | Out-Null
|
||||
|
||||
# Copy ModuleLoader
|
||||
$loaderPath = "$Platform\$Configuration\ModuleLoader.exe"
|
||||
if (Test-Path $loaderPath) {
|
||||
Copy-Item $loaderPath "$packagePath\"
|
||||
Write-Host "? Copied ModuleLoader.exe" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "? ModuleLoader.exe not found at $loaderPath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy Module DLL
|
||||
$modulePath = "$Platform\$Configuration\$ModuleName.dll"
|
||||
if (Test-Path $modulePath) {
|
||||
Copy-Item $modulePath "$packagePath\modules\"
|
||||
Write-Host "? Copied $ModuleName.dll" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "? $ModuleName.dll not found at $modulePath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy Settings
|
||||
$settingsPath = "$env:LOCALAPPDATA\Microsoft\PowerToys\$ModuleName\settings.json"
|
||||
if (Test-Path $settingsPath) {
|
||||
Copy-Item $settingsPath "$packagePath\settings\$ModuleName\"
|
||||
Write-Host "? Copied settings.json" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "? Settings not found at $settingsPath - creating placeholder" -ForegroundColor Yellow
|
||||
@"
|
||||
{
|
||||
"name": "$ModuleName",
|
||||
"version": "1.0"
|
||||
}
|
||||
"@ | Out-File "$packagePath\settings\$ModuleName\settings.json"
|
||||
}
|
||||
|
||||
# Create README
|
||||
@"
|
||||
PowerToys $ModuleName Testing Package
|
||||
======================================
|
||||
|
||||
Configuration: $Configuration
|
||||
Platform: $Platform
|
||||
|
||||
Setup Instructions:
|
||||
-------------------
|
||||
1. Copy the 'settings\$ModuleName' folder to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\
|
||||
|
||||
2. Run:
|
||||
ModuleLoader.exe modules\$ModuleName.dll
|
||||
|
||||
3. Press Ctrl+C to exit
|
||||
|
||||
Logs are written to:
|
||||
%LOCALAPPDATA%\Microsoft\PowerToys\$ModuleName\Logs\
|
||||
|
||||
For more information, see:
|
||||
https://github.com/microsoft/PowerToys/tree/main/tools/module_loader
|
||||
"@ | Out-File "$packagePath\README.txt"
|
||||
|
||||
# Create ZIP
|
||||
$zipPath = "$packageName.zip"
|
||||
Compress-Archive -Path $packagePath -DestinationPath $zipPath -Force
|
||||
Write-Host "? Created $zipPath" -ForegroundColor Green
|
||||
|
||||
# Show summary
|
||||
Write-Host "`nPackage Contents:" -ForegroundColor Cyan
|
||||
Get-ChildItem $packagePath -Recurse | ForEach-Object {
|
||||
Write-Host " $($_.FullName.Replace($packagePath, ''))"
|
||||
}
|
||||
|
||||
Write-Host "`nPackage ready: $zipPath" -ForegroundColor Green
|
||||
Write-Host "Size: $([math]::Round((Get-Item $zipPath).Length / 1KB, 2)) KB"
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
```powershell
|
||||
.\CreateModulePackage.ps1 -ModuleName "CursorWrap" -Configuration Release -Platform x64
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: Can I share just ModuleLoader.exe and the module DLL?
|
||||
**A**: Yes, but the recipient must have PowerToys installed (or manually create the settings file).
|
||||
|
||||
### Q: Does the tester need PowerToys installed?
|
||||
**A**: No, if you provide the complete package with settings. ModuleLoader is fully standalone.
|
||||
|
||||
### Q: What if settings.json doesn't exist?
|
||||
**A**: ModuleLoader will show an error. Either:
|
||||
1. Run PowerToys once with the module enabled to generate settings
|
||||
2. Manually create a minimal settings.json file
|
||||
3. Include a sample settings.json in your package
|
||||
|
||||
### Q: Can I test modules on a virtual machine?
|
||||
**A**: Yes! This is a great use case. Just copy the package to the VM - no PowerToys installation needed.
|
||||
|
||||
### Q: Do I need to include PDB files?
|
||||
**A**: Only for debugging. For normal testing, just the EXE and DLL are sufficient.
|
||||
|
||||
### Q: Can I distribute this to end users?
|
||||
**A**: ModuleLoader is a **development/testing tool**, not intended for end-user distribution. For production use, direct users to install PowerToys.
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When sharing module DLLs:
|
||||
|
||||
1. **Verify Source**: Only share modules you built from trusted source code
|
||||
2. **Scan for Malware**: Run antivirus scans on the package before sharing
|
||||
3. **HTTPS Only**: Use secure channels (HTTPS, OneDrive, SharePoint) for distribution
|
||||
4. **Hash Verification**: Consider providing SHA256 hashes for file integrity:
|
||||
```powershell
|
||||
Get-FileHash ModuleLoader.exe -Algorithm SHA256
|
||||
Get-FileHash modules\CursorWrap.dll -Algorithm SHA256
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example Package (CursorWrap)
|
||||
|
||||
Here's what a complete CursorWrap testing package looks like:
|
||||
|
||||
```
|
||||
ModuleLoader-CursorWrap-x64-Debug.zip (220 KB)
|
||||
?
|
||||
??? ModuleLoader-CursorWrap-x64-Debug\
|
||||
??? ModuleLoader.exe (160 KB)
|
||||
??? README.txt (2 KB)
|
||||
??? modules\
|
||||
? ??? CursorWrap.dll (55 KB)
|
||||
??? settings\
|
||||
??? CursorWrap\
|
||||
??? settings.json (3 KB)
|
||||
```
|
||||
|
||||
**Total package size**: ~220 KB (compressed)
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues with ModuleLoader, see:
|
||||
- [ModuleLoader README](./README.md)
|
||||
- [PowerToys Documentation](https://aka.ms/PowerToysOverview)
|
||||
- [PowerToys GitHub Issues](https://github.com/microsoft/PowerToys/issues)
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
ModuleLoader is part of PowerToys and is licensed under the MIT License.
|
||||
See the LICENSE file in the PowerToys repository root for details.
|
||||
80
tools/module_loader/src/ConsoleHost.cpp
Normal file
80
tools/module_loader/src/ConsoleHost.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#include "ConsoleHost.h"
|
||||
#include <iostream>
|
||||
|
||||
bool ConsoleHost::s_exitRequested = false;
|
||||
|
||||
ConsoleHost::ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager)
|
||||
: m_moduleLoader(moduleLoader)
|
||||
, m_hotkeyManager(hotkeyManager)
|
||||
{
|
||||
}
|
||||
|
||||
ConsoleHost::~ConsoleHost()
|
||||
{
|
||||
}
|
||||
|
||||
BOOL WINAPI ConsoleHost::ConsoleCtrlHandler(DWORD ctrlType)
|
||||
{
|
||||
switch (ctrlType)
|
||||
{
|
||||
case CTRL_C_EVENT:
|
||||
case CTRL_BREAK_EVENT:
|
||||
case CTRL_CLOSE_EVENT:
|
||||
std::wcout << L"\nCtrl+C received, shutting down...\n";
|
||||
s_exitRequested = true;
|
||||
|
||||
// Post a quit message to break the message loop
|
||||
PostQuitMessage(0);
|
||||
return TRUE;
|
||||
|
||||
default:
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
void ConsoleHost::Run()
|
||||
{
|
||||
// Install console control handler
|
||||
if (!SetConsoleCtrlHandler(ConsoleCtrlHandler, TRUE))
|
||||
{
|
||||
std::wcerr << L"Warning: Failed to set console control handler\n";
|
||||
}
|
||||
|
||||
s_exitRequested = false;
|
||||
|
||||
// Message loop
|
||||
MSG msg;
|
||||
while (!s_exitRequested)
|
||||
{
|
||||
// Wait for a message with a timeout so we can check s_exitRequested
|
||||
DWORD result = MsgWaitForMultipleObjects(0, nullptr, FALSE, 100, QS_ALLINPUT);
|
||||
|
||||
if (result == WAIT_OBJECT_0)
|
||||
{
|
||||
// Process all pending messages
|
||||
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
|
||||
{
|
||||
if (msg.message == WM_QUIT)
|
||||
{
|
||||
s_exitRequested = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (msg.message == WM_HOTKEY)
|
||||
{
|
||||
m_hotkeyManager.HandleHotkey(static_cast<int>(msg.wParam), m_moduleLoader);
|
||||
}
|
||||
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove console control handler
|
||||
SetConsoleCtrlHandler(ConsoleCtrlHandler, FALSE);
|
||||
}
|
||||
38
tools/module_loader/src/ConsoleHost.h
Normal file
38
tools/module_loader/src/ConsoleHost.h
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include "ModuleLoader.h"
|
||||
#include "HotkeyManager.h"
|
||||
|
||||
/// <summary>
|
||||
/// Console host that runs the message loop and handles Ctrl+C
|
||||
/// </summary>
|
||||
class ConsoleHost
|
||||
{
|
||||
public:
|
||||
ConsoleHost(ModuleLoader& moduleLoader, HotkeyManager& hotkeyManager);
|
||||
~ConsoleHost();
|
||||
|
||||
// Prevent copying
|
||||
ConsoleHost(const ConsoleHost&) = delete;
|
||||
ConsoleHost& operator=(const ConsoleHost&) = delete;
|
||||
|
||||
/// <summary>
|
||||
/// Run the message loop until Ctrl+C is pressed
|
||||
/// </summary>
|
||||
void Run();
|
||||
|
||||
private:
|
||||
ModuleLoader& m_moduleLoader;
|
||||
HotkeyManager& m_hotkeyManager;
|
||||
static bool s_exitRequested;
|
||||
|
||||
/// <summary>
|
||||
/// Console control handler (for Ctrl+C)
|
||||
/// </summary>
|
||||
static BOOL WINAPI ConsoleCtrlHandler(DWORD ctrlType);
|
||||
};
|
||||
279
tools/module_loader/src/HotkeyManager.cpp
Normal file
279
tools/module_loader/src/HotkeyManager.cpp
Normal file
@@ -0,0 +1,279 @@
|
||||
// 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.
|
||||
|
||||
#include "HotkeyManager.h"
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
HotkeyManager::HotkeyManager()
|
||||
: m_nextHotkeyId(1) // Start from 1
|
||||
, m_hotkeyExRegistered(false)
|
||||
, m_hotkeyExId(0)
|
||||
{
|
||||
}
|
||||
|
||||
HotkeyManager::~HotkeyManager()
|
||||
{
|
||||
UnregisterAll();
|
||||
}
|
||||
|
||||
UINT HotkeyManager::ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const
|
||||
{
|
||||
UINT modifiers = MOD_NOREPEAT; // Prevent repeat events
|
||||
if (win) modifiers |= MOD_WIN;
|
||||
if (ctrl) modifiers |= MOD_CONTROL;
|
||||
if (alt) modifiers |= MOD_ALT;
|
||||
if (shift) modifiers |= MOD_SHIFT;
|
||||
return modifiers;
|
||||
}
|
||||
|
||||
bool HotkeyManager::RegisterModuleHotkeys(ModuleLoader& moduleLoader)
|
||||
{
|
||||
if (!moduleLoader.IsLoaded())
|
||||
{
|
||||
std::wcerr << L"Error: Module not loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool anyRegistered = false;
|
||||
|
||||
// First, try the newer GetHotkeyEx() API
|
||||
auto hotkeyEx = moduleLoader.GetHotkeyEx();
|
||||
if (hotkeyEx.has_value())
|
||||
{
|
||||
std::wcout << L"Module has HotkeyEx activation hotkey\n";
|
||||
|
||||
UINT modifiers = hotkeyEx->modifiersMask | MOD_NOREPEAT;
|
||||
UINT vkCode = hotkeyEx->vkCode;
|
||||
|
||||
if (vkCode != 0)
|
||||
{
|
||||
int hotkeyId = m_nextHotkeyId++;
|
||||
|
||||
std::wcout << L" Registering HotkeyEx: ";
|
||||
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
|
||||
|
||||
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
|
||||
{
|
||||
m_hotkeyExRegistered = true;
|
||||
m_hotkeyExId = hotkeyId;
|
||||
|
||||
std::wcout << L" - OK (Activation/Toggle)\n";
|
||||
anyRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
std::wcout << L" - FAILED (Error: " << error << L")\n";
|
||||
|
||||
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
|
||||
{
|
||||
std::wcout << L" (Hotkey is already registered by another application)\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check the legacy get_hotkeys() API
|
||||
size_t hotkeyCount = moduleLoader.GetHotkeys(nullptr, 0);
|
||||
if (hotkeyCount > 0)
|
||||
{
|
||||
std::wcout << L"Module reports " << hotkeyCount << L" legacy hotkey(s)\n";
|
||||
|
||||
// Allocate buffer and get the hotkeys
|
||||
std::vector<PowertoyModuleIface::Hotkey> hotkeys(hotkeyCount);
|
||||
size_t actualCount = moduleLoader.GetHotkeys(hotkeys.data(), hotkeyCount);
|
||||
|
||||
// Register each hotkey
|
||||
for (size_t i = 0; i < actualCount; i++)
|
||||
{
|
||||
const auto& hotkey = hotkeys[i];
|
||||
|
||||
UINT modifiers = ConvertModifiers(hotkey.win, hotkey.ctrl, hotkey.alt, hotkey.shift);
|
||||
UINT vkCode = hotkey.key;
|
||||
|
||||
if (vkCode == 0)
|
||||
{
|
||||
std::wcout << L" Skipping hotkey " << i << L" (no key code)\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
int hotkeyId = m_nextHotkeyId++;
|
||||
|
||||
std::wcout << L" Registering hotkey " << i << L": ";
|
||||
std::wcout << ModifiersToString(modifiers) << L"+" << VKeyToString(vkCode);
|
||||
|
||||
if (RegisterHotKey(nullptr, hotkeyId, modifiers, vkCode))
|
||||
{
|
||||
HotkeyInfo info;
|
||||
info.id = hotkeyId;
|
||||
info.moduleHotkeyId = i;
|
||||
info.modifiers = modifiers;
|
||||
info.vkCode = vkCode;
|
||||
info.description = ModifiersToString(modifiers) + L"+" + VKeyToString(vkCode);
|
||||
|
||||
m_registeredHotkeys.push_back(info);
|
||||
std::wcout << L" - OK\n";
|
||||
anyRegistered = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
std::wcout << L" - FAILED (Error: " << error << L")\n";
|
||||
|
||||
if (error == ERROR_HOTKEY_ALREADY_REGISTERED)
|
||||
{
|
||||
std::wcout << L" (Hotkey is already registered by another application)\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyRegistered && hotkeyCount == 0 && !hotkeyEx.has_value())
|
||||
{
|
||||
std::wcout << L"Module has no hotkeys\n";
|
||||
}
|
||||
|
||||
return anyRegistered;
|
||||
}
|
||||
|
||||
void HotkeyManager::UnregisterAll()
|
||||
{
|
||||
for (const auto& hotkey : m_registeredHotkeys)
|
||||
{
|
||||
UnregisterHotKey(nullptr, hotkey.id);
|
||||
}
|
||||
m_registeredHotkeys.clear();
|
||||
|
||||
if (m_hotkeyExRegistered)
|
||||
{
|
||||
UnregisterHotKey(nullptr, m_hotkeyExId);
|
||||
m_hotkeyExRegistered = false;
|
||||
m_hotkeyExId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool HotkeyManager::HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader)
|
||||
{
|
||||
// Check if it's the HotkeyEx activation hotkey
|
||||
if (m_hotkeyExRegistered && hotkeyId == m_hotkeyExId)
|
||||
{
|
||||
std::wcout << L"\nActivation hotkey triggered (HotkeyEx)\n";
|
||||
|
||||
moduleLoader.OnHotkeyEx();
|
||||
|
||||
std::wcout << L"Module toggled via activation hotkey\n";
|
||||
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check legacy hotkeys
|
||||
for (const auto& hotkey : m_registeredHotkeys)
|
||||
{
|
||||
if (hotkey.id == hotkeyId)
|
||||
{
|
||||
std::wcout << L"\nHotkey triggered: " << hotkey.description << L"\n";
|
||||
|
||||
bool result = moduleLoader.OnHotkey(hotkey.moduleHotkeyId);
|
||||
|
||||
std::wcout << L"Module handled hotkey: " << (result ? L"Swallowed" : L"Not swallowed") << L"\n";
|
||||
std::wcout << L"Module enabled: " << (moduleLoader.IsEnabled() ? L"Yes" : L"No") << L"\n\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void HotkeyManager::PrintHotkeys() const
|
||||
{
|
||||
for (const auto& hotkey : m_registeredHotkeys)
|
||||
{
|
||||
std::wcout << L" " << hotkey.description << L"\n";
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring HotkeyManager::ModifiersToString(UINT modifiers) const
|
||||
{
|
||||
std::wstringstream ss;
|
||||
bool first = true;
|
||||
|
||||
if (modifiers & MOD_WIN)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Win";
|
||||
first = false;
|
||||
}
|
||||
if (modifiers & MOD_CONTROL)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Ctrl";
|
||||
first = false;
|
||||
}
|
||||
if (modifiers & MOD_ALT)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Alt";
|
||||
first = false;
|
||||
}
|
||||
if (modifiers & MOD_SHIFT)
|
||||
{
|
||||
if (!first) ss << L"+";
|
||||
ss << L"Shift";
|
||||
first = false;
|
||||
}
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::wstring HotkeyManager::VKeyToString(UINT vkCode) const
|
||||
{
|
||||
// Handle special keys
|
||||
switch (vkCode)
|
||||
{
|
||||
case VK_SPACE: return L"Space";
|
||||
case VK_RETURN: return L"Enter";
|
||||
case VK_ESCAPE: return L"Esc";
|
||||
case VK_TAB: return L"Tab";
|
||||
case VK_BACK: return L"Backspace";
|
||||
case VK_DELETE: return L"Del";
|
||||
case VK_INSERT: return L"Ins";
|
||||
case VK_HOME: return L"Home";
|
||||
case VK_END: return L"End";
|
||||
case VK_PRIOR: return L"PgUp";
|
||||
case VK_NEXT: return L"PgDn";
|
||||
case VK_LEFT: return L"Left";
|
||||
case VK_RIGHT: return L"Right";
|
||||
case VK_UP: return L"Up";
|
||||
case VK_DOWN: return L"Down";
|
||||
case VK_F1: return L"F1";
|
||||
case VK_F2: return L"F2";
|
||||
case VK_F3: return L"F3";
|
||||
case VK_F4: return L"F4";
|
||||
case VK_F5: return L"F5";
|
||||
case VK_F6: return L"F6";
|
||||
case VK_F7: return L"F7";
|
||||
case VK_F8: return L"F8";
|
||||
case VK_F9: return L"F9";
|
||||
case VK_F10: return L"F10";
|
||||
case VK_F11: return L"F11";
|
||||
case VK_F12: return L"F12";
|
||||
}
|
||||
|
||||
// For alphanumeric keys, use MapVirtualKey
|
||||
wchar_t keyName[256];
|
||||
UINT scanCode = MapVirtualKeyW(vkCode, MAPVK_VK_TO_VSC);
|
||||
|
||||
if (GetKeyNameTextW(scanCode << 16, keyName, 256) > 0)
|
||||
{
|
||||
return keyName;
|
||||
}
|
||||
|
||||
// Fallback to hex code
|
||||
std::wstringstream ss;
|
||||
ss << L"0x" << std::hex << vkCode;
|
||||
return ss.str();
|
||||
}
|
||||
86
tools/module_loader/src/HotkeyManager.h
Normal file
86
tools/module_loader/src/HotkeyManager.h
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include "ModuleLoader.h"
|
||||
|
||||
/// <summary>
|
||||
/// Manages hotkey registration using RegisterHotKey API
|
||||
/// </summary>
|
||||
class HotkeyManager
|
||||
{
|
||||
public:
|
||||
HotkeyManager();
|
||||
~HotkeyManager();
|
||||
|
||||
// Prevent copying
|
||||
HotkeyManager(const HotkeyManager&) = delete;
|
||||
HotkeyManager& operator=(const HotkeyManager&) = delete;
|
||||
|
||||
/// <summary>
|
||||
/// Register all hotkeys from a module
|
||||
/// </summary>
|
||||
/// <param name="moduleLoader">Module to get hotkeys from</param>
|
||||
/// <returns>True if at least one hotkey was registered</returns>
|
||||
bool RegisterModuleHotkeys(ModuleLoader& moduleLoader);
|
||||
|
||||
/// <summary>
|
||||
/// Unregister all hotkeys
|
||||
/// </summary>
|
||||
void UnregisterAll();
|
||||
|
||||
/// <summary>
|
||||
/// Handle a WM_HOTKEY message
|
||||
/// </summary>
|
||||
/// <param name="hotkeyId">ID from the WM_HOTKEY message</param>
|
||||
/// <param name="moduleLoader">Module to trigger the hotkey on</param>
|
||||
/// <returns>True if the hotkey was handled</returns>
|
||||
bool HandleHotkey(int hotkeyId, ModuleLoader& moduleLoader);
|
||||
|
||||
/// <summary>
|
||||
/// Get the number of registered hotkeys
|
||||
/// </summary>
|
||||
/// <returns>Number of registered hotkeys</returns>
|
||||
size_t GetRegisteredCount() const { return m_registeredHotkeys.size() + (m_hotkeyExRegistered ? 1 : 0); }
|
||||
|
||||
/// <summary>
|
||||
/// Print registered hotkeys to console
|
||||
/// </summary>
|
||||
void PrintHotkeys() const;
|
||||
|
||||
private:
|
||||
struct HotkeyInfo
|
||||
{
|
||||
int id = 0;
|
||||
size_t moduleHotkeyId = 0;
|
||||
UINT modifiers = 0;
|
||||
UINT vkCode = 0;
|
||||
std::wstring description;
|
||||
};
|
||||
|
||||
std::vector<HotkeyInfo> m_registeredHotkeys;
|
||||
int m_nextHotkeyId;
|
||||
bool m_hotkeyExRegistered;
|
||||
int m_hotkeyExId;
|
||||
|
||||
/// <summary>
|
||||
/// Convert modifier bools to RegisterHotKey modifiers
|
||||
/// </summary>
|
||||
UINT ConvertModifiers(bool win, bool ctrl, bool alt, bool shift) const;
|
||||
|
||||
/// <summary>
|
||||
/// Get a string representation of modifiers
|
||||
/// </summary>
|
||||
std::wstring ModifiersToString(UINT modifiers) const;
|
||||
|
||||
/// <summary>
|
||||
/// Get a string representation of a virtual key code
|
||||
/// </summary>
|
||||
std::wstring VKeyToString(UINT vkCode) const;
|
||||
};
|
||||
183
tools/module_loader/src/ModuleLoader.cpp
Normal file
183
tools/module_loader/src/ModuleLoader.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
// 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.
|
||||
|
||||
#include "ModuleLoader.h"
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
ModuleLoader::ModuleLoader()
|
||||
: m_hModule(nullptr)
|
||||
, m_module(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
ModuleLoader::~ModuleLoader()
|
||||
{
|
||||
if (m_module)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_module->destroy();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Ignore exceptions during cleanup
|
||||
}
|
||||
m_module = nullptr;
|
||||
}
|
||||
|
||||
if (m_hModule)
|
||||
{
|
||||
FreeLibrary(m_hModule);
|
||||
m_hModule = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool ModuleLoader::Load(const std::wstring& dllPath)
|
||||
{
|
||||
if (m_hModule || m_module)
|
||||
{
|
||||
std::wcerr << L"Error: Module already loaded\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
m_dllPath = dllPath;
|
||||
|
||||
// Load the DLL
|
||||
m_hModule = LoadLibraryW(dllPath.c_str());
|
||||
if (!m_hModule)
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
std::wcerr << L"Error: Failed to load DLL. Error code: " << error << L"\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the powertoy_create function
|
||||
using powertoy_create_func = PowertoyModuleIface* (*)();
|
||||
auto create_func = reinterpret_cast<powertoy_create_func>(
|
||||
GetProcAddress(m_hModule, "powertoy_create"));
|
||||
|
||||
if (!create_func)
|
||||
{
|
||||
std::wcerr << L"Error: DLL does not export 'powertoy_create' function\n";
|
||||
FreeLibrary(m_hModule);
|
||||
m_hModule = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the module instance
|
||||
m_module = create_func();
|
||||
if (!m_module)
|
||||
{
|
||||
std::wcerr << L"Error: powertoy_create() returned nullptr\n";
|
||||
FreeLibrary(m_hModule);
|
||||
m_hModule = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wcout << L"Module instance created successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
void ModuleLoader::Enable()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
throw std::runtime_error("Module not loaded");
|
||||
}
|
||||
|
||||
m_module->enable();
|
||||
}
|
||||
|
||||
void ModuleLoader::Disable()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_module->disable();
|
||||
}
|
||||
|
||||
bool ModuleLoader::IsEnabled() const
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_module->is_enabled();
|
||||
}
|
||||
|
||||
void ModuleLoader::SetConfig(const std::wstring& configJson)
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
throw std::runtime_error("Module not loaded");
|
||||
}
|
||||
|
||||
m_module->set_config(configJson.c_str());
|
||||
}
|
||||
|
||||
std::wstring ModuleLoader::GetModuleName() const
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return L"<not loaded>";
|
||||
}
|
||||
|
||||
const wchar_t* name = m_module->get_name();
|
||||
return name ? name : L"<unknown>";
|
||||
}
|
||||
|
||||
std::wstring ModuleLoader::GetModuleKey() const
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return L"<not loaded>";
|
||||
}
|
||||
|
||||
const wchar_t* key = m_module->get_key();
|
||||
return key ? key : L"<unknown>";
|
||||
}
|
||||
|
||||
size_t ModuleLoader::GetHotkeys(PowertoyModuleIface::Hotkey* buffer, size_t bufferSize)
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_module->get_hotkeys(buffer, bufferSize);
|
||||
}
|
||||
|
||||
bool ModuleLoader::OnHotkey(size_t hotkeyId)
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_module->on_hotkey(hotkeyId);
|
||||
}
|
||||
|
||||
std::optional<PowertoyModuleIface::HotkeyEx> ModuleLoader::GetHotkeyEx()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return m_module->GetHotkeyEx();
|
||||
}
|
||||
|
||||
void ModuleLoader::OnHotkeyEx()
|
||||
{
|
||||
if (!m_module)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_module->OnHotkeyEx();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user