fix conflict

This commit is contained in:
vanzue
2025-12-01 23:57:55 +08:00
85 changed files with 3309 additions and 568 deletions

View File

@@ -2,8 +2,8 @@ AAAAs
abcdefghjkmnpqrstuvxyz abcdefghjkmnpqrstuvxyz
abgr abgr
ABlocked ABlocked
ABOUTBOX
ABORTIFHUNG ABORTIFHUNG
ABOUTBOX
Abug Abug
Acceleratorkeys Acceleratorkeys
ACCEPTFILES ACCEPTFILES
@@ -56,6 +56,7 @@ ANull
AOC AOC
aocfnapldcnfbofgmbbllojgocaelgdd aocfnapldcnfbofgmbbllojgocaelgdd
AOklab AOklab
aot
APARTMENTTHREADED APARTMENTTHREADED
APeriod APeriod
apicontract apicontract
@@ -97,8 +98,8 @@ ASSOCSTR
ASYNCWINDOWPLACEMENT ASYNCWINDOWPLACEMENT
ASYNCWINDOWPOS ASYNCWINDOWPOS
atl atl
ATX
ATRIOX ATRIOX
ATX
aumid aumid
authenticode authenticode
AUTOBUDDY AUTOBUDDY
@@ -117,10 +118,10 @@ azman
azureaiinference azureaiinference
azureinference azureinference
azureopenai azureopenai
backticks
bbwe bbwe
BCIE BCIE
bck bck
backticks
BESTEFFORT BESTEFFORT
bezelled bezelled
bhid bhid
@@ -148,8 +149,8 @@ bmi
BNumber BNumber
BODGY BODGY
BOklab BOklab
Bootstrappers
BOOTSTRAPPERINSTALLFOLDER BOOTSTRAPPERINSTALLFOLDER
Bootstrappers
BOTTOMALIGN BOTTOMALIGN
boxmodel boxmodel
BPBF BPBF
@@ -176,17 +177,16 @@ BYPOSITION
CALCRECT CALCRECT
CALG CALG
callbackptr callbackptr
cabstr
calpwstr calpwstr
caub
Cangjie Cangjie
CANRENAME CANRENAME
Carlseibert
Canvascustomlayout Canvascustomlayout
CAPTUREBLT CAPTUREBLT
CAPTURECHANGED CAPTURECHANGED
CARETBLINKING CARETBLINKING
Carlseibert
CAtl CAtl
caub
CBN CBN
cch cch
CCHDEVICENAME CCHDEVICENAME
@@ -206,11 +206,9 @@ changecursor
CHILDACTIVATE CHILDACTIVATE
CHILDWINDOW CHILDWINDOW
CHOOSEFONT CHOOSEFONT
CIBUILD
cidl cidl
CIELCh CIELCh
cim cim
claude
CImage CImage
cla cla
CLASSDC CLASSDC
@@ -264,7 +262,6 @@ CONFIGW
CONFLICTINGMODIFIERKEY CONFLICTINGMODIFIERKEY
CONFLICTINGMODIFIERSHORTCUT CONFLICTINGMODIFIERSHORTCUT
CONOUT CONOUT
coreclr
constexpr constexpr
contentdialog contentdialog
contentfiles contentfiles
@@ -276,6 +273,7 @@ copiedcolorrepresentation
coppied coppied
copyable copyable
COPYPEN COPYPEN
coreclr
COREWINDOW COREWINDOW
Corpor Corpor
cotaskmem cotaskmem
@@ -284,18 +282,18 @@ countof
covrun covrun
cpcontrols cpcontrols
cph cph
cppcoreguidelines
cplusplus cplusplus
CPower CPower
cppcoreguidelines
cpptools cpptools
cppvsdbg cppvsdbg
cppwinrt cppwinrt
createdump createdump
creativecommons
CREATEPROCESS CREATEPROCESS
CREATESCHEDULEDTASK CREATESCHEDULEDTASK
CREATESTRUCT CREATESTRUCT
CREATEWINDOWFAILED CREATEWINDOWFAILED
creativecommons
CRECT CRECT
CRH CRH
critsec critsec
@@ -331,7 +329,6 @@ CYSCREEN
CYSMICON CYSMICON
CYVIRTUALSCREEN CYVIRTUALSCREEN
Czechia Czechia
cziplib
Dac Dac
dacl dacl
DAffine DAffine
@@ -355,9 +352,7 @@ Deact
debugbreak debugbreak
decryptor decryptor
Dedup Dedup
dfx
Deduplicator Deduplicator
Deeplink
DEFAULTBOOTSTRAPPERINSTALLFOLDER DEFAULTBOOTSTRAPPERINSTALLFOLDER
DEFAULTCOLOR DEFAULTCOLOR
DEFAULTFLAGS DEFAULTFLAGS
@@ -404,7 +399,6 @@ DISPLAYFREQUENCY
displayname displayname
DISPLAYORIENTATION DISPLAYORIENTATION
divyan divyan
djwsxzxb
Dlg Dlg
DLGFRAME DLGFRAME
DLGMODALFRAME DLGMODALFRAME
@@ -417,7 +411,6 @@ DONTVALIDATEPATH
dotnet dotnet
downsampled downsampled
downsampling downsampling
Downsampled
downscale downscale
DPICHANGED DPICHANGED
DPIs DPIs
@@ -531,7 +524,6 @@ EXTRINSICPROPERTIES
eyetracker eyetracker
FANCYZONESDRAWLAYOUTTEST FANCYZONESDRAWLAYOUTTEST
FANCYZONESEDITOR FANCYZONESEDITOR
FNumber
FARPROC FARPROC
fdx fdx
fesf fesf
@@ -563,8 +555,8 @@ FIXEDSYS
flac flac
flyouts flyouts
FMask FMask
foundrylocal
fmtid fmtid
FNumber
FOF FOF
FOFX FOFX
FOLDERID FOLDERID
@@ -575,6 +567,7 @@ FORCEMINIMIZE
FORMATDLGORD FORMATDLGORD
formatetc formatetc
FORPARSING FORPARSING
foundrylocal
FRAMECHANGED FRAMECHANGED
frm frm
FROMTOUCH FROMTOUCH
@@ -593,13 +586,13 @@ gdi
gdiplus gdiplus
GDIPVER GDIPVER
GDISCALED GDISCALED
geolocator
GETCLIENTAREAANIMATION GETCLIENTAREAANIMATION
GETCURSEL GETCURSEL
GETDESKWALLPAPER GETDESKWALLPAPER
GETDLGCODE GETDLGCODE
GETDPISCALEDSIZE GETDPISCALEDSIZE
getfilesiginforedist getfilesiginforedist
geolocator
GETHOTKEY GETHOTKEY
GETICON GETICON
GETLBTEXT GETLBTEXT
@@ -610,11 +603,12 @@ GETSCREENSAVERRUNNING
GETSECKEY GETSECKEY
GETSTICKYKEYS GETSTICKYKEYS
GETTEXTLENGTH GETTEXTLENGTH
GIFs
gitmodules
GHND GHND
gitmodules
GMEM GMEM
GNumber GNumber
googleai
googlegemini
gpedit gpedit
gpo gpo
GPOCA GPOCA
@@ -631,8 +625,6 @@ GValue
gwl gwl
GWLP GWLP
GWLSTYLE GWLSTYLE
googleai
googlegemini
hangeul hangeul
Hanzi Hanzi
Hardlines Hardlines
@@ -743,9 +735,7 @@ IDCANCEL
IDD IDD
idk idk
idl idl
IIM
idlist idlist
ifd
IDOK IDOK
IDOn IDOn
IDR IDR
@@ -754,15 +744,16 @@ ietf
IEXPLORE IEXPLORE
IFACEMETHOD IFACEMETHOD
IFACEMETHODIMP IFACEMETHODIMP
ifd
IGNOREUNKNOWN IGNOREUNKNOWN
IGo IGo
iid iid
IIM
Iindex Iindex
Ijwhost Ijwhost
ILD ILD
IMAGEHLP IMAGEHLP
IMAGERESIZERCONTEXTMENU IMAGERESIZERCONTEXTMENU
IPTC
IMAGERESIZEREXT IMAGERESIZEREXT
imageresizerinput imageresizerinput
imageresizersettings imageresizersettings
@@ -798,7 +789,6 @@ INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
INSTALLLOCATION INSTALLLOCATION
INSTALLMESSAGE INSTALLMESSAGE
INSTALLPROPERTY INSTALLPROPERTY
installscopeperuser
INSTALLSTARTMENUSHORTCUT INSTALLSTARTMENUSHORTCUT
INSTALLSTATE INSTALLSTATE
Inste Inste
@@ -811,6 +801,7 @@ invokecommand
ipcmanager ipcmanager
IPREVIEW IPREVIEW
ipreviewhandlervisualssetfont ipreviewhandlervisualssetfont
IPTC
irow irow
irprops irprops
isbi isbi
@@ -854,15 +845,14 @@ keyvault
KILLFOCUS KILLFOCUS
killrunner killrunner
kmph kmph
ksa
kvp kvp
Kybd Kybd
LARGEICON LARGEICON
lastcodeanalysissucceeded lastcodeanalysissucceeded
LASTEXITCODE LASTEXITCODE
LAYOUTRTL LAYOUTRTL
LCh
lbl lbl
LCh
lcid lcid
LCIDTo LCIDTo
lcl lcl
@@ -878,10 +868,10 @@ LExit
lhwnd lhwnd
LIBFUZZER LIBFUZZER
LIBID LIBID
lightswitch
LIMITSIZE LIMITSIZE
LIMITTEXT LIMITTEXT
lindex lindex
lightswitch
linkid linkid
LINKOVERLAY LINKOVERLAY
LINQTo LINQTo
@@ -892,6 +882,7 @@ LLKH
llkhf llkhf
LMEM LMEM
LMENU LMENU
lng
LOADFROMFILE LOADFROMFILE
LOBYTE LOBYTE
localappdata localappdata
@@ -901,17 +892,14 @@ LOCATIONCHANGE
LOCKTYPE LOCKTYPE
LOGFONT LOGFONT
LOGFONTW LOGFONTW
logon
lon
LOGMSG LOGMSG
logon
LOGPIXELSX LOGPIXELSX
LOGPIXELSY LOGPIXELSY
lng
lon lon
longdate longdate
LONGNAMES LONGNAMES
lowlevel lowlevel
lquadrant
LOWORD LOWORD
lparam lparam
LPBITMAPINFOHEADER LPBITMAPINFOHEADER
@@ -945,6 +933,7 @@ lpv
LPW LPW
lpwcx lpwcx
lpwndpl lpwndpl
lquadrant
LReader LReader
LRESULT LRESULT
LSTATUS LSTATUS
@@ -971,6 +960,7 @@ MAKELONG
MAKELPARAM MAKELPARAM
makepri makepri
MAKEWPARAM MAKEWPARAM
Malware
manifestdependency manifestdependency
MAPPEDTOSAMEKEY MAPPEDTOSAMEKEY
MAPTOSAMESHORTCUT MAPTOSAMESHORTCUT
@@ -993,8 +983,8 @@ MENUITEMINFO
MENUITEMINFOW MENUITEMINFOW
MERGECOPY MERGECOPY
MERGEPAINT MERGEPAINT
Metadatas
metadatamatters metadatamatters
Metadatas
metafile metafile
mfc mfc
Mgmt Mgmt
@@ -1040,9 +1030,6 @@ mousepointer
mouseutils mouseutils
MOVESIZEEND MOVESIZEEND
MOVESIZESTART MOVESIZESTART
muxx
muxxc
muxxh
MRM MRM
MRT MRT
mru mru
@@ -1071,10 +1058,14 @@ msrc
msstore msstore
msvcp msvcp
MT MT
mstsc
MTND MTND
MULTIPLEUSE MULTIPLEUSE
multizone multizone
muxc muxc
muxx
muxxc
muxxh
MVPs MVPs
mvvm mvvm
MVVMTK MVVMTK
@@ -1157,7 +1148,6 @@ nonstd
NOOWNERZORDER NOOWNERZORDER
NOPARENTNOTIFY NOPARENTNOTIFY
NOPREFIX NOPREFIX
NPU
NOREDIRECTIONBITMAP NOREDIRECTIONBITMAP
NOREDRAW NOREDRAW
NOREMOVE NOREMOVE
@@ -1186,6 +1176,7 @@ nowarn
NOZORDER NOZORDER
NPH NPH
npmjs npmjs
NPU
NResize NResize
NTAPI NTAPI
ntdll ntdll
@@ -1210,15 +1201,17 @@ oldpath
oldtheme oldtheme
oleaut oleaut
OLECHAR OLECHAR
ollama
onebranch onebranch
onnx
OOBEUI OOBEUI
openas openas
opencode opencode
OPENFILENAME OPENFILENAME
openrdp
opensource opensource
openxmlformats openxmlformats
ollama ollama
Olllama
onnx onnx
OPTIMIZEFORINVOKE OPTIMIZEFORINVOKE
ORPHANEDDIALOGTITLE ORPHANEDDIALOGTITLE
@@ -1292,6 +1285,7 @@ pguid
phbm phbm
phbmp phbmp
phicon phicon
Photoshop
phwnd phwnd
pici pici
pidl pidl
@@ -1314,7 +1308,6 @@ pnid
PNMLINK PNMLINK
Poc Poc
Podcasts Podcasts
Photoshop
POINTERID POINTERID
POINTERUPDATE POINTERUPDATE
Pokedex Pokedex
@@ -1409,10 +1402,9 @@ pwsz
pwtd pwtd
QDC QDC
qit qit
QNN
Qualcomm
QITAB QITAB
QITABENT QITABENT
QNN
qoi qoi
Quarternary Quarternary
QUERYENDSESSION QUERYENDSESSION
@@ -1422,8 +1414,8 @@ quickaccent
QUNS QUNS
RAII RAII
RAlt RAlt
RAquadrant
randi randi
RAquadrant
rasterization rasterization
Rasterize Rasterize
RAWINPUTDEVICE RAWINPUTDEVICE
@@ -1433,6 +1425,8 @@ RAWPATH
rbhid rbhid
rclsid rclsid
RCZOOMIT RCZOOMIT
remotedesktop
rdp
RDW RDW
READMODE READMODE
READOBJECTS READOBJECTS
@@ -1450,9 +1444,7 @@ regfile
REGISTERCLASSFAILED REGISTERCLASSFAILED
REGISTRYHEADER REGISTRYHEADER
REGISTRYPREVIEWEXT REGISTRYPREVIEWEXT
registryroot
regkey regkey
regroot
regsvr regsvr
REINSTALLMODE REINSTALLMODE
releaseblog releaseblog
@@ -1505,7 +1497,6 @@ rstringalpha
rstringdigit rstringdigit
rtb rtb
RTLREADING RTLREADING
rtm
runas runas
rundll rundll
rungameid rungameid
@@ -1562,8 +1553,8 @@ SETRULES
SETSCREENSAVEACTIVE SETSCREENSAVEACTIVE
SETSTICKYKEYS SETSTICKYKEYS
SETTEXT SETTEXT
settingscard
SETTINGCHANGE SETTINGCHANGE
settingscard
SETTINGSCHANGED SETTINGSCHANGED
settingsheader settingsheader
settingshotkeycontrol settingshotkeycontrol
@@ -1708,6 +1699,7 @@ stringtable
stringval stringval
Strm Strm
strret strret
STRSAFE
stscanf stscanf
sttngs sttngs
Stubless Stubless
@@ -1719,7 +1711,6 @@ sublang
SUBMODULEUPDATE SUBMODULEUPDATE
subresource subresource
Superbar Superbar
suntimes
sut sut
svchost svchost
SVGIn SVGIn
@@ -1753,7 +1744,6 @@ SYSTEMMODAL
SYSTEMTIME SYSTEMTIME
TARG TARG
TARGETAPPHEADER TARGETAPPHEADER
TARGETDIR
targetentrypoint targetentrypoint
TARGETHEADER TARGETHEADER
targetver targetver
@@ -1783,10 +1773,10 @@ textextractor
TEXTINCLUDE TEXTINCLUDE
tfopen tfopen
tgz tgz
THEMECHANGED
themeresources themeresources
THH THH
THICKFRAME THICKFRAME
THEMECHANGED
THISCOMPONENT THISCOMPONENT
throughs throughs
TILEDWINDOW TILEDWINDOW
@@ -1883,7 +1873,6 @@ USEINSTALLERFORTEST
USESHOWWINDOW USESHOWWINDOW
USESTDHANDLES USESTDHANDLES
USRDLL USRDLL
utm
UType UType
uuidv uuidv
uwp uwp
@@ -1956,11 +1945,11 @@ Wca
WCE WCE
wcex wcex
WClass WClass
WCRAPI
wcsicmp wcsicmp
wcsncpy wcsncpy
wcsnicmp wcsnicmp
WCT WCT
WCRAPI
WDA WDA
wdm wdm
wdp wdp
@@ -1988,6 +1977,7 @@ WINDOWPLACEMENT
WINDOWPOSCHANGED WINDOWPOSCHANGED
WINDOWPOSCHANGING WINDOWPOSCHANGING
WINDOWSBUILDNUMBER WINDOWSBUILDNUMBER
windowsml
windowssearch windowssearch
windowssettings windowssettings
WINDOWSTYLES WINDOWSTYLES
@@ -2003,9 +1993,8 @@ Winhook
WINL WINL
winlogon winlogon
winmd winmd
WINNT
windowsml
winml winml
WINNT
winres winres
winrt winrt
winsdk winsdk
@@ -2067,20 +2056,21 @@ WTSAT
Wubi Wubi
WUX WUX
Wwanpp Wwanpp
xap
XAxis XAxis
XButton XButton
xclip xclip
xcopy xcopy
xap
XDeployment XDeployment
XDimension
xdf xdf
XDimension
XDocument XDocument
XElement XElement
xfd xfd
XFile XFile
XIncrement XIncrement
XLoc XLoc
xmp
XNamespace XNamespace
Xoshiro Xoshiro
XPels XPels
@@ -2091,23 +2081,22 @@ xsi
XSpeed XSpeed
XStr XStr
xstyler xstyler
xmp
XTimer XTimer
XUP XUP
XVIRTUALSCREEN XVIRTUALSCREEN
xxxxxx xxxxxx
YAxis YAxis
ycombinator ycombinator
YIncrement
YDimension YDimension
YIncrement
yinle yinle
yinyue yinyue
YPels YPels
YPos YPos
YResolution YResolution
YSpeed YSpeed
YTimer
YStr YStr
YTimer
YVIRTUALSCREEN YVIRTUALSCREEN
ZEROINIT ZEROINIT
zonability zonability

View File

@@ -21,6 +21,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: 'Checkout Repository'
uses: actions/checkout@v4 uses: actions/checkout@v6
- name: 'Dependency Review' - name: 'Dependency Review'
uses: actions/dependency-review-action@v4 uses: actions/dependency-review-action@v4

View File

@@ -27,7 +27,7 @@ jobs:
issue: ${{ fromJson(github.event.inputs.issue_numbers) }} issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v6
- name: Run GenAI Issue Deduplicator - name: Run GenAI Issue Deduplicator
uses: pelikhan/action-genai-issue-dedup@v0 uses: pelikhan/action-genai-issue-dedup@v0

View File

@@ -1,4 +1,4 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled> <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
@@ -71,74 +71,74 @@
TODO: in Common.Dotnet.CsWinRT.props, on upgrade, verify RemoveCsWinRTPackageAnalyzer is no longer needed. TODO: in Common.Dotnet.CsWinRT.props, on upgrade, verify RemoveCsWinRTPackageAnalyzer is no longer needed.
This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail. This is present due to a bug in CsWinRT where WPF projects cause the analyzer to fail.
--> -->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" /> <PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.2.0" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.4948" /> <PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" /> <PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" /> <PackageVersion Include="Microsoft.WindowsAppSDK.AI" Version="1.8.37" />
<PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" /> <PackageVersion Include="Microsoft.WindowsAppSDK.Runtime" Version="1.8.250907003" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" /> <PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" /> <PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" /> <PackageVersion Include="ModernWpfUI" Version="0.9.4" />
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed --> <!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="MSTest" Version="3.8.3" /> <PackageVersion Include="MSTest" Version="3.8.3" />
<PackageVersion Include="NJsonSchema" Version="11.4.0" /> <PackageVersion Include="NJsonSchema" Version="11.4.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NLog" Version="5.2.8" /> <PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" /> <PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" /> <PackageVersion Include="NLog.Schema" Version="5.2.8" />
<PackageVersion Include="OpenAI" Version="2.5.0" /> <PackageVersion Include="OpenAI" Version="2.5.0" />
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" /> <PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" /> <PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" /> <PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
<PackageVersion Include="SharpCompress" Version="0.37.2" /> <PackageVersion Include="SharpCompress" Version="0.37.2" />
<!-- Don't update SkiaSharp.Views.WinUI to version 3.* branch as this brakes the HexBox control in Registry Preview. --> <!-- Don't update SkiaSharp.Views.WinUI to version 3.* branch as this brakes the HexBox control in Registry Preview. -->
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="2.88.9" /> <PackageVersion Include="SkiaSharp.Views.WinUI" Version="2.88.9" />
<PackageVersion Include="StreamJsonRpc" Version="2.21.69" /> <PackageVersion Include="StreamJsonRpc" Version="2.21.69" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. --> <!-- Package System.CodeDom added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Management but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.CodeDom" Version="9.0.10" /> <PackageVersion Include="System.CodeDom" Version="9.0.10" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> <PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" /> <PackageVersion Include="System.ComponentModel.Composition" Version="9.0.10" />
<PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" /> <PackageVersion Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
<PackageVersion Include="System.Data.OleDb" Version="9.0.10" /> <PackageVersion Include="System.Data.OleDb" Version="9.0.10" />
<!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. --> <!-- Package System.Data.SqlClient added to force it as a dependency of Microsoft.Windows.Compatibility to the latest version available at this time. -->
<PackageVersion Include="System.Data.SqlClient" Version="4.9.0" /> <PackageVersion Include="System.Data.SqlClient" Version="4.9.0" />
<!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. --> <!-- Package System.Diagnostics.EventLog added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Data.OleDb but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.10" /> <PackageVersion Include="System.Diagnostics.EventLog" Version="9.0.10" />
<!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. --> <!-- Package System.Diagnostics.PerformanceCounter added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.11. -->
<PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.10" /> <PackageVersion Include="System.Diagnostics.PerformanceCounter" Version="9.0.10" />
<PackageVersion Include="System.ClientModel" Version="1.7.0" /> <PackageVersion Include="System.ClientModel" Version="1.7.0" />
<PackageVersion Include="System.Drawing.Common" Version="9.0.10" /> <PackageVersion Include="System.Drawing.Common" Version="9.0.10" />
<PackageVersion Include="System.IO.Abstractions" Version="22.0.13" /> <PackageVersion Include="System.IO.Abstractions" Version="22.0.13" />
<PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" /> <PackageVersion Include="System.IO.Abstractions.TestingHelpers" Version="22.0.13" />
<PackageVersion Include="System.Management" Version="9.0.10" /> <PackageVersion Include="System.Management" Version="9.0.10" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" /> <PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" /> <PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reactive" Version="6.0.1" /> <PackageVersion Include="System.Reactive" Version="6.0.1" />
<PackageVersion Include="System.Runtime.Caching" Version="9.0.10" /> <PackageVersion Include="System.Runtime.Caching" Version="9.0.10" />
<PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.10" /> <PackageVersion Include="System.ServiceProcess.ServiceController" Version="9.0.10" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" /> <PackageVersion Include="System.Text.Json" Version="9.0.10" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" /> <PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" /> <PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="5.56.0" /> <PackageVersion Include="UnitsNet" Version="5.56.0" />
<PackageVersion Include="UTF.Unknown" Version="2.6.0" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="WinUIEx" Version="2.8.0" /> <PackageVersion Include="WinUIEx" Version="2.8.0" />
<PackageVersion Include="WPF-UI" Version="3.0.5" /> <PackageVersion Include="WPF-UI" Version="3.0.5" />
<PackageVersion Include="WyHash" Version="1.0.5" /> <PackageVersion Include="WyHash" Version="1.0.5" />
<PackageVersion Include="WixToolset.Heat" Version="5.0.2" /> <PackageVersion Include="WixToolset.Heat" Version="5.0.2" />
<PackageVersion Include="WixToolset.Firewall.wixext" Version="5.0.2" /> <PackageVersion Include="WixToolset.Firewall.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.Util.wixext" Version="5.0.2" /> <PackageVersion Include="WixToolset.Util.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.UI.wixext" Version="5.0.2" /> <PackageVersion Include="WixToolset.UI.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.NetFx.wixext" Version="5.0.2" /> <PackageVersion Include="WixToolset.NetFx.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.Bal.wixext" Version="5.0.2" /> <PackageVersion Include="WixToolset.Bal.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.BootstrapperApplicationApi" Version="5.0.2" /> <PackageVersion Include="WixToolset.BootstrapperApplicationApi" Version="5.0.2" />
<PackageVersion Include="WixToolset.WixStandardBootstrapperApplicationFunctionApi" Version="5.0.2" /> <PackageVersion Include="WixToolset.WixStandardBootstrapperApplicationFunctionApi" Version="5.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="'$(IsExperimentationLive)'!=''"> <ItemGroup Condition="'$(IsExperimentationLive)'!=''">
<!-- Additional dependencies used by experimentation --> <!-- Additional dependencies used by experimentation -->
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" /> <PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" /> <PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -832,6 +832,9 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Awake.ModuleServices", "src\modules\awake\Awake.ModuleServices\Awake.ModuleServices.csproj", "{2141FF78-5F51-ED6B-E11B-C7079CCA1456}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Awake.ModuleServices", "src\modules\awake\Awake.ModuleServices\Awake.ModuleServices.csproj", "{2141FF78-5F51-ED6B-E11B-C7079CCA1456}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPicker.ModuleServices", "src\modules\colorPicker\ColorPicker.ModuleServices\ColorPicker.ModuleServices.csproj", "{C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ColorPicker.ModuleServices", "src\modules\colorPicker\ColorPicker.ModuleServices\ColorPicker.ModuleServices.csproj", "{C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop", "src\modules\cmdpal\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj", "{2B3FB837-23DE-629F-82C6-42304E7083C9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests\Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests.csproj", "{DB34808A-FF91-D06E-A426-AFB5A8BD583B}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -5291,6 +5294,22 @@ Global
{C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}.Release|x64.Build.0 = Release|x64 {C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}.Release|x64.Build.0 = Release|x64
{C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}.Release|x86.ActiveCfg = Release|x64 {C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}.Release|x86.ActiveCfg = Release|x64
{C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}.Release|x86.Build.0 = Release|x64 {C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52}.Release|x86.Build.0 = Release|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.ActiveCfg = Debug|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|ARM64.Build.0 = Debug|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.ActiveCfg = Debug|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Debug|x64.Build.0 = Debug|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.ActiveCfg = Release|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|ARM64.Build.0 = Release|ARM64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.ActiveCfg = Release|x64
{2B3FB837-23DE-629F-82C6-42304E7083C9}.Release|x64.Build.0 = Release|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|ARM64.Build.0 = Debug|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.ActiveCfg = Debug|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Debug|x64.Build.0 = Debug|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.ActiveCfg = Release|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|ARM64.Build.0 = Release|ARM64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.ActiveCfg = Release|x64
{DB34808A-FF91-D06E-A426-AFB5A8BD583B}.Release|x64.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -5626,6 +5645,8 @@ Global
{094E65D5-585E-4898-B465-97A47CD44380} = {1AFB6476-670D-4E80-A464-657E01DFF482} {094E65D5-585E-4898-B465-97A47CD44380} = {1AFB6476-670D-4E80-A464-657E01DFF482}
{2141FF78-5F51-ED6B-E11B-C7079CCA1456} = {127F38E0-40AA-4594-B955-5616BF206882} {2141FF78-5F51-ED6B-E11B-C7079CCA1456} = {127F38E0-40AA-4594-B955-5616BF206882}
{C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0} {C9F3F4D1-A457-2A6E-DE4E-ED0DDB8DDA52} = {1D78B84B-CA39-406C-98F4-71F7EC266CC0}
{2B3FB837-23DE-629F-82C6-42304E7083C9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{DB34808A-FF91-D06E-A426-AFB5A8BD583B} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -53,17 +53,17 @@ Go to the [PowerToys GitHub releases][github-release-link], click Assets to reve
<!-- items that need to be updated release to release --> <!-- 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.97%22 [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 [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.0/PowerToysUserSetup-0.96.0-x64.exe [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.0/PowerToysUserSetup-0.96.0-arm64.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.0/PowerToysSetup-0.96.0-x64.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.0/PowerToysSetup-0.96.0-arm64.exe [ptMachineArm64]: https://github.com/microsoft/PowerToys/releases/download/v0.96.1/PowerToysSetup-0.96.1-arm64.exe
| Description | Filename | | Description | Filename |
|----------------|----------| |----------------|----------|
| Per user - x64 | [PowerToysUserSetup-0.96.0-x64.exe][ptUserX64] | | Per user - x64 | [PowerToysUserSetup-0.96.1-x64.exe][ptUserX64] |
| Per user - ARM64 | [PowerToysUserSetup-0.96.0-arm64.exe][ptUserArm64] | | Per user - ARM64 | [PowerToysUserSetup-0.96.1-arm64.exe][ptUserArm64] |
| Machine wide - x64 | [PowerToysSetup-0.96.0-x64.exe][ptMachineX64] | | Machine wide - x64 | [PowerToysSetup-0.96.1-x64.exe][ptMachineX64] |
| Machine wide - ARM64 | [PowerToysSetup-0.96.0-arm64.exe][ptMachineArm64] | | Machine wide - ARM64 | [PowerToysSetup-0.96.1-arm64.exe][ptMachineArm64] |
</details> </details>

View File

@@ -31,6 +31,11 @@ namespace ManagedCommon
/// </summary> /// </summary>
public static string CurrentVersionLogDirectoryPath { get; private set; } public static string CurrentVersionLogDirectoryPath { get; private set; }
/// <summary>
/// Gets the path to the current log file.
/// </summary>
public static string CurrentLogFile { get; private set; }
/// <summary> /// <summary>
/// Gets the path to the log directory for the app. /// Gets the path to the log directory for the app.
/// </summary> /// </summary>
@@ -55,7 +60,9 @@ namespace ManagedCommon
AppLogDirectoryPath = basePath; AppLogDirectoryPath = basePath;
CurrentVersionLogDirectoryPath = versionedPath; CurrentVersionLogDirectoryPath = versionedPath;
var logFilePath = Path.Combine(versionedPath, "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"); var logFile = "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
var logFilePath = Path.Combine(versionedPath, logFile);
CurrentLogFile = logFilePath;
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath)); Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));

View File

@@ -11,15 +11,15 @@ public class GalleryGridPropertiesViewModel : IGridPropertiesViewModel
{ {
private readonly ExtensionObject<IGalleryGridLayout> _model; private readonly ExtensionObject<IGalleryGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout) public GalleryGridPropertiesViewModel(IGalleryGridLayout galleryGridLayout)
{ {
_model = new(galleryGridLayout); _model = new(galleryGridLayout);
} }
public bool ShowTitle { get; set; }
public bool ShowSubtitle { get; set; }
public void InitializeProperties() public void InitializeProperties()
{ {
var model = _model.Unsafe; var model = _model.Unsafe;

View File

@@ -6,5 +6,9 @@ namespace Microsoft.CmdPal.Core.ViewModels;
public interface IGridPropertiesViewModel public interface IGridPropertiesViewModel
{ {
bool ShowTitle { get; }
bool ShowSubtitle { get; }
void InitializeProperties(); void InitializeProperties();
} }

View File

@@ -10,10 +10,9 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Core.ViewModels; namespace Microsoft.CmdPal.Core.ViewModels;
public partial class ListItemViewModel(IListItem model, WeakReference<IPageContext> context) public partial class ListItemViewModel : CommandItemViewModel
: CommandItemViewModel(new(model), context)
{ {
public new ExtensionObject<IListItem> Model { get; } = new(model); public new ExtensionObject<IListItem> Model { get; }
public List<TagViewModel>? Tags { get; set; } public List<TagViewModel>? Tags { get; set; }
@@ -32,6 +31,40 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
public string AccessibleName { get; private set; } = string.Empty; public string AccessibleName { get; private set; } = string.Empty;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle { get; private set; }
public bool LayoutShowsTitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsTitle();
}
}
}
public bool LayoutShowsSubtitle
{
get;
set
{
if (SetProperty(ref field, value))
{
UpdateShowsSubtitle();
}
}
}
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
: base(new(model), context)
{
Model = new ExtensionObject<IListItem>(model);
}
public override void InitializeProperties() public override void InitializeProperties()
{ {
if (IsInitialized) if (IsInitialized)
@@ -93,16 +126,18 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
switch (propertyName) switch (propertyName)
{ {
case nameof(Tags): case nameof(model.Tags):
UpdateTags(model.Tags); UpdateTags(model.Tags);
break; break;
case nameof(TextToSuggest): case nameof(model.TextToSuggest):
this.TextToSuggest = model.TextToSuggest ?? string.Empty; TextToSuggest = model.TextToSuggest ?? string.Empty;
UpdateProperty(nameof(TextToSuggest));
break; break;
case nameof(Section): case nameof(model.Section):
this.Section = model.Section ?? string.Empty; Section = model.Section ?? string.Empty;
UpdateProperty(nameof(Section));
break; break;
case nameof(Details): case nameof(model.Details):
var extensionDetails = model.Details; var extensionDetails = model.Details;
Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null; Details = extensionDetails is not null ? new(extensionDetails, PageContext) : null;
Details?.InitializeProperties(); Details?.InitializeProperties();
@@ -110,16 +145,24 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
UpdateProperty(nameof(HasDetails)); UpdateProperty(nameof(HasDetails));
UpdateShowDetailsCommand(); UpdateShowDetailsCommand();
break; break;
case nameof(MoreCommands): case nameof(model.MoreCommands):
UpdateProperty(nameof(MoreCommands));
AddShowDetailsCommands(); AddShowDetailsCommands();
break; break;
case nameof(Title): case nameof(model.Title):
case nameof(Subtitle): UpdateProperty(nameof(Title));
UpdateShowsTitle();
UpdateAccessibleName(); UpdateAccessibleName();
break; break;
case nameof(model.Subtitle):
UpdateProperty(nameof(Subtitle));
UpdateShowsSubtitle();
UpdateAccessibleName();
break;
default:
UpdateProperty(propertyName);
break;
} }
UpdateProperty(propertyName);
} }
// TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes? // TODO: Do we want filters to match descriptions and other properties? Tags, etc... Yes?
@@ -206,11 +249,32 @@ public partial class ListItemViewModel(IListItem model, WeakReference<IPageConte
// many COM exception issues. // many COM exception issues.
Tags = [.. newTags]; Tags = [.. newTags];
UpdateProperty(nameof(Tags)); // We're already in UI thread, so just raise the events
UpdateProperty(nameof(HasTags)); OnPropertyChanged(nameof(Tags));
OnPropertyChanged(nameof(HasTags));
}); });
} }
private void UpdateShowsTitle()
{
var oldShowTitle = ShowTitle;
ShowTitle = LayoutShowsTitle;
if (oldShowTitle != ShowTitle)
{
UpdateProperty(nameof(ShowTitle));
}
}
private void UpdateShowsSubtitle()
{
var oldShowSubtitle = ShowSubtitle;
ShowSubtitle = LayoutShowsSubtitle && !string.IsNullOrWhiteSpace(Subtitle);
if (oldShowSubtitle != ShowSubtitle)
{
UpdateProperty(nameof(ShowSubtitle));
}
}
protected override void UnsafeCleanup() protected override void UnsafeCleanup()
{ {
base.UnsafeCleanup(); base.UnsafeCleanup();

View File

@@ -24,8 +24,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change // Observable from MVVM Toolkit will auto create public properties that use INotifyPropertyChange change
// https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support // https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablegroupedcollections for grouping support
[ObservableProperty] public ObservableCollection<ListItemViewModel> FilteredItems { get; } = [];
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
public FiltersViewModel? Filters { get; set; } public FiltersViewModel? Filters { get; set; }
@@ -224,6 +223,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// TODO we can probably further optimize this by also keeping a // TODO we can probably further optimize this by also keeping a
// HashSet of every ExtensionObject we currently have, and only // HashSet of every ExtensionObject we currently have, and only
// building new viewmodels for the ones we haven't already built. // building new viewmodels for the ones we haven't already built.
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in newItems) foreach (var item in newItems)
{ {
// Check for cancellation during item processing // Check for cancellation during item processing
@@ -237,6 +238,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
// If an item fails to load, silently ignore it. // If an item fails to load, silently ignore it.
if (viewModel.SafeFastInit()) if (viewModel.SafeFastInit())
{ {
viewModel.LayoutShowsTitle = showsTitle;
viewModel.LayoutShowsSubtitle = showsSubtitle;
newViewModels.Add(viewModel); newViewModels.Add(viewModel);
} }
} }
@@ -583,6 +586,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties); GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties(); GridProperties?.InitializeProperties();
UpdateProperty(nameof(GridProperties)); UpdateProperty(nameof(GridProperties));
ApplyLayoutToItems();
ShowDetails = model.ShowDetails; ShowDetails = model.ShowDetails;
UpdateProperty(nameof(ShowDetails)); UpdateProperty(nameof(ShowDetails));
@@ -608,22 +612,15 @@ public partial class ListViewModel : PageViewModel, IDisposable
model.ItemsChanged += Model_ItemsChanged; model.ItemsChanged += Model_ItemsChanged;
} }
private IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties) private static IGridPropertiesViewModel? LoadGridPropertiesViewModel(IGridProperties? gridProperties)
{ {
if (gridProperties is IMediumGridLayout mediumGridLayout) return gridProperties switch
{ {
return new MediumGridPropertiesViewModel(mediumGridLayout); IMediumGridLayout mediumGridLayout => new MediumGridPropertiesViewModel(mediumGridLayout),
} IGalleryGridLayout galleryGridLayout => new GalleryGridPropertiesViewModel(galleryGridLayout),
else if (gridProperties is IGalleryGridLayout galleryGridLayout) ISmallGridLayout smallGridLayout => new SmallGridPropertiesViewModel(smallGridLayout),
{ _ => null,
return new GalleryGridPropertiesViewModel(galleryGridLayout); };
}
else if (gridProperties is ISmallGridLayout smallGridLayout)
{
return new SmallGridPropertiesViewModel(smallGridLayout);
}
return null;
} }
public void LoadMoreIfNeeded() public void LoadMoreIfNeeded()
@@ -685,6 +682,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
GridProperties = LoadGridPropertiesViewModel(model.GridProperties); GridProperties = LoadGridPropertiesViewModel(model.GridProperties);
GridProperties?.InitializeProperties(); GridProperties?.InitializeProperties();
UpdateProperty(nameof(IsGridView)); UpdateProperty(nameof(IsGridView));
ApplyLayoutToItems();
break; break;
case nameof(ShowDetails): case nameof(ShowDetails):
ShowDetails = model.ShowDetails; ShowDetails = model.ShowDetails;
@@ -730,6 +728,21 @@ public partial class ListViewModel : PageViewModel, IDisposable
}); });
} }
private void ApplyLayoutToItems()
{
lock (_listLock)
{
var showsTitle = GridProperties?.ShowTitle ?? true;
var showsSubtitle = GridProperties?.ShowSubtitle ?? true;
foreach (var item in Items)
{
item.LayoutShowsTitle = showsTitle;
item.LayoutShowsSubtitle = showsSubtitle;
}
}
}
public void Dispose() public void Dispose()
{ {
GC.SuppressFinalize(this); GC.SuppressFinalize(this);

View File

@@ -11,13 +11,15 @@ public class MediumGridPropertiesViewModel : IGridPropertiesViewModel
{ {
private readonly ExtensionObject<IMediumGridLayout> _model; private readonly ExtensionObject<IMediumGridLayout> _model;
public bool ShowTitle { get; private set; }
public bool ShowSubtitle => false;
public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout) public MediumGridPropertiesViewModel(IMediumGridLayout mediumGridLayout)
{ {
_model = new(mediumGridLayout); _model = new(mediumGridLayout);
} }
public bool ShowTitle { get; set; }
public void InitializeProperties() public void InitializeProperties()
{ {
var model = _model.Unsafe; var model = _model.Unsafe;

View File

@@ -11,6 +11,10 @@ public class SmallGridPropertiesViewModel : IGridPropertiesViewModel
{ {
private readonly ExtensionObject<ISmallGridLayout> _model; private readonly ExtensionObject<ISmallGridLayout> _model;
public bool ShowTitle => false;
public bool ShowSubtitle => false;
public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout) public SmallGridPropertiesViewModel(ISmallGridLayout smallGridLayout)
{ {
_model = new(smallGridLayout); _model = new(smallGridLayout);

View File

@@ -36,6 +36,7 @@ public partial class MainListPage : DynamicListPage,
"com.microsoft.cmdpal.builtin.websearch", "com.microsoft.cmdpal.builtin.websearch",
"com.microsoft.cmdpal.builtin.windowssettings", "com.microsoft.cmdpal.builtin.windowssettings",
"com.microsoft.cmdpal.builtin.datetime", "com.microsoft.cmdpal.builtin.datetime",
"com.microsoft.cmdpal.builtin.remotedesktop",
]; ];
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;

View File

@@ -6,7 +6,9 @@ using System.Diagnostics;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using ManagedCommon;
using Microsoft.CmdPal.UI.ViewModels.Settings; using Microsoft.CmdPal.UI.ViewModels.Settings;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation; using Windows.Foundation;
@@ -15,6 +17,8 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsModel : ObservableObject public partial class SettingsModel : ObservableObject
{ {
private const string DeprecatedHotkeyGoesHomeKey = "HotkeyGoesHome";
[JsonIgnore] [JsonIgnore]
public static readonly string FilePath; public static readonly string FilePath;
@@ -30,8 +34,6 @@ public partial class SettingsModel : ObservableObject
public bool ShowAppDetails { get; set; } public bool ShowAppDetails { get; set; }
public bool HotkeyGoesHome { get; set; }
public bool BackspaceGoesBack { get; set; } public bool BackspaceGoesBack { get; set; }
public bool SingleClickActivates { get; set; } public bool SingleClickActivates { get; set; }
@@ -56,6 +58,8 @@ public partial class SettingsModel : ObservableObject
public WindowPosition? LastWindowPosition { get; set; } public WindowPosition? LastWindowPosition { get; set; }
public TimeSpan AutoGoHomeInterval { get; set; } = Timeout.InfiniteTimeSpan;
// END SETTINGS // END SETTINGS
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@@ -98,12 +102,29 @@ public partial class SettingsModel : ObservableObject
{ {
// Read the JSON content from the file // Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath); 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) catch (Exception ex)
{ {
@@ -113,6 +134,51 @@ public partial class SettingsModel : ObservableObject
return new(); 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) public static void SaveSettings(SettingsModel model)
{ {
if (string.IsNullOrEmpty(FilePath)) if (string.IsNullOrEmpty(FilePath))
@@ -139,6 +205,9 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value?.DeepClone(); savedSettings[item.Key] = item.Value?.DeepClone();
} }
// Remove deprecated keys
savedSettings.Remove(DeprecatedHotkeyGoesHomeKey);
var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options); var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized); File.WriteAllText(FilePath, serialized);

View File

@@ -11,6 +11,19 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class SettingsViewModel : INotifyPropertyChanged 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 SettingsModel _settings;
private readonly IServiceProvider _serviceProvider; 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 public bool BackspaceGoesBack
{ {
get => _settings.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 ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = [];
public SettingsExtensionsViewModel Extensions { get; } public SettingsExtensionsViewModel Extensions { get; }

View File

@@ -13,6 +13,7 @@ using Microsoft.CmdPal.Ext.Calc;
using Microsoft.CmdPal.Ext.ClipboardHistory; using Microsoft.CmdPal.Ext.ClipboardHistory;
using Microsoft.CmdPal.Ext.Indexer; using Microsoft.CmdPal.Ext.Indexer;
using Microsoft.CmdPal.Ext.Registry; using Microsoft.CmdPal.Ext.Registry;
using Microsoft.CmdPal.Ext.RemoteDesktop;
using Microsoft.CmdPal.Ext.Shell; using Microsoft.CmdPal.Ext.Shell;
using Microsoft.CmdPal.Ext.System; using Microsoft.CmdPal.Ext.System;
using Microsoft.CmdPal.Ext.TimeDate; using Microsoft.CmdPal.Ext.TimeDate;
@@ -151,6 +152,7 @@ public partial class App : Application
services.AddSingleton<ICommandProvider, BuiltInsCommandProvider>(); services.AddSingleton<ICommandProvider, BuiltInsCommandProvider>();
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>(); services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>(); services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
// Models // Models
services.AddSingleton<TopLevelCommandManager>(); services.AddSingleton<TopLevelCommandManager>();

View File

@@ -0,0 +1,257 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.DevRibbon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
mc:Ignorable="d">
<UserControl.Resources>
<DataTemplate x:Key="LogEntryTemplate" x:DataType="viewModels:LogEntryViewModel">
<controls:SettingsExpander Description="{x:Bind Description}" Header="{x:Bind Header}">
<controls:SettingsExpander.HeaderIcon>
<FontIcon Glyph="{x:Bind SeverityGlyph}" />
</controls:SettingsExpander.HeaderIcon>
<controls:SettingsExpander.Items>
<controls:SettingsCard HorizontalContentAlignment="Stretch" ContentAlignment="Vertical">
<ScrollViewer
MaxWidth="1160"
HorizontalScrollMode="Auto"
VerticalScrollMode="Auto">
<TextBlock
FontFamily="Consolas"
FontSize="11"
Foreground="{ThemeResource SystemControlPageTextBaseMediumBrush}"
IsTextSelectionEnabled="True"
Text="{x:Bind Details}"
TextWrapping="NoWrap" />
</ScrollViewer>
</controls:SettingsCard>
</controls:SettingsExpander.Items>
</controls:SettingsExpander>
</DataTemplate>
<converters:BoolToVisibilityConverter
x:Key="InvertedBoolToVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
</UserControl.Resources>
<Grid>
<Border
x:Name="RootBorder"
Height="26"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="{ThemeResource SettingsCardBackground}"
BorderBrush="{ThemeResource SurfaceStrokeColorFlyoutBrush}"
BorderThickness="1,0,1,1"
CornerRadius="0,0,8,8"
Opacity="0.3">
<Button
Padding="0"
CornerRadius="0,0,8,8"
FontSize="11"
PointerEntered="DevRibbonButton_PointerEntered"
PointerExited="DevRibbonButton_PointerExited">
<StackPanel Orientation="Horizontal">
<StackPanel
Padding="8,4"
VerticalAlignment="Center"
Background="DarkOrange"
Orientation="Horizontal"
Visibility="{x:Bind VisibleIfGreaterThanZero(ViewModel.WarningCount), Mode=OneWay}">
<FontIcon
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xE7BA;" />
<TextBlock VerticalAlignment="Center">
<Run Text="{x:Bind ViewModel.WarningCount, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<StackPanel
Padding="8,4"
VerticalAlignment="Center"
Background="Maroon"
Orientation="Horizontal"
Visibility="{x:Bind VisibleIfGreaterThanZero(ViewModel.ErrorCount), Mode=OneWay}">
<FontIcon
Margin="0,0,8,0"
VerticalAlignment="Center"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xEA39;" />
<TextBlock VerticalAlignment="Center">
<Run Text="{x:Bind ViewModel.ErrorCount, Mode=OneWay}" />
</TextBlock>
</StackPanel>
<Border Padding="8,4">
<Border.Background>
<SolidColorBrush Color="{x:Bind ViewModel.TagColor}" />
</Border.Background>
<TextBlock Padding="4" VerticalAlignment="Center">
<Run Text="{x:Bind ViewModel.Tag}" />
</TextBlock>
</Border>
</StackPanel>
<Button.Flyout>
<Flyout
Placement="Bottom"
ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
<Flyout.FlyoutPresenterStyle>
<Style BasedOn="{StaticResource DefaultFlyoutPresenterStyle}" TargetType="FlyoutPresenter">
<Setter Property="MinWidth" Value="600" />
<Setter Property="MaxWidth" Value="1200" />
<Setter Property="Padding" Value="0" />
</Style>
</Flyout.FlyoutPresenterStyle>
<Grid x:Name="FlyoutContent">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Padding="16" Spacing="8">
<!-- Logs section -->
<TextBlock
Margin="1,0,0,6"
Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}"
Text="Logs" />
<ItemsControl ItemTemplate="{StaticResource LogEntryTemplate}" ItemsSource="{x:Bind ViewModel.LatestLogs, Mode=OneWay}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Command="{x:Bind ViewModel.OpenLogFileCommand}" Content="Open Log File" />
<Button Command="{x:Bind ViewModel.OpenLogFolderCommand}" Content="Open Log Folder" />
<Button Command="{x:Bind ViewModel.ResetErrorCountersCommand}" Content="Clear Counters" />
</StackPanel>
<!-- Build info section -->
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="Build Info" />
<Border
Padding="16"
Background="{ThemeResource SettingsCardBackground}"
BorderBrush="{ThemeResource SettingsCardBorderBrush}"
BorderThickness="1">
<Grid ColumnSpacing="8">
<Grid.Resources>
<Style
x:Key="KeyTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="Foreground" Value="{ThemeResource TextFillColorSecondaryBrush}" />
</Style>
<Style
x:Key="ValueTextBlockStyle"
BasedOn="{StaticResource CaptionTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="IsTextSelectionEnabled" Value="True" />
<Setter Property="TextAlignment" Value="Right" />
</Style>
</Grid.Resources>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Grid.Column="0"
Style="{StaticResource KeyTextBlockStyle}"
Text="Configuration:" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Style="{StaticResource ValueTextBlockStyle}"
Text="{x:Bind ViewModel.BuildConfiguration, Mode=OneWay}" />
<TextBlock
Grid.Row="1"
Grid.Column="0"
Style="{StaticResource KeyTextBlockStyle}"
Text="AOT:" />
<TextBlock
Grid.Row="1"
Grid.Column="1"
Style="{StaticResource ValueTextBlockStyle}"
Text="{x:Bind ViewModel.IsAot, Mode=OneWay}" />
<TextBlock
Grid.Row="2"
Grid.Column="0"
Style="{StaticResource KeyTextBlockStyle}"
Text="Trimmed:" />
<TextBlock
Grid.Row="2"
Grid.Column="1"
Style="{StaticResource ValueTextBlockStyle}"
Text="{x:Bind ViewModel.IsPublishTrimmed, Mode=OneWay}" />
</Grid>
</Border>
</StackPanel>
<!-- Footer -->
<Border
Grid.Row="1"
Padding="16"
Background="{ThemeResource SettingsCardBackground}"
BorderBrush="{ThemeResource SettingsCardBorderBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource ControlCornerRadius}"
Visibility="{x:Bind ViewModel.IsAotReleaseConfiguration, Mode=OneWay, Converter={StaticResource InvertedBoolToVisibilityConverter}}">
<TextBlock Text="Warning: Test in Release/AOT configuration to verify everything works." TextWrapping="Wrap" />
</Border>
</Grid>
</Flyout>
</Button.Flyout>
</Button>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="RootBorder"
Storyboard.TargetProperty="Opacity"
To="1.0"
Duration="0:0:0.1" />
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="SeverityStates">
<VisualState x:Name="NoLog" />
<VisualState x:Name="WarningLog">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SeverityIcon" Storyboard.TargetProperty="Glyph">
<DiscreteObjectKeyFrame KeyTime="0" Value="&#xE7BA;" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ErrorLog">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="SeverityIcon" Storyboard.TargetProperty="Glyph">
<DiscreteObjectKeyFrame KeyTime="0" Value="&#xEA39;" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>

View File

@@ -0,0 +1,40 @@
// 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.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Microsoft.CmdPal.UI.Controls;
internal sealed partial class DevRibbon : UserControl
{
public ViewModels.DevRibbonViewModel ViewModel { get; }
public DevRibbon()
{
InitializeComponent();
ViewModel = new ViewModels.DevRibbonViewModel();
if (FlyoutContent != null)
{
FlyoutContent.DataContext = ViewModel;
}
}
private void DevRibbonButton_PointerEntered(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "PointerOver", true);
}
private void DevRibbonButton_PointerExited(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(this, "Normal", true);
}
private Visibility VisibleIfGreaterThanZero(int value)
{
return value > 0 ? Visibility.Visible : Visibility.Collapsed;
}
}

View File

@@ -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 Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
internal sealed partial class GridItemContainerStyleSelector : StyleSelector
{
public IGridPropertiesViewModel? GridProperties { get; set; }
public Style? Small { get; set; }
public Style? Medium { get; set; }
public Style? Gallery { get; set; }
protected override Style? SelectStyleCore(object item, DependencyObject container)
{
return GridProperties switch
{
SmallGridPropertiesViewModel => Small,
MediumGridPropertiesViewModel => Medium,
GalleryGridPropertiesViewModel => Gallery,
_ => Medium,
};
}
}

View File

@@ -20,21 +20,12 @@ internal sealed partial class GridItemTemplateSelector : DataTemplateSelector
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject) protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{ {
DataTemplate? dataTemplate = Medium; return GridProperties switch
if (GridProperties is SmallGridPropertiesViewModel)
{ {
dataTemplate = Small; SmallGridPropertiesViewModel => Small,
} MediumGridPropertiesViewModel => Medium,
else if (GridProperties is MediumGridPropertiesViewModel) GalleryGridPropertiesViewModel => Gallery,
{ _ => Medium,
dataTemplate = Medium; };
}
else if (GridProperties is GalleryGridPropertiesViewModel)
{
dataTemplate = Gallery;
}
return dataTemplate;
} }
} }

View File

@@ -5,33 +5,151 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI" xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels" xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls" xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers" xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:local="using:Microsoft.CmdPal.UI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
x:Name="PageRoot" x:Name="PageRoot"
Background="Transparent" Background="Transparent"
DataContext="{x:Bind ViewModel, Mode=OneWay}" DataContext="{x:Bind ViewModel, Mode=OneWay}"
mc:Ignorable="d"> mc:Ignorable="d">
<Page.Resources> <Page.Resources>
<!-- TODO: Figure out what we want to do here for filtering/grouping and where -->
<!-- https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.data.collectionviewsource -->
<!--<CollectionViewSource
x:Name="ItemsCVS"
IsSourceGrouped="True"
Source="{x:Bind ViewModel.Items, Mode=OneWay}" />-->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" /> <!--
<converters:StringVisibilityConverter GridViewItemCornerRadius is the corner radius defined in GridView template; make
x:Key="StringVisibilityConverter" it bigger to match the radii of the gallery
EmptyValue="Collapsed" -->
NotEmptyValue="Visible" /> <CornerRadius x:Key="GalleryGridViewItemContainerCornerRadius">6</CornerRadius>
<CornerRadius x:Key="IconGridViewItemContainerCornerRadius">4</CornerRadius>
<CornerRadius x:Key="GalleryGridViewItemRadius">4</CornerRadius>
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource IconGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GridViewItem">
<ListViewItemPresenter
x:Name="Root"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
CheckBoxBorderBrush="{ThemeResource GridViewItemCheckBoxBorderBrush}"
CheckBoxBrush="{ThemeResource GridViewItemCheckBoxBrush}"
CheckBoxCornerRadius="{ThemeResource GridViewItemCheckBoxCornerRadius}"
CheckBoxDisabledBorderBrush="{ThemeResource GridViewItemCheckBoxDisabledBorderBrush}"
CheckBoxDisabledBrush="{ThemeResource GridViewItemCheckBoxDisabledBrush}"
CheckBoxPointerOverBorderBrush="{ThemeResource GridViewItemCheckBoxPointerOverBorderBrush}"
CheckBoxPointerOverBrush="{ThemeResource GridViewItemCheckBoxPointerOverBrush}"
CheckBoxPressedBorderBrush="{ThemeResource GridViewItemCheckBoxPressedBorderBrush}"
CheckBoxPressedBrush="{ThemeResource GridViewItemCheckBoxPressedBrush}"
CheckBoxSelectedBrush="{ThemeResource GridViewItemCheckBoxSelectedBrush}"
CheckBoxSelectedDisabledBrush="{ThemeResource GridViewItemCheckBoxSelectedDisabledBrush}"
CheckBoxSelectedPointerOverBrush="{ThemeResource GridViewItemCheckBoxSelectedPointerOverBrush}"
CheckBoxSelectedPressedBrush="{ThemeResource GridViewItemCheckBoxSelectedPressedBrush}"
CheckBrush="{ThemeResource GridViewItemCheckBrush}"
CheckDisabledBrush="{ThemeResource GridViewItemCheckDisabledBrush}"
CheckMode="{ThemeResource GridViewItemCheckMode}"
CheckPressedBrush="{ThemeResource GridViewItemCheckPressedBrush}"
ContentMargin="{TemplateBinding Padding}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Control.IsTemplateFocusTarget="True"
CornerRadius="{StaticResource GalleryGridViewItemContainerCornerRadius}"
DisabledOpacity="{ThemeResource ListViewItemDisabledThemeOpacity}"
DragBackground="{ThemeResource GridViewItemDragBackground}"
DragForeground="{ThemeResource GridViewItemDragForeground}"
DragOpacity="{ThemeResource ListViewItemDragThemeOpacity}"
FocusBorderBrush="{ThemeResource GridViewItemFocusBorderBrush}"
FocusVisualMargin="{TemplateBinding FocusVisualMargin}"
FocusVisualPrimaryBrush="{TemplateBinding FocusVisualPrimaryBrush}"
FocusVisualPrimaryThickness="{TemplateBinding FocusVisualPrimaryThickness}"
FocusVisualSecondaryBrush="{TemplateBinding FocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="{TemplateBinding FocusVisualSecondaryThickness}"
PlaceholderBackground="{ThemeResource GridViewItemPlaceholderBackground}"
PointerOverBackground="{ThemeResource GridViewItemBackgroundPointerOver}"
PointerOverBorderBrush="{ThemeResource GridViewItemPointerOverBorderBrush}"
PointerOverForeground="{ThemeResource GridViewItemForegroundPointerOver}"
PressedBackground="{ThemeResource GridViewItemBackgroundPressed}"
ReorderHintOffset="{ThemeResource GridViewItemReorderHintThemeOffset}"
SelectedBackground="{ThemeResource GridViewItemBackgroundSelected}"
SelectedBorderBrush="{ThemeResource GridViewItemSelectedBorderBrush}"
SelectedBorderThickness="{ThemeResource GridViewItemSelectedBorderThickness}"
SelectedDisabledBackground="{ThemeResource GridViewItemBackgroundSelectedDisabled}"
SelectedDisabledBorderBrush="{ThemeResource GridViewItemSelectedDisabledBorderBrush}"
SelectedForeground="{ThemeResource GridViewItemForegroundSelected}"
SelectedInnerBorderBrush="{ThemeResource GridViewItemSelectedInnerBorderBrush}"
SelectedPointerOverBackground="{ThemeResource GridViewItemBackgroundSelectedPointerOver}"
SelectedPointerOverBorderBrush="{ThemeResource GridViewItemSelectedPointerOverBorderBrush}"
SelectedPressedBackground="{ThemeResource GridViewItemBackgroundSelectedPressed}"
SelectedPressedBorderBrush="{ThemeResource GridViewItemSelectedPressedBorderBrush}"
SelectionCheckMarkVisualEnabled="{ThemeResource GridViewItemSelectionCheckMarkVisualEnabled}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel"> <DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
<cpcontrols:Tag <cpcontrols:Tag
@@ -48,10 +166,17 @@
x:Key="GridItemTemplateSelector" x:Key="GridItemTemplateSelector"
x:DataType="coreViewModels:ListItemViewModel" x:DataType="coreViewModels:ListItemViewModel"
Gallery="{StaticResource GalleryGridItemViewModelTemplate}" Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
GridProperties="{x:Bind ViewModel.GridProperties}" GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource MediumGridItemViewModelTemplate}" Medium="{StaticResource MediumGridItemViewModelTemplate}"
Small="{StaticResource SmallGridItemViewModelTemplate}" /> Small="{StaticResource SmallGridItemViewModelTemplate}" />
<cmdpalUI:GridItemContainerStyleSelector
x:Key="GridItemContainerStyleSelector"
Gallery="{StaticResource GalleryGridViewItemStyle}"
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
Medium="{StaticResource IconGridViewItemStyle}"
Small="{StaticResource IconGridViewItemStyle}" />
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items --> <!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
<DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel"> <DataTemplate x:Key="ListItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<Grid <Grid
@@ -94,7 +219,7 @@
Text="{x:Bind Subtitle, Mode=OneWay}" Text="{x:Bind Subtitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Mode=OneWay, Converter={StaticResource StringVisibilityConverter}}" /> Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel> </StackPanel>
<ItemsControl <ItemsControl
@@ -124,11 +249,11 @@
Padding="8,16" Padding="8,16"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0" BorderThickness="0"
CornerRadius="8" CornerRadius="{StaticResource SmallGridViewItemCornerRadius}"
Orientation="Vertical" Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}"> ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<cpcontrols:IconBox <cpcontrols:IconBox
x:Name="GridIconBorder" x:Name="GridIconBorder"
@@ -145,23 +270,22 @@
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel"> <DataTemplate x:Key="MediumGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
<StackPanel <Grid
Width="100" Width="100"
Height="100" Height="100"
Padding="8,16" Padding="8"
HorizontalAlignment="Center" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
VerticalAlignment="Center" CornerRadius="{StaticResource MediumGridViewItemCornerRadius}"
AutomationProperties.Name="{x:Bind Title}" ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
BorderThickness="0" <Grid.RowDefinitions>
CornerRadius="8" <RowDefinition Height="*" />
Orientation="Vertical" <RowDefinition Height="Auto" />
ToolTipService.ToolTip="{x:Bind Title}"> </Grid.RowDefinitions>
<cpcontrols:IconBox <cpcontrols:IconBox
x:Name="GridIconBorder" x:Name="GridIconBorder"
Grid.Row="0"
Width="36" Width="36"
Height="36" Height="36"
Margin="0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
CharacterSpacing="12" CharacterSpacing="12"
@@ -169,21 +293,20 @@
Foreground="{ThemeResource TextFillColorPrimary}" Foreground="{ThemeResource TextFillColorPrimary}"
SourceKey="{x:Bind Icon, Mode=OneWay}" SourceKey="{x:Bind Icon, Mode=OneWay}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" /> SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock <TextBlock
x:Name="TitleTextBlock" x:Name="TitleTextBlock"
MaxHeight="40" Grid.Row="1"
Margin="0,8,0,4" Height="32"
Margin="0,8,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center"
CharacterSpacing="12" CharacterSpacing="12"
FontSize="12" FontSize="12"
Text="{x:Bind Title}" Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center" TextAlignment="Center"
TextTrimming="WordEllipsis" TextTrimming="WordEllipsis"
TextWrapping="Wrap" TextWrapping="Wrap"
Visibility="{Binding ElementName=PageRoot, Path=DataContext.GridProperties.ShowTitle, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" /> Visibility="{x:Bind LayoutShowsTitle, Mode=OneWay}" />
</StackPanel> </Grid>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel"> <DataTemplate x:Key="GalleryGridItemViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
@@ -193,11 +316,11 @@
Padding="0" Padding="0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
AutomationProperties.Name="{x:Bind Title}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}"
BorderThickness="0" BorderThickness="0"
CornerRadius="4" CornerRadius="{StaticResource GalleryGridViewItemRadius}"
Orientation="Vertical" Orientation="Vertical"
ToolTipService.ToolTip="{x:Bind Title}"> ToolTipService.ToolTip="{x:Bind Title, Mode=OneWay}">
<Grid <Grid
Width="160" Width="160"
@@ -205,12 +328,8 @@
Margin="0" Margin="0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
CornerRadius="4"> CornerRadius="{StaticResource GalleryGridViewItemRadius}">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Viewbox <Viewbox
Grid.Row="1"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Stretch="UniformToFill" Stretch="UniformToFill"
StretchDirection="Both"> StretchDirection="Both">
@@ -222,35 +341,39 @@
</Viewbox> </Viewbox>
</Grid> </Grid>
<StackPanel Padding="4" Orientation="Vertical"> <StackPanel
Padding="4"
Orientation="Vertical"
Spacing="4"
Visibility="{x:Bind help:BindTransformers.VisibleWhenAny(ShowTitle, ShowSubtitle)}">
<TextBlock <TextBlock
x:Name="TitleTextBlock" x:Name="TitleTextBlock"
MaxWidth="152" MaxWidth="152"
MaxHeight="40" MaxHeight="40"
Margin="0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
CharacterSpacing="12" CharacterSpacing="12"
FontSize="14" FontSize="14"
Foreground="{ThemeResource TextFillColorPrimary}" Foreground="{ThemeResource TextFillColorPrimary}"
Text="{x:Bind Title}" Text="{x:Bind Title, Mode=OneWay}"
TextAlignment="Center" TextAlignment="Center"
TextTrimming="WordEllipsis" TextTrimming="WordEllipsis"
TextWrapping="NoWrap" /> TextWrapping="NoWrap"
Visibility="{x:Bind ShowTitle, Mode=OneWay}" />
<TextBlock <TextBlock
x:Name="SubTitleTextBlock" x:Name="SubTitleTextBlock"
MaxWidth="152" MaxWidth="152"
MaxHeight="40" MaxHeight="40"
Margin="0,4,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
CharacterSpacing="11" CharacterSpacing="11"
FontSize="11" FontSize="11"
Foreground="{ThemeResource TextFillColorTertiary}" Foreground="{ThemeResource TextFillColorTertiary}"
Text="{x:Bind Subtitle}" Text="{x:Bind Subtitle, Mode=OneWay}"
TextAlignment="Center" TextAlignment="Center"
TextTrimming="WordEllipsis" TextTrimming="WordEllipsis"
TextWrapping="NoWrap" /> TextWrapping="NoWrap"
Visibility="{x:Bind ShowSubtitle, Mode=OneWay}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
@@ -295,6 +418,7 @@
IsDoubleTapEnabled="True" IsDoubleTapEnabled="True"
IsItemClickEnabled="True" IsItemClickEnabled="True"
ItemClick="Items_ItemClick" ItemClick="Items_ItemClick"
ItemContainerStyleSelector="{StaticResource GridItemContainerStyleSelector}"
ItemTemplateSelector="{StaticResource GridItemTemplateSelector}" ItemTemplateSelector="{StaticResource GridItemTemplateSelector}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}" ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
RightTapped="Items_RightTapped" RightTapped="Items_RightTapped"
@@ -302,6 +426,7 @@
<GridView.ItemContainerTransitions> <GridView.ItemContainerTransitions>
<TransitionCollection /> <TransitionCollection />
</GridView.ItemContainerTransitions> </GridView.ItemContainerTransitions>
<GridView.ItemContainerStyle />
</GridView> </GridView>
</controls:Case> </controls:Case>
</controls:SwitchPresenter> </controls:SwitchPresenter>

View File

@@ -15,4 +15,7 @@ internal static class BindTransformers
public static Visibility EmptyOrWhitespaceToCollapsed(string? input) public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible; => string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
public static Visibility VisibleWhenAny(bool value1, bool value2)
=> (value1 || value2) ? Visibility.Visible : Visibility.Collapsed;
} }

View File

@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Microsoft.CmdPal.UI.Helpers;
internal static class BuildInfo
{
#if DEBUG
public const string Configuration = "Debug";
#else
public const string Configuration = "Release";
#endif
// Runtime AOT detection
public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported;
// From assembly metadata (build-time values)
public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false);
// From assembly metadata (build-time values)
public static bool PublishAot => GetBoolMetadata("PublishAot", false);
public static bool IsCiBuild => GetBoolMetadata("CIBuild", false);
private static string? GetMetadata(string key) =>
Assembly.GetExecutingAssembly()
.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(a => a.Key == key)?.Value;
private static bool GetBoolMetadata(string key, bool defaultValue) =>
bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue;
}

View File

@@ -14,5 +14,7 @@
Activated="MainWindow_Activated" Activated="MainWindow_Activated"
Closed="MainWindow_Closed" Closed="MainWindow_Closed"
mc:Ignorable="d"> mc:Ignorable="d">
<pages:ShellPage x:Name="RootShellPage" /> <Grid x:Name="RootElement">
<pages:ShellPage />
</Grid>
</winuiex:WindowEx> </winuiex:WindowEx>

View File

@@ -11,6 +11,7 @@ using Microsoft.CmdPal.Core.Common.Helpers;
using Microsoft.CmdPal.Core.Common.Services; using Microsoft.CmdPal.Core.Common.Services;
using Microsoft.CmdPal.Core.ViewModels.Messages; using Microsoft.CmdPal.Core.ViewModels.Messages;
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages; using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
using Microsoft.CmdPal.UI.Controls;
using Microsoft.CmdPal.UI.Events; using Microsoft.CmdPal.UI.Events;
using Microsoft.CmdPal.UI.Helpers; using Microsoft.CmdPal.UI.Helpers;
using Microsoft.CmdPal.UI.Messages; using Microsoft.CmdPal.UI.Messages;
@@ -58,6 +59,7 @@ public sealed partial class MainWindow : WindowEx,
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1306:Field names should begin with lower-case letter", 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 uint WM_TASKBAR_RESTART;
private readonly HWND _hwnd; private readonly HWND _hwnd;
private readonly DispatcherTimer _autoGoHomeTimer;
private readonly WNDPROC? _hotkeyWndProc; private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc; private readonly WNDPROC? _originalWndProc;
private readonly List<TopLevelHotkey> _hotkeys = []; private readonly List<TopLevelHotkey> _hotkeys = [];
@@ -68,6 +70,7 @@ public sealed partial class MainWindow : WindowEx,
private DesktopAcrylicController? _acrylicController; private DesktopAcrylicController? _acrylicController;
private SystemBackdropConfiguration? _configurationSource; private SystemBackdropConfiguration? _configurationSource;
private TimeSpan _autoGoHomeInterval = Timeout.InfiniteTimeSpan;
private WindowPosition _currentWindowPosition = new(); private WindowPosition _currentWindowPosition = new();
@@ -75,6 +78,9 @@ public sealed partial class MainWindow : WindowEx,
{ {
InitializeComponent(); InitializeComponent();
_autoGoHomeTimer = new DispatcherTimer();
_autoGoHomeTimer.Tick += OnAutoGoHomeTimerOnTick;
_hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32()); _hwnd = new HWND(WinRT.Interop.WindowNative.GetWindowHandle(this).ToInt32());
unsafe unsafe
@@ -108,7 +114,7 @@ public sealed partial class MainWindow : WindowEx,
ExtendsContentIntoTitleBar = true; ExtendsContentIntoTitleBar = true;
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed; AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
SizeChanged += WindowSizeChanged; SizeChanged += WindowSizeChanged;
RootShellPage.Loaded += RootShellPage_Loaded; RootElement.Loaded += RootElementLoaded;
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated"); WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
@@ -125,7 +131,7 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler; App.Current.Services.GetService<SettingsModel>()!.SettingsChanged += SettingsChangedHandler;
// Make sure that we update the acrylic theme when the OS theme changes // Make sure that we update the acrylic theme when the OS theme changes
RootShellPage.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic); RootElement.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic);
// Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h // Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h
NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () => NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () =>
@@ -141,6 +147,15 @@ public sealed partial class MainWindow : WindowEx,
HideWindow(); 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) private static void LocalKeyboardListener_OnKeyPressed(object? sender, LocalKeyboardListenerKeyPressedEventArgs e)
{ {
if (e.Key == VirtualKey.GoBack) if (e.Key == VirtualKey.GoBack)
@@ -151,11 +166,18 @@ public sealed partial class MainWindow : WindowEx,
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(); private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings();
private void RootShellPage_Loaded(object sender, RoutedEventArgs e) => private void RootElementLoaded(object sender, RoutedEventArgs e)
{
// Now that our content has loaded, we can update our draggable regions // Now that our content has loaded, we can update our draggable regions
UpdateRegionsForCustomTitleBar(); UpdateRegionsForCustomTitleBar();
// Add dev ribbon if enabled
if (!BuildInfo.IsCiBuild)
{
RootElement.Children.Add(new DevRibbon { Margin = new Thickness(-1, -1, 120, -1) });
}
}
private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar(); private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs args) => UpdateRegionsForCustomTitleBar();
private void PositionCentered() private void PositionCentered()
@@ -220,6 +242,9 @@ public sealed partial class MainWindow : WindowEx,
App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon); App.Current.Services.GetService<TrayIconService>()!.SetupTrayIcon(settings.ShowSystemTrayIcon);
_ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen; _ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
_autoGoHomeInterval = settings.AutoGoHomeInterval;
_autoGoHomeTimer.Interval = _autoGoHomeInterval;
} }
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material // We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
@@ -279,6 +304,8 @@ public sealed partial class MainWindow : WindowEx,
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target) private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
{ {
StopAutoGoHome();
var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd); var hwnd = new HWND(hwndValue != 0 ? hwndValue : _hwnd);
// Remember, IsIconic == "minimized", which is entirely different state // Remember, IsIconic == "minimized", which is entirely different state
@@ -533,6 +560,25 @@ public sealed partial class MainWindow : WindowEx,
// If the window was not cloaked, then leave it hidden. // If the window was not cloaked, then leave it hidden.
// Sure, it's not ideal, but at least it's not visible. // 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() private bool Cloak()
@@ -620,28 +666,28 @@ public sealed partial class MainWindow : WindowEx,
private void UpdateRegionsForCustomTitleBar() private void UpdateRegionsForCustomTitleBar()
{ {
// Specify the interactive regions of the title bar. // Specify the interactive regions of the title bar.
var scaleAdjustment = RootShellPage.XamlRoot.RasterizationScale; var scaleAdjustment = RootElement.XamlRoot.RasterizationScale;
// Get the rectangle around our XAML content. We're going to mark this // Get the rectangle around our XAML content. We're going to mark this
// rectangle as "Passthrough", so that the normal window operations // rectangle as "Passthrough", so that the normal window operations
// (resizing, dragging) don't apply in this space. // (resizing, dragging) don't apply in this space.
var transform = RootShellPage.TransformToVisual(null); var transform = RootElement.TransformToVisual(null);
// Reserve 16px of space at the top for dragging. // Reserve 16px of space at the top for dragging.
var topHeight = 16; var topHeight = 16;
var bounds = transform.TransformBounds(new Rect( var bounds = transform.TransformBounds(new Rect(
0, 0,
topHeight, topHeight,
RootShellPage.ActualWidth, RootElement.ActualWidth,
RootShellPage.ActualHeight)); RootElement.ActualHeight));
var contentRect = GetRect(bounds, scaleAdjustment); var contentRect = GetRect(bounds, scaleAdjustment);
var rectArray = new RectInt32[] { contentRect }; var rectArray = new RectInt32[] { contentRect };
var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id); var nonClientInputSrc = InputNonClientPointerSource.GetForWindowId(this.AppWindow.Id);
nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray); nonClientInputSrc.SetRegionRects(NonClientRegionKind.Passthrough, rectArray);
// Add a drag-able region on top // Add a drag-able region on top
var w = RootShellPage.ActualWidth; var w = RootElement.ActualWidth;
_ = RootShellPage.ActualHeight; _ = RootElement.ActualHeight;
var dragSides = new RectInt32[] var dragSides = new RectInt32[]
{ {
GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall GetRect(new Rect(0, 0, w, topHeight), scaleAdjustment), // the top, {topHeight=16} tall

View File

@@ -15,6 +15,7 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained> <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<LangVersion>preview</LangVersion>
<Version>$(CmdPalVersion)</Version> <Version>$(CmdPalVersion)</Version>
@@ -26,10 +27,10 @@
</PropertyGroup> </PropertyGroup>
<!-- For debugging purposes, uncomment this block to enable AOT builds --> <!-- For debugging purposes, uncomment this block to enable AOT builds -->
<!-- <PropertyGroup> <!--<PropertyGroup>
<EnableCmdPalAOT>true</EnableCmdPalAOT> <EnableCmdPalAOT>true</EnableCmdPalAOT>
<CIBuild>true</CIBuild> <GeneratePackageLocally>true</GeneratePackageLocally>
</PropertyGroup> --> </PropertyGroup>-->
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'"> <PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
@@ -38,7 +39,7 @@
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(CIBuild)'=='true'"> <PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild> <GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
<AppxBundle>Never</AppxBundle> <AppxBundle>Never</AppxBundle>
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir> <AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
@@ -67,6 +68,7 @@
<ItemGroup> <ItemGroup>
<None Remove="Controls\ActionBar.xaml" /> <None Remove="Controls\ActionBar.xaml" />
<None Remove="Controls\DevRibbon.xaml" />
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" /> <None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
<None Remove="Controls\SearchBar.xaml" /> <None Remove="Controls\SearchBar.xaml" />
<None Remove="IsEnabledTextBlock.xaml" /> <None Remove="IsEnabledTextBlock.xaml" />
@@ -119,6 +121,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" /> <ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" /> <ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.ClipboardHistory\Microsoft.CmdPal.Ext.ClipboardHistory.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" /> <ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.System\Microsoft.CmdPal.Ext.System.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" /> <ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" />
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" /> <ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
@@ -168,6 +171,9 @@
<Page Update="Controls\SearchBar.xaml"> <Page Update="Controls\SearchBar.xaml">
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
</Page> </Page>
<Page Update="Controls\DevRibbon.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Update="Styles\TextBox.xaml"> <Page Update="Styles\TextBox.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime> <XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page> </Page>
@@ -235,4 +241,24 @@
</ItemGroup> </ItemGroup>
<!-- </AdaptiveCardsWorkaround> --> <!-- </AdaptiveCardsWorkaround> -->
<!-- Metadata for build information -->
<ItemGroup>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>PublishTrimmed</_Parameter1>
<_Parameter2>$(PublishTrimmed)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>PublishAot</_Parameter1>
<_Parameter2>$(PublishAot)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CIBuild</_Parameter1>
<_Parameter2>$(CIBuild)</_Parameter2>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
<_Parameter1>CommandPaletteBranding</_Parameter1>
<_Parameter2>$(CommandPaletteBranding)</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

View File

@@ -345,7 +345,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
// Depending on the settings, either // Depending on the settings, either
// * Go home, or // * Go home, or
// * Select the search text (if we should remain open on this page) // * Select the search text (if we should remain open on this page)
if (settings.HotkeyGoesHome) if (settings.AutoGoHomeInterval == TimeSpan.Zero)
{ {
GoHome(false); GoHome(false);
} }

View File

@@ -51,8 +51,18 @@
</controls:SettingsCard> </controls:SettingsCard>
</controls:SettingsExpander.Items> </controls:SettingsExpander.Items>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsCard x:Uid="Settings_GeneralPage_GoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}"> <controls:SettingsCard x:Uid="Settings_GeneralPage_AutoGoHome_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE80F;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HotkeyGoesHome, Mode=TwoWay}" /> <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>
<controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE933;}"> <controls:SettingsCard x:Uid="Settings_GeneralPage_HighlightSearch_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=&#xE933;}">
<ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" /> <ToggleSwitch IsOn="{x:Bind viewModel.HighlightSearchOnActivate, Mode=TwoWay}" />

View File

@@ -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"> <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> <value>Preventing disruption of the program running in fullscreen by unintentional activation of shortcut</value>
</data> </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"> <data name="Settings_GeneralPage_HighlightSearch_SettingsCard.Header" xml:space="preserve">
<value>Highlight search on activate</value> <value>Highlight search on activate</value>
</data> </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"> <data name="GlobalErrorHandler_CrashMessageBox_Caption" xml:space="preserve">
<value>Command Palette - Fatal error</value> <value>Command Palette - Fatal error</value>
</data> </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> </root>

View File

@@ -0,0 +1,190 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ManagedCommon;
using Microsoft.CmdPal.UI.Helpers;
using Microsoft.UI;
using Windows.System;
using Windows.UI;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace Microsoft.CmdPal.UI.ViewModels;
internal sealed partial class DevRibbonViewModel : ObservableObject
{
private const int MaxLogEntries = 2;
private const string Release = "Release";
private const string Debug = "Debug";
private static readonly Color ReleaseAotColor = ColorHelper.FromArgb(255, 124, 58, 237);
private static readonly Color ReleaseColor = ColorHelper.FromArgb(255, 51, 65, 85);
private static readonly Color DebugAotColor = ColorHelper.FromArgb(255, 99, 102, 241);
private static readonly Color DebugColor = ColorHelper.FromArgb(255, 107, 114, 128);
private readonly DispatcherQueue _dispatcherQueue;
public DevRibbonViewModel()
{
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
Trace.Listeners.Add(new DevRibbonTraceListener(this));
var configLabel = BuildConfiguration == Release ? "RLS" : "DBG"; /* #no-spell-check-line */
var aotLabel = BuildInfo.IsNativeAot ? "⚡AOT" : "NO AOT";
Tag = $"{configLabel} | {aotLabel}";
TagColor = (BuildConfiguration, BuildInfo.IsNativeAot) switch
{
(Release, true) => ReleaseAotColor,
(Release, false) => ReleaseColor,
(Debug, true) => DebugAotColor,
(Debug, false) => DebugColor,
_ => Colors.Fuchsia,
};
}
public string BuildConfiguration => BuildInfo.Configuration;
public bool IsAotReleaseConfiguration => BuildConfiguration == Release && BuildInfo.IsNativeAot;
public bool IsAot => BuildInfo.IsNativeAot;
public bool IsPublishTrimmed => BuildInfo.PublishTrimmed;
public ObservableCollection<LogEntryViewModel> LatestLogs { get; } = [];
[ObservableProperty]
public partial int WarningCount { get; private set; }
[ObservableProperty]
public partial int ErrorCount { get; private set; }
[ObservableProperty]
public partial string Tag { get; private set; }
[ObservableProperty]
public partial Color TagColor { get; private set; }
[RelayCommand]
private async Task OpenLogFileAsync()
{
var logPath = Logger.CurrentLogFile;
if (File.Exists(logPath))
{
await Launcher.LaunchUriAsync(new Uri(logPath));
}
}
[RelayCommand]
private async Task OpenLogFolderAsync()
{
var logFolderPath = Logger.CurrentVersionLogDirectoryPath;
if (Directory.Exists(logFolderPath))
{
await Launcher.LaunchFolderPathAsync(logFolderPath);
}
}
[RelayCommand]
private void ResetErrorCounters()
{
WarningCount = 0;
ErrorCount = 0;
LatestLogs.Clear();
}
private sealed partial class DevRibbonTraceListener(DevRibbonViewModel viewModel) : TraceListener
{
private const string TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff";
[GeneratedRegex(@"^\[(?<timestamp>.*?)\] \[(?<severity>.*?)\] (?<message>.*)")]
private static partial Regex LogRegex();
private readonly Lock _lock = new();
private LogEntryViewModel? _latestLogEntry;
public override void Write(string? message)
{
// Not required for this scenario.
}
public override void WriteLine(string? message)
{
if (message is null)
{
return;
}
lock (_lock)
{
var match = LogRegex().Match(message);
if (match.Success)
{
var severity = match.Groups["severity"].Value;
var isWarning = severity.Equals("Warning", StringComparison.OrdinalIgnoreCase);
var isError = severity.Equals("Error", StringComparison.OrdinalIgnoreCase);
if (isWarning || isError)
{
var timestampStr = match.Groups["timestamp"].Value;
var timestamp = DateTimeOffset.TryParseExact(
timestampStr,
TimestampFormat,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal,
out var parsed)
? parsed
: DateTimeOffset.Now;
var logEntry = new LogEntryViewModel(
timestamp,
severity,
match.Groups["message"].Value,
string.Empty);
_latestLogEntry = logEntry;
viewModel._dispatcherQueue.TryEnqueue(() =>
{
if (isWarning)
{
viewModel.WarningCount++;
}
else
{
viewModel.ErrorCount++;
}
viewModel.LatestLogs.Insert(0, logEntry);
while (viewModel.LatestLogs.Count > MaxLogEntries)
{
viewModel.LatestLogs.RemoveAt(viewModel.LatestLogs.Count - 1);
}
});
}
else
{
_latestLogEntry = null;
}
return;
}
if (IndentLevel > 0 && _latestLogEntry is { } latest)
{
viewModel._dispatcherQueue.TryEnqueue(() =>
{
latest.AppendDetails(message);
});
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
// 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.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Microsoft.CmdPal.UI.ViewModels;
internal sealed partial class LogEntryViewModel : ObservableObject
{
private const int HeaderMaxLength = 80;
private const string WarningGlyph = "\uE7BA";
private const string ErrorGlyph = "\uEA39";
private const string TimestampFormat = "HH:mm:ss";
private DateTimeOffset Timestamp { get; }
private string Severity { get; }
private string Message { get; }
private string FormattedTimestamp { get; }
public string SeverityGlyph { get; }
[ObservableProperty]
public partial string Header { get; private set; }
[ObservableProperty]
public partial string Description { get; private set; }
[ObservableProperty]
public partial string Details { get; private set; }
public LogEntryViewModel(DateTimeOffset timestamp, string severity, string message, string details)
{
Timestamp = timestamp;
Severity = severity;
Message = message;
Details = details;
SeverityGlyph = severity.ToUpperInvariant() switch
{
"WARNING" => WarningGlyph,
"ERROR" => ErrorGlyph,
_ => string.Empty,
};
FormattedTimestamp = timestamp.ToString(TimestampFormat, CultureInfo.CurrentCulture);
Description = $"{FormattedTimestamp} • {Message}";
Header = Message;
}
public void AppendDetails(string? message)
{
if (string.IsNullOrEmpty(message))
{
return;
}
Details += Environment.NewLine + message;
// Make header the second line of details (because that's actually the message itself):
var detailsLines = Details.Split([Environment.NewLine], StringSplitOptions.None);
if (detailsLines.Length < 2)
{
return;
}
Header = detailsLines[1].Trim();
if (Header.Length > HeaderMaxLength)
{
Header = Header[..(HeaderMaxLength - 1)] + "…";
}
}
}

View File

@@ -0,0 +1,125 @@
// 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.Globalization;
using System.Reflection;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
[TestClass]
public class FallbackRemoteDesktopItemTests
{
private static readonly CompositeFormat OpenHostCompositeFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
[TestMethod]
public void UpdateQuery_WhenMatchingConnectionExists_UsesConnectionName()
{
var connectionName = "my-rdp-server";
// Arrange
var setup = CreateFallback(connectionName);
var fallback = setup.Fallback;
// Act
fallback.UpdateQuery("my-rdp-server");
// Assert
Assert.AreEqual(connectionName, fallback.Title);
var expectedSubtitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, connectionName);
Assert.AreEqual(expectedSubtitle, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
Assert.AreEqual(connectionName, GetCommandHost(command));
}
[TestMethod]
public void UpdateQuery_WhenQueryIsValidHostWithoutExistingConnection_UsesQuery()
{
// Arrange
var setup = CreateFallback();
var fallback = setup.Fallback;
const string hostname = "test.corp";
// Act
fallback.UpdateQuery(hostname);
// Assert
var expectedTitle = string.Format(CultureInfo.CurrentCulture, OpenHostCompositeFormat, hostname);
Assert.AreEqual(expectedTitle, fallback.Title);
Assert.AreEqual(Resources.remotedesktop_title, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
Assert.AreEqual(Resources.remotedesktop_command_connect, command.Name);
Assert.AreEqual(hostname, GetCommandHost(command));
}
[TestMethod]
public void UpdateQuery_WhenQueryIsWhitespace_ResetsCommand()
{
// Arrange
var setup = CreateFallback("rdp-server-two");
var fallback = setup.Fallback;
// Act
fallback.UpdateQuery(" ");
// Assert
Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title);
Assert.AreEqual(string.Empty, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
Assert.AreEqual(Resources.remotedesktop_command_open, command.Name);
Assert.AreEqual(string.Empty, GetCommandHost(command));
}
[TestMethod]
public void UpdateQuery_WhenQueryIsInvalidHost_ClearsCommand()
{
// Arrange
var setup = CreateFallback("rdp-server-three");
var fallback = setup.Fallback;
// Act
fallback.UpdateQuery("not a valid host");
// Assert
Assert.AreEqual(Resources.remotedesktop_command_open, fallback.Title);
Assert.AreEqual(string.Empty, fallback.Subtitle);
var command = fallback.Command as OpenRemoteDesktopCommand;
Assert.IsNotNull(command);
Assert.AreEqual(Resources.remotedesktop_command_open, command.Name);
Assert.AreEqual(string.Empty, GetCommandHost(command));
}
private static string GetCommandHost(OpenRemoteDesktopCommand command)
{
var field = typeof(OpenRemoteDesktopCommand).GetField("_rdpHost", BindingFlags.NonPublic | BindingFlags.Instance);
if (field is null)
{
return string.Empty;
}
return field.GetValue(command) as string ?? string.Empty;
}
private static (FallbackRemoteDesktopItem Fallback, IRdpConnectionsManager Manager) CreateFallback(params string[] connectionNames)
{
var settingsManager = new MockSettingsManager(connectionNames);
var connectionsManager = new MockRdpConnectionsManager(settingsManager);
var fallback = new FallbackRemoteDesktopItem(connectionsManager);
return (fallback, connectionsManager);
}
}

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.RemoteDesktop\Microsoft.CmdPal.Ext.RemoteDesktop.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
internal sealed class MockRdpConnectionsManager : IRdpConnectionsManager
{
private readonly List<ConnectionListItem> _connections = new();
public IReadOnlyCollection<ConnectionListItem> Connections => _connections.AsReadOnly();
public MockRdpConnectionsManager(ISettingsInterface settingsManager)
{
_connections.AddRange(settingsManager.PredefinedConnections.Select(ConnectionHelpers.MapToResult));
}
}

View File

@@ -0,0 +1,23 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
internal sealed class MockSettingsManager : ISettingsInterface
{
private readonly List<string> _connections;
public IReadOnlyCollection<string> PredefinedConnections => _connections;
public ToolkitSettings Settings { get; } = new();
public MockSettingsManager(params string[] predefinedConnections)
{
_connections = new(predefinedConnections);
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
[TestClass]
public class RdpConnectionsManagerTests
{
[TestMethod]
public void Constructor_AddsOpenCommandItem()
{
// Act
var manager = new RdpConnectionsManager(new MockSettingsManager(["test.local"]));
// Assert
Assert.IsTrue(manager.Connections.Any(item => string.IsNullOrEmpty(item.ConnectionName)));
}
[TestMethod]
public void FindConnection_ReturnsExactMatch()
{
// Arrange
var connectionName = "rdp-test";
var connection = new ConnectionListItem(connectionName);
// Act
var result = ConnectionHelpers.FindConnection(connectionName, new[] { connection });
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(connectionName, result.ConnectionName);
}
[TestMethod]
public void FindConnection_ReturnsNullForWhitespaceQuery()
{
// Arrange
var connection = new ConnectionListItem("rdp-test");
// Act
var result = ConnectionHelpers.FindConnection(" ", new[] { connection });
// Assert
Assert.IsNull(result);
}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests;
[TestClass]
public class RemoteDesktopCommandProviderTests
{
[TestMethod]
public void ProviderHasCorrectId()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Assert
Assert.AreEqual("com.microsoft.cmdpal.builtin.remotedesktop", provider.Id);
}
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void FallbackCommandsNotEmpty()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.FallbackCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void TopLevelCommandsContainListPageCommand()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.AreEqual(1, commands.Length);
Assert.IsInstanceOfType(commands.Single().Command, typeof(RemoteDesktopListPage));
}
[TestMethod]
public void FallbackCommandsContainFallbackItem()
{
// Setup
var provider = new RemoteDesktopCommandProvider();
// Act
var commands = provider.FallbackCommands();
// Assert
Assert.AreEqual(1, commands.Length);
Assert.IsInstanceOfType(commands.Single(), typeof(FallbackRemoteDesktopItem));
}
}

View File

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

View File

@@ -18,6 +18,8 @@ public class MockSettingsInterface : ISettingsInterface
public int HistoryItemCount { get; set; } public int HistoryItemCount { get; set; }
public string CustomSearchUri { get; }
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems; public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null) public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)

View File

@@ -26,8 +26,9 @@ public class QueryTests : CommandPaletteUnitTestBase
{ {
// Setup // Setup
var settings = new MockSettingsInterface(); var settings = new MockSettingsInterface();
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings, browserInfoService);
// Act // Act
page.UpdateSearchText(string.Empty, query); page.UpdateSearchText(string.Empty, query);
@@ -55,8 +56,9 @@ public class QueryTests : CommandPaletteUnitTestBase
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5); var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings, browserInfoService);
// Act // Act
page.UpdateSearchText("abcdef", string.Empty); page.UpdateSearchText("abcdef", string.Empty);
@@ -90,8 +92,9 @@ public class QueryTests : CommandPaletteUnitTestBase
}; };
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 5); 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))); 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 settings = new MockSettingsInterface(mockHistory: mockHistoryItems, historyItemCount: 0);
var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings); var page = new WebSearchListPage(settings, browserInfoService);
// Act // Act
page.UpdateSearchText("abcdef", string.Empty); page.UpdateSearchText("abcdef", string.Empty);

View File

@@ -20,7 +20,9 @@ public class SettingsManagerTests : CommandPaletteUnitTestBase
{ {
// Setup // Setup
var settings = new MockSettingsInterface(historyItemCount: 5); var settings = new MockSettingsInterface(historyItemCount: 5);
var page = new WebSearchListPage(settings); var browserInfoService = new MockBrowserInfoService();
var page = new WebSearchListPage(settings, browserInfoService);
var eventRaised = false; var eventRaised = false;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,21 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3.55078" width="9.36537" height="9.36537" rx="0.720413" fill="url(#paint0_linear_2155_27162)"/>
<rect x="1" y="2" width="13" height="9" rx="0.722222" fill="url(#paint1_linear_2155_27162)"/>
<circle cx="11.4" cy="9.4" r="4.4" fill="url(#paint2_radial_2155_27162)"/>
<path d="M13.8703 11.2497C13.964 11.3434 14.116 11.3434 14.2097 11.2497C14.3034 11.156 14.3034 11.004 14.2097 10.9103L12.4594 9.16L14.2097 7.40971C14.3034 7.31598 14.3034 7.16402 14.2097 7.07029C14.116 6.97657 13.964 6.97657 13.8703 7.07029L11.9503 8.9903C11.8566 9.08402 11.8566 9.23598 11.9503 9.32971L13.8703 11.2497ZM9.40971 8.0303C9.31598 7.93657 9.16402 7.93657 9.07029 8.0303C8.97657 8.12402 8.97657 8.27598 9.07029 8.36971L10.8206 10.12L9.07029 11.8703C8.97657 11.964 8.97657 12.116 9.07029 12.2097C9.16402 12.3034 9.31598 12.3034 9.40971 12.2097L11.3297 10.2897C11.4234 10.196 11.4234 10.044 11.3297 9.95031L9.40971 8.0303Z" fill="#666666" stroke="#666666" stroke-width="0.146667"/>
<defs>
<linearGradient id="paint0_linear_2155_27162" x1="3.22298" y1="3.55078" x2="8.52487" y2="6.68847" gradientUnits="userSpaceOnUse">
<stop stop-color="#246FB0"/>
<stop offset="1" stop-color="#14518A"/>
</linearGradient>
<linearGradient id="paint1_linear_2155_27162" x1="1.15476" y1="1.66667" x2="14.867" y2="9.90553" gradientUnits="userSpaceOnUse">
<stop stop-color="#86D6F9"/>
<stop offset="1" stop-color="#1FA3E4"/>
</linearGradient>
<radialGradient id="paint2_radial_2155_27162" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.9111 6.22222) rotate(90) scale(7.57778)">
<stop stop-color="#E7ECF1"/>
<stop offset="0.84" stop-color="#D2D4D6"/>
<stop offset="1" stop-color="#A9ABAC"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,35 @@
// 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.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
internal sealed partial class ConnectionListItem : ListItem
{
public ConnectionListItem(string connectionName)
{
ConnectionName = connectionName;
if (string.IsNullOrEmpty(connectionName))
{
Title = Resources.remotedesktop_open_rdp;
Subtitle = Resources.remotedesktop_subtitle;
}
else
{
Title = connectionName;
CompositeFormat remoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
Subtitle = string.Format(CultureInfo.CurrentCulture, remoteDesktopOpenHostFormat, connectionName);
}
Icon = Icons.RDPIcon;
Command = new OpenRemoteDesktopCommand(connectionName);
}
public string ConnectionName { get; }
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
internal sealed partial class FallbackRemoteDesktopItem : FallbackCommandItem
{
private const string _id = "com.microsoft.cmdpal.builtin.remotedesktop.fallback";
private static readonly UriHostNameType[] ValidUriHostNameTypes = [
UriHostNameType.IPv6,
UriHostNameType.IPv4,
UriHostNameType.Dns
];
private static readonly CompositeFormat RemoteDesktopOpenHostFormat = CompositeFormat.Parse(Resources.remotedesktop_open_host);
private readonly IRdpConnectionsManager _rdpConnectionsManager;
public FallbackRemoteDesktopItem(IRdpConnectionsManager rdpConnectionsManager)
: base(new OpenRemoteDesktopCommand(string.Empty), Resources.remotedesktop_title)
{
_rdpConnectionsManager = rdpConnectionsManager;
Title = string.Empty;
Subtitle = string.Empty;
Icon = Icons.RDPIcon;
}
public override void UpdateQuery(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
Title = string.Empty;
Subtitle = string.Empty;
Command = new OpenRemoteDesktopCommand(string.Empty);
return;
}
var connections = _rdpConnectionsManager.Connections.Where(w => !string.IsNullOrWhiteSpace(w.ConnectionName));
var queryConnection = ConnectionHelpers.FindConnection(query, connections);
if (queryConnection is not null && !string.IsNullOrWhiteSpace(queryConnection.ConnectionName))
{
var connectionName = queryConnection.ConnectionName;
Command = new OpenRemoteDesktopCommand(connectionName);
Title = connectionName;
Subtitle = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
}
else if (ValidUriHostNameTypes.Contains(Uri.CheckHostName(query)))
{
var connectionName = query.Trim();
Command = new OpenRemoteDesktopCommand(connectionName);
Title = string.Format(CultureInfo.CurrentCulture, RemoteDesktopOpenHostFormat, connectionName);
Subtitle = Resources.remotedesktop_title;
}
else
{
Title = string.Empty;
Subtitle = string.Empty;
Command = new OpenRemoteDesktopCommand(string.Empty);
}
}
}

View File

@@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using System.Text;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
internal sealed partial class OpenRemoteDesktopCommand : BaseObservable, IInvokableCommand
{
private static readonly CompositeFormat ProcessErrorFormat =
CompositeFormat.Parse(Resources.remotedesktop_log_mstsc_error);
private static readonly CompositeFormat InvalidHostnameFormat =
CompositeFormat.Parse(Resources.remotedesktop_log_invalid_hostname);
public string Name { get; }
public string Id { get; } = "com.microsoft.cmdpal.builtin.remotedesktop.openrdp";
public IIconInfo Icon => Icons.RDPIcon;
private readonly string _rdpHost;
public OpenRemoteDesktopCommand(string rdpHost)
{
_rdpHost = rdpHost;
Name = string.IsNullOrWhiteSpace(_rdpHost) ?
Resources.remotedesktop_command_open :
Resources.remotedesktop_command_connect;
}
public ICommandResult Invoke(object sender)
{
using var process = new Process();
process.StartInfo.UseShellExecute = false;
process.StartInfo.WorkingDirectory = Environment.SpecialFolder.MyDocuments.ToString();
process.StartInfo.FileName = "mstsc";
if (!string.IsNullOrWhiteSpace(_rdpHost))
{
// validate that _rdpHost is a proper hostname or IP address
if (Uri.CheckHostName(_rdpHost) == UriHostNameType.Unknown)
{
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(
System.Globalization.CultureInfo.CurrentCulture,
InvalidHostnameFormat,
_rdpHost),
Result = CommandResult.KeepOpen(),
});
}
process.StartInfo.Arguments = $"/v:{_rdpHost}";
}
try
{
process.Start();
return CommandResult.Dismiss();
}
catch (Exception ex)
{
return CommandResult.ShowToast(new ToastArgs()
{
Message = string.Format(
System.Globalization.CultureInfo.CurrentCulture,
ProcessErrorFormat,
ex.Message),
Result = CommandResult.KeepOpen(),
});
}
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
internal static class ConnectionHelpers
{
public static ConnectionListItem MapToResult(string item) => new(item);
public static ConnectionListItem? FindConnection(string query, IEnumerable<ConnectionListItem> connections)
{
if (string.IsNullOrWhiteSpace(query))
{
return null;
}
var matchedConnection = ListHelpers.FilterList(
connections,
query,
(s, i) => ListHelpers.ScoreListItem(s, i))
.FirstOrDefault();
return matchedConnection;
}
}

View File

@@ -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.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
internal interface IRdpConnectionsManager
{
IReadOnlyCollection<ConnectionListItem> Connections { get; }
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using Microsoft.CmdPal.Ext.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
using Microsoft.Win32;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
internal class RdpConnectionsManager : IRdpConnectionsManager
{
private readonly ISettingsInterface _settingsManager;
private readonly ConnectionListItem _openRdpCommandListItem = new(string.Empty);
private ReadOnlyCollection<ConnectionListItem> _connections = new(Array.Empty<ConnectionListItem>());
private const int MinutesToCache = 1;
private DateTime? _connectionsLastLoaded;
public RdpConnectionsManager(ISettingsInterface settingsManager)
{
_settingsManager = settingsManager;
_settingsManager.Settings.SettingsChanged += (s, e) =>
{
_connectionsLastLoaded = null;
};
}
public IReadOnlyCollection<ConnectionListItem> Connections
{
get
{
if (!_connectionsLastLoaded.HasValue ||
(DateTime.Now - _connectionsLastLoaded.Value).TotalMinutes >= MinutesToCache)
{
var registryConnections = GetRdpConnectionsFromRegistry();
var predefinedConnections = GetPredefinedConnectionsFromSettings();
_connectionsLastLoaded = DateTime.Now;
var newConnections = new List<ConnectionListItem>(registryConnections.Count + predefinedConnections.Count + 1);
newConnections.AddRange(registryConnections);
newConnections.AddRange(predefinedConnections);
newConnections.Insert(0, _openRdpCommandListItem);
Interlocked.Exchange(ref _connections, new ReadOnlyCollection<ConnectionListItem>(newConnections));
}
return _connections;
}
}
private List<ConnectionListItem> GetRdpConnectionsFromRegistry()
{
using var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Terminal Server Client\Default");
var validConnections = new List<ConnectionListItem>();
if (key is not null)
{
validConnections = key.GetValueNames()
.Select(name => key.GetValue(name))
.OfType<string>() // Keep only string values
.Select(v => v.Trim()) // Normalize
.Where(v => !string.IsNullOrWhiteSpace(v))
.Distinct() // Remove dupes if any
.Select(ConnectionHelpers.MapToResult)
.ToList();
}
return validConnections;
}
private List<ConnectionListItem> GetPredefinedConnectionsFromSettings()
{
var validConnections = _settingsManager.PredefinedConnections
.Select(s => s.Trim())
.Where(value => !string.IsNullOrWhiteSpace(value))
.Select(ConnectionHelpers.MapToResult)
.ToList();
return validConnections;
}
}

View File

@@ -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.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop;
internal static class Icons
{
internal static IconInfo RDPIcon { get; } = IconHelpers.FromRelativePath("Assets\\RemoteDesktop.svg");
}

View File

@@ -0,0 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
<Import Project="..\Common.ExtDependencies.props" />
<PropertyGroup>
<RootNamespace>Microsoft.CmdPal.Ext.RemoteDesktop</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
<ProjectPriFileName>Microsoft.CmdPal.Ext.RemoteDesktop.pri</ProjectPriFileName>
<nullable>enable</nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
</ItemGroup>
<ItemGroup>
<Content Update="Assets\RemoteDesktop.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Assets\RemoteDesktop.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
internal sealed partial class RemoteDesktopListPage : ListPage
{
private readonly IRdpConnectionsManager _rdpConnectionsManager;
public RemoteDesktopListPage(IRdpConnectionsManager rdpConnectionsManager)
{
Icon = Icons.RDPIcon;
Name = Resources.remotedesktop_title;
Id = "com.microsoft.cmdpal.builtin.remotedesktop";
_rdpConnectionsManager = rdpConnectionsManager;
}
public override IListItem[] GetItems() => _rdpConnectionsManager.Connections.ToArray();
}

View File

@@ -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.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.RemoteDesktop.UnitTests")]

View File

@@ -0,0 +1,153 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.RemoteDesktop.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Connect.
/// </summary>
public static string remotedesktop_command_connect {
get {
return ResourceManager.GetString("remotedesktop_command_connect", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open.
/// </summary>
public static string remotedesktop_command_open {
get {
return ResourceManager.GetString("remotedesktop_command_open", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The hostname &apos;{0}&apos; was invalid. Ensure you&apos;re using a valid hostname or IP address..
/// </summary>
public static string remotedesktop_log_invalid_hostname {
get {
return ResourceManager.GetString("remotedesktop_log_invalid_hostname", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}.
/// </summary>
public static string remotedesktop_log_mstsc_error {
get {
return ResourceManager.GetString("remotedesktop_log_mstsc_error", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connect to {0}.
/// </summary>
public static string remotedesktop_open_host {
get {
return ResourceManager.GetString("remotedesktop_open_host", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Open Remote Desktop Client.
/// </summary>
public static string remotedesktop_open_rdp {
get {
return ResourceManager.GetString("remotedesktop_open_rdp", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to A list of connections to include in the query results by default.
/// </summary>
public static string remotedesktop_settings_predefined_connections_description {
get {
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Predefined connections.
/// </summary>
public static string remotedesktop_settings_predefined_connections_title {
get {
return ResourceManager.GetString("remotedesktop_settings_predefined_connections_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Establish Remote Desktop connections.
/// </summary>
public static string remotedesktop_subtitle {
get {
return ResourceManager.GetString("remotedesktop_subtitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Remote Desktop.
/// </summary>
public static string remotedesktop_title {
get {
return ResourceManager.GetString("remotedesktop_title", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="remotedesktop_title" xml:space="preserve">
<value>Remote Desktop</value>
</data>
<data name="remotedesktop_subtitle" xml:space="preserve">
<value>Establish Remote Desktop connections</value>
</data>
<data name="remotedesktop_command_open" xml:space="preserve">
<value>Open</value>
</data>
<data name="remotedesktop_open_host" xml:space="preserve">
<value>Connect to {0}</value>
</data>
<data name="remotedesktop_command_connect" xml:space="preserve">
<value>Connect</value>
</data>
<data name="remotedesktop_open_rdp" xml:space="preserve">
<value>Open Remote Desktop Client</value>
</data>
<data name="remotedesktop_settings_predefined_connections_title" xml:space="preserve">
<value>Predefined connections</value>
</data>
<data name="remotedesktop_settings_predefined_connections_description" xml:space="preserve">
<value>A list of connections to include in the query results by default</value>
</data>
<data name="remotedesktop_log_mstsc_error" xml:space="preserve">
<value>Unable to initialize Microsoft Terminal Service Client. Ensure it is enabled in Windows Settings.\r{0}</value>
</data>
<data name="remotedesktop_log_invalid_hostname" xml:space="preserve">
<value>The hostname '{0}' was invalid. Ensure you're using a valid hostname or IP address.</value>
</data>
</root>

View File

@@ -0,0 +1,45 @@
// 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.RemoteDesktop.Commands;
using Microsoft.CmdPal.Ext.RemoteDesktop.Helper;
using Microsoft.CmdPal.Ext.RemoteDesktop.Pages;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop;
public partial class RemoteDesktopCommandProvider : CommandProvider
{
private readonly CommandItem listPageCommand;
private readonly FallbackRemoteDesktopItem fallback;
public RemoteDesktopCommandProvider()
{
Id = "com.microsoft.cmdpal.builtin.remotedesktop";
DisplayName = Resources.remotedesktop_title;
Icon = Icons.RDPIcon;
var settingsManager = new SettingsManager();
var rdpConnectionsManager = new RdpConnectionsManager(settingsManager);
var listPage = new RemoteDesktopListPage(rdpConnectionsManager);
fallback = new FallbackRemoteDesktopItem(rdpConnectionsManager);
listPageCommand = new CommandItem(listPage)
{
Subtitle = Resources.remotedesktop_subtitle,
Icon = Icons.RDPIcon,
MoreCommands = [
new CommandContextItem(settingsManager.Settings.SettingsPage),
],
};
}
public override ICommandItem[] TopLevelCommands() => [listPageCommand];
public override IFallbackCommandItem[] FallbackCommands() => [fallback];
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using ToolkitSettings = Microsoft.CommandPalette.Extensions.Toolkit.Settings;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
internal interface ISettingsInterface
{
public IReadOnlyCollection<string> PredefinedConnections { get; }
public ToolkitSettings Settings { get; }
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.CmdPal.Ext.RemoteDesktop.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.RemoteDesktop.Settings;
internal class SettingsManager : JsonSettingsManager, ISettingsInterface
{
// Line break character used in WinUI3 TextBox and TextBlock.
private const char TEXTBOXNEWLINE = '\r';
private static readonly string _namespace = "com.microsoft.cmdpal.builtin.remotedesktop";
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
private readonly TextSetting _predefinedConnections = new(
Namespaced(nameof(PredefinedConnections)),
Resources.remotedesktop_settings_predefined_connections_title,
Resources.remotedesktop_settings_predefined_connections_description,
string.Empty)
{
Multiline = true,
Placeholder = $"server1.domain.com{TEXTBOXNEWLINE}server2.domain.com{TEXTBOXNEWLINE}192.168.1.1",
};
public IReadOnlyCollection<string> PredefinedConnections => _predefinedConnections.Value?.Split(TEXTBOXNEWLINE).ToList() ?? [];
internal static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_predefinedConnections);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();
}
}

View File

@@ -2,32 +2,28 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands; namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class OpenURLCommand : InvokableCommand internal sealed partial class OpenURLCommand : InvokableCommand
{ {
private readonly IBrowserInfoService _browserInfoService;
public string Url { get; internal set; } = string.Empty; public string Url { get; internal set; } = string.Empty;
internal OpenURLCommand(string url) internal OpenURLCommand(string url, IBrowserInfoService browserInfoService)
{ {
_browserInfoService = browserInfoService;
Url = url; Url = url;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
Name = string.Empty; Name = string.Empty;
} }
public override CommandResult Invoke() public override CommandResult Invoke()
{ {
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"{Url}")) // TODO GH# 138 --> actually display feedback from the extension somewhere.
{ return _browserInfoService.Open(Url) ? CommandResult.Dismiss() : CommandResult.KeepOpen();
// TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen();
}
return CommandResult.Dismiss();
} }
} }

View File

@@ -4,36 +4,39 @@
using System; using System;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands; namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
internal sealed partial class SearchWebCommand : InvokableCommand internal sealed partial class SearchWebCommand : InvokableCommand
{ {
private readonly ISettingsInterface _settingsManager; 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; Arguments = arguments;
BrowserInfo.UpdateIfTimePassed();
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
Name = Properties.Resources.open_in_default_browser; Name = Resources.open_in_default_browser;
_settingsManager = settingsManager; _settingsManager = settingsManager;
_browserInfoService = browserInfoService;
} }
public override CommandResult Invoke() public override CommandResult Invoke()
{ {
if (!ShellHelpers.OpenCommandInShell(BrowserInfo.Path, BrowserInfo.ArgumentsPattern, $"? {Arguments}")) var uri = BuildUri();
if (!_browserInfoService.Open(uri))
{ {
// TODO GH# 138 --> actually display feedback from the extension somewhere. // TODO GH# 138 --> actually display feedback from the extension somewhere.
return CommandResult.KeepOpen(); return CommandResult.KeepOpen();
} }
// remember only the query, not the full URI
if (_settingsManager.HistoryItemCount != 0) if (_settingsManager.HistoryItemCount != 0)
{ {
_settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now)); _settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
@@ -41,4 +44,28 @@ internal sealed partial class SearchWebCommand : InvokableCommand
return CommandResult.Dismiss(); return CommandResult.Dismiss();
} }
private string BuildUri()
{
if (string.IsNullOrWhiteSpace(_settingsManager.CustomSearchUri))
{
return $"? " + Arguments;
}
// if the custom search URI contains query placeholder, replace it with the actual query
// otherwise append the query to the end of the URI
// support {query}, %query% or %s as placeholder
var placeholderVariants = new[] { "{query}", "%query%", "%s" };
foreach (var placeholder in placeholderVariants)
{
if (_settingsManager.CustomSearchUri.Contains(placeholder, StringComparison.OrdinalIgnoreCase))
{
return _settingsManager.CustomSearchUri.Replace(placeholder, Uri.EscapeDataString(Arguments), StringComparison.OrdinalIgnoreCase);
}
}
// is this too smart?
var separator = _settingsManager.CustomSearchUri.Contains('?') ? '&' : '?';
return $"{_settingsManager.CustomSearchUri}{separator}q={Uri.EscapeDataString(Arguments)}";
}
} }

View File

@@ -5,9 +5,9 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Commands; namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
@@ -16,25 +16,34 @@ internal sealed partial class FallbackExecuteSearchItem : FallbackCommandItem
private readonly SearchWebCommand _executeItem; private readonly SearchWebCommand _executeItem;
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); 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 static readonly CompositeFormat SubtitleText = System.Text.CompositeFormat.Parse(Properties.Resources.web_search_fallback_subtitle);
private string _title;
public FallbackExecuteSearchItem(SettingsManager settings) private readonly IBrowserInfoService _browserInfoService;
: base(new SearchWebCommand(string.Empty, settings) { Id = "com.microsoft.websearch.fallback" }, Resources.command_item_title)
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; Title = string.Empty;
Subtitle = string.Empty; Subtitle = string.Empty;
_executeItem.Name = string.Empty; _executeItem.Name = string.Empty;
_title = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName);
Icon = Icons.WebSearch; 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) public override void UpdateQuery(string query)
{ {
_executeItem.Arguments = query; _executeItem.Arguments = query;
var isEmpty = string.IsNullOrEmpty(query); var isEmpty = string.IsNullOrEmpty(query);
_executeItem.Name = isEmpty ? string.Empty : Properties.Resources.open_in_default_browser; _executeItem.Name = isEmpty ? string.Empty : Resources.open_in_default_browser;
Title = isEmpty ? string.Empty : _title; Title = isEmpty ? string.Empty : UpdateBrowserName(_browserInfoService);
Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query); Subtitle = string.Format(CultureInfo.CurrentCulture, SubtitleText, query);
} }
} }

View File

@@ -7,21 +7,26 @@ using System.Globalization;
using System.Text; using System.Text;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; 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 Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch; namespace Microsoft.CmdPal.Ext.WebSearch;
internal sealed partial class FallbackOpenURLItem : FallbackCommandItem internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
{ {
private readonly IBrowserInfoService _browserInfoService;
private readonly OpenURLCommand _executeItem; private readonly OpenURLCommand _executeItem;
private static readonly CompositeFormat PluginOpenURL = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url); 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); private static readonly CompositeFormat PluginOpenUrlInBrowser = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open_url_in_browser);
public FallbackOpenURLItem(SettingsManager settings) public FallbackOpenURLItem(ISettingsInterface settings, IBrowserInfoService browserInfoService)
: base(new OpenURLCommand(string.Empty), Properties.Resources.open_url_fallback_title) : 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; Title = string.Empty;
_executeItem.Name = string.Empty; _executeItem.Name = string.Empty;
Subtitle = string.Empty; Subtitle = string.Empty;
@@ -39,7 +44,7 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
return; 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 url not contain schema, add http:// by default.
if (!success) if (!success)
@@ -48,13 +53,15 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem
} }
_executeItem.Url = query; _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); 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)) if (string.IsNullOrWhiteSpace(url))
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,5 +18,7 @@ public interface ISettingsInterface
public IReadOnlyList<HistoryItem> HistoryItems { get; } public IReadOnlyList<HistoryItem> HistoryItems { get; }
string CustomSearchUri { get; }
public void AddHistoryItem(HistoryItem historyItem); public void AddHistoryItem(HistoryItem historyItem);
} }

View File

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

View File

@@ -41,6 +41,15 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Resources.plugin_global_if_uri, Resources.plugin_global_if_uri,
false); false);
private readonly TextSetting _customSearchUri = new(
Namespaced(nameof(CustomSearchUri)),
Resources.plugin_custom_search_uri,
Resources.plugin_custom_search_uri,
string.Empty)
{
Placeholder = Resources.plugin_custom_search_uri_placeholder,
};
private readonly ChoiceSetSetting _historyItemCount = new( private readonly ChoiceSetSetting _historyItemCount = new(
Namespaced(HistoryItemCountLegacySettingsKey), Namespaced(HistoryItemCountLegacySettingsKey),
Resources.plugin_history_item_count, Resources.plugin_history_item_count,
@@ -51,6 +60,8 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0; public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
public string CustomSearchUri => _customSearchUri.Value ?? string.Empty;
public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems; public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
public SettingsManager() public SettingsManager()
@@ -59,6 +70,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
Settings.Add(_globalIfURI); Settings.Add(_globalIfURI);
Settings.Add(_historyItemCount); Settings.Add(_historyItemCount);
Settings.Add(_customSearchUri);
LoadSettings(); LoadSettings();

View File

@@ -9,23 +9,24 @@ using System.Text;
using System.Threading; using System.Threading;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
namespace Microsoft.CmdPal.Ext.WebSearch.Pages; namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
{ {
private readonly ISettingsInterface _settingsManager; private readonly ISettingsInterface _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
private readonly Lock _sync = new(); private readonly Lock _sync = new();
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name); 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 static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
private IListItem[] _allItems = []; private IListItem[] _allItems = [];
private List<ListItem> _historyItems = []; private List<ListItem> _historyItems = [];
public WebSearchListPage(ISettingsInterface settingsManager) public WebSearchListPage(ISettingsInterface settingsManager, IBrowserInfoService browserInfoService)
{ {
ArgumentNullException.ThrowIfNull(settingsManager); ArgumentNullException.ThrowIfNull(settingsManager);
@@ -35,6 +36,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
Id = "com.microsoft.cmdpal.websearch"; Id = "com.microsoft.cmdpal.websearch";
_settingsManager = settingsManager; _settingsManager = settingsManager;
_browserInfoService = browserInfoService;
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged; _settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
// It just looks viewer to have string twice on the page, and default placeholder is good enough // 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()) EmptyContent = new CommandItem(new NoOpCommand())
{ {
Icon = Icon, Icon = Icon,
Title = Properties.Resources.plugin_description, Title = Resources.plugin_description,
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, browserInfoService.GetDefaultBrowser()?.Name ?? Resources.default_browser),
}; };
UpdateHistory(); UpdateHistory();
@@ -67,7 +69,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
for (var index = items.Count - 1; index >= 0; index--) for (var index = items.Count - 1; index >= 0; index--)
{ {
var historyItem = items[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, Icon = Icons.History,
Title = historyItem.SearchString, 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); ArgumentNullException.ThrowIfNull(query);
@@ -95,10 +97,10 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
if (!string.IsNullOrEmpty(query)) if (!string.IsNullOrEmpty(query))
{ {
var searchTerm = query; var searchTerm = query;
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager)) var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager, browserInfoService))
{ {
Title = searchTerm, 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, Icon = Icons.Search,
}; };
results.Add(result); results.Add(result);
@@ -117,7 +119,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
historySnapshot = _historyItems; historySnapshot = _historyItems;
} }
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager); var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _browserInfoService);
lock (_sync) lock (_sync)
{ {

View File

@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // 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.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources { 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> /// <summary>
/// Looks up a localized string similar to Web Search. /// Looks up a localized string similar to Web Search.
/// </summary> /// </summary>
@@ -150,6 +159,24 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Custom search engine URL.
/// </summary>
public static string plugin_custom_search_uri {
get {
return ResourceManager.GetString("plugin_custom_search_uri", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query}.
/// </summary>
public static string plugin_custom_search_uri_placeholder {
get {
return ResourceManager.GetString("plugin_custom_search_uri_placeholder", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Searches the web with your default search engine. /// Looks up a localized string similar to Searches the web with your default search engine.
/// </summary> /// </summary>

View File

@@ -184,4 +184,13 @@
<data name="open_url_fallback_title" xml:space="preserve"> <data name="open_url_fallback_title" xml:space="preserve">
<value>Open URL</value> <value>Open URL</value>
</data> </data>
<data name="default_browser" xml:space="preserve">
<value>default browser</value>
</data>
<data name="plugin_custom_search_uri" xml:space="preserve">
<value>Custom search engine URL</value>
</data>
<data name="plugin_custom_search_uri_placeholder" xml:space="preserve">
<value>Use {query} or %s as the search query placeholder; e.g. https://www.bing.com/search?q={query}</value>
</data>
</root> </root>

View File

@@ -5,6 +5,7 @@
using System; using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -19,6 +20,7 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem; private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
private readonly ICommandItem[] _topLevelItems; private readonly ICommandItem[] _topLevelItems;
private readonly IFallbackCommandItem[] _fallbackCommands; private readonly IFallbackCommandItem[] _fallbackCommands;
private readonly IBrowserInfoService _browserInfoService = new DefaultBrowserInfoService();
public WebSearchCommandsProvider() public WebSearchCommandsProvider()
{ {
@@ -27,10 +29,10 @@ public sealed partial class WebSearchCommandsProvider : CommandProvider
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
Settings = _settingsManager.Settings; Settings = _settingsManager.Settings;
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager); _fallbackItem = new FallbackExecuteSearchItem(_settingsManager, _browserInfoService);
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager); _openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager, _browserInfoService);
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager) _webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager, _browserInfoService)
{ {
MoreCommands = MoreCommands =
[ [

View File

@@ -5,6 +5,7 @@
using System; using System;
using Microsoft.CmdPal.Ext.WebSearch.Commands; using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers; using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Helpers.Browser;
using Microsoft.CmdPal.Ext.WebSearch.Pages; using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.CmdPal.Ext.WebSearch.Properties; using Microsoft.CmdPal.Ext.WebSearch.Properties;
using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions;
@@ -15,13 +16,15 @@ namespace Microsoft.CmdPal.Ext.WebSearch;
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
{ {
private readonly SettingsManager _settingsManager; private readonly SettingsManager _settingsManager;
private readonly IBrowserInfoService _browserInfoService;
public WebSearchTopLevelCommandItem(SettingsManager settingsManager) public WebSearchTopLevelCommandItem(SettingsManager settingsManager, IBrowserInfoService browserInfoService)
: base(new WebSearchListPage(settingsManager)) : base(new WebSearchListPage(settingsManager, browserInfoService))
{ {
Icon = Icons.WebSearch; Icon = Icons.WebSearch;
SetDefaultTitle(); SetDefaultTitle();
_settingsManager = settingsManager; _settingsManager = settingsManager;
_browserInfoService = browserInfoService;
} }
private void SetDefaultTitle() => Title = Resources.command_item_title; private void SetDefaultTitle() => Title = Resources.command_item_title;
@@ -37,12 +40,12 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
if (string.IsNullOrEmpty(query)) if (string.IsNullOrEmpty(query))
{ {
SetDefaultTitle(); SetDefaultTitle();
ReplaceCommand(new WebSearchListPage(_settingsManager)); ReplaceCommand(new WebSearchListPage(_settingsManager, _browserInfoService));
} }
else else
{ {
Title = query; Title = query;
ReplaceCommand(new SearchWebCommand(query, _settingsManager)); ReplaceCommand(new SearchWebCommand(query, _settingsManager, _browserInfoService));
} }
} }

View File

@@ -9,13 +9,6 @@ namespace SamplePagesExtension;
internal sealed partial class SampleGalleryListPage : ListPage internal sealed partial class SampleGalleryListPage : ListPage
{ {
public SampleGalleryListPage()
{
Icon = new IconInfo("\uE7C5");
Name = "Sample Gallery List Page";
GridProperties = new GalleryGridLayout();
}
public override IListItem[] GetItems() public override IListItem[] GetItems()
{ {
return [ return [

View File

@@ -0,0 +1,59 @@
// 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;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace SamplePagesExtension;
internal sealed partial class SampleGridsListPage : ListPage
{
private readonly IListItem[] _items =
[
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = true } })
{
Title = "Gallery list page (title and subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = true, ShowSubtitle = false } })
{
Title = "Gallery list page (title, no subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new GalleryGridLayout { ShowTitle = false, ShowSubtitle = false } })
{
Title = "Gallery list page (no title, no subtitle)",
Subtitle = "A sample gallery list page with images",
Icon = IconHelpers.FromRelativePath("Assets/Images/Swirls.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new SmallGridLayout() })
{
Title = "Small grid list page",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = true } })
{
Title = "Medium grid (with title)",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
},
new ListItem(new SampleGalleryListPage { GridProperties = new MediumGridLayout { ShowTitle = false } })
{
Title = "Medium grid (hidden title)",
Subtitle = "A sample grid list page with text items",
Icon = IconHelpers.FromRelativePath("Assets/Images/Win-Digital.png"),
}
];
public SampleGridsListPage()
{
Icon = new IconInfo("\uE7C5");
Name = "Grid and gallery lists";
}
public override IListItem[] GetItems() => _items;
}

View File

@@ -34,9 +34,9 @@ public partial class SamplesListPage : ListPage
Title = "Dynamic List Page Command", Title = "Dynamic List Page Command",
Subtitle = "Changes the list of items in response to the typed query", Subtitle = "Changes the list of items in response to the typed query",
}, },
new ListItem(new SampleGalleryListPage()) new ListItem(new SampleGridsListPage())
{ {
Title = "Gallery List Page Command", Title = "Grid views and galleries",
Subtitle = "Displays items as a gallery", Subtitle = "Displays items as a gallery",
}, },
new ListItem(new OnLoadPage()) new ListItem(new OnLoadPage())