mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 11:40:16 +01:00
Compare commits
178 Commits
issue/4483
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36dacd9f54 | ||
|
|
8044d758fb | ||
|
|
fd2df1c7e1 | ||
|
|
0cae32df79 | ||
|
|
224962a08a | ||
|
|
778d151c3e | ||
|
|
f7d8768951 | ||
|
|
ab2346866d | ||
|
|
965520ffe6 | ||
|
|
84706830e7 | ||
|
|
c203335558 | ||
|
|
673db83022 | ||
|
|
a0a1f275b4 | ||
|
|
340bc7fe4b | ||
|
|
58850be91d | ||
|
|
cc87469ada | ||
|
|
7159b8c17b | ||
|
|
ea354b6a8b | ||
|
|
f087c4c239 | ||
|
|
8b07c05887 | ||
|
|
b023a6bf96 | ||
|
|
a567cd6b19 | ||
|
|
f0179f2455 | ||
|
|
7d9b7582f9 | ||
|
|
637d6d7e96 | ||
|
|
c98689e385 | ||
|
|
a53dd0f040 | ||
|
|
5a328c429b | ||
|
|
41f14c4bf4 | ||
|
|
2913477f07 | ||
|
|
0cc5c08ea4 | ||
|
|
2494bf84b3 | ||
|
|
7f217609f9 | ||
|
|
a0c8b70697 | ||
|
|
7833526186 | ||
|
|
b56e4ea560 | ||
|
|
80e734587b | ||
|
|
daf586b86d | ||
|
|
95247aa6d5 | ||
|
|
947457d20c | ||
|
|
5d4a971bcf | ||
|
|
0cacfa3cc9 | ||
|
|
40f3425a1b | ||
|
|
c32f67bc5a | ||
|
|
ba2ad4b317 | ||
|
|
d46bb66c11 | ||
|
|
ea0af2bb9c | ||
|
|
806d9eebe6 | ||
|
|
e24cf24835 | ||
|
|
30ba1e7aca | ||
|
|
2996d4b9d5 | ||
|
|
efec6cfc03 | ||
|
|
d5017fffe4 | ||
|
|
1bffcfb6fa | ||
|
|
5d55c5c120 | ||
|
|
90df6f15ae | ||
|
|
aa6acf8145 | ||
|
|
3bae9a57e7 | ||
|
|
e0972996ff | ||
|
|
90afe7e9f5 | ||
|
|
884d1ec6d6 | ||
|
|
0b38be2c01 | ||
|
|
5d7211cf85 | ||
|
|
a5e93da8ad | ||
|
|
17aa472af5 | ||
|
|
96581fd24c | ||
|
|
41d12017b6 | ||
|
|
6e7add6feb | ||
|
|
d55bad1457 | ||
|
|
e02d384051 | ||
|
|
2293b76f7b | ||
|
|
b7ea22e017 | ||
|
|
d83dc94841 | ||
|
|
031f29418e | ||
|
|
fe533cd350 | ||
|
|
d4802fec40 | ||
|
|
12af2770e6 | ||
|
|
d64ccf686f | ||
|
|
17b840c583 | ||
|
|
421df9f1e0 | ||
|
|
806d383272 | ||
|
|
bffce430cc | ||
|
|
b310df6e2e | ||
|
|
1e1f0118d9 | ||
|
|
037062e754 | ||
|
|
19d77a4d02 | ||
|
|
72c0922af7 | ||
|
|
329a6434f8 | ||
|
|
b0250fc0f4 | ||
|
|
029c894ee4 | ||
|
|
f624625344 | ||
|
|
2c61ec1a8c | ||
|
|
3e4e3dd6f1 | ||
|
|
edcce595df | ||
|
|
ef0efcbe2f | ||
|
|
4de34eca96 | ||
|
|
f886d52484 | ||
|
|
017966e3db | ||
|
|
e499f90ee5 | ||
|
|
c0fe992e37 | ||
|
|
bd316d4d34 | ||
|
|
f03eb96b9c | ||
|
|
bbd15a3ae8 | ||
|
|
777a301666 | ||
|
|
f9e3ab4852 | ||
|
|
f311a65708 | ||
|
|
b9040d82c3 | ||
|
|
1d8b45f824 | ||
|
|
221cf083bc | ||
|
|
ccac1e1ac9 | ||
|
|
fb428b2d61 | ||
|
|
acb933643a | ||
|
|
f63785d80d | ||
|
|
87c1a73ecc | ||
|
|
44b0b9ac67 | ||
|
|
7629c6fbfa | ||
|
|
b8c024ac07 | ||
|
|
640c1a8388 | ||
|
|
78b2b23764 | ||
|
|
46d26041b9 | ||
|
|
08454f8b18 | ||
|
|
b7a65ab609 | ||
|
|
08d3435a0d | ||
|
|
46b8eea695 | ||
|
|
5b255011c7 | ||
|
|
6782829cdd | ||
|
|
6ed8d73b50 | ||
|
|
38dfee0234 | ||
|
|
d547a6f613 | ||
|
|
58bea1c813 | ||
|
|
5ad2bdf6c2 | ||
|
|
44f739a289 | ||
|
|
f3d9fc2342 | ||
|
|
90d4ca060e | ||
|
|
6554a4aaaa | ||
|
|
cac0048ca7 | ||
|
|
ddb28a8606 | ||
|
|
a7206863bc | ||
|
|
96def3b79a | ||
|
|
5231543ed2 | ||
|
|
2462da68bc | ||
|
|
bbfa6c6ccb | ||
|
|
f0ea908ee6 | ||
|
|
6e11230fed | ||
|
|
6c26e86e9a | ||
|
|
1d19705568 | ||
|
|
e5e20eca9c | ||
|
|
ef0639602f | ||
|
|
fdd4416049 | ||
|
|
0dab46e58f | ||
|
|
86d1061a25 | ||
|
|
e0197dd7a5 | ||
|
|
64ea63b77d | ||
|
|
bc6b2af03c | ||
|
|
c1af5fdc57 | ||
|
|
5be208520e | ||
|
|
5aaf0e010a | ||
|
|
48eee1b0d9 | ||
|
|
1447a825ee | ||
|
|
76f7dd3b09 | ||
|
|
ee174ddd1d | ||
|
|
35c4f8fdaa | ||
|
|
2ec7ae664e | ||
|
|
1b8ddaa849 | ||
|
|
d6bca1d38e | ||
|
|
b1d7626ab7 | ||
|
|
91598c091e | ||
|
|
fd3e73ee7e | ||
|
|
06a664a53a | ||
|
|
87d2509380 | ||
|
|
c1dc487f2c | ||
|
|
e0dd7ad44a | ||
|
|
aaa68fa351 | ||
|
|
d9e4133b5a | ||
|
|
821b99c4e0 | ||
|
|
8b5a2e9537 | ||
|
|
2e49835b4d | ||
|
|
ef106f6811 |
57
.github/actions/spell-check/expect.txt
vendored
57
.github/actions/spell-check/expect.txt
vendored
@@ -22,6 +22,7 @@ ADate
|
||||
ADDSTRING
|
||||
ADDUNDORECORD
|
||||
ADifferent
|
||||
adjacents
|
||||
ADMINS
|
||||
adml
|
||||
admx
|
||||
@@ -218,11 +219,10 @@ CIELCh
|
||||
cim
|
||||
CImage
|
||||
cla
|
||||
claude
|
||||
CLASSDC
|
||||
classguid
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
claude
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
@@ -261,6 +261,7 @@ colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
Convs
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -281,7 +282,6 @@ CONTEXTHELP
|
||||
CONTEXTMENUHANDLER
|
||||
contractversion
|
||||
CONTROLPARENT
|
||||
Convs
|
||||
copiedcolorrepresentation
|
||||
coppied
|
||||
copyable
|
||||
@@ -348,14 +348,12 @@ datareader
|
||||
datatracker
|
||||
dataversion
|
||||
Dayof
|
||||
dbcc
|
||||
DBID
|
||||
DBLCLKS
|
||||
DBLEPSILON
|
||||
DBPROP
|
||||
DBPROPIDSET
|
||||
DBPROPSET
|
||||
DBT
|
||||
DCBA
|
||||
DCOM
|
||||
DComposition
|
||||
@@ -373,7 +371,8 @@ DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
defaulttonearest
|
||||
DEFAULTTONEAREST
|
||||
Defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -395,19 +394,14 @@ DESKTOPVERTRES
|
||||
devblogs
|
||||
devdocs
|
||||
devenv
|
||||
DEVICEINTERFACE
|
||||
devicetype
|
||||
DEVINTERFACE
|
||||
devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
DEVNODES
|
||||
devpal
|
||||
DEVTYP
|
||||
dfx
|
||||
DIALOGEX
|
||||
diffs
|
||||
digicert
|
||||
diffs
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
@@ -550,6 +544,7 @@ fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FInc
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -570,7 +565,6 @@ FILESYSPATH
|
||||
Filetime
|
||||
FILEVERSION
|
||||
FILTERMODE
|
||||
FInc
|
||||
findfast
|
||||
findmymouse
|
||||
FIXEDFILEINFO
|
||||
@@ -672,14 +666,13 @@ HCRYPTPROV
|
||||
hcursor
|
||||
hcwhite
|
||||
hdc
|
||||
HDEVNOTIFY
|
||||
hdr
|
||||
hdrop
|
||||
hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
hgdiobj
|
||||
HGFE
|
||||
hgdiobj
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
@@ -755,9 +748,9 @@ HWNDPARENT
|
||||
HWNDPREV
|
||||
hyjiacan
|
||||
IAI
|
||||
icf
|
||||
ICONERROR
|
||||
ICONLOCATION
|
||||
icf
|
||||
IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
@@ -848,8 +841,8 @@ jeli
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jjw
|
||||
jobject
|
||||
JOBOBJECT
|
||||
jobject
|
||||
jpe
|
||||
jpnime
|
||||
Jsons
|
||||
@@ -936,9 +929,9 @@ LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
lpch
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpch
|
||||
lpcmi
|
||||
LPCMINVOKECOMMANDINFO
|
||||
LPCREATESTRUCT
|
||||
@@ -954,7 +947,6 @@ LPMONITORINFO
|
||||
LPOSVERSIONINFOEXW
|
||||
LPQUERY
|
||||
lprc
|
||||
LPrivate
|
||||
LPSAFEARRAY
|
||||
lpstr
|
||||
lpsz
|
||||
@@ -964,6 +956,7 @@ lptpm
|
||||
LPTR
|
||||
LPTSTR
|
||||
lpv
|
||||
LPrivate
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
@@ -1007,13 +1000,13 @@ mber
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
mcp
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
meme
|
||||
mcp
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
@@ -1049,8 +1042,8 @@ mmi
|
||||
mmsys
|
||||
mobileredirect
|
||||
mockapi
|
||||
MODALFRAME
|
||||
modelcontextprotocol
|
||||
MODALFRAME
|
||||
MODESPRUNED
|
||||
MONITORENUMPROC
|
||||
MONITORINFO
|
||||
@@ -1094,9 +1087,9 @@ MSLLHOOKSTRUCT
|
||||
Mso
|
||||
msrc
|
||||
msstore
|
||||
mstsc
|
||||
msvcp
|
||||
MT
|
||||
mstsc
|
||||
MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
@@ -1106,11 +1099,11 @@ muxxc
|
||||
muxxh
|
||||
MVPs
|
||||
mvvm
|
||||
myorg
|
||||
myrepo
|
||||
MVVMTK
|
||||
MWBEx
|
||||
MYICON
|
||||
myorg
|
||||
myrepo
|
||||
NAMECHANGE
|
||||
namespaceanddescendants
|
||||
nao
|
||||
@@ -1251,8 +1244,10 @@ opencode
|
||||
OPENFILENAME
|
||||
openrdp
|
||||
opensource
|
||||
openurl
|
||||
openxmlformats
|
||||
ollama
|
||||
onnx
|
||||
openurl
|
||||
OPTIMIZEFORINVOKE
|
||||
ORPHANEDDIALOGTITLE
|
||||
ORSCANS
|
||||
@@ -1469,6 +1464,7 @@ rbhid
|
||||
Rbuttondown
|
||||
rclsid
|
||||
RCZOOMIT
|
||||
remotedesktop
|
||||
rdp
|
||||
RDW
|
||||
READMODE
|
||||
@@ -1497,7 +1493,6 @@ remappings
|
||||
REMAPSUCCESSFUL
|
||||
REMAPUNSUCCESSFUL
|
||||
Remotable
|
||||
remotedesktop
|
||||
remoteip
|
||||
Removelnk
|
||||
renamable
|
||||
@@ -1531,8 +1526,8 @@ RIGHTSCROLLBAR
|
||||
riid
|
||||
RKey
|
||||
RNumber
|
||||
rollups
|
||||
rop
|
||||
rollups
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
rpcrt
|
||||
@@ -1771,7 +1766,8 @@ SVGIO
|
||||
svgz
|
||||
SVSI
|
||||
SWFO
|
||||
swp
|
||||
SWP
|
||||
Swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1790,7 +1786,8 @@ SYSKEY
|
||||
syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
sysmenu
|
||||
SYSMENU
|
||||
Sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
@@ -1894,9 +1891,9 @@ uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
Ultrawide
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
ums
|
||||
uncompilable
|
||||
UNCPRIORITY
|
||||
UNDNAME
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate an 80-character git commit title for the local diff'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/create-pr-summary.prompt.md
vendored
1
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -1,5 +1,6 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Generate a PowerToys-ready pull request description from the local diff'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/fix-issue.prompt.md
vendored
1
.github/prompts/fix-issue.prompt.md
vendored
@@ -1,5 +1,6 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Execute the fix for a GitHub issue using the previously generated implementation plan'
|
||||
---
|
||||
|
||||
|
||||
70
.github/prompts/fix-pr-active-comments.prompt.md
vendored
70
.github/prompts/fix-pr-active-comments.prompt.md
vendored
@@ -1,70 +0,0 @@
|
||||
---
|
||||
description: 'Fix active pull request comments with scoped changes'
|
||||
name: 'fix-pr-active-comments'
|
||||
agent: 'agent'
|
||||
argument-hint: 'PR number or active PR URL'
|
||||
---
|
||||
|
||||
# Fix Active PR Comments
|
||||
|
||||
## Mission
|
||||
Resolve active pull request comments by applying only simple fixes. For complex refactors, write a plan instead of changing code.
|
||||
|
||||
## Scope & Preconditions
|
||||
- You must have an active pull request context or a provided PR number.
|
||||
- Only implement simple changes. Do not implement large refactors.
|
||||
- If required context is missing, request it and stop.
|
||||
|
||||
## Inputs
|
||||
- Required: ${input:pr_number:PR number or URL}
|
||||
- Optional: ${input:comment_scope:files or areas to focus on}
|
||||
- Optional: ${input:fixing_guidelines:additional fixing guidelines from the user}
|
||||
|
||||
## Workflow
|
||||
1. Locate all active (unresolved) PR review comments for the given PR.
|
||||
2. For each comment, classify the change scope:
|
||||
- Simple change: limited edits, localized fix, low risk, no broad redesign.
|
||||
- Large refactor: multi-file redesign, architecture change, or risky behavior change.
|
||||
3. For each large refactor request:
|
||||
- Do not modify code.
|
||||
- Write a planning document to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
4. For each simple change request:
|
||||
- Implement the fix with minimal edits.
|
||||
- Run quick checks if needed.
|
||||
- Commit and push the change.
|
||||
5. For comments that seem invalid, unclear, or not applicable (even if simple):
|
||||
- Do not change code.
|
||||
- Add the item to a summary table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Consult back to the end user in a friendly, polite tone.
|
||||
6. Respond to each comment that you fixed:
|
||||
- Reply in the active conversation.
|
||||
- Use a polite or friendly tone.
|
||||
- Keep the response under 200 words.
|
||||
- Resolve the comment after replying.
|
||||
|
||||
## Output Expectations
|
||||
- Simple fixes: code changes committed and pushed.
|
||||
- Large refactors: a plan file saved to Generated Files/prReview/${input:pr_number}/fixPlan/.
|
||||
- Invalid or unclear comments: captured in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md.
|
||||
- Each fixed comment has a reply under 200 words and is resolved.
|
||||
|
||||
## Plan File Template
|
||||
Use this template for each large refactor item:
|
||||
|
||||
# Fix Plan: <short title>
|
||||
|
||||
## Context
|
||||
- Comment link:
|
||||
- Impacted areas:
|
||||
|
||||
## Overview Table Template
|
||||
Use this table in Generated Files/prReview/${input:pr_number}/fixPlan/overview.md:
|
||||
|
||||
| Comment link | Summary | Reason not applied | Suggested follow-up |
|
||||
| --- | --- | --- | --- |
|
||||
| | | | |
|
||||
|
||||
## Quality Assurance
|
||||
- Verify plan file path exists.
|
||||
- Ensure no code changes were made for large refactor items.
|
||||
- Confirm replies are under 200 words and comments are resolved.
|
||||
1
.github/prompts/fix-spelling.prompt.md
vendored
1
.github/prompts/fix-spelling.prompt.md
vendored
@@ -1,5 +1,6 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/review-issue.prompt.md
vendored
1
.github/prompts/review-issue.prompt.md
vendored
@@ -1,5 +1,6 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Review a GitHub issue, score it (0-100), and generate an implementation plan'
|
||||
---
|
||||
|
||||
|
||||
1
.github/prompts/review-pr.prompt.md
vendored
1
.github/prompts/review-pr.prompt.md
vendored
@@ -1,5 +1,6 @@
|
||||
---
|
||||
agent: 'agent'
|
||||
model: 'GPT-5.1-Codex-Max'
|
||||
description: 'Perform a comprehensive PR review with per-step Markdown and machine-readable outputs'
|
||||
---
|
||||
|
||||
|
||||
@@ -218,6 +218,10 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PerformanceMonitor/Microsoft.CmdPal.Ext.PerformanceMonitor.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
<Project Path="src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.PowerToys/Microsoft.CmdPal.Ext.PowerToys.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
|
||||
@@ -104,67 +104,6 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (action == RUN_AS_USER || action == RUN_AS_ADMIN)
|
||||
{
|
||||
// Handle "Run as different user" and "Run as administrator" actions.
|
||||
// This is used by Command Palette to work around WinUI3/MSIX packaging limitations
|
||||
// where ShellExecute with "runas user"/"runas" verbs doesn't work properly from packaged apps.
|
||||
int nextArg = 2;
|
||||
|
||||
std::wstring_view target;
|
||||
std::wstring_view workingDir;
|
||||
|
||||
while (nextArg < nArgs)
|
||||
{
|
||||
if (std::wstring_view(args[nextArg]) == L"-target" && nextArg + 1 < nArgs)
|
||||
{
|
||||
target = args[nextArg + 1];
|
||||
nextArg += 2;
|
||||
}
|
||||
else if (std::wstring_view(args[nextArg]) == L"-workingDir" && nextArg + 1 < nArgs)
|
||||
{
|
||||
workingDir = args[nextArg + 1];
|
||||
nextArg += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
nextArg++;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.empty())
|
||||
{
|
||||
Logger::error(L"ActionRunner: {} called without -target argument", action);
|
||||
return 1;
|
||||
}
|
||||
|
||||
Logger::trace(L"ActionRunner: {} target='{}' workingDir='{}'", action, target, workingDir);
|
||||
|
||||
SHELLEXECUTEINFOW sei = { sizeof(sei) };
|
||||
sei.fMask = SEE_MASK_FLAG_NO_UI;
|
||||
sei.lpFile = target.data();
|
||||
sei.lpDirectory = workingDir.empty() ? nullptr : workingDir.data();
|
||||
// cspell:ignore runasuser (Windows shell verb for "Run as different user")
|
||||
sei.lpVerb = (action == RUN_AS_ADMIN) ? L"runas" : L"runasuser";
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
|
||||
if (!ShellExecuteExW(&sei))
|
||||
{
|
||||
DWORD error = GetLastError();
|
||||
if (error == ERROR_CANCELLED)
|
||||
{
|
||||
// User cancelled the UAC/credential dialog - this is expected behavior
|
||||
Logger::trace(L"ActionRunner: User cancelled {} dialog for '{}'", action, target);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"ActionRunner: ShellExecuteEx failed for {} '{}': error {}", action, target, error);
|
||||
}
|
||||
return static_cast<int>(error);
|
||||
}
|
||||
|
||||
Logger::trace(L"ActionRunner: Successfully launched '{}' with {}", target, action);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License. -->
|
||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.18" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyNamespaces>
|
||||
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
|
||||
</policyNamespaces>
|
||||
<resources minRequiredRevision="1.19"/><!-- Last changed with PowerToys v0.97.0 -->
|
||||
<resources minRequiredRevision="1.18"/><!-- Last changed with PowerToys v0.96.0 -->
|
||||
<supportedOn>
|
||||
<definitions>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
|
||||
@@ -27,7 +27,6 @@
|
||||
<definition name="SUPPORTED_POWERTOYS_0_89_0" displayName="$(string.SUPPORTED_POWERTOYS_0_89_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_90_0" displayName="$(string.SUPPORTED_POWERTOYS_0_90_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_96_0" displayName="$(string.SUPPORTED_POWERTOYS_0_96_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_97_0" displayName="$(string.SUPPORTED_POWERTOYS_0_97_0)"/>
|
||||
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
|
||||
</definitions>
|
||||
</supportedOn>
|
||||
@@ -339,16 +338,6 @@
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="ConfigureEnabledUtilityCursorWrap" class="Both" displayName="$(string.ConfigureEnabledUtilityCursorWrap)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityCursorWrap">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_97_0" />
|
||||
<enabledValue>
|
||||
<decimal value="1" />
|
||||
</enabledValue>
|
||||
<disabledValue>
|
||||
<decimal value="0" />
|
||||
</disabledValue>
|
||||
</policy>
|
||||
<policy name="ConfigureEnabledUtilityFindMyMouse" class="Both" displayName="$(string.ConfigureEnabledUtilityFindMyMouse)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityFindMyMouse">
|
||||
<parentCategory ref="PowerToys" />
|
||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (c) Microsoft Corporation.
|
||||
Licensed under the MIT License. -->
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.18" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||
<displayName>PowerToys</displayName>
|
||||
<description>PowerToys</description>
|
||||
<resources>
|
||||
@@ -34,7 +34,6 @@
|
||||
<string id="SUPPORTED_POWERTOYS_0_89_0">PowerToys version 0.89.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_90_0">PowerToys version 0.90.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_96_0">PowerToys version 0.96.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_97_0">PowerToys version 0.97.0 or later</string>
|
||||
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
|
||||
|
||||
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
|
||||
@@ -267,7 +266,6 @@ If you don't configure this policy, the user will be able to control the setting
|
||||
<string id="ConfigureEnabledUtilityKeyboardManager">Keyboard Manager: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityFindMyMouse">Find My Mouse: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityMouseHighlighter">Mouse Highlighter: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityCursorWrap">CursorWrap: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityMouseJump">Mouse Jump: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityMousePointerCrosshairs">Mouse Pointer Crosshairs: Configure enabled state</string>
|
||||
<string id="ConfigureEnabledUtilityMouseWithoutBorders">Mouse Without Borders: Configure enabled state</string>
|
||||
|
||||
@@ -84,17 +84,14 @@
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="CursorWrapCore.h" />
|
||||
<ClInclude Include="CursorWrapTests.h" />
|
||||
<ClInclude Include="MonitorTopology.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="CursorWrapCore.cpp" />
|
||||
<ClCompile Include="dllmain.cpp" />
|
||||
<ClCompile Include="MonitorTopology.cpp" />
|
||||
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
|
||||
@@ -1,268 +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.
|
||||
|
||||
#include "pch.h"
|
||||
#include "CursorWrapCore.h"
|
||||
#include "../../../common/logger/logger.h"
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <ctime>
|
||||
|
||||
CursorWrapCore::CursorWrapCore()
|
||||
{
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
std::wstring CursorWrapCore::GenerateTopologyJSON() const
|
||||
{
|
||||
std::wostringstream json;
|
||||
|
||||
// Get current time
|
||||
auto now = std::time(nullptr);
|
||||
std::tm tm{};
|
||||
localtime_s(&tm, &now);
|
||||
|
||||
wchar_t computerName[MAX_COMPUTERNAME_LENGTH + 1] = {0};
|
||||
DWORD size = MAX_COMPUTERNAME_LENGTH + 1;
|
||||
GetComputerNameW(computerName, &size);
|
||||
|
||||
wchar_t userName[256] = {0};
|
||||
size = 256;
|
||||
GetUserNameW(userName, &size);
|
||||
|
||||
json << L"{\n";
|
||||
json << L" \"captured_at\": \"" << std::put_time(&tm, L"%Y-%m-%dT%H:%M:%S%z") << L"\",\n";
|
||||
json << L" \"computer_name\": \"" << computerName << L"\",\n";
|
||||
json << L" \"user_name\": \"" << userName << L"\",\n";
|
||||
json << L" \"monitor_count\": " << m_monitors.size() << L",\n";
|
||||
json << L" \"monitors\": [\n";
|
||||
|
||||
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||
{
|
||||
const auto& monitor = m_monitors[i];
|
||||
|
||||
// Get DPI for this monitor
|
||||
UINT dpiX = 96, dpiY = 96;
|
||||
POINT center = {
|
||||
(monitor.rect.left + monitor.rect.right) / 2,
|
||||
(monitor.rect.top + monitor.rect.bottom) / 2
|
||||
};
|
||||
HMONITOR hMon = MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST);
|
||||
if (hMon)
|
||||
{
|
||||
// Try GetDpiForMonitor (requires linking Shcore.lib)
|
||||
using GetDpiForMonitorFunc = HRESULT (WINAPI *)(HMONITOR, int, UINT*, UINT*);
|
||||
HMODULE shcore = LoadLibraryW(L"Shcore.dll");
|
||||
if (shcore)
|
||||
{
|
||||
auto getDpi = reinterpret_cast<GetDpiForMonitorFunc>(GetProcAddress(shcore, "GetDpiForMonitor"));
|
||||
if (getDpi)
|
||||
{
|
||||
getDpi(hMon, 0, &dpiX, &dpiY); // MDT_EFFECTIVE_DPI = 0
|
||||
}
|
||||
FreeLibrary(shcore);
|
||||
}
|
||||
}
|
||||
|
||||
int scalingPercent = static_cast<int>((dpiX / 96.0) * 100);
|
||||
|
||||
json << L" {\n";
|
||||
json << L" \"left\": " << monitor.rect.left << L",\n";
|
||||
json << L" \"top\": " << monitor.rect.top << L",\n";
|
||||
json << L" \"right\": " << monitor.rect.right << L",\n";
|
||||
json << L" \"bottom\": " << monitor.rect.bottom << L",\n";
|
||||
json << L" \"width\": " << (monitor.rect.right - monitor.rect.left) << L",\n";
|
||||
json << L" \"height\": " << (monitor.rect.bottom - monitor.rect.top) << L",\n";
|
||||
json << L" \"dpi\": " << dpiX << L",\n";
|
||||
json << L" \"scaling_percent\": " << scalingPercent << L",\n";
|
||||
json << L" \"primary\": " << (monitor.isPrimary ? L"true" : L"false") << L",\n";
|
||||
json << L" \"monitor_id\": " << monitor.monitorId << L"\n";
|
||||
json << L" }";
|
||||
if (i < m_monitors.size() - 1)
|
||||
{
|
||||
json << L",";
|
||||
}
|
||||
json << L"\n";
|
||||
}
|
||||
|
||||
json << L" ]\n";
|
||||
json << L"}";
|
||||
|
||||
return json.str();
|
||||
}
|
||||
#endif
|
||||
|
||||
void CursorWrapCore::UpdateMonitorInfo()
|
||||
{
|
||||
size_t previousMonitorCount = m_monitors.size();
|
||||
Logger::info(L"======= UPDATE MONITOR INFO START =======");
|
||||
Logger::info(L"Previous monitor count: {}", previousMonitorCount);
|
||||
|
||||
m_monitors.clear();
|
||||
|
||||
EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) -> BOOL {
|
||||
auto* self = reinterpret_cast<CursorWrapCore*>(lParam);
|
||||
|
||||
MONITORINFO mi{};
|
||||
mi.cbSize = sizeof(MONITORINFO);
|
||||
if (GetMonitorInfo(hMonitor, &mi))
|
||||
{
|
||||
MonitorInfo info{};
|
||||
info.hMonitor = hMonitor; // Store handle for direct comparison later
|
||||
info.rect = mi.rcMonitor;
|
||||
info.isPrimary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0;
|
||||
info.monitorId = static_cast<int>(self->m_monitors.size());
|
||||
self->m_monitors.push_back(info);
|
||||
|
||||
Logger::info(L"Enumerated monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}",
|
||||
info.monitorId, reinterpret_cast<uintptr_t>(hMonitor),
|
||||
mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom,
|
||||
info.isPrimary ? L"yes" : L"no");
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}, reinterpret_cast<LPARAM>(this));
|
||||
|
||||
if (previousMonitorCount != m_monitors.size())
|
||||
{
|
||||
Logger::info(L"*** MONITOR CONFIGURATION CHANGED: {} -> {} monitors ***",
|
||||
previousMonitorCount, m_monitors.size());
|
||||
}
|
||||
|
||||
m_topology.Initialize(m_monitors);
|
||||
|
||||
// Log monitor configuration summary
|
||||
Logger::info(L"Monitor configuration updated: {} monitor(s)", m_monitors.size());
|
||||
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||
{
|
||||
const auto& m = m_monitors[i];
|
||||
int width = m.rect.right - m.rect.left;
|
||||
int height = m.rect.bottom - m.rect.top;
|
||||
Logger::info(L" Monitor {}: {}x{} at ({}, {}){}",
|
||||
i, width, height, m.rect.left, m.rect.top,
|
||||
m.isPrimary ? L" [PRIMARY]" : L"");
|
||||
}
|
||||
Logger::info(L" Detected {} outer edges for cursor wrapping", m_topology.GetOuterEdges().size());
|
||||
|
||||
// Detect and log monitor gaps
|
||||
auto gaps = m_topology.DetectMonitorGaps();
|
||||
if (!gaps.empty())
|
||||
{
|
||||
Logger::warn(L"Monitor configuration has coordinate gaps that may prevent wrapping:");
|
||||
for (const auto& gap : gaps)
|
||||
{
|
||||
Logger::warn(L" Gap between Monitor {} and Monitor {}: {}px horizontal gap, {}px vertical overlap",
|
||||
gap.monitor1Index, gap.monitor2Index, gap.horizontalGap, gap.verticalOverlap);
|
||||
}
|
||||
Logger::warn(L" If monitors appear snapped in Display Settings but show gaps here:");
|
||||
Logger::warn(L" 1. Try dragging monitors apart and snapping them back together");
|
||||
Logger::warn(L" 2. Update your GPU drivers");
|
||||
}
|
||||
|
||||
Logger::info(L"======= UPDATE MONITOR INFO END =======");
|
||||
}
|
||||
|
||||
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode)
|
||||
{
|
||||
// Check if wrapping should be disabled during drag
|
||||
if (disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
OutputDebugStringW(L"[CursorWrap] [DRAG] Left mouse button down - skipping wrap\n");
|
||||
#endif
|
||||
return currentPos;
|
||||
}
|
||||
|
||||
// Convert int wrapMode to WrapMode enum
|
||||
WrapMode mode = static_cast<WrapMode>(wrapMode);
|
||||
|
||||
#ifdef _DEBUG
|
||||
{
|
||||
std::wostringstream oss;
|
||||
oss << L"[CursorWrap] [MOVE] Cursor at (" << currentPos.x << L", " << currentPos.y << L")";
|
||||
|
||||
// Get current monitor and identify which one
|
||||
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
|
||||
RECT monitorRect;
|
||||
if (m_topology.GetMonitorRect(currentMonitor, monitorRect))
|
||||
{
|
||||
// Find monitor ID
|
||||
int monitorId = -1;
|
||||
for (const auto& monitor : m_monitors)
|
||||
{
|
||||
if (monitor.rect.left == monitorRect.left &&
|
||||
monitor.rect.top == monitorRect.top &&
|
||||
monitor.rect.right == monitorRect.right &&
|
||||
monitor.rect.bottom == monitorRect.bottom)
|
||||
{
|
||||
monitorId = monitor.monitorId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
oss << L" on Monitor " << monitorId << L" [" << monitorRect.left << L".." << monitorRect.right
|
||||
<< L", " << monitorRect.top << L".." << monitorRect.bottom << L"]";
|
||||
}
|
||||
else
|
||||
{
|
||||
oss << L" (beyond monitor bounds)";
|
||||
}
|
||||
oss << L"\n";
|
||||
OutputDebugStringW(oss.str().c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
// Get current monitor
|
||||
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
// Check if cursor is on an outer edge (filtered by wrap mode)
|
||||
EdgeType edgeType;
|
||||
if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
static bool lastWasNotOuter = false;
|
||||
if (!lastWasNotOuter)
|
||||
{
|
||||
OutputDebugStringW(L"[CursorWrap] [MOVE] Not on outer edge - no wrapping\n");
|
||||
lastWasNotOuter = true;
|
||||
}
|
||||
#endif
|
||||
return currentPos; // Not on an outer edge
|
||||
}
|
||||
|
||||
#ifdef _DEBUG
|
||||
{
|
||||
const wchar_t* edgeStr = L"Unknown";
|
||||
switch (edgeType)
|
||||
{
|
||||
case EdgeType::Left: edgeStr = L"Left"; break;
|
||||
case EdgeType::Right: edgeStr = L"Right"; break;
|
||||
case EdgeType::Top: edgeStr = L"Top"; break;
|
||||
case EdgeType::Bottom: edgeStr = L"Bottom"; break;
|
||||
}
|
||||
std::wostringstream oss;
|
||||
oss << L"[CursorWrap] [EDGE] Detected outer " << edgeStr << L" edge at (" << currentPos.x << L", " << currentPos.y << L")\n";
|
||||
OutputDebugStringW(oss.str().c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
// Calculate wrap destination
|
||||
POINT newPos = m_topology.GetWrapDestination(currentMonitor, currentPos, edgeType);
|
||||
|
||||
#ifdef _DEBUG
|
||||
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
|
||||
{
|
||||
std::wostringstream oss;
|
||||
oss << L"[CursorWrap] [WRAP] Position change: (" << currentPos.x << L", " << currentPos.y
|
||||
<< L") -> (" << newPos.x << L", " << newPos.y << L")\n";
|
||||
oss << L"[CursorWrap] [WRAP] Delta: (" << (newPos.x - currentPos.x) << L", " << (newPos.y - currentPos.y) << L")\n";
|
||||
OutputDebugStringW(oss.str().c_str());
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputDebugStringW(L"[CursorWrap] [WRAP] No position change (same-monitor wrap?)\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
return newPos;
|
||||
}
|
||||
@@ -1,33 +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.
|
||||
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include "MonitorTopology.h"
|
||||
|
||||
// Core cursor wrapping engine
|
||||
class CursorWrapCore
|
||||
{
|
||||
public:
|
||||
CursorWrapCore();
|
||||
|
||||
void UpdateMonitorInfo();
|
||||
|
||||
// Handle mouse move with wrap mode filtering
|
||||
// wrapMode: 0=Both, 1=VerticalOnly, 2=HorizontalOnly
|
||||
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode);
|
||||
|
||||
const std::vector<MonitorInfo>& GetMonitors() const { return m_monitors; }
|
||||
const MonitorTopology& GetTopology() const { return m_topology; }
|
||||
|
||||
private:
|
||||
#ifdef _DEBUG
|
||||
std::wstring GenerateTopologyJSON() const;
|
||||
#endif
|
||||
|
||||
std::vector<MonitorInfo> m_monitors;
|
||||
MonitorTopology m_topology;
|
||||
};
|
||||
@@ -1,546 +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.
|
||||
|
||||
#include "pch.h"
|
||||
#include "MonitorTopology.h"
|
||||
#include "../../../common/logger/logger.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
|
||||
{
|
||||
Logger::info(L"======= TOPOLOGY INITIALIZATION START =======");
|
||||
Logger::info(L"Initializing edge-based topology for {} monitors", monitors.size());
|
||||
|
||||
m_monitors = monitors;
|
||||
m_outerEdges.clear();
|
||||
m_edgeMap.clear();
|
||||
|
||||
if (monitors.empty())
|
||||
{
|
||||
Logger::warn(L"No monitors provided to Initialize");
|
||||
return;
|
||||
}
|
||||
|
||||
// Log monitor details
|
||||
for (size_t i = 0; i < monitors.size(); ++i)
|
||||
{
|
||||
const auto& m = monitors[i];
|
||||
Logger::info(L"Monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}",
|
||||
i, reinterpret_cast<uintptr_t>(m.hMonitor),
|
||||
m.rect.left, m.rect.top, m.rect.right, m.rect.bottom,
|
||||
m.isPrimary ? L"yes" : L"no");
|
||||
}
|
||||
|
||||
BuildEdgeMap();
|
||||
IdentifyOuterEdges();
|
||||
|
||||
Logger::info(L"Found {} outer edges", m_outerEdges.size());
|
||||
for (const auto& edge : m_outerEdges)
|
||||
{
|
||||
const wchar_t* typeStr = L"Unknown";
|
||||
switch (edge.type)
|
||||
{
|
||||
case EdgeType::Left: typeStr = L"Left"; break;
|
||||
case EdgeType::Right: typeStr = L"Right"; break;
|
||||
case EdgeType::Top: typeStr = L"Top"; break;
|
||||
case EdgeType::Bottom: typeStr = L"Bottom"; break;
|
||||
}
|
||||
Logger::info(L"Outer edge: Monitor {} {} at position {}, range [{}, {}]",
|
||||
edge.monitorIndex, typeStr, edge.position, edge.start, edge.end);
|
||||
}
|
||||
Logger::info(L"======= TOPOLOGY INITIALIZATION COMPLETE =======");
|
||||
}
|
||||
|
||||
void MonitorTopology::BuildEdgeMap()
|
||||
{
|
||||
// Create edges for each monitor using monitor index (not HMONITOR)
|
||||
// This is important because HMONITOR handles can change when monitors are
|
||||
// added/removed dynamically, but indices remain stable within a single
|
||||
// topology configuration
|
||||
for (size_t idx = 0; idx < m_monitors.size(); ++idx)
|
||||
{
|
||||
const auto& monitor = m_monitors[idx];
|
||||
int monitorIndex = static_cast<int>(idx);
|
||||
|
||||
// Left edge
|
||||
MonitorEdge leftEdge;
|
||||
leftEdge.monitorIndex = monitorIndex;
|
||||
leftEdge.type = EdgeType::Left;
|
||||
leftEdge.position = monitor.rect.left;
|
||||
leftEdge.start = monitor.rect.top;
|
||||
leftEdge.end = monitor.rect.bottom;
|
||||
leftEdge.isOuter = true; // Will be updated in IdentifyOuterEdges
|
||||
m_edgeMap[{monitorIndex, EdgeType::Left}] = leftEdge;
|
||||
|
||||
// Right edge
|
||||
MonitorEdge rightEdge;
|
||||
rightEdge.monitorIndex = monitorIndex;
|
||||
rightEdge.type = EdgeType::Right;
|
||||
rightEdge.position = monitor.rect.right - 1;
|
||||
rightEdge.start = monitor.rect.top;
|
||||
rightEdge.end = monitor.rect.bottom;
|
||||
rightEdge.isOuter = true;
|
||||
m_edgeMap[{monitorIndex, EdgeType::Right}] = rightEdge;
|
||||
|
||||
// Top edge
|
||||
MonitorEdge topEdge;
|
||||
topEdge.monitorIndex = monitorIndex;
|
||||
topEdge.type = EdgeType::Top;
|
||||
topEdge.position = monitor.rect.top;
|
||||
topEdge.start = monitor.rect.left;
|
||||
topEdge.end = monitor.rect.right;
|
||||
topEdge.isOuter = true;
|
||||
m_edgeMap[{monitorIndex, EdgeType::Top}] = topEdge;
|
||||
|
||||
// Bottom edge
|
||||
MonitorEdge bottomEdge;
|
||||
bottomEdge.monitorIndex = monitorIndex;
|
||||
bottomEdge.type = EdgeType::Bottom;
|
||||
bottomEdge.position = monitor.rect.bottom - 1;
|
||||
bottomEdge.start = monitor.rect.left;
|
||||
bottomEdge.end = monitor.rect.right;
|
||||
bottomEdge.isOuter = true;
|
||||
m_edgeMap[{monitorIndex, EdgeType::Bottom}] = bottomEdge;
|
||||
}
|
||||
}
|
||||
|
||||
void MonitorTopology::IdentifyOuterEdges()
|
||||
{
|
||||
const int tolerance = 50;
|
||||
|
||||
// Check each edge against all other edges to find adjacent ones
|
||||
for (auto& [key1, edge1] : m_edgeMap)
|
||||
{
|
||||
for (const auto& [key2, edge2] : m_edgeMap)
|
||||
{
|
||||
if (edge1.monitorIndex == edge2.monitorIndex)
|
||||
{
|
||||
continue; // Same monitor
|
||||
}
|
||||
|
||||
// Check if edges are adjacent
|
||||
if (EdgesAreAdjacent(edge1, edge2, tolerance))
|
||||
{
|
||||
edge1.isOuter = false;
|
||||
break; // This edge has an adjacent monitor
|
||||
}
|
||||
}
|
||||
|
||||
if (edge1.isOuter)
|
||||
{
|
||||
m_outerEdges.push_back(edge1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MonitorTopology::EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance) const
|
||||
{
|
||||
// Edges must be opposite types to be adjacent
|
||||
bool oppositeTypes = false;
|
||||
|
||||
if ((edge1.type == EdgeType::Left && edge2.type == EdgeType::Right) ||
|
||||
(edge1.type == EdgeType::Right && edge2.type == EdgeType::Left) ||
|
||||
(edge1.type == EdgeType::Top && edge2.type == EdgeType::Bottom) ||
|
||||
(edge1.type == EdgeType::Bottom && edge2.type == EdgeType::Top))
|
||||
{
|
||||
oppositeTypes = true;
|
||||
}
|
||||
|
||||
if (!oppositeTypes)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if positions are within tolerance
|
||||
if (abs(edge1.position - edge2.position) > tolerance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if perpendicular ranges overlap
|
||||
int overlapStart = max(edge1.start, edge2.start);
|
||||
int overlapEnd = min(edge1.end, edge2.end);
|
||||
|
||||
return overlapEnd > overlapStart + tolerance;
|
||||
}
|
||||
|
||||
bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const
|
||||
{
|
||||
RECT monitorRect;
|
||||
if (!GetMonitorRect(monitor, monitorRect))
|
||||
{
|
||||
Logger::warn(L"IsOnOuterEdge: GetMonitorRect failed for monitor handle {}", reinterpret_cast<uintptr_t>(monitor));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get monitor index for edge map lookup
|
||||
int monitorIndex = GetMonitorIndex(monitor);
|
||||
if (monitorIndex < 0)
|
||||
{
|
||||
Logger::warn(L"IsOnOuterEdge: Monitor index not found for handle {} at cursor ({}, {})",
|
||||
reinterpret_cast<uintptr_t>(monitor), cursorPos.x, cursorPos.y);
|
||||
return false; // Monitor not found in our list
|
||||
}
|
||||
|
||||
// Check each edge type
|
||||
const int edgeThreshold = 1;
|
||||
|
||||
// At corners, multiple edges may match - collect all candidates and try each
|
||||
// to find one with a valid wrap destination
|
||||
std::vector<EdgeType> candidateEdges;
|
||||
|
||||
// Left edge - only if mode allows horizontal wrapping
|
||||
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) &&
|
||||
cursorPos.x <= monitorRect.left + edgeThreshold)
|
||||
{
|
||||
auto it = m_edgeMap.find({monitorIndex, EdgeType::Left});
|
||||
if (it != m_edgeMap.end() && it->second.isOuter)
|
||||
{
|
||||
candidateEdges.push_back(EdgeType::Left);
|
||||
}
|
||||
}
|
||||
|
||||
// Right edge - only if mode allows horizontal wrapping
|
||||
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) &&
|
||||
cursorPos.x >= monitorRect.right - 1 - edgeThreshold)
|
||||
{
|
||||
auto it = m_edgeMap.find({monitorIndex, EdgeType::Right});
|
||||
if (it != m_edgeMap.end())
|
||||
{
|
||||
if (it->second.isOuter)
|
||||
{
|
||||
candidateEdges.push_back(EdgeType::Right);
|
||||
}
|
||||
// Debug: Log why right edge isn't outer
|
||||
else
|
||||
{
|
||||
Logger::trace(L"IsOnOuterEdge: Monitor {} right edge is NOT outer (inner edge)", monitorIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Top edge - only if mode allows vertical wrapping
|
||||
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) &&
|
||||
cursorPos.y <= monitorRect.top + edgeThreshold)
|
||||
{
|
||||
auto it = m_edgeMap.find({monitorIndex, EdgeType::Top});
|
||||
if (it != m_edgeMap.end() && it->second.isOuter)
|
||||
{
|
||||
candidateEdges.push_back(EdgeType::Top);
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom edge - only if mode allows vertical wrapping
|
||||
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) &&
|
||||
cursorPos.y >= monitorRect.bottom - 1 - edgeThreshold)
|
||||
{
|
||||
auto it = m_edgeMap.find({monitorIndex, EdgeType::Bottom});
|
||||
if (it != m_edgeMap.end() && it->second.isOuter)
|
||||
{
|
||||
candidateEdges.push_back(EdgeType::Bottom);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateEdges.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try each candidate edge and return first with valid wrap destination
|
||||
for (EdgeType candidate : candidateEdges)
|
||||
{
|
||||
MonitorEdge oppositeEdge = FindOppositeOuterEdge(candidate,
|
||||
(candidate == EdgeType::Left || candidate == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||
|
||||
if (oppositeEdge.monitorIndex >= 0)
|
||||
{
|
||||
outEdgeType = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const
|
||||
{
|
||||
// Get monitor index for edge map lookup
|
||||
int monitorIndex = GetMonitorIndex(fromMonitor);
|
||||
if (monitorIndex < 0)
|
||||
{
|
||||
return cursorPos; // Monitor not found
|
||||
}
|
||||
|
||||
auto it = m_edgeMap.find({monitorIndex, edgeType});
|
||||
if (it == m_edgeMap.end())
|
||||
{
|
||||
return cursorPos; // Edge not found
|
||||
}
|
||||
|
||||
const MonitorEdge& fromEdge = it->second;
|
||||
|
||||
// Calculate relative position on current edge (0.0 to 1.0)
|
||||
double relativePos = GetRelativePosition(fromEdge,
|
||||
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||
|
||||
// Find opposite outer edge
|
||||
MonitorEdge oppositeEdge = FindOppositeOuterEdge(edgeType,
|
||||
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||
|
||||
if (oppositeEdge.monitorIndex < 0)
|
||||
{
|
||||
// No opposite edge found, wrap within same monitor
|
||||
RECT monitorRect;
|
||||
if (GetMonitorRect(fromMonitor, monitorRect))
|
||||
{
|
||||
POINT result = cursorPos;
|
||||
switch (edgeType)
|
||||
{
|
||||
case EdgeType::Left:
|
||||
result.x = monitorRect.right - 2;
|
||||
break;
|
||||
case EdgeType::Right:
|
||||
result.x = monitorRect.left + 1;
|
||||
break;
|
||||
case EdgeType::Top:
|
||||
result.y = monitorRect.bottom - 2;
|
||||
break;
|
||||
case EdgeType::Bottom:
|
||||
result.y = monitorRect.top + 1;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return cursorPos;
|
||||
}
|
||||
|
||||
// Calculate target position on opposite edge
|
||||
POINT result;
|
||||
|
||||
if (edgeType == EdgeType::Left || edgeType == EdgeType::Right)
|
||||
{
|
||||
// Horizontal edge -> vertical movement
|
||||
result.x = oppositeEdge.position;
|
||||
result.y = GetAbsolutePosition(oppositeEdge, relativePos);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Vertical edge -> horizontal movement
|
||||
result.y = oppositeEdge.position;
|
||||
result.x = GetAbsolutePosition(oppositeEdge, relativePos);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MonitorEdge MonitorTopology::FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const
|
||||
{
|
||||
EdgeType targetType;
|
||||
bool findMax; // true = find max position, false = find min position
|
||||
|
||||
switch (fromEdge)
|
||||
{
|
||||
case EdgeType::Left:
|
||||
targetType = EdgeType::Right;
|
||||
findMax = true;
|
||||
break;
|
||||
case EdgeType::Right:
|
||||
targetType = EdgeType::Left;
|
||||
findMax = false;
|
||||
break;
|
||||
case EdgeType::Top:
|
||||
targetType = EdgeType::Bottom;
|
||||
findMax = true;
|
||||
break;
|
||||
case EdgeType::Bottom:
|
||||
targetType = EdgeType::Top;
|
||||
findMax = false;
|
||||
break;
|
||||
default:
|
||||
return { .monitorIndex = -1 }; // Invalid edge type
|
||||
}
|
||||
|
||||
MonitorEdge result = { .monitorIndex = -1 }; // -1 indicates not found
|
||||
int extremePosition = findMax ? INT_MIN : INT_MAX;
|
||||
|
||||
for (const auto& edge : m_outerEdges)
|
||||
{
|
||||
if (edge.type != targetType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this edge overlaps with the relative position
|
||||
if (relativePosition >= edge.start && relativePosition <= edge.end)
|
||||
{
|
||||
if ((findMax && edge.position > extremePosition) ||
|
||||
(!findMax && edge.position < extremePosition))
|
||||
{
|
||||
extremePosition = edge.position;
|
||||
result = edge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
double MonitorTopology::GetRelativePosition(const MonitorEdge& edge, int coordinate) const
|
||||
{
|
||||
if (edge.end == edge.start)
|
||||
{
|
||||
return 0.5; // Avoid division by zero
|
||||
}
|
||||
|
||||
int clamped = max(edge.start, min(coordinate, edge.end));
|
||||
// Use int64_t to avoid overflow warning C26451
|
||||
int64_t numerator = static_cast<int64_t>(clamped) - static_cast<int64_t>(edge.start);
|
||||
int64_t denominator = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
|
||||
return static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||
}
|
||||
|
||||
int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const
|
||||
{
|
||||
// Use int64_t to prevent arithmetic overflow during subtraction and multiplication
|
||||
int64_t range = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
|
||||
int64_t offset = static_cast<int64_t>(relativePosition * static_cast<double>(range));
|
||||
// Clamp result to int range before returning
|
||||
int64_t result = static_cast<int64_t>(edge.start) + offset;
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
|
||||
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
|
||||
{
|
||||
std::vector<GapInfo> gaps;
|
||||
const int gapThreshold = 50; // Same as ADJACENCY_TOLERANCE
|
||||
|
||||
// Check each pair of monitors
|
||||
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||
{
|
||||
for (size_t j = i + 1; j < m_monitors.size(); ++j)
|
||||
{
|
||||
const auto& m1 = m_monitors[i];
|
||||
const auto& m2 = m_monitors[j];
|
||||
|
||||
// Check vertical overlap
|
||||
int vOverlapStart = max(m1.rect.top, m2.rect.top);
|
||||
int vOverlapEnd = min(m1.rect.bottom, m2.rect.bottom);
|
||||
int vOverlap = vOverlapEnd - vOverlapStart;
|
||||
|
||||
if (vOverlap <= 0)
|
||||
{
|
||||
continue; // No vertical overlap, skip
|
||||
}
|
||||
|
||||
// Check horizontal gap
|
||||
int hGap = min(abs(m1.rect.right - m2.rect.left), abs(m2.rect.right - m1.rect.left));
|
||||
|
||||
if (hGap > gapThreshold)
|
||||
{
|
||||
GapInfo gap;
|
||||
gap.monitor1Index = static_cast<int>(i);
|
||||
gap.monitor2Index = static_cast<int>(j);
|
||||
gap.horizontalGap = hGap;
|
||||
gap.verticalOverlap = vOverlap;
|
||||
gaps.push_back(gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gaps;
|
||||
}
|
||||
|
||||
HMONITOR MonitorTopology::GetMonitorFromPoint(const POINT& pt) const
|
||||
{
|
||||
return MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
|
||||
}
|
||||
|
||||
bool MonitorTopology::GetMonitorRect(HMONITOR monitor, RECT& rect) const
|
||||
{
|
||||
// First try direct HMONITOR comparison
|
||||
for (const auto& monitorInfo : m_monitors)
|
||||
{
|
||||
if (monitorInfo.hMonitor == monitor)
|
||||
{
|
||||
rect = monitorInfo.rect;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: If direct comparison fails, try matching by current monitor info
|
||||
MONITORINFO mi{};
|
||||
mi.cbSize = sizeof(MONITORINFO);
|
||||
if (GetMonitorInfo(monitor, &mi))
|
||||
{
|
||||
for (const auto& monitorInfo : m_monitors)
|
||||
{
|
||||
if (monitorInfo.rect.left == mi.rcMonitor.left &&
|
||||
monitorInfo.rect.top == mi.rcMonitor.top &&
|
||||
monitorInfo.rect.right == mi.rcMonitor.right &&
|
||||
monitorInfo.rect.bottom == mi.rcMonitor.bottom)
|
||||
{
|
||||
rect = monitorInfo.rect;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
HMONITOR MonitorTopology::GetMonitorFromRect(const RECT& rect) const
|
||||
{
|
||||
return MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST);
|
||||
}
|
||||
|
||||
int MonitorTopology::GetMonitorIndex(HMONITOR monitor) const
|
||||
{
|
||||
// First try direct HMONITOR comparison (fast and accurate)
|
||||
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||
{
|
||||
if (m_monitors[i].hMonitor == monitor)
|
||||
{
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: If direct comparison fails (e.g., handle changed after display reconfiguration),
|
||||
// try matching by position. Get the monitor's current rect and find matching stored rect.
|
||||
MONITORINFO mi{};
|
||||
mi.cbSize = sizeof(MONITORINFO);
|
||||
if (GetMonitorInfo(monitor, &mi))
|
||||
{
|
||||
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||
{
|
||||
// Match by rect bounds
|
||||
if (m_monitors[i].rect.left == mi.rcMonitor.left &&
|
||||
m_monitors[i].rect.top == mi.rcMonitor.top &&
|
||||
m_monitors[i].rect.right == mi.rcMonitor.right &&
|
||||
m_monitors[i].rect.bottom == mi.rcMonitor.bottom)
|
||||
{
|
||||
Logger::trace(L"GetMonitorIndex: Found monitor {} via rect fallback (handle changed from {} to {})",
|
||||
i, reinterpret_cast<uintptr_t>(m_monitors[i].hMonitor), reinterpret_cast<uintptr_t>(monitor));
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Log all stored monitors vs the requested one for debugging
|
||||
Logger::warn(L"GetMonitorIndex: No match found. Requested monitor rect=({},{},{},{})",
|
||||
mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
|
||||
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||
{
|
||||
Logger::warn(L" Stored monitor {}: rect=({},{},{},{})",
|
||||
i, m_monitors[i].rect.left, m_monitors[i].rect.top,
|
||||
m_monitors[i].rect.right, m_monitors[i].rect.bottom);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"GetMonitorIndex: GetMonitorInfo failed for handle {}", reinterpret_cast<uintptr_t>(monitor));
|
||||
}
|
||||
|
||||
return -1; // Not found
|
||||
}
|
||||
|
||||
@@ -1,106 +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.
|
||||
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// Monitor information structure
|
||||
struct MonitorInfo
|
||||
{
|
||||
HMONITOR hMonitor; // Direct handle for accurate lookup after display changes
|
||||
RECT rect;
|
||||
bool isPrimary;
|
||||
int monitorId;
|
||||
};
|
||||
|
||||
// Edge type enumeration
|
||||
enum class EdgeType
|
||||
{
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
Top = 2,
|
||||
Bottom = 3
|
||||
};
|
||||
|
||||
// Wrap mode enumeration (matches Settings UI dropdown)
|
||||
enum class WrapMode
|
||||
{
|
||||
Both = 0, // Wrap in both directions
|
||||
VerticalOnly = 1, // Only wrap top/bottom
|
||||
HorizontalOnly = 2 // Only wrap left/right
|
||||
};
|
||||
|
||||
// Represents a single edge of a monitor
|
||||
struct MonitorEdge
|
||||
{
|
||||
int monitorIndex; // Index into m_monitors (stable across display changes)
|
||||
EdgeType type;
|
||||
int start; // For vertical edges: Y start; horizontal: X start
|
||||
int end; // For vertical edges: Y end; horizontal: X end
|
||||
int position; // For vertical edges: X coord; horizontal: Y coord
|
||||
bool isOuter; // True if no adjacent monitor touches this edge
|
||||
};
|
||||
|
||||
// Monitor topology helper - manages edge-based monitor layout
|
||||
struct MonitorTopology
|
||||
{
|
||||
void Initialize(const std::vector<MonitorInfo>& monitors);
|
||||
|
||||
// Check if cursor is on an outer edge of the given monitor
|
||||
// wrapMode filters which edges are considered (Both, VerticalOnly, HorizontalOnly)
|
||||
bool IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const;
|
||||
|
||||
// Get the wrap destination point for a cursor on an outer edge
|
||||
POINT GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const;
|
||||
|
||||
// Get monitor at point (helper)
|
||||
HMONITOR GetMonitorFromPoint(const POINT& pt) const;
|
||||
|
||||
// Get monitor rectangle (helper)
|
||||
bool GetMonitorRect(HMONITOR monitor, RECT& rect) const;
|
||||
|
||||
// Get outer edges collection (for debugging)
|
||||
const std::vector<MonitorEdge>& GetOuterEdges() const { return m_outerEdges; }
|
||||
|
||||
// Detect gaps between monitors that should be snapped together
|
||||
struct GapInfo {
|
||||
int monitor1Index;
|
||||
int monitor2Index;
|
||||
int horizontalGap;
|
||||
int verticalOverlap;
|
||||
};
|
||||
std::vector<GapInfo> DetectMonitorGaps() const;
|
||||
|
||||
private:
|
||||
std::vector<MonitorInfo> m_monitors;
|
||||
std::vector<MonitorEdge> m_outerEdges;
|
||||
|
||||
// Map from (monitor index, edge type) to edge info
|
||||
// Using monitor index instead of HMONITOR because HMONITOR handles can change
|
||||
// when monitors are added/removed dynamically
|
||||
std::map<std::pair<int, EdgeType>, MonitorEdge> m_edgeMap;
|
||||
|
||||
// Helper to resolve HMONITOR to monitor index at runtime
|
||||
int GetMonitorIndex(HMONITOR monitor) const;
|
||||
|
||||
// Helper to get consistent HMONITOR from RECT
|
||||
HMONITOR GetMonitorFromRect(const RECT& rect) const;
|
||||
|
||||
void BuildEdgeMap();
|
||||
void IdentifyOuterEdges();
|
||||
|
||||
// Check if two edges are adjacent (within tolerance)
|
||||
bool EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance = 50) const;
|
||||
|
||||
// Find the opposite outer edge for wrapping
|
||||
MonitorEdge FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const;
|
||||
|
||||
// Calculate relative position along an edge (0.0 to 1.0)
|
||||
double GetRelativePosition(const MonitorEdge& edge, int coordinate) const;
|
||||
|
||||
// Convert relative position to absolute coordinate on target edge
|
||||
int GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,11 @@
|
||||
"input": "pushd .\\ExtensionTemplate\\ ; git archive -o ..\\Microsoft.CmdPal.UI.ViewModels\\Assets\\template.zip HEAD -- .\\TemplateCmdPalExtension\\ ; popd",
|
||||
"name": "Update template project",
|
||||
"description": "zips up the ExtensionTemplate into our assets. Run this in the cmdpal/ directory."
|
||||
},
|
||||
{
|
||||
"input": " .\\extensionsdk\\nuget\\BuildSDKHelper.ps1 -VersionOfSDK 0.0.1",
|
||||
"name": "Build SDK",
|
||||
"description": "Builds the SDK nuget package with the specified version."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.Common;
|
||||
|
||||
public static class CoreLogger
|
||||
@@ -15,6 +13,8 @@ public static class CoreLogger
|
||||
|
||||
private static ILogger? _logger;
|
||||
|
||||
public static ILogger? Instance => _logger;
|
||||
|
||||
public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
|
||||
{
|
||||
_logger?.LogError(message, ex, memberName, sourceFilePath, sourceLineNumber);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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 Microsoft.CmdPal.Core.Common.Helpers;
|
||||
|
||||
public partial class PinnedDockItem : WrappedDockItem
|
||||
{
|
||||
public override string Title => $"{base.Title} ({Properties.Resources.PinnedItemSuffix})";
|
||||
|
||||
public PinnedDockItem(ICommand command)
|
||||
: base(command, command.Name)
|
||||
{
|
||||
}
|
||||
|
||||
public PinnedDockItem(IListItem item, string id)
|
||||
: base([item], id, item.Title)
|
||||
{
|
||||
Icon = item.Icon;
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,19 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" />
|
||||
</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>
|
||||
|
||||
</Project>
|
||||
|
||||
72
src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Properties/Resources.Designer.cs
generated
Normal file
72
src/modules/cmdpal/Core/Microsoft.CmdPal.Core.Common/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,72 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <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.Core.Common.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", "18.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.Core.Common.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 Pinned.
|
||||
/// </summary>
|
||||
public static string PinnedItemSuffix {
|
||||
get {
|
||||
return ResourceManager.GetString("PinnedItemSuffix", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?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="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -96,9 +96,10 @@ public partial class CommandBarViewModel : ObservableObject,
|
||||
|
||||
SecondaryCommand = SelectedItem.SecondaryCommand;
|
||||
|
||||
ShouldShowContextMenu = SelectedItem.MoreCommands
|
||||
.OfType<CommandContextItemViewModel>()
|
||||
.Count() > 1;
|
||||
var hasMoreThanOneContextItem = SelectedItem.MoreCommands.Count() > 1;
|
||||
var hasMoreThanOneCommand = SelectedItem.MoreCommands.OfType<CommandContextItemViewModel>().Any();
|
||||
|
||||
ShouldShowContextMenu = hasMoreThanOneContextItem && hasMoreThanOneCommand;
|
||||
|
||||
OnPropertyChanged(nameof(HasSecondaryCommand));
|
||||
OnPropertyChanged(nameof(SecondaryCommand));
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context), IContextItemViewModel
|
||||
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) :
|
||||
CommandItemViewModel(new(contextItem), context),
|
||||
ICommandContextItemViewModel
|
||||
{
|
||||
private readonly KeyChord nullKeyChord = new(0, 0, 0);
|
||||
|
||||
@@ -45,3 +46,16 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
|
||||
contextItem.RequestedShortcut.ScanCode);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICommandContextItemViewModel : IContextItemViewModel
|
||||
{
|
||||
public string Title { get; }
|
||||
|
||||
public IconInfoViewModel Icon { get; }
|
||||
|
||||
public bool IsCritical { get; }
|
||||
|
||||
public KeyChord? RequestedShortcut { get; }
|
||||
|
||||
public bool HasRequestedShortcut { get; }
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
private string _itemTitle = string.Empty;
|
||||
|
||||
public string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
protected string ItemTitle => _itemTitle;
|
||||
|
||||
public virtual string Title => string.IsNullOrEmpty(_itemTitle) ? Name : _itemTitle;
|
||||
|
||||
public string Subtitle { get; private set; } = string.Empty;
|
||||
|
||||
@@ -64,10 +66,30 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
|
||||
public CommandItemViewModel? PrimaryCommand => this;
|
||||
|
||||
public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? ActualCommands[0] : null;
|
||||
public CommandItemViewModel? SecondaryCommand
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HasMoreCommands)
|
||||
{
|
||||
if (MoreCommands[0] is CommandContextItemViewModel command)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldBeVisible => !string.IsNullOrEmpty(Name);
|
||||
|
||||
public bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
public bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
public virtual bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
public DataPackageView? DataPackage { get; private set; }
|
||||
|
||||
public List<IContextItemViewModel> AllCommands
|
||||
@@ -331,16 +353,19 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
UpdateProperty(nameof(Name));
|
||||
UpdateProperty(nameof(Title));
|
||||
UpdateProperty(nameof(Icon));
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Title):
|
||||
_itemTitle = model.Title;
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Subtitle):
|
||||
var modelSubtitle = model.Subtitle;
|
||||
this.Subtitle = modelSubtitle;
|
||||
_defaultCommandContextItemViewModel?.Subtitle = modelSubtitle;
|
||||
UpdateProperty(nameof(HasText));
|
||||
break;
|
||||
|
||||
case nameof(Icon):
|
||||
@@ -427,11 +452,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDefaultContextItemIcon()
|
||||
{
|
||||
private void UpdateDefaultContextItemIcon() =>
|
||||
|
||||
// Command icon takes precedence over our icon on the primary command
|
||||
_defaultCommandContextItemViewModel?.UpdateIcon(Command.Icon.IsSet ? Command.Icon : _icon);
|
||||
}
|
||||
|
||||
private void UpdateTitle(string? title)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -13,15 +13,20 @@ using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class ContextMenuViewModel : ObservableObject,
|
||||
IRecipient<UpdateCommandBarMessage>
|
||||
public partial class ContextMenuViewModel : ObservableObject
|
||||
{
|
||||
public ICommandBarContext? SelectedItem
|
||||
public IContextMenuContext? SelectedItem
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
if (field == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(SelectedItem));
|
||||
UpdateContextItems();
|
||||
}
|
||||
}
|
||||
@@ -39,25 +44,12 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
|
||||
private string _lastSearchText = string.Empty;
|
||||
|
||||
public ContextMenuViewModel()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
}
|
||||
|
||||
public void Receive(UpdateCommandBarMessage message)
|
||||
{
|
||||
SelectedItem = message.ViewModel;
|
||||
}
|
||||
|
||||
public void UpdateContextItems()
|
||||
{
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
if (SelectedItem.PrimaryCommand is not null || SelectedItem.HasMoreCommands)
|
||||
{
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public abstract partial class ExtensionObjectViewModel : ObservableObject
|
||||
PageContext = new(realContext);
|
||||
}
|
||||
|
||||
internal ExtensionObjectViewModel(WeakReference<IPageContext> context)
|
||||
protected ExtensionObjectViewModel(WeakReference<IPageContext> context)
|
||||
{
|
||||
PageContext = context;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class ListItemViewModel : CommandItemViewModel
|
||||
public partial class ListItemViewModel : CommandItemViewModel, ICommandContextItemViewModel
|
||||
{
|
||||
public new ExtensionObject<IListItem> Model { get; }
|
||||
|
||||
@@ -61,6 +61,12 @@ public partial class ListItemViewModel : CommandItemViewModel
|
||||
}
|
||||
}
|
||||
|
||||
bool ICommandContextItemViewModel.IsCritical => false;
|
||||
|
||||
KeyChord? ICommandContextItemViewModel.RequestedShortcut => null;
|
||||
|
||||
bool ICommandContextItemViewModel.HasRequestedShortcut => false;
|
||||
|
||||
public ListItemViewModel(IListItem model, WeakReference<IPageContext> context)
|
||||
: base(new(model), context)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
@@ -15,7 +14,7 @@ using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class ListViewModel : PageViewModel, IDisposable
|
||||
public partial class ListViewModel : PageViewModel, IDisposable, IContextMenuContext
|
||||
{
|
||||
// private readonly HashSet<ListItemViewModel> _itemCache = [];
|
||||
private readonly TaskFactory filterTaskFactory = new(new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler);
|
||||
@@ -65,6 +64,14 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
|
||||
public bool IsMainPage { get; init; }
|
||||
|
||||
// IContextMenuContext implementation
|
||||
bool IContextMenuContext.HasMoreCommands => true;
|
||||
|
||||
// TODO! should this be FilteredItems? We'd need a way to plumb searchText through IContextMenuContext (but we probably should anyways)
|
||||
List<IContextItemViewModel> IContextMenuContext.AllCommands => Items.Cast<IContextItemViewModel>().ToList();
|
||||
|
||||
IEnumerable<IContextItemViewModel> IContextMenuContext.MoreCommands => ((IContextMenuContext)this).AllCommands;
|
||||
|
||||
private bool _isDynamic;
|
||||
|
||||
private Task? _initializeItemsTask;
|
||||
|
||||
@@ -12,6 +12,7 @@ public partial class LoadingPageViewModel : PageViewModel
|
||||
: base(model, scheduler, host)
|
||||
{
|
||||
ModelIsLoading = true;
|
||||
HasBackButton = false;
|
||||
IsInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken);
|
||||
public record NavigateToPageMessage(PageViewModel Page, bool WithAnimation, CancellationToken CancellationToken, bool TransientPage = false);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
@@ -18,12 +19,30 @@ public record PerformCommandMessage
|
||||
|
||||
public bool WithAnimation { get; set; } = true;
|
||||
|
||||
public bool TransientPage { get; set; }
|
||||
|
||||
public bool OpenAsFlyout { get; private set; }
|
||||
|
||||
public object? FlyoutUiContext { get; private set; }
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command)
|
||||
{
|
||||
Command = command;
|
||||
Context = null;
|
||||
}
|
||||
|
||||
public static PerformCommandMessage CreateFlyoutMessage(ExtensionObject<ICommand> command, object flyoutUiContext)
|
||||
{
|
||||
var message = new PerformCommandMessage(command)
|
||||
{
|
||||
WithAnimation = false,
|
||||
TransientPage = true,
|
||||
OpenAsFlyout = true,
|
||||
FlyoutUiContext = flyoutUiContext,
|
||||
};
|
||||
return message;
|
||||
}
|
||||
|
||||
public PerformCommandMessage(ExtensionObject<ICommand> command, ExtensionObject<IListItem> context)
|
||||
{
|
||||
Command = command;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
|
||||
public record ShowCommandInContextMenuMessage(IContextMenuContext Context, object? UiContext);
|
||||
@@ -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.Core.ViewModels.Messages;
|
||||
|
||||
public sealed record WindowHiddenMessage();
|
||||
@@ -27,7 +27,10 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
public partial string ErrorMessage { get; protected set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsNested { get; set; } = true;
|
||||
public partial bool IsRootPage { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool HasBackButton { get; set; } = true;
|
||||
|
||||
// This is set from the SearchBar
|
||||
[ObservableProperty]
|
||||
|
||||
@@ -121,4 +121,8 @@
|
||||
<value>Show details</value>
|
||||
<comment>Name for the command that shows details of an item</comment>
|
||||
</data>
|
||||
<data name="PinnedItemSuffix" xml:space="preserve">
|
||||
<value>Pinned</value>
|
||||
<comment>Suffix shown for pinned items in the dock</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -10,13 +10,15 @@ using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
public partial class ShellViewModel : ObservableObject,
|
||||
IDisposable,
|
||||
IRecipient<PerformCommandMessage>,
|
||||
IRecipient<HandleCommandResultMessage>
|
||||
IRecipient<HandleCommandResultMessage>,
|
||||
IRecipient<WindowHiddenMessage>
|
||||
{
|
||||
private readonly IRootPageService _rootPageService;
|
||||
private readonly IAppHostService _appHostService;
|
||||
@@ -79,8 +81,9 @@ public partial class ShellViewModel : ObservableObject,
|
||||
private IPage? _rootPage;
|
||||
|
||||
private bool _isNested;
|
||||
private bool _currentlyTransient;
|
||||
|
||||
public bool IsNested => _isNested;
|
||||
public bool IsNested => _isNested && !_currentlyTransient;
|
||||
|
||||
public PageViewModel NullPage { get; private set; }
|
||||
|
||||
@@ -96,11 +99,13 @@ public partial class ShellViewModel : ObservableObject,
|
||||
_appHostService = appHostService;
|
||||
|
||||
NullPage = new NullPageViewModel(_scheduler, appHostService.GetDefaultHost());
|
||||
NullPage.HasBackButton = false;
|
||||
_currentPage = new LoadingPageViewModel(null, _scheduler, appHostService.GetDefaultHost());
|
||||
|
||||
// Register to receive messages
|
||||
WeakReferenceMessenger.Default.Register<PerformCommandMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HandleCommandResultMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<WindowHiddenMessage>(this);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -132,93 +137,113 @@ public partial class ShellViewModel : ObservableObject,
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ShowPageAsFlyoutAsync(PageViewModel viewModel, object flyoutUiContext, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (viewModel is not IContextMenuContext ctx)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Instead of doing normal page navigation, we want to open this
|
||||
// page as a flyout anchored to the given position.
|
||||
var initialized = await InitializePageViewModelAsync(viewModel, cancellationToken);
|
||||
if (initialized)
|
||||
{
|
||||
await SetCurrentPageAsync(viewModel, cancellationToken);
|
||||
|
||||
// send message
|
||||
WeakReferenceMessenger.Default.Send(new ShowCommandInContextMenuMessage(ctx, flyoutUiContext));
|
||||
|
||||
//// now cleanup navigation
|
||||
// await CleanupNavigationTokenAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPageViewModelAsync(PageViewModel viewModel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Note: We removed the general loading state, extensions sometimes use their `IsLoading`, but it's inconsistently implemented it seems.
|
||||
// IsInitialized is our main indicator of the general overall state of loading props/items from a page we use for the progress bar
|
||||
// This triggers that load generally with the InitializeCommand asynchronously when we navigate to a page.
|
||||
// We could re-track the page loading status, if we need it more granularly below again, but between the initialized and error message, we can infer some status.
|
||||
// We could also maybe move this thread offloading we do for loading into PageViewModel and better communicate between the two... a few different options.
|
||||
|
||||
////LoadedState = ViewModelLoadedState.Loading;
|
||||
if (!viewModel.IsInitialized
|
||||
&& viewModel.InitializeCommand is not null)
|
||||
var initialized = await InitializePageViewModelAsync(viewModel, cancellationToken);
|
||||
if (initialized)
|
||||
{
|
||||
var outer = Task.Run(
|
||||
async () =>
|
||||
{
|
||||
// You know, this creates the situation where we wait for
|
||||
// both loading page properties, AND the items, before we
|
||||
// display anything.
|
||||
//
|
||||
// We almost need to do an async await on initialize, then
|
||||
// just a fire-and-forget on FetchItems.
|
||||
// RE: We do set the CurrentPage in ShellPage.xaml.cs as well, so, we kind of are doing two different things here.
|
||||
// Definitely some more clean-up to do, but at least its centralized to one spot now.
|
||||
viewModel.InitializeCommand.Execute(null);
|
||||
|
||||
await viewModel.InitializeCommand.ExecutionTask!;
|
||||
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var t = Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (viewModel is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentPage = viewModel;
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
await t;
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
await outer;
|
||||
await SetCurrentPageAsync(viewModel, cancellationToken);
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the async initialization for a page view model on a background thread.
|
||||
/// Returns true if the page is ready to be displayed, false otherwise.
|
||||
/// </summary>
|
||||
private async Task<bool> InitializePageViewModelAsync(PageViewModel viewModel, CancellationToken cancellationToken)
|
||||
{
|
||||
if (viewModel.IsInitialized
|
||||
|| viewModel.InitializeCommand is null)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
// Already initialized (or nothing to do), ready to display.
|
||||
return true;
|
||||
}
|
||||
|
||||
var success = false;
|
||||
await Task.Run(
|
||||
async () =>
|
||||
{
|
||||
if (viewModel is IDisposable disposable)
|
||||
viewModel.InitializeCommand.Execute(null);
|
||||
await viewModel.InitializeCommand.ExecutionTask!;
|
||||
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Status != TaskStatus.RanToCompletion)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
if (viewModel.InitializeCommand.ExecutionTask.Exception is AggregateException ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
success = true;
|
||||
}
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
CurrentPage = viewModel;
|
||||
/// <summary>
|
||||
/// Hops to the UI thread to set <see cref="CurrentPage"/>, or disposes the
|
||||
/// view model if the operation was cancelled.
|
||||
/// </summary>
|
||||
private async Task SetCurrentPageAsync(PageViewModel viewModel, CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Factory.StartNew(
|
||||
() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (viewModel is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CoreLogger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentPage = viewModel;
|
||||
},
|
||||
cancellationToken,
|
||||
TaskCreationOptions.None,
|
||||
_scheduler);
|
||||
}
|
||||
|
||||
private async Task CleanupNavigationTokenAsync(CancellationTokenSource cts)
|
||||
{
|
||||
// clean up the navigation token if it's still ours
|
||||
if (Interlocked.CompareExchange(ref _navigationCts, null, cts) == cts)
|
||||
{
|
||||
cts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,58 +284,14 @@ public partial class ShellViewModel : ObservableObject,
|
||||
|
||||
var host = _appHostService.GetHostForCommand(message.Context, CurrentPage.ExtensionHost);
|
||||
|
||||
_rootPageService.OnPerformCommand(message.Context, !CurrentPage.IsNested, host);
|
||||
_rootPageService.OnPerformCommand(message.Context, CurrentPage.IsRootPage, host);
|
||||
|
||||
try
|
||||
{
|
||||
if (command is IPage page)
|
||||
{
|
||||
CoreLogger.LogDebug($"Navigating to page");
|
||||
|
||||
var isMainPage = command == _rootPage;
|
||||
_isNested = !isMainPage;
|
||||
|
||||
// Telemetry: Track extension page navigation for session metrics
|
||||
if (host is not null)
|
||||
{
|
||||
string extensionId = host.GetExtensionDisplayName() ?? "builtin";
|
||||
string commandId = command?.Id ?? "unknown";
|
||||
string commandName = command?.Name ?? "unknown";
|
||||
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
|
||||
new(extensionId, commandId, commandName, true, 0));
|
||||
}
|
||||
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host!);
|
||||
if (pageViewModel is null)
|
||||
{
|
||||
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
// Clear command bar, ViewModel initialization can already set new commands if it wants to
|
||||
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
|
||||
|
||||
// Kick off async loading of our ViewModel
|
||||
LoadPageViewModelAsync(pageViewModel, navigationToken)
|
||||
.ContinueWith(
|
||||
(Task t) =>
|
||||
{
|
||||
// clean up the navigation token if it's still ours
|
||||
if (Interlocked.CompareExchange(ref _navigationCts, null, newCts) == newCts)
|
||||
{
|
||||
newCts.Dispose();
|
||||
}
|
||||
},
|
||||
navigationToken,
|
||||
TaskContinuationOptions.None,
|
||||
_scheduler);
|
||||
|
||||
// While we're loading in the background, immediately move to the next page.
|
||||
WeakReferenceMessenger.Default.Send<NavigateToPageMessage>(new(pageViewModel, message.WithAnimation, navigationToken));
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
StartOpenPage(message, page, host, newCts, navigationToken);
|
||||
}
|
||||
else if (command is IInvokableCommand invokable)
|
||||
{
|
||||
@@ -352,10 +333,10 @@ public partial class ShellViewModel : ObservableObject,
|
||||
// Telemetry: Track command execution time and success
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var command = message.Command.Unsafe;
|
||||
string extensionId = host?.GetExtensionDisplayName() ?? "builtin";
|
||||
string commandId = command?.Id ?? "unknown";
|
||||
string commandName = command?.Name ?? "unknown";
|
||||
bool success = false;
|
||||
var extensionId = host?.GetExtensionDisplayName() ?? "builtin";
|
||||
var commandId = command?.Id ?? "unknown";
|
||||
var commandName = command?.Name ?? "unknown";
|
||||
var success = false;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -462,6 +443,72 @@ public partial class ShellViewModel : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
private void StartOpenPage(PerformCommandMessage message, IPage page, AppExtensionHost? host, CancellationTokenSource cts, CancellationToken navigationToken)
|
||||
{
|
||||
var isMainPage = page == _rootPage;
|
||||
|
||||
// Telemetry: Track extension page navigation for session metrics
|
||||
// TODO! this block is unsafe
|
||||
if (host is not null)
|
||||
{
|
||||
var extensionId = host.GetExtensionDisplayName() ?? "builtin";
|
||||
var commandId = page.Id ?? "unknown";
|
||||
var commandName = page.Name ?? "unknown";
|
||||
WeakReferenceMessenger.Default.Send<TelemetryExtensionInvokedMessage>(
|
||||
new(extensionId, commandId, commandName, true, 0));
|
||||
}
|
||||
|
||||
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
|
||||
var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, !isMainPage, host!);
|
||||
if (pageViewModel is null)
|
||||
{
|
||||
CoreLogger.LogError($"Failed to create ViewModel for page {page.GetType().Name}");
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
if (pageViewModel is ListViewModel listViewModel
|
||||
&& message.OpenAsFlyout
|
||||
&& message.FlyoutUiContext is not null)
|
||||
{
|
||||
ShowPageAsFlyoutAsync(listViewModel, message.FlyoutUiContext, navigationToken)
|
||||
.ContinueWith(
|
||||
(Task t) => CleanupNavigationTokenAsync(cts),
|
||||
navigationToken,
|
||||
TaskContinuationOptions.None,
|
||||
_scheduler);
|
||||
return;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// Slice it here.
|
||||
// Stuff above this, we need to always do, for both commands in the palette and flyout items
|
||||
//
|
||||
// Below here, this is all specific to navigating the current page of the palette
|
||||
_isNested = !isMainPage;
|
||||
_currentlyTransient = message.TransientPage;
|
||||
|
||||
pageViewModel.IsRootPage = isMainPage;
|
||||
pageViewModel.HasBackButton = IsNested;
|
||||
|
||||
// Clear command bar, ViewModel initialization can already set new commands if it wants to
|
||||
OnUIThread(() => WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null)));
|
||||
|
||||
// Kick off async loading of our ViewModel
|
||||
LoadPageViewModelAsync(pageViewModel, navigationToken)
|
||||
.ContinueWith(
|
||||
(Task t) => CleanupNavigationTokenAsync(cts),
|
||||
navigationToken,
|
||||
TaskContinuationOptions.None,
|
||||
_scheduler);
|
||||
|
||||
// While we're loading in the background, immediately move to the next page.
|
||||
NavigateToPageMessage msg = new(pageViewModel, message.WithAnimation, navigationToken, message.TransientPage);
|
||||
WeakReferenceMessenger.Default.Send(msg);
|
||||
|
||||
// Note: Originally we set our page back in the ViewModel here, but that now happens in response to the Frame navigating triggered from the above
|
||||
// See RootFrame_Navigated event handler.
|
||||
}
|
||||
|
||||
public void GoHome(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
_rootPageService.GoHome();
|
||||
@@ -478,6 +525,19 @@ public partial class ShellViewModel : ObservableObject,
|
||||
UnsafeHandleCommandResult(message.Result.Unsafe);
|
||||
}
|
||||
|
||||
public void Receive(WindowHiddenMessage message)
|
||||
{
|
||||
// If the window was hidden while we had a transient page, we need to reset that state.
|
||||
if (_currentlyTransient)
|
||||
{
|
||||
_currentlyTransient = false;
|
||||
|
||||
// navigate back to the main page without animation
|
||||
GoHome(withAnimation: false, focusSearch: false);
|
||||
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(new ExtensionObject<ICommand>(_rootPage)));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUIThread(Action action)
|
||||
{
|
||||
_ = Task.Factory.StartNew(
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"solution": {
|
||||
"path": "..\\..\\..\\PowerToys.slnx",
|
||||
"projects": [
|
||||
"src\\common\\Common.Search\\Common.Search.csproj",
|
||||
"src\\common\\Common.UI\\Common.UI.csproj",
|
||||
"src\\common\\ManagedCommon\\ManagedCommon.csproj",
|
||||
"src\\common\\ManagedTelemetry\\Telemetry\\ManagedTelemetry.csproj",
|
||||
"src\\common\\PowerToys.ModuleContracts\\PowerToys.ModuleContracts.csproj",
|
||||
"src\\common\\SettingsAPI\\SettingsAPI.vcxproj",
|
||||
"src\\common\\interop\\PowerToys.Interop.vcxproj",
|
||||
"src\\common\\logger\\logger.vcxproj",
|
||||
"src\\common\\version\\version.vcxproj",
|
||||
"src\\logging\\logging.vcxproj",
|
||||
"src\\modules\\MouseUtils\\MouseJump.Common\\MouseJump.Common.csproj",
|
||||
"src\\modules\\Workspaces\\Workspaces.ModuleServices\\Workspaces.ModuleServices.csproj",
|
||||
"src\\modules\\Workspaces\\WorkspacesCsharpLibrary\\WorkspacesCsharpLibrary.csproj",
|
||||
"src\\modules\\ZoomIt\\ZoomItSettingsInterop\\ZoomItSettingsInterop.vcxproj",
|
||||
"src\\modules\\awake\\Awake.ModuleServices\\Awake.ModuleServices.csproj",
|
||||
"src\\modules\\cmdpal\\ext\\Microsoft.CmdPal.Ext.PowerToys\\Microsoft.CmdPal.Ext.PowerToys.csproj",
|
||||
"src\\modules\\colorPicker\\ColorPicker.ModuleServices\\ColorPicker.ModuleServices.csproj",
|
||||
"src\\modules\\fancyzones\\FancyZonesEditorCommon\\FancyZonesEditorCommon.csproj",
|
||||
"src\\settings-ui\\Settings.UI.Library\\Settings.UI.Library.csproj"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public sealed partial class AppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
internal static readonly ObservableCollection<Color> WindowsColorSwatches = [
|
||||
|
||||
// row 0
|
||||
Color.FromArgb(255, 255, 185, 0), // #ffb900
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CommandPalettePageViewModelFactory
|
||||
{
|
||||
return page switch
|
||||
{
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested },
|
||||
IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsRootPage = !nested },
|
||||
IContentPage contentPage => new CommandPaletteContentPageViewModel(contentPage, _scheduler, host),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
using Windows.Foundation;
|
||||
@@ -27,6 +29,8 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
public TopLevelViewModel[] FallbackItems { get; private set; } = [];
|
||||
|
||||
public TopLevelViewModel[] DockBandItems { get; private set; } = [];
|
||||
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
public IExtensionWrapper? Extension { get; }
|
||||
@@ -141,6 +145,7 @@ public sealed class CommandProviderWrapper
|
||||
|
||||
ICommandItem[]? commands = null;
|
||||
IFallbackCommandItem[]? fallbacks = null;
|
||||
ICommandItem[] dockBands = []; // do not initialize me to null
|
||||
|
||||
try
|
||||
{
|
||||
@@ -158,6 +163,16 @@ public sealed class CommandProviderWrapper
|
||||
UnsafePreCacheApiAdditions(two);
|
||||
}
|
||||
|
||||
if (model is ICommandProvider3 supportsDockBands)
|
||||
{
|
||||
var bands = supportsDockBands.GetDockBands();
|
||||
if (bands is not null)
|
||||
{
|
||||
CoreLogger.LogDebug($"Found {bands.Length} bands on {DisplayName} ({ProviderId}) ");
|
||||
dockBands = bands;
|
||||
}
|
||||
}
|
||||
|
||||
Id = model.Id;
|
||||
DisplayName = model.DisplayName;
|
||||
Icon = new(model.Icon);
|
||||
@@ -168,7 +183,8 @@ public sealed class CommandProviderWrapper
|
||||
Settings = new(model.Settings, this, _taskScheduler);
|
||||
|
||||
// We do need to explicitly initialize commands though
|
||||
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
|
||||
var objects = new TopLevelObjects(commands, fallbacks, dockBands);
|
||||
InitializeCommands(objects, serviceProvider, pageContext);
|
||||
|
||||
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
|
||||
}
|
||||
@@ -180,33 +196,68 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeCommands(ICommandItem[] commands, IFallbackCommandItem[] fallbacks, IServiceProvider serviceProvider, WeakReference<IPageContext> pageContext)
|
||||
private record TopLevelObjects(
|
||||
ICommandItem[]? Commands,
|
||||
IFallbackCommandItem[]? Fallbacks,
|
||||
ICommandItem[]? DockBands);
|
||||
|
||||
private void InitializeCommands(
|
||||
TopLevelObjects objects,
|
||||
IServiceProvider serviceProvider,
|
||||
WeakReference<IPageContext> pageContext)
|
||||
{
|
||||
var settings = serviceProvider.GetService<SettingsModel>()!;
|
||||
var state = serviceProvider.GetService<AppStateModel>()!;
|
||||
var providerSettings = GetProviderSettings(settings);
|
||||
|
||||
Func<ICommandItem?, bool, TopLevelViewModel> makeAndAdd = (ICommandItem? i, bool fallback) =>
|
||||
Func<ICommandItem?, TopLevelType, TopLevelViewModel> make = (ICommandItem? i, TopLevelType t) =>
|
||||
{
|
||||
CommandItemViewModel commandItemViewModel = new(new(i), pageContext);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, fallback, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
|
||||
TopLevelViewModel topLevelViewModel = new(commandItemViewModel, t, ExtensionHost, ProviderId, settings, providerSettings, serviceProvider, i);
|
||||
topLevelViewModel.InitializeProperties();
|
||||
|
||||
return topLevelViewModel;
|
||||
};
|
||||
|
||||
if (commands is not null)
|
||||
if (objects.Commands is not null)
|
||||
{
|
||||
TopLevelItems = commands
|
||||
.Select(c => makeAndAdd(c, false))
|
||||
TopLevelItems = objects.Commands
|
||||
.Select(c => make(c, TopLevelType.Normal))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (fallbacks is not null)
|
||||
if (objects.Fallbacks is not null)
|
||||
{
|
||||
FallbackItems = fallbacks
|
||||
.Select(c => makeAndAdd(c, true))
|
||||
FallbackItems = objects.Fallbacks
|
||||
.Select(c => make(c, TopLevelType.Fallback))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (objects.DockBands is not null)
|
||||
{
|
||||
List<TopLevelViewModel> bands = new();
|
||||
foreach (var b in objects.DockBands)
|
||||
{
|
||||
var bandVm = make(b, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
}
|
||||
|
||||
foreach (var c in TopLevelItems)
|
||||
{
|
||||
foreach (var pinnedId in settings.DockSettings.PinnedCommands)
|
||||
{
|
||||
if (pinnedId == c.Id)
|
||||
{
|
||||
var bandModel = c.ToPinnedDockBandItem();
|
||||
var bandVm = make(bandModel, TopLevelType.DockBand);
|
||||
bands.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DockBandItems = bands.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
|
||||
@@ -219,6 +270,10 @@ public sealed class CommandProviderWrapper
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
|
||||
}
|
||||
else if (a is ICommandItem[] commands)
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an ICommandItem[]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,4 +290,14 @@ public sealed class CommandProviderWrapper
|
||||
// In handling this, a call will be made to `LoadTopLevelCommands` to
|
||||
// retrieve the new items.
|
||||
this.CommandsChanged?.Invoke(this, args);
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
Logger.LogDebug($"CommandProviderWrapper.PinDockBand: {ProviderId} - {bandVm.Id}");
|
||||
|
||||
var bands = this.DockBandItems.ToList();
|
||||
bands.Add(bandVm);
|
||||
this.DockBandItems = bands.ToArray();
|
||||
this.CommandsChanged?.Invoke(this, new ItemsChangedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
|
||||
@@ -18,6 +21,8 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
private readonly FallbackLogItem _fallbackLogItem = new();
|
||||
private readonly NewExtensionPage _newExtension = new();
|
||||
|
||||
private readonly IRootPageService _rootPageService;
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { },
|
||||
@@ -37,11 +42,22 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
_fallbackLogItem,
|
||||
];
|
||||
|
||||
public BuiltInsCommandProvider()
|
||||
public BuiltInsCommandProvider(IRootPageService rootPageService)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.builtin.core";
|
||||
DisplayName = Properties.Resources.builtin_display_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Square44x44Logo.altform-unplated_targetsize-256.png");
|
||||
|
||||
_rootPageService = rootPageService;
|
||||
}
|
||||
|
||||
public override ICommandItem[]? GetDockBands()
|
||||
{
|
||||
var rootPage = _rootPageService.GetRootPage();
|
||||
List<ICommandItem> bandItems = new();
|
||||
bandItems.Add(new WrappedDockItem(rootPage, Properties.Resources.builtin_command_palette_title));
|
||||
|
||||
return bandItems.ToArray();
|
||||
}
|
||||
|
||||
public override void InitializeWithHost(IExtensionHost host) => BuiltinsExtensionHost.Instance.Initialize(host);
|
||||
|
||||
@@ -50,6 +50,7 @@ public partial class MainListPage : DynamicListPage,
|
||||
|
||||
public MainListPage(TopLevelCommandManager topLevelCommandManager, SettingsModel settings, AliasManager aliasManager, AppStateModel appStateModel)
|
||||
{
|
||||
Id = "com.microsoft.cmdpal.home";
|
||||
Title = Resources.builtin_home_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
||||
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -19,7 +19,7 @@ public partial class OpenSettingsCommand : InvokableCommand
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -70,6 +70,15 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
StateJson = model.StateJson;
|
||||
DataJson = model.DataJson;
|
||||
|
||||
RenderCard();
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
private void RenderCard()
|
||||
{
|
||||
if (TryBuildCard(TemplateJson, DataJson, out var builtCard, out var renderingError))
|
||||
{
|
||||
Card = builtCard;
|
||||
@@ -93,8 +102,41 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
UpdateProperty(nameof(Card));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateProperty(nameof(Card));
|
||||
private void Model_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
try
|
||||
{
|
||||
FetchProperty(args.PropertyName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void FetchProperty(string propertyName)
|
||||
{
|
||||
var model = this._formModel.Unsafe;
|
||||
if (model is null)
|
||||
{
|
||||
return; // throw?
|
||||
}
|
||||
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(DataJson):
|
||||
DataJson = model.DataJson;
|
||||
RenderCard();
|
||||
break;
|
||||
case nameof(TemplateJson):
|
||||
TemplateJson = model.TemplateJson;
|
||||
RenderCard();
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateProperty(propertyName);
|
||||
}
|
||||
|
||||
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public partial class DockBandSettingsViewModel : ObservableObject
|
||||
{
|
||||
private static readonly CompositeFormat PluralItemsFormatString = CompositeFormat.Parse(Properties.Resources.dock_item_count_plural);
|
||||
private readonly SettingsModel _settingsModel;
|
||||
private readonly DockBandSettings _dockSettingsModel;
|
||||
private readonly TopLevelViewModel _adapter;
|
||||
private readonly DockBandViewModel? _bandViewModel;
|
||||
|
||||
public string Title => _adapter.Title;
|
||||
|
||||
public string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
List<string> parts = [_adapter.ExtensionName];
|
||||
|
||||
// Add the number of items in the band
|
||||
var itemCount = NumItemsInBand();
|
||||
if (itemCount > 0)
|
||||
{
|
||||
var itemsString = itemCount == 1 ?
|
||||
Properties.Resources.dock_item_count_singular :
|
||||
string.Format(CultureInfo.CurrentCulture, PluralItemsFormatString, itemCount);
|
||||
parts.Add(itemsString);
|
||||
}
|
||||
|
||||
return string.Join(" - ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
public string ProviderId => _adapter.CommandProviderId;
|
||||
|
||||
public IconInfoViewModel Icon => _adapter.IconViewModel;
|
||||
|
||||
private ShowLabelsOption _showLabels;
|
||||
|
||||
public ShowLabelsOption ShowLabels
|
||||
{
|
||||
get => _showLabels;
|
||||
set
|
||||
{
|
||||
if (value != _showLabels)
|
||||
{
|
||||
_showLabels = value;
|
||||
_dockSettingsModel.ShowLabels = value switch
|
||||
{
|
||||
ShowLabelsOption.Default => null,
|
||||
ShowLabelsOption.ShowLabels => true,
|
||||
ShowLabelsOption.HideLabels => false,
|
||||
_ => null,
|
||||
};
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ShowLabelsOption FetchShowLabels()
|
||||
{
|
||||
if (_dockSettingsModel.ShowLabels == null)
|
||||
{
|
||||
return ShowLabelsOption.Default;
|
||||
}
|
||||
|
||||
return _dockSettingsModel.ShowLabels.Value ? ShowLabelsOption.ShowLabels : ShowLabelsOption.HideLabels;
|
||||
}
|
||||
|
||||
// used to map to ComboBox selection
|
||||
public int ShowLabelsIndex
|
||||
{
|
||||
get => (int)ShowLabels;
|
||||
set => ShowLabels = (ShowLabelsOption)value;
|
||||
}
|
||||
|
||||
private DockPinSide PinSide
|
||||
{
|
||||
get => _pinSide;
|
||||
set
|
||||
{
|
||||
if (value != _pinSide)
|
||||
{
|
||||
UpdatePinSide(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DockPinSide _pinSide;
|
||||
|
||||
public int PinSideIndex
|
||||
{
|
||||
get => (int)PinSide;
|
||||
set => PinSide = (DockPinSide)value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the band is pinned to the dock.
|
||||
/// When enabled, pins to Center. When disabled, removes from all sides.
|
||||
/// </summary>
|
||||
public bool IsPinned
|
||||
{
|
||||
get => PinSide != DockPinSide.None;
|
||||
set
|
||||
{
|
||||
if (value && PinSide == DockPinSide.None)
|
||||
{
|
||||
// Pin to Center by default when enabling
|
||||
PinSide = DockPinSide.Center;
|
||||
}
|
||||
else if (!value && PinSide != DockPinSide.None)
|
||||
{
|
||||
// Remove from dock when disabling
|
||||
PinSide = DockPinSide.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DockBandSettingsViewModel(
|
||||
DockBandSettings dockSettingsModel,
|
||||
TopLevelViewModel topLevelAdapter,
|
||||
DockBandViewModel? bandViewModel,
|
||||
SettingsModel settingsModel)
|
||||
{
|
||||
_dockSettingsModel = dockSettingsModel;
|
||||
_adapter = topLevelAdapter;
|
||||
_bandViewModel = bandViewModel;
|
||||
_settingsModel = settingsModel;
|
||||
_pinSide = FetchPinSide();
|
||||
_showLabels = FetchShowLabels();
|
||||
}
|
||||
|
||||
private DockPinSide FetchPinSide()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
var inStart = dockSettings.StartBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inStart)
|
||||
{
|
||||
return DockPinSide.Start;
|
||||
}
|
||||
|
||||
var inCenter = dockSettings.CenterBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inCenter)
|
||||
{
|
||||
return DockPinSide.Center;
|
||||
}
|
||||
|
||||
var inEnd = dockSettings.EndBands.Any(b => b.Id == _dockSettingsModel.Id);
|
||||
if (inEnd)
|
||||
{
|
||||
return DockPinSide.End;
|
||||
}
|
||||
|
||||
return DockPinSide.None;
|
||||
}
|
||||
|
||||
private int NumItemsInBand()
|
||||
{
|
||||
var bandVm = _bandViewModel;
|
||||
if (bandVm is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _bandViewModel!.Items.Count;
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
private void UpdatePinSide(DockPinSide value)
|
||||
{
|
||||
OnPinSideChanged(value);
|
||||
OnPropertyChanged(nameof(PinSideIndex));
|
||||
OnPropertyChanged(nameof(PinSide));
|
||||
OnPropertyChanged(nameof(IsPinned));
|
||||
}
|
||||
|
||||
public void SetBandPosition(DockPinSide side, int? index)
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from all sides first
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == _dockSettingsModel.Id);
|
||||
|
||||
// Add to the selected side
|
||||
switch (side)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.StartBands.Count;
|
||||
dockSettings.StartBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.CenterBands.Count;
|
||||
dockSettings.CenterBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var insertIndex = index ?? dockSettings.EndBands.Count;
|
||||
dockSettings.EndBands.Insert(insertIndex, _dockSettingsModel);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.None:
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
private void OnPinSideChanged(DockPinSide value)
|
||||
{
|
||||
SetBandPosition(value, null);
|
||||
_pinSide = value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DockPinSide
|
||||
{
|
||||
None,
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
}
|
||||
|
||||
public enum ShowLabelsOption
|
||||
{
|
||||
Default,
|
||||
ShowLabels,
|
||||
HideLabels,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,274 @@
|
||||
// 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 Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockBandViewModel : ExtensionObjectViewModel
|
||||
{
|
||||
private readonly CommandItemViewModel _rootItem;
|
||||
private readonly DockBandSettings _bandSettings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly Action _saveSettings;
|
||||
|
||||
public ObservableCollection<DockItemViewModel> Items { get; } = new();
|
||||
|
||||
private bool _showTitles = true;
|
||||
private bool _showSubtitles = true;
|
||||
private bool? _showTitlesSnapshot;
|
||||
private bool? _showSubtitlesSnapshot;
|
||||
|
||||
public string Id => _rootItem.Command.Id;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether titles are shown for items in this band.
|
||||
/// This is a preview value - call <see cref="SaveLabelSettings"/> to persist or
|
||||
/// <see cref="RestoreLabelSettings"/> to discard changes.
|
||||
/// </summary>
|
||||
public bool ShowTitles
|
||||
{
|
||||
get => _showTitles;
|
||||
set
|
||||
{
|
||||
if (_showTitles != value)
|
||||
{
|
||||
_showTitles = value;
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.ShowTitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether subtitles are shown for items in this band.
|
||||
/// This is a preview value - call <see cref="SaveLabelSettings"/> to persist or
|
||||
/// <see cref="RestoreLabelSettings"/> to discard changes.
|
||||
/// </summary>
|
||||
public bool ShowSubtitles
|
||||
{
|
||||
get => _showSubtitles;
|
||||
set
|
||||
{
|
||||
if (_showSubtitles != value)
|
||||
{
|
||||
_showSubtitles = value;
|
||||
foreach (var item in Items)
|
||||
{
|
||||
item.ShowSubtitle = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether labels (both titles and subtitles) are shown.
|
||||
/// Provided for backward compatibility - setting this sets both ShowTitles and ShowSubtitles.
|
||||
/// </summary>
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => _showTitles && _showSubtitles;
|
||||
set
|
||||
{
|
||||
ShowTitles = value;
|
||||
ShowSubtitles = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current label settings before editing.
|
||||
/// </summary>
|
||||
internal void SnapshotShowLabels()
|
||||
{
|
||||
_showTitlesSnapshot = _showTitles;
|
||||
_showSubtitlesSnapshot = _showSubtitles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current label settings to settings.
|
||||
/// </summary>
|
||||
internal void SaveShowLabels()
|
||||
{
|
||||
_bandSettings.ShowTitles = _showTitles;
|
||||
_bandSettings.ShowSubtitles = _showSubtitles;
|
||||
_showTitlesSnapshot = null;
|
||||
_showSubtitlesSnapshot = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the label settings from the snapshot.
|
||||
/// </summary>
|
||||
internal void RestoreShowLabels()
|
||||
{
|
||||
if (_showTitlesSnapshot.HasValue)
|
||||
{
|
||||
ShowTitles = _showTitlesSnapshot.Value;
|
||||
_showTitlesSnapshot = null;
|
||||
}
|
||||
|
||||
if (_showSubtitlesSnapshot.HasValue)
|
||||
{
|
||||
ShowSubtitles = _showSubtitlesSnapshot.Value;
|
||||
_showSubtitlesSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
internal DockBandViewModel(
|
||||
CommandItemViewModel commandItemViewModel,
|
||||
WeakReference<IPageContext> errorContext,
|
||||
DockBandSettings settings,
|
||||
DockSettings dockSettings,
|
||||
Action saveSettings)
|
||||
: base(errorContext)
|
||||
{
|
||||
_rootItem = commandItemViewModel;
|
||||
_bandSettings = settings;
|
||||
_dockSettings = dockSettings;
|
||||
_saveSettings = saveSettings;
|
||||
|
||||
_showTitles = settings.ResolveShowTitles(dockSettings.ShowLabels);
|
||||
_showSubtitles = settings.ResolveShowSubtitles(dockSettings.ShowLabels);
|
||||
}
|
||||
|
||||
private void InitializeFromList(IListPage list)
|
||||
{
|
||||
var items = list.GetItems();
|
||||
var newViewModels = new List<DockItemViewModel>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
var newItemVm = new DockItemViewModel(new(item), this.PageContext, _showTitles, _showSubtitles);
|
||||
newItemVm.SlowInitializeProperties();
|
||||
newViewModels.Add(newItemVm);
|
||||
}
|
||||
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(Items, newViewModels, out var removed);
|
||||
});
|
||||
|
||||
// TODO! dispose removed VMs
|
||||
}
|
||||
|
||||
public override void InitializeProperties()
|
||||
{
|
||||
var command = _rootItem.Command;
|
||||
var list = command.Model.Unsafe as IListPage;
|
||||
if (list is not null)
|
||||
{
|
||||
InitializeFromList(list);
|
||||
list.ItemsChanged += HandleItemsChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
var dockItem = new DockItemViewModel(_rootItem, _showTitles, _showSubtitles);
|
||||
dockItem.SlowInitializeProperties();
|
||||
Items.Add(dockItem);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleItemsChanged(object sender, IItemsChangedEventArgs args)
|
||||
{
|
||||
if (_rootItem.Command.Model.Unsafe is IListPage p)
|
||||
{
|
||||
InitializeFromList(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public partial class DockItemViewModel : CommandItemViewModel
|
||||
{
|
||||
private bool _showTitle = true;
|
||||
private bool _showSubtitle = true;
|
||||
|
||||
public bool ShowTitle
|
||||
{
|
||||
get => _showTitle;
|
||||
internal set
|
||||
{
|
||||
if (_showTitle != value)
|
||||
{
|
||||
_showTitle = value;
|
||||
UpdateProperty(nameof(ShowTitle));
|
||||
UpdateProperty(nameof(ShowLabel));
|
||||
UpdateProperty(nameof(HasText));
|
||||
UpdateProperty(nameof(Title));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSubtitle
|
||||
{
|
||||
get => _showSubtitle;
|
||||
internal set
|
||||
{
|
||||
if (_showSubtitle != value)
|
||||
{
|
||||
_showSubtitle = value;
|
||||
UpdateProperty(nameof(ShowSubtitle));
|
||||
UpdateProperty(nameof(ShowLabel));
|
||||
UpdateProperty(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether labels are shown (either titles or subtitles).
|
||||
/// Setting this sets both ShowTitle and ShowSubtitle.
|
||||
/// </summary>
|
||||
public bool ShowLabel
|
||||
{
|
||||
get => _showTitle || _showSubtitle;
|
||||
internal set
|
||||
{
|
||||
ShowTitle = value;
|
||||
ShowSubtitle = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string Title => _showTitle ? ItemTitle : string.Empty;
|
||||
|
||||
public new string Subtitle => _showSubtitle ? base.Subtitle : string.Empty;
|
||||
|
||||
public override bool HasText => (_showTitle && !string.IsNullOrEmpty(ItemTitle)) || (_showSubtitle && !string.IsNullOrEmpty(base.Subtitle));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tooltip for the dock item, which includes the title and
|
||||
/// subtitle. If it doesn't have one part, it just returns the other.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Trickery: in the case one is empty, we can just concatenate, and it will
|
||||
/// always only be the one that's non-empty
|
||||
/// </remarks>
|
||||
public string Tooltip =>
|
||||
!string.IsNullOrEmpty(ItemTitle) && !string.IsNullOrEmpty(base.Subtitle) ?
|
||||
$"{ItemTitle}\n{base.Subtitle}" :
|
||||
ItemTitle + base.Subtitle;
|
||||
|
||||
public DockItemViewModel(CommandItemViewModel root, bool showTitle, bool showSubtitle)
|
||||
: this(root.Model, root.PageContext, showTitle, showSubtitle)
|
||||
{
|
||||
}
|
||||
|
||||
public DockItemViewModel(ExtensionObject<ICommandItem> item, WeakReference<IPageContext> errorContext, bool showTitle, bool showSubtitle)
|
||||
: base(item, errorContext)
|
||||
{
|
||||
_showTitle = showTitle;
|
||||
_showSubtitle = showSubtitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,602 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public sealed partial class DockViewModel : IDisposable,
|
||||
IRecipient<CommandsReloadedMessage>,
|
||||
IPageContext
|
||||
{
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly SettingsModel _settingsModel;
|
||||
|
||||
private DockSettings _settings;
|
||||
|
||||
public TaskScheduler Scheduler { get; }
|
||||
|
||||
public ObservableCollection<DockBandViewModel> StartItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> CenterItems { get; } = new();
|
||||
|
||||
public ObservableCollection<DockBandViewModel> EndItems { get; } = new();
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> AllItems => _topLevelCommandManager.DockBands;
|
||||
|
||||
public DockViewModel(
|
||||
TopLevelCommandManager tlcManager,
|
||||
SettingsModel settings,
|
||||
TaskScheduler scheduler)
|
||||
{
|
||||
_topLevelCommandManager = tlcManager;
|
||||
_settingsModel = settings;
|
||||
_settings = settings.DockSettings;
|
||||
Scheduler = scheduler;
|
||||
WeakReferenceMessenger.Default.Register<CommandsReloadedMessage>(this);
|
||||
}
|
||||
|
||||
public void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
Logger.LogDebug($"DockViewModel.UpdateSettings");
|
||||
_settings = settings;
|
||||
SetupBands();
|
||||
}
|
||||
|
||||
private void SetupBands()
|
||||
{
|
||||
Logger.LogDebug($"Setting up dock bands");
|
||||
SetupBands(_settings.StartBands, StartItems);
|
||||
SetupBands(_settings.CenterBands, CenterItems);
|
||||
SetupBands(_settings.EndBands, EndItems);
|
||||
}
|
||||
|
||||
private void SetupBands(
|
||||
List<DockBandSettings> bands,
|
||||
ObservableCollection<DockBandViewModel> target)
|
||||
{
|
||||
List<DockBandViewModel> newBands = new();
|
||||
foreach (var band in bands)
|
||||
{
|
||||
var commandId = band.Id;
|
||||
var topLevelCommand = _topLevelCommandManager.LookupDockBand(commandId);
|
||||
|
||||
if (topLevelCommand is null)
|
||||
{
|
||||
Logger.LogWarning($"Failed to find band {commandId}");
|
||||
}
|
||||
|
||||
if (topLevelCommand is not null)
|
||||
{
|
||||
var bandVm = CreateBandItem(band, topLevelCommand.ItemViewModel);
|
||||
newBands.Add(bandVm);
|
||||
}
|
||||
}
|
||||
|
||||
var beforeCount = target.Count;
|
||||
var afterCount = newBands.Count;
|
||||
|
||||
DoOnUiThread(() =>
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList(target, newBands, out var removed);
|
||||
var isStartBand = target == StartItems;
|
||||
var label = isStartBand ? "Start bands:" : "End bands:";
|
||||
Logger.LogDebug($"{label} ({beforeCount}) -> ({afterCount}), Removed {removed?.Count ?? 0} items");
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void Receive(CommandsReloadedMessage message)
|
||||
{
|
||||
SetupBands();
|
||||
CoreLogger.LogDebug("Bands reloaded");
|
||||
}
|
||||
|
||||
private DockBandViewModel CreateBandItem(
|
||||
DockBandSettings bandSettings,
|
||||
CommandItemViewModel commandItem)
|
||||
{
|
||||
DockBandViewModel band = new(commandItem, new(this), bandSettings, _settings, SaveSettings);
|
||||
band.InitializeProperties(); // TODO! make async
|
||||
return band;
|
||||
}
|
||||
|
||||
private void SaveSettings()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandByTopLevel(TopLevelViewModel tlc)
|
||||
{
|
||||
var id = tlc.Id;
|
||||
return FindBandById(id);
|
||||
}
|
||||
|
||||
public DockBandViewModel? FindBandById(string id)
|
||||
{
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
if (band.Id == id)
|
||||
{
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Syncs the band position in settings after a same-list reorder.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void SyncBandPosition(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all settings lists
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
|
||||
// Add to target settings list at the correct index
|
||||
var targetSettings = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => dockSettings.StartBands,
|
||||
DockPinSide.Center => dockSettings.CenterBands,
|
||||
DockPinSide.End => dockSettings.EndBands,
|
||||
_ => dockSettings.StartBands,
|
||||
};
|
||||
var insertIndex = Math.Min(targetIndex, targetSettings.Count);
|
||||
targetSettings.Insert(insertIndex, bandSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a dock band to a new position (cross-list drop).
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void MoveBandWithoutSaving(DockBandViewModel band, DockPinSide targetSide, int targetIndex)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
var bandSettings = dockSettings.StartBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.CenterBands.FirstOrDefault(b => b.Id == bandId)
|
||||
?? dockSettings.EndBands.FirstOrDefault(b => b.Id == bandId);
|
||||
|
||||
if (bandSettings == null)
|
||||
{
|
||||
Logger.LogWarning($"Could not find band settings for band {bandId}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from all sides (settings and UI)
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
// Add to the target side at the specified index
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.StartBands.Count);
|
||||
dockSettings.StartBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, StartItems.Count);
|
||||
StartItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.Center:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.CenterBands.Count);
|
||||
dockSettings.CenterBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, CenterItems.Count);
|
||||
CenterItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
|
||||
case DockPinSide.End:
|
||||
{
|
||||
var settingsIndex = Math.Min(targetIndex, dockSettings.EndBands.Count);
|
||||
dockSettings.EndBands.Insert(settingsIndex, bandSettings);
|
||||
|
||||
var uiIndex = Math.Min(targetIndex, EndItems.Count);
|
||||
EndItems.Insert(uiIndex, band);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Moved band {bandId} to {targetSide} at index {targetIndex} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current band order and label settings to settings.
|
||||
/// Call this when exiting edit mode.
|
||||
/// </summary>
|
||||
public void SaveBandOrder()
|
||||
{
|
||||
// Save ShowLabels for all bands
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
band.SaveShowLabels();
|
||||
}
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotPinnedCommands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
SettingsModel.SaveSettings(_settingsModel);
|
||||
Logger.LogDebug("Saved band order to settings");
|
||||
}
|
||||
|
||||
private List<DockBandSettings>? _snapshotStartBands;
|
||||
private List<DockBandSettings>? _snapshotCenterBands;
|
||||
private List<DockBandSettings>? _snapshotEndBands;
|
||||
private List<string>? _snapshotPinnedCommands;
|
||||
private Dictionary<string, DockBandViewModel>? _snapshotBandViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Takes a snapshot of the current band order and label settings before editing.
|
||||
/// Call this when entering edit mode.
|
||||
/// </summary>
|
||||
public void SnapshotBandOrder()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
_snapshotStartBands = dockSettings.StartBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotCenterBands = dockSettings.CenterBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotEndBands = dockSettings.EndBands.Select(b => new DockBandSettings { Id = b.Id, ShowLabels = b.ShowLabels }).ToList();
|
||||
_snapshotPinnedCommands = dockSettings.PinnedCommands.ToList();
|
||||
|
||||
// Snapshot band ViewModels so we can restore unpinned bands
|
||||
// Use a dictionary but handle potential duplicates gracefully
|
||||
_snapshotBandViewModels = new Dictionary<string, DockBandViewModel>();
|
||||
foreach (var band in StartItems.Concat(CenterItems).Concat(EndItems))
|
||||
{
|
||||
_snapshotBandViewModels.TryAdd(band.Id, band);
|
||||
}
|
||||
|
||||
// Snapshot ShowLabels for all bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.SnapshotShowLabels();
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Snapshot taken: {_snapshotStartBands.Count} start bands, {_snapshotCenterBands.Count} center bands, {_snapshotEndBands.Count} end bands");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the band order and label settings from the snapshot taken when entering edit mode.
|
||||
/// Call this when discarding edit mode changes.
|
||||
/// </summary>
|
||||
public void RestoreBandOrder()
|
||||
{
|
||||
if (_snapshotStartBands == null || _snapshotCenterBands == null || _snapshotEndBands == null || _snapshotBandViewModels == null || _snapshotPinnedCommands == null)
|
||||
{
|
||||
Logger.LogWarning("No snapshot to restore from");
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore ShowLabels for all snapshotted bands
|
||||
foreach (var band in _snapshotBandViewModels.Values)
|
||||
{
|
||||
band.RestoreShowLabels();
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Restore PinnedCommands from snapshot
|
||||
dockSettings.PinnedCommands.Clear();
|
||||
dockSettings.PinnedCommands.AddRange(_snapshotPinnedCommands);
|
||||
|
||||
// Restore settings from snapshot
|
||||
dockSettings.StartBands.Clear();
|
||||
dockSettings.CenterBands.Clear();
|
||||
dockSettings.EndBands.Clear();
|
||||
|
||||
foreach (var bandSnapshot in _snapshotStartBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotCenterBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
foreach (var bandSnapshot in _snapshotEndBands)
|
||||
{
|
||||
var bandSettings = new DockBandSettings { Id = bandSnapshot.Id, ShowLabels = bandSnapshot.ShowLabels };
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
}
|
||||
|
||||
// Rebuild UI collections from restored settings using the snapshotted ViewModels
|
||||
RebuildUICollectionsFromSnapshot();
|
||||
|
||||
_snapshotStartBands = null;
|
||||
_snapshotCenterBands = null;
|
||||
_snapshotEndBands = null;
|
||||
_snapshotPinnedCommands = null;
|
||||
_snapshotBandViewModels = null;
|
||||
Logger.LogDebug("Restored band order from snapshot");
|
||||
}
|
||||
|
||||
private void RebuildUICollectionsFromSnapshot()
|
||||
{
|
||||
if (_snapshotBandViewModels == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (_snapshotBandViewModels.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildUICollections()
|
||||
{
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Create a lookup of all current band ViewModels
|
||||
var allBands = StartItems.Concat(CenterItems).Concat(EndItems).ToDictionary(b => b.Id);
|
||||
|
||||
StartItems.Clear();
|
||||
CenterItems.Clear();
|
||||
EndItems.Clear();
|
||||
|
||||
foreach (var bandSettings in dockSettings.StartBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
StartItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.CenterBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
CenterItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var bandSettings in dockSettings.EndBands)
|
||||
{
|
||||
if (allBands.TryGetValue(bandSettings.Id, out var bandVM))
|
||||
{
|
||||
EndItems.Add(bandVM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of dock bands that are not currently pinned to any section.
|
||||
/// </summary>
|
||||
public IEnumerable<TopLevelViewModel> GetAvailableBandsToAdd()
|
||||
{
|
||||
// Get IDs of all bands currently in the dock
|
||||
var pinnedBandIds = new HashSet<string>();
|
||||
foreach (var band in StartItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in CenterItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
foreach (var band in EndItems)
|
||||
{
|
||||
pinnedBandIds.Add(band.Id);
|
||||
}
|
||||
|
||||
// Return all dock bands that are not already pinned
|
||||
return AllItems.Where(tlc => !pinnedBandIds.Contains(tlc.Id));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a band to the specified dock section.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void AddBandToSection(TopLevelViewModel topLevel, DockPinSide targetSide)
|
||||
{
|
||||
var bandId = topLevel.Id;
|
||||
|
||||
// Check if already in the dock
|
||||
if (FindBandById(bandId) != null)
|
||||
{
|
||||
Logger.LogWarning($"Band {bandId} is already in the dock");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create settings for the new band
|
||||
var bandSettings = new DockBandSettings { Id = bandId, ShowLabels = null };
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// If this is not an explicit dock band (i.e., it's from TopLevelCommands),
|
||||
// add it to PinnedCommands so it gets loaded as a dock band on restart
|
||||
if (!topLevel.IsDockBand && !dockSettings.PinnedCommands.Contains(bandId))
|
||||
{
|
||||
dockSettings.PinnedCommands.Add(bandId);
|
||||
}
|
||||
|
||||
// Create the band view model
|
||||
var bandVm = CreateBandItem(bandSettings, topLevel.ItemViewModel);
|
||||
|
||||
// Add to the appropriate section
|
||||
switch (targetSide)
|
||||
{
|
||||
case DockPinSide.Start:
|
||||
dockSettings.StartBands.Add(bandSettings);
|
||||
StartItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.Center:
|
||||
dockSettings.CenterBands.Add(bandSettings);
|
||||
CenterItems.Add(bandVm);
|
||||
break;
|
||||
case DockPinSide.End:
|
||||
dockSettings.EndBands.Add(bandSettings);
|
||||
EndItems.Add(bandVm);
|
||||
break;
|
||||
}
|
||||
|
||||
// Snapshot the new band so it can be removed on discard
|
||||
bandVm.SnapshotShowLabels();
|
||||
|
||||
Logger.LogDebug($"Added band {bandId} to {targetSide} (not saved yet)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpins a band from the dock, removing it from whichever section it's in.
|
||||
/// Does not save to disk - call SaveBandOrder() when done editing.
|
||||
/// </summary>
|
||||
public void UnpinBand(DockBandViewModel band)
|
||||
{
|
||||
var bandId = band.Id;
|
||||
var dockSettings = _settingsModel.DockSettings;
|
||||
|
||||
// Remove from settings
|
||||
dockSettings.StartBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.CenterBands.RemoveAll(b => b.Id == bandId);
|
||||
dockSettings.EndBands.RemoveAll(b => b.Id == bandId);
|
||||
|
||||
// Also remove from PinnedCommands if it was pinned from TopLevelCommands
|
||||
dockSettings.PinnedCommands.Remove(bandId);
|
||||
|
||||
// Remove from UI collections
|
||||
StartItems.Remove(band);
|
||||
CenterItems.Remove(band);
|
||||
EndItems.Remove(band);
|
||||
|
||||
Logger.LogDebug($"Unpinned band {bandId} (not saved yet)");
|
||||
}
|
||||
|
||||
public void ShowException(Exception ex, string? extensionHint = null)
|
||||
{
|
||||
var extensionText = extensionHint ?? "<unknown>";
|
||||
CoreLogger.LogError($"Error in extension {extensionText}", ex);
|
||||
}
|
||||
|
||||
private void DoOnUiThread(Action action)
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
Scheduler);
|
||||
}
|
||||
|
||||
public CommandItemViewModel GetContextMenuForDock()
|
||||
{
|
||||
var model = new DockContextMenuItem();
|
||||
var vm = new CommandItemViewModel(new(model), new(this));
|
||||
vm.SlowInitializeProperties();
|
||||
return vm;
|
||||
}
|
||||
|
||||
private sealed partial class DockContextMenuItem : CommandItem
|
||||
{
|
||||
public DockContextMenuItem()
|
||||
{
|
||||
var editDockCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new EnterDockEditModeMessage());
|
||||
})
|
||||
{
|
||||
Name = Properties.Resources.dock_edit_dock_name,
|
||||
Icon = Icons.EditIcon,
|
||||
};
|
||||
|
||||
var openSettingsCommand = new AnonymousCommand(
|
||||
action: () =>
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new OpenSettingsMessage("Dock"));
|
||||
})
|
||||
{
|
||||
Name = Properties.Resources.dock_settings_name,
|
||||
Icon = Icons.SettingsIcon,
|
||||
};
|
||||
|
||||
MoreCommands = new CommandContextItem[]
|
||||
{
|
||||
new CommandContextItem(editDockCommand),
|
||||
new CommandContextItem(openSettingsCommand),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
|
||||
public partial class DockWindowViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueue _uiDispatcherQueue = DispatcherQueue.GetForCurrentThread()!;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ImageSource? BackgroundImageSource { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Stretch BackgroundImageStretch { get; private set; } = Stretch.Fill;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageOpacity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundImageTint { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageTintIntensity { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int BackgroundImageBlurAmount { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double BackgroundImageBrightness { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowBackgroundImage { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ShowColorizationOverlay { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color ColorizationColor { get; private set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial double ColorizationOpacity { get; private set; }
|
||||
|
||||
public DockWindowViewModel(IThemeService themeService)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
UpdateFromThemeSnapshot();
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_uiDispatcherQueue.TryEnqueue(UpdateFromThemeSnapshot);
|
||||
}
|
||||
|
||||
private void UpdateFromThemeSnapshot()
|
||||
{
|
||||
var snapshot = _themeService.CurrentDockTheme;
|
||||
|
||||
BackgroundImageSource = snapshot.BackgroundImageSource;
|
||||
BackgroundImageStretch = snapshot.BackgroundImageStretch;
|
||||
BackgroundImageOpacity = snapshot.BackgroundImageOpacity;
|
||||
|
||||
BackgroundImageBrightness = snapshot.BackgroundBrightness;
|
||||
BackgroundImageTint = snapshot.Tint;
|
||||
BackgroundImageTintIntensity = snapshot.TintIntensity;
|
||||
BackgroundImageBlurAmount = snapshot.BlurAmount;
|
||||
|
||||
ShowBackgroundImage = BackgroundImageSource != null;
|
||||
|
||||
// Colorization overlay for transparent backdrop
|
||||
ShowColorizationOverlay = snapshot.Backdrop == DockBackdrop.Transparent && snapshot.TintIntensity > 0;
|
||||
ColorizationColor = snapshot.Tint;
|
||||
ColorizationOpacity = snapshot.TintIntensity;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
using Windows.UI.ViewManagement;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// View model for dock appearance settings, controlling theme, backdrop, colorization,
|
||||
/// and background image settings for the dock.
|
||||
/// </summary>
|
||||
public sealed partial class DockAppearanceSettingsViewModel : ObservableObject, IDisposable
|
||||
{
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly DockSettings _dockSettings;
|
||||
private readonly UISettings _uiSettings;
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DispatcherQueueTimer _saveTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
|
||||
private readonly DispatcherQueue _uiDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
private ElementTheme? _elementThemeOverride;
|
||||
private Color _currentSystemAccentColor;
|
||||
|
||||
public ObservableCollection<Color> Swatches => AppearanceSettingsViewModel.WindowsColorSwatches;
|
||||
|
||||
public int ThemeIndex
|
||||
{
|
||||
get => (int)_dockSettings.Theme;
|
||||
set => Theme = (UserTheme)value;
|
||||
}
|
||||
|
||||
public UserTheme Theme
|
||||
{
|
||||
get => _dockSettings.Theme;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Theme != value)
|
||||
{
|
||||
_dockSettings.Theme = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ThemeIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackdropIndex
|
||||
{
|
||||
get => (int)_dockSettings.Backdrop;
|
||||
set => Backdrop = (DockBackdrop)value;
|
||||
}
|
||||
|
||||
public DockBackdrop Backdrop
|
||||
{
|
||||
get => _dockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.Backdrop != value)
|
||||
{
|
||||
_dockSettings.Backdrop = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackdropIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ColorizationMode ColorizationMode
|
||||
{
|
||||
get => _dockSettings.ColorizationMode;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.ColorizationMode != value)
|
||||
{
|
||||
_dockSettings.ColorizationMode = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(ColorizationModeIndex));
|
||||
OnPropertyChanged(nameof(IsCustomTintVisible));
|
||||
OnPropertyChanged(nameof(IsCustomTintIntensityVisible));
|
||||
OnPropertyChanged(nameof(IsBackgroundControlsVisible));
|
||||
OnPropertyChanged(nameof(IsNoBackgroundVisible));
|
||||
OnPropertyChanged(nameof(IsAccentColorControlsVisible));
|
||||
|
||||
if (value == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
|
||||
IsColorizationDetailsExpanded = value != ColorizationMode.None;
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorizationModeIndex
|
||||
{
|
||||
get => (int)_dockSettings.ColorizationMode;
|
||||
set => ColorizationMode = (ColorizationMode)value;
|
||||
}
|
||||
|
||||
public Color ThemeColor
|
||||
{
|
||||
get => _dockSettings.CustomThemeColor;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.CustomThemeColor != value)
|
||||
{
|
||||
_dockSettings.CustomThemeColor = value;
|
||||
|
||||
OnPropertyChanged();
|
||||
|
||||
if (ColorIntensity == 0)
|
||||
{
|
||||
ColorIntensity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ColorIntensity
|
||||
{
|
||||
get => _dockSettings.CustomThemeColorIntensity;
|
||||
set
|
||||
{
|
||||
_dockSettings.CustomThemeColorIntensity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public string BackgroundImagePath
|
||||
{
|
||||
get => _dockSettings.BackgroundImagePath ?? string.Empty;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImagePath != value)
|
||||
{
|
||||
_dockSettings.BackgroundImagePath = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (BackgroundImageOpacity == 0)
|
||||
{
|
||||
BackgroundImageOpacity = 100;
|
||||
}
|
||||
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageOpacity
|
||||
{
|
||||
get => _dockSettings.BackgroundImageOpacity;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageOpacity != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageOpacity = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBrightness
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBrightness;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBrightness != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBrightness = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageBlurAmount
|
||||
{
|
||||
get => _dockSettings.BackgroundImageBlurAmount;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageBlurAmount != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageBlurAmount = value;
|
||||
OnPropertyChanged();
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit
|
||||
{
|
||||
get => _dockSettings.BackgroundImageFit;
|
||||
set
|
||||
{
|
||||
if (_dockSettings.BackgroundImageFit != value)
|
||||
{
|
||||
_dockSettings.BackgroundImageFit = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(BackgroundImageFitIndex));
|
||||
Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int BackgroundImageFitIndex
|
||||
{
|
||||
get => BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => 1,
|
||||
_ => 0,
|
||||
};
|
||||
set => BackgroundImageFit = value switch
|
||||
{
|
||||
1 => BackgroundImageFit.Fill,
|
||||
_ => BackgroundImageFit.UniformToFill,
|
||||
};
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsColorizationDetailsExpanded { get; set; }
|
||||
|
||||
public bool IsCustomTintVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsCustomTintIntensityVisible => _dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image;
|
||||
|
||||
public bool IsBackgroundControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.Image;
|
||||
|
||||
public bool IsNoBackgroundVisible => _dockSettings.ColorizationMode is ColorizationMode.None;
|
||||
|
||||
public bool IsAccentColorControlsVisible => _dockSettings.ColorizationMode is ColorizationMode.WindowsAccentColor;
|
||||
|
||||
public ElementTheme EffectiveTheme => _elementThemeOverride ?? _themeService.Current.Theme;
|
||||
|
||||
public Color EffectiveThemeColor => ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.WindowsAccentColor => _currentSystemAccentColor,
|
||||
ColorizationMode.CustomColor or ColorizationMode.Image => ThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
|
||||
// Since the blur amount is absolute, we need to scale it down for the preview (which is smaller than full screen).
|
||||
public int EffectiveBackgroundImageBlurAmount => (int)Math.Round(BackgroundImageBlurAmount / 4f);
|
||||
|
||||
public double EffectiveBackgroundImageBrightness => BackgroundImageBrightness / 100.0;
|
||||
|
||||
public ImageSource? EffectiveBackgroundImageSource =>
|
||||
ColorizationMode is ColorizationMode.Image
|
||||
&& !string.IsNullOrWhiteSpace(BackgroundImagePath)
|
||||
&& Uri.TryCreate(BackgroundImagePath, UriKind.RelativeOrAbsolute, out var uri)
|
||||
? new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri)
|
||||
: null;
|
||||
|
||||
public DockAppearanceSettingsViewModel(IThemeService themeService, SettingsModel settings)
|
||||
{
|
||||
_themeService = themeService;
|
||||
_themeService.ThemeChanged += ThemeServiceOnThemeChanged;
|
||||
_settings = settings;
|
||||
_dockSettings = settings.DockSettings;
|
||||
|
||||
_uiSettings = new UISettings();
|
||||
_uiSettings.ColorValuesChanged += UiSettingsOnColorValuesChanged;
|
||||
UpdateAccentColor(_uiSettings);
|
||||
|
||||
Reapply();
|
||||
|
||||
IsColorizationDetailsExpanded = _dockSettings.ColorizationMode != ColorizationMode.None;
|
||||
}
|
||||
|
||||
private void UiSettingsOnColorValuesChanged(UISettings sender, object args) => _uiDispatcher.TryEnqueue(() => UpdateAccentColor(sender));
|
||||
|
||||
private void UpdateAccentColor(UISettings sender)
|
||||
{
|
||||
_currentSystemAccentColor = sender.GetColorValue(UIColorType.Accent);
|
||||
if (ColorizationMode == ColorizationMode.WindowsAccentColor)
|
||||
{
|
||||
ThemeColor = _currentSystemAccentColor;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeServiceOnThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Save()
|
||||
{
|
||||
SettingsModel.SaveSettings(_settings);
|
||||
_saveTimer.Debounce(Reapply, TimeSpan.FromMilliseconds(200));
|
||||
}
|
||||
|
||||
private void Reapply()
|
||||
{
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBrightness));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageSource));
|
||||
OnPropertyChanged(nameof(EffectiveThemeColor));
|
||||
OnPropertyChanged(nameof(EffectiveBackgroundImageBlurAmount));
|
||||
|
||||
// LOAD BEARING:
|
||||
// We need to cycle through the EffectiveTheme property to force reload of resources.
|
||||
_elementThemeOverride = ElementTheme.Light;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = ElementTheme.Dark;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
_elementThemeOverride = null;
|
||||
OnPropertyChanged(nameof(EffectiveTheme));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ResetBackgroundImageProperties()
|
||||
{
|
||||
BackgroundImageBrightness = 0;
|
||||
BackgroundImageBlurAmount = 0;
|
||||
BackgroundImageFit = BackgroundImageFit.UniformToFill;
|
||||
BackgroundImageOpacity = 100;
|
||||
ColorIntensity = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_uiSettings.ColorValuesChanged -= UiSettingsOnColorValuesChanged;
|
||||
_themeService.ThemeChanged -= ThemeServiceOnThemeChanged;
|
||||
}
|
||||
}
|
||||
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
18
src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Icons.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// 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.UI.ViewModels;
|
||||
|
||||
internal sealed class Icons
|
||||
{
|
||||
internal static IconInfo PinIcon => new("\uE718"); // Pin icon
|
||||
|
||||
internal static IconInfo UnpinIcon => new("\uE77A"); // Unpin icon
|
||||
|
||||
internal static IconInfo SettingsIcon => new("\uE713"); // Settings icon
|
||||
|
||||
internal static IconInfo EditIcon => new("\uE70F"); // Edit icon
|
||||
}
|
||||
@@ -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.UI.ViewModels.Messages;
|
||||
|
||||
public record CommandsReloadedMessage();
|
||||
@@ -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.UI.ViewModels.Messages;
|
||||
|
||||
public record EnterDockEditModeMessage();
|
||||
@@ -4,6 +4,6 @@
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Messages;
|
||||
|
||||
public record OpenSettingsMessage()
|
||||
public record OpenSettingsMessage(string? Page = null)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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.UI.ViewModels.Messages;
|
||||
|
||||
public record ShowHideDockMessage(bool ShowDock);
|
||||
@@ -60,6 +60,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open Command Palette.
|
||||
/// </summary>
|
||||
public static string builtin_command_palette_title {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_command_palette_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Create another.
|
||||
/// </summary>
|
||||
@@ -285,6 +294,15 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Built-in.
|
||||
/// </summary>
|
||||
public static string builtin_extension_name_fallback {
|
||||
get {
|
||||
return ResourceManager.GetString("builtin_extension_name_fallback", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Home.
|
||||
/// </summary>
|
||||
@@ -429,6 +447,60 @@ namespace Microsoft.CmdPal.UI.ViewModels.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Edit dock.
|
||||
/// </summary>
|
||||
public static string dock_edit_dock_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_edit_dock_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} items.
|
||||
/// </summary>
|
||||
public static string dock_item_count_plural {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_plural", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to 1 item.
|
||||
/// </summary>
|
||||
public static string dock_item_count_singular {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_item_count_singular", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Pin to dock.
|
||||
/// </summary>
|
||||
public static string dock_pin_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_pin_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dock settings.
|
||||
/// </summary>
|
||||
public static string dock_settings_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_settings_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unpin from dock.
|
||||
/// </summary>
|
||||
public static string dock_unpin_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("dock_unpin_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Fallbacks.
|
||||
/// </summary>
|
||||
|
||||
@@ -239,10 +239,42 @@
|
||||
<data name="builtin_settings_extension_n_extensions_installed" xml:space="preserve">
|
||||
<value>{0} extensions installed</value>
|
||||
</data>
|
||||
<data name="builtin_extension_name_fallback" xml:space="preserve">
|
||||
<value>Built-in</value>
|
||||
<comment>Fallback name for built-in extensions</comment>
|
||||
</data>
|
||||
<data name="dock_pin_command_name" xml:space="preserve">
|
||||
<value>Pin to dock</value>
|
||||
<comment>Command name for pinning an item to the dock</comment>
|
||||
</data>
|
||||
<data name="dock_unpin_command_name" xml:space="preserve">
|
||||
<value>Unpin from dock</value>
|
||||
<comment>Command name for unpinning an item from the dock</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_singular" xml:space="preserve">
|
||||
<value>1 item</value>
|
||||
<comment>Singular form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="dock_item_count_plural" xml:space="preserve">
|
||||
<value>{0} items</value>
|
||||
<comment>Plural form for item count in dock band</comment>
|
||||
</data>
|
||||
<data name="builtin_command_palette_title" xml:space="preserve">
|
||||
<value>Open Command Palette</value>
|
||||
<comment>Title for the command to open the command palette</comment>
|
||||
</data>
|
||||
<data name="builtin_settings_appearance_pick_background_image_title" xml:space="preserve">
|
||||
<value>Pick background image</value>
|
||||
</data>
|
||||
<data name="fallbacks" xml:space="preserve">
|
||||
<value>Fallbacks</value>
|
||||
</data>
|
||||
<data name="dock_edit_dock_name" xml:space="preserve">
|
||||
<value>Edit dock</value>
|
||||
<comment>Command name for editing the dock</comment>
|
||||
</data>
|
||||
<data name="dock_settings_name" xml:space="preserve">
|
||||
<value>Dock settings</value>
|
||||
<comment>Command name for opening dock settings</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,73 @@
|
||||
// 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.UI.ViewModels.Settings;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a snapshot of dock theme-related visual settings, including accent color, theme preference,
|
||||
/// backdrop, and background image configuration, for use in rendering the Dock UI.
|
||||
/// </summary>
|
||||
public sealed class DockThemeSnapshot
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the accent tint color used by the Dock visuals.
|
||||
/// </summary>
|
||||
public required Color Tint { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the intensity of the accent tint color (0-1 range).
|
||||
/// </summary>
|
||||
public required float TintIntensity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configured application theme preference for the Dock.
|
||||
/// </summary>
|
||||
public required ElementTheme Theme { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the backdrop type for the Dock.
|
||||
/// </summary>
|
||||
public required DockBackdrop Backdrop { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image source to render as the background, if any.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns <see langword="null"/> when no background image is configured.
|
||||
/// </remarks>
|
||||
public required ImageSource? BackgroundImageSource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stretch mode used to lay out the background image.
|
||||
/// </summary>
|
||||
public required Stretch BackgroundImageStretch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the opacity applied to the background image.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A value in the range [0, 1], where 0 is fully transparent and 1 is fully opaque.
|
||||
/// </value>
|
||||
public required double BackgroundImageOpacity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the effective acrylic backdrop parameters based on current settings and theme.
|
||||
/// </summary>
|
||||
public required AcrylicBackdropParameters BackdropParameters { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blur amount for the background image.
|
||||
/// </summary>
|
||||
public required int BlurAmount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the brightness adjustment for the background (0-1 range).
|
||||
/// </summary>
|
||||
public required float BackgroundBrightness { get; init; }
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
// 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.UI.ViewModels.Settings;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,4 +38,9 @@ public interface IThemeService
|
||||
/// Gets the current theme settings.
|
||||
/// </summary>
|
||||
ThemeSnapshot Current { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current dock theme settings.
|
||||
/// </summary>
|
||||
DockThemeSnapshot CurrentDockTheme { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
/// <summary>
|
||||
/// Settings for the Dock. These are settings for _the whole dock_. Band-specific
|
||||
/// settings are in <see cref="DockBandSettings"/>.
|
||||
/// </summary>
|
||||
public class DockSettings
|
||||
{
|
||||
public DockSide Side { get; set; } = DockSide.Top;
|
||||
|
||||
public DockSize DockSize { get; set; } = DockSize.Small;
|
||||
|
||||
public DockSize DockIconsSize { get; set; } = DockSize.Small;
|
||||
|
||||
// Theme settings
|
||||
public DockBackdrop Backdrop { get; set; } = DockBackdrop.Acrylic;
|
||||
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
|
||||
public Color CustomThemeColor { get; set; } = Colors.Transparent;
|
||||
|
||||
public int CustomThemeColorIntensity { get; set; } = 100;
|
||||
|
||||
public int BackgroundImageOpacity { get; set; } = 20;
|
||||
|
||||
public int BackgroundImageBlurAmount { get; set; }
|
||||
|
||||
public int BackgroundImageBrightness { get; set; }
|
||||
|
||||
public BackgroundImageFit BackgroundImageFit { get; set; }
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
// /Theme settings
|
||||
public List<string> PinnedCommands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> StartBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> CenterBands { get; set; } = [];
|
||||
|
||||
public List<DockBandSettings> EndBands { get; set; } = [];
|
||||
|
||||
public bool ShowLabels { get; set; } = true;
|
||||
|
||||
public DockSettings()
|
||||
{
|
||||
// Initialize with default values
|
||||
PinnedCommands = [
|
||||
"com.microsoft.cmdpal.winget"
|
||||
];
|
||||
|
||||
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.home" });
|
||||
StartBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.winget", ShowLabels = false });
|
||||
|
||||
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.performanceWidget" });
|
||||
EndBands.Add(new DockBandSettings { Id = "com.microsoft.cmdpal.timedate.dockband" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Settings for a specific dock band. These are per-band settings stored
|
||||
/// within the overall <see cref="DockSettings"/>.
|
||||
/// </summary>
|
||||
public class DockBandSettings
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether titles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowTitles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether subtitles are shown for items in this band.
|
||||
/// If null, falls back to dock-wide ShowLabels setting.
|
||||
/// </summary>
|
||||
public bool? ShowSubtitles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for backward compatibility. Maps to ShowTitles.
|
||||
/// </summary>
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool? ShowLabels
|
||||
{
|
||||
get => ShowTitles;
|
||||
set => ShowTitles = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowTitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
public bool ResolveShowTitles(bool defaultValue) => ShowTitles ?? defaultValue;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the effective value of <see cref="ShowSubtitles"/> for this band.
|
||||
/// If this band doesn't have a specific value set, we'll fall back to the
|
||||
/// dock-wide setting (passed as <paramref name="defaultValue"/>).
|
||||
/// </summary>
|
||||
public bool ResolveShowSubtitles(bool defaultValue) => ShowSubtitles ?? defaultValue;
|
||||
}
|
||||
|
||||
public enum DockSide
|
||||
{
|
||||
Left = 0,
|
||||
Top = 1,
|
||||
Right = 2,
|
||||
Bottom = 3,
|
||||
}
|
||||
|
||||
public enum DockSize
|
||||
{
|
||||
Small,
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
||||
public enum DockBackdrop
|
||||
{
|
||||
Transparent,
|
||||
Acrylic,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -66,6 +66,11 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public EscapeKeyBehavior EscapeKeyBehaviorSetting { get; set; } = EscapeKeyBehavior.ClearSearchFirstThenGoBack;
|
||||
|
||||
public bool EnableDock { get; set; }
|
||||
|
||||
public DockSettings DockSettings { get; set; } = new();
|
||||
|
||||
// Theme settings
|
||||
public UserTheme Theme { get; set; } = UserTheme.Default;
|
||||
|
||||
public ColorizationMode ColorizationMode { get; set; }
|
||||
@@ -84,6 +89,8 @@ public partial class SettingsModel : ObservableObject
|
||||
|
||||
public string? BackgroundImagePath { get; set; }
|
||||
|
||||
// </Theme settings>
|
||||
|
||||
// END SETTINGS
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -303,6 +310,7 @@ public partial class SettingsModel : ObservableObject
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(bool))]
|
||||
[JsonSerializable(typeof(Color))]
|
||||
[JsonSerializable(typeof(HistoryItem))]
|
||||
[JsonSerializable(typeof(SettingsModel))]
|
||||
[JsonSerializable(typeof(WindowPosition))]
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -32,6 +35,8 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
|
||||
public AppearanceSettingsViewModel Appearance { get; }
|
||||
|
||||
public DockAppearanceSettingsViewModel DockAppearance { get; }
|
||||
|
||||
public HotkeySettings? Hotkey
|
||||
{
|
||||
get => _settings.Hotkey;
|
||||
@@ -173,6 +178,58 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public DockSide Dock_Side
|
||||
{
|
||||
get => _settings.DockSettings.Side;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Side = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockSize Dock_DockSize
|
||||
{
|
||||
get => _settings.DockSettings.DockSize;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.DockSize = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public DockBackdrop Dock_Backdrop
|
||||
{
|
||||
get => _settings.DockSettings.Backdrop;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.Backdrop = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dock_ShowLabels
|
||||
{
|
||||
get => _settings.DockSettings.ShowLabels;
|
||||
set
|
||||
{
|
||||
_settings.DockSettings.ShowLabels = value;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnableDock
|
||||
{
|
||||
get => _settings.EnableDock;
|
||||
set
|
||||
{
|
||||
_settings.EnableDock = value;
|
||||
Save();
|
||||
WeakReferenceMessenger.Default.Send(new ShowHideDockMessage(value));
|
||||
WeakReferenceMessenger.Default.Send(new ReloadCommandsMessage()); // TODO! we need to update the MoreCommands of all top level items, but we don't _really_ want to reload
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ProviderSettingsViewModel> CommandProviders { get; } = new();
|
||||
|
||||
public ObservableCollection<FallbackSettingsViewModel> FallbackRankings { get; set; } = new();
|
||||
@@ -185,6 +242,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
|
||||
Appearance = new AppearanceSettingsViewModel(themeService, _settings);
|
||||
DockAppearance = new DockAppearanceSettingsViewModel(themeService, _settings);
|
||||
|
||||
var activeProviders = GetCommandProviders();
|
||||
var allProviderSettings = _settings.ProviderSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -44,6 +44,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> TopLevelCommands { get; set; } = [];
|
||||
|
||||
public ObservableCollection<TopLevelViewModel> DockBands { get; set; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsLoading { get; private set; } = true;
|
||||
|
||||
@@ -79,12 +81,23 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
_builtInCommands.Add(wrapper);
|
||||
}
|
||||
|
||||
var commands = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
var objects = await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
if (objects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (objects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +110,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
|
||||
// May be called from a background thread
|
||||
private async Task<IEnumerable<TopLevelViewModel>> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
private async Task<TopLevelObjectSets> LoadTopLevelCommandsFromProvider(CommandProviderWrapper commandProvider)
|
||||
{
|
||||
WeakReference<IPageContext> weakSelf = new(this);
|
||||
|
||||
@@ -107,6 +120,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
() =>
|
||||
{
|
||||
List<TopLevelViewModel> commands = [];
|
||||
List<TopLevelViewModel> bands = [];
|
||||
foreach (var item in commandProvider.TopLevelItems)
|
||||
{
|
||||
commands.Add(item);
|
||||
@@ -120,7 +134,15 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
return commands;
|
||||
foreach (var item in commandProvider.DockBandItems)
|
||||
{
|
||||
bands.Add(item);
|
||||
}
|
||||
|
||||
var commandsCount = commands.Count;
|
||||
var bandsCount = bands.Count;
|
||||
Logger.LogDebug($"{commandProvider.ProviderId}: Loaded {commandsCount} commands, {bandsCount} bands");
|
||||
return new TopLevelObjectSets(commands, bands);
|
||||
},
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
@@ -160,6 +182,8 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
List<TopLevelViewModel> newBands = [.. sender.DockBandItems];
|
||||
|
||||
// modify the TopLevelCommands under shared lock; event if we clone it, we don't want
|
||||
// TopLevelCommands to get modified while we're working on it. Otherwise, we might
|
||||
// out clone would be stale at the end of this method.
|
||||
@@ -176,6 +200,13 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
clone.InsertRange(startIndex, newItems);
|
||||
|
||||
ListHelpers.InPlaceUpdateList(TopLevelCommands, clone);
|
||||
|
||||
// same idea for DockBands
|
||||
List<TopLevelViewModel> dockClone = [.. DockBands];
|
||||
var dockStartIndex = FindIndexForFirstProviderItem(dockClone, sender.ProviderId);
|
||||
dockClone.RemoveAll(item => item.CommandProviderId == sender.ProviderId);
|
||||
dockClone.InsertRange(dockStartIndex, newBands);
|
||||
ListHelpers.InPlaceUpdateList(DockBands, dockClone);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -222,6 +253,7 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
TopLevelCommands.Clear();
|
||||
DockBands.Clear();
|
||||
}
|
||||
|
||||
await LoadBuiltinsAsync();
|
||||
@@ -300,17 +332,34 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var commands in commandSets)
|
||||
foreach (var providerObjects in commandSets)
|
||||
{
|
||||
foreach (var c in commands)
|
||||
var commandsCount = providerObjects.Commands?.Count() ?? 0;
|
||||
var bandsCount = providerObjects.DockBands?.Count() ?? 0;
|
||||
Logger.LogDebug($"(some provider) Loaded {commandsCount} commands and {bandsCount} bands");
|
||||
|
||||
if (providerObjects.Commands is IEnumerable<TopLevelViewModel> commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
foreach (var c in commands)
|
||||
{
|
||||
TopLevelCommands.Add(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (providerObjects.DockBands is IEnumerable<TopLevelViewModel> bands)
|
||||
{
|
||||
foreach (var c in bands)
|
||||
{
|
||||
DockBands.Add(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timer.Stop();
|
||||
Logger.LogDebug($"Loading extensions took {timer.ElapsedMilliseconds} ms");
|
||||
|
||||
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
|
||||
}
|
||||
|
||||
private async Task<CommandProviderWrapper?> StartExtensionWithTimeoutAsync(IExtensionWrapper extension)
|
||||
@@ -328,7 +377,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<TopLevelViewModel>?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
private record TopLevelObjectSets(IEnumerable<TopLevelViewModel>? Commands, IEnumerable<TopLevelViewModel>? DockBands);
|
||||
|
||||
private async Task<TopLevelObjectSets?> LoadCommandsWithTimeoutAsync(CommandProviderWrapper wrapper)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -408,6 +459,23 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
return null;
|
||||
}
|
||||
|
||||
public TopLevelViewModel? LookupDockBand(string id)
|
||||
{
|
||||
// TODO! bad that we're using TopLevelCommands as the object to lock, even for bands
|
||||
lock (TopLevelCommands)
|
||||
{
|
||||
foreach (var command in DockBands)
|
||||
{
|
||||
if (command.Id == id)
|
||||
{
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Receive(ReloadCommandsMessage message) =>
|
||||
ReloadAllCommandsAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -426,6 +494,52 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
}
|
||||
}
|
||||
|
||||
internal void PinDockBand(TopLevelViewModel bandVm)
|
||||
{
|
||||
lock (DockBands)
|
||||
{
|
||||
foreach (var existing in DockBands)
|
||||
{
|
||||
if (existing.Id == bandVm.Id)
|
||||
{
|
||||
// already pinned
|
||||
Logger.LogDebug($"Dock band '{bandVm.Id}' is already pinned.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Attempting to pin dock band '{bandVm.Id}' from provider '{bandVm.CommandProviderId}'.");
|
||||
var providerId = bandVm.CommandProviderId;
|
||||
var foundProvider = false;
|
||||
foreach (var provider in CommandProviders)
|
||||
{
|
||||
if (provider.Id == providerId)
|
||||
{
|
||||
Logger.LogDebug($"Found provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
provider.PinDockBand(bandVm);
|
||||
foundProvider = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundProvider)
|
||||
{
|
||||
Logger.LogWarning($"Could not find provider '{providerId}' to pin dock band '{bandVm.Id}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the band to DockBands if not already present
|
||||
if (!DockBands.Any(b => b.Id == bandVm.Id))
|
||||
{
|
||||
DockBands.Add(bandVm);
|
||||
}
|
||||
|
||||
// Notify DockViewModel to update its collections
|
||||
WeakReferenceMessenger.Default.Send<CommandsReloadedMessage>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_reloadCommandsGate.Dispose();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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 CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.Common.Helpers;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -22,6 +24,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
private readonly ProviderSettings _providerSettings;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly CommandItemViewModel _commandItemViewModel;
|
||||
private readonly DockViewModel? _dockViewModel;
|
||||
|
||||
private readonly string _commandProviderId;
|
||||
|
||||
@@ -45,39 +48,28 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
|
||||
public CommandPaletteHost ExtensionHost { get; private set; }
|
||||
|
||||
public string ExtensionName => ExtensionHost.Extension?.ExtensionDisplayName ?? Properties.Resources.builtin_extension_name_fallback;
|
||||
|
||||
public CommandViewModel CommandViewModel => _commandItemViewModel.Command;
|
||||
|
||||
public CommandItemViewModel ItemViewModel => _commandItemViewModel;
|
||||
|
||||
public string CommandProviderId => _commandProviderId;
|
||||
|
||||
public IconInfoViewModel IconViewModel => _commandItemViewModel.Icon;
|
||||
|
||||
////// ICommandItem
|
||||
public string Title => _commandItemViewModel.Title;
|
||||
|
||||
public string Subtitle => _commandItemViewModel.Subtitle;
|
||||
|
||||
public IIconInfo Icon => _commandItemViewModel.Icon;
|
||||
public IIconInfo Icon => (IIconInfo)IconViewModel;
|
||||
|
||||
public IIconInfo InitialIcon => _initialIcon ?? _commandItemViewModel.Icon;
|
||||
|
||||
ICommand? ICommandItem.Command => _commandItemViewModel.Command.Model.Unsafe;
|
||||
|
||||
IContextItem?[] ICommandItem.MoreCommands => _commandItemViewModel.MoreCommands
|
||||
.Select(item =>
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
return item as IContextItem;
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
return commandItem.Model.Unsafe;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).ToArray();
|
||||
IContextItem?[] ICommandItem.MoreCommands => BuildContextMenu();
|
||||
|
||||
////// IListItem
|
||||
ITag[] IListItem.Tags => Tags.ToArray();
|
||||
@@ -176,9 +168,37 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
}
|
||||
}
|
||||
|
||||
// Dock properties
|
||||
public bool IsDockBand { get; private set; }
|
||||
|
||||
public DockBandSettings? DockBandSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsDockBand)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bandSettings = _settings.DockSettings.StartBands
|
||||
.Concat(_settings.DockSettings.EndBands)
|
||||
.FirstOrDefault(band => band.Id == this.Id);
|
||||
if (bandSettings is null)
|
||||
{
|
||||
return new DockBandSettings()
|
||||
{
|
||||
Id = this.Id,
|
||||
ShowLabels = true,
|
||||
};
|
||||
}
|
||||
|
||||
return bandSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public TopLevelViewModel(
|
||||
CommandItemViewModel item,
|
||||
bool isFallback,
|
||||
TopLevelType topLevelType,
|
||||
CommandPaletteHost extensionHost,
|
||||
string commandProviderId,
|
||||
SettingsModel settings,
|
||||
@@ -192,23 +212,23 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
_commandProviderId = commandProviderId;
|
||||
_commandItemViewModel = item;
|
||||
|
||||
IsFallback = isFallback;
|
||||
IsFallback = topLevelType == TopLevelType.Fallback;
|
||||
IsDockBand = topLevelType == TopLevelType.DockBand;
|
||||
ExtensionHost = extensionHost;
|
||||
if (isFallback && commandItem is FallbackCommandItem fallback)
|
||||
if (IsFallback && commandItem is FallbackCommandItem fallback)
|
||||
{
|
||||
_fallbackId = fallback.Id;
|
||||
}
|
||||
|
||||
item.PropertyChanged += Item_PropertyChanged;
|
||||
|
||||
// UpdateAlias();
|
||||
// UpdateHotkey();
|
||||
// UpdateTags();
|
||||
_dockViewModel = serviceProvider.GetService<DockViewModel>();
|
||||
}
|
||||
|
||||
internal void InitializeProperties()
|
||||
{
|
||||
ItemViewModel.SlowInitializeProperties();
|
||||
GenerateId();
|
||||
|
||||
if (IsFallback)
|
||||
{
|
||||
@@ -260,7 +280,7 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
return;
|
||||
}
|
||||
|
||||
_initialIcon = _commandItemViewModel.Icon;
|
||||
_initialIcon = (IIconInfo?)_commandItemViewModel.Icon;
|
||||
|
||||
if (raiseNotification)
|
||||
{
|
||||
@@ -420,4 +440,167 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem, IEx
|
||||
[WellKnownExtensionAttributes.DataPackage] = _commandItemViewModel?.DataPackage,
|
||||
};
|
||||
}
|
||||
|
||||
private IContextItem?[] BuildContextMenu()
|
||||
{
|
||||
List<IContextItem?> contextItems = new();
|
||||
|
||||
foreach (var item in _commandItemViewModel.MoreCommands)
|
||||
{
|
||||
if (item is ISeparatorContextItem)
|
||||
{
|
||||
contextItems.Add(item as IContextItem);
|
||||
}
|
||||
else if (item is CommandContextItemViewModel commandItem)
|
||||
{
|
||||
contextItems.Add(commandItem.Model.Unsafe);
|
||||
}
|
||||
}
|
||||
|
||||
var dockEnabled = _settings.EnableDock;
|
||||
if (dockEnabled && _dockViewModel is not null)
|
||||
{
|
||||
// Add a separator
|
||||
contextItems.Add(new Separator());
|
||||
|
||||
var inStartBands = _settings.DockSettings.StartBands.Any(band => band.Id == this.Id);
|
||||
var inEndBands = _settings.DockSettings.EndBands.Any(band => band.Id == this.Id);
|
||||
var alreadyPinned = (inStartBands || inEndBands) &&
|
||||
_settings.DockSettings.PinnedCommands.Contains(this.Id);
|
||||
|
||||
var pinCommand = new PinToDockCommand(
|
||||
this,
|
||||
!alreadyPinned,
|
||||
_dockViewModel,
|
||||
_settings,
|
||||
_serviceProvider.GetService<TopLevelCommandManager>()!);
|
||||
|
||||
var contextItem = new CommandContextItem(pinCommand);
|
||||
|
||||
contextItems.Add(contextItem);
|
||||
}
|
||||
|
||||
return contextItems.ToArray();
|
||||
}
|
||||
|
||||
internal ICommandItem ToPinnedDockBandItem()
|
||||
{
|
||||
var item = new PinnedDockItem(item: this, id: Id);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
internal TopLevelViewModel CloneAsBand()
|
||||
{
|
||||
return new TopLevelViewModel(
|
||||
_commandItemViewModel,
|
||||
TopLevelType.DockBand,
|
||||
ExtensionHost,
|
||||
_commandProviderId,
|
||||
_settings,
|
||||
_providerSettings,
|
||||
_serviceProvider,
|
||||
_commandItemViewModel.Model.Unsafe);
|
||||
}
|
||||
|
||||
private sealed partial class PinToDockCommand : InvokableCommand
|
||||
{
|
||||
private readonly TopLevelViewModel _topLevelViewModel;
|
||||
private readonly DockViewModel _dockViewModel;
|
||||
private readonly SettingsModel _settings;
|
||||
private readonly TopLevelCommandManager _topLevelCommandManager;
|
||||
private readonly bool _pin;
|
||||
|
||||
public override IconInfo Icon => _pin ? Icons.PinIcon : Icons.UnpinIcon;
|
||||
|
||||
public override string Name => _pin ? Properties.Resources.dock_pin_command_name : Properties.Resources.dock_unpin_command_name;
|
||||
|
||||
public PinToDockCommand(
|
||||
TopLevelViewModel topLevelViewModel,
|
||||
bool pin,
|
||||
DockViewModel dockViewModel,
|
||||
SettingsModel settings,
|
||||
TopLevelCommandManager topLevelCommandManager)
|
||||
{
|
||||
_topLevelViewModel = topLevelViewModel;
|
||||
_dockViewModel = dockViewModel;
|
||||
_settings = settings;
|
||||
_topLevelCommandManager = topLevelCommandManager;
|
||||
_pin = pin;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Logger.LogDebug($"PinToDockCommand.Invoke({_pin}): {_topLevelViewModel.Id}");
|
||||
if (_pin)
|
||||
{
|
||||
PinToDock();
|
||||
}
|
||||
else
|
||||
{
|
||||
UnpinFromDock();
|
||||
}
|
||||
|
||||
// Notify that the MoreCommands have changed, so the context menu updates
|
||||
_topLevelViewModel.PropChanged?.Invoke(
|
||||
_topLevelViewModel,
|
||||
new PropChangedEventArgs(nameof(ICommandItem.MoreCommands)));
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
private void PinToDock()
|
||||
{
|
||||
// It's possible that the top-level command shares an ID with a
|
||||
// band. In that case, we don't want to add it to PinnedCommands.
|
||||
// PinnedCommands is just for top-level commands IDs that aren't
|
||||
// otherwise bands.
|
||||
//
|
||||
// Check the top-level command ID against the bands first.
|
||||
if (_topLevelCommandManager.DockBands.Any(band => band.Id == _topLevelViewModel.Id))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
// In this case, the ID isn't another band, so add it to PinnedCommands.
|
||||
if (!_settings.DockSettings.PinnedCommands.Contains(_topLevelViewModel.Id))
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Add(_topLevelViewModel.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO! Deal with "the command ID is already pinned in
|
||||
// PinnedCommands but not in one of StartBands/EndBands". I think
|
||||
// we're already avoiding adding it to PinnedCommands above, but I
|
||||
// think that PinDockBand below will create a duplicate VM for it.
|
||||
_settings.DockSettings.StartBands.Add(new DockBandSettings()
|
||||
{
|
||||
Id = _topLevelViewModel.Id,
|
||||
ShowLabels = true,
|
||||
});
|
||||
|
||||
// Create a new band VM from our current TLVM. This will allow us to
|
||||
// update the bands in the CommandProviderWrapper and the TLCM,
|
||||
// without forcing a whole reload
|
||||
var bandVm = _topLevelViewModel.CloneAsBand();
|
||||
_topLevelCommandManager.PinDockBand(bandVm);
|
||||
|
||||
_topLevelViewModel.Save();
|
||||
}
|
||||
|
||||
private void UnpinFromDock()
|
||||
{
|
||||
_settings.DockSettings.PinnedCommands.Remove(_topLevelViewModel.Id);
|
||||
_settings.DockSettings.StartBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
|
||||
_settings.DockSettings.EndBands.RemoveAll(band => band.Id == _topLevelViewModel.Id);
|
||||
|
||||
_topLevelViewModel.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TopLevelType
|
||||
{
|
||||
Normal,
|
||||
Fallback,
|
||||
DockBand,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI"
|
||||
xmlns:services="using:Microsoft.CmdPal.UI.Services">
|
||||
<Application.Resources>
|
||||
@@ -11,13 +12,14 @@
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Colors.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TeachingTip.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/TextBox.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Settings.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/Tag.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/KeyVisual/KeyVisual.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Controls/IsEnabledTextBlock.xaml" />
|
||||
<ResourceDictionary Source="ms-appx:///Dock/DockItemControl.xaml" />
|
||||
<!-- Default theme dictionary -->
|
||||
<ResourceDictionary Source="ms-appx:///Styles/Theme.Normal.xaml" />
|
||||
<services:MutableOverridesDictionary />
|
||||
@@ -26,6 +28,14 @@
|
||||
|
||||
<x:Double x:Key="SettingActionControlMinWidth">240</x:Double>
|
||||
<Style BasedOn="{StaticResource DefaultCheckBoxStyle}" TargetType="controls:CheckBoxWithDescriptionControl" />
|
||||
|
||||
<converters:StringVisibilityConverter
|
||||
x:Key="StringNotEmptyToVisibilityConverter"
|
||||
EmptyValue="Collapsed"
|
||||
NotEmptyValue="Visible" />
|
||||
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -12,6 +12,7 @@ using Microsoft.CmdPal.Ext.Bookmarks;
|
||||
using Microsoft.CmdPal.Ext.Calc;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
using Microsoft.CmdPal.Ext.Indexer;
|
||||
using Microsoft.CmdPal.Ext.PerformanceMonitor;
|
||||
using Microsoft.CmdPal.Ext.Registry;
|
||||
using Microsoft.CmdPal.Ext.RemoteDesktop;
|
||||
using Microsoft.CmdPal.Ext.Shell;
|
||||
@@ -27,6 +28,7 @@ using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -167,6 +169,7 @@ public partial class App : Application
|
||||
services.AddSingleton<ICommandProvider, TimeDateCommandsProvider>();
|
||||
services.AddSingleton<ICommandProvider, SystemCommandExtensionProvider>();
|
||||
services.AddSingleton<ICommandProvider, RemoteDesktopCommandProvider>();
|
||||
services.AddSingleton<ICommandProvider, PerformanceMonitorCommandsProvider>();
|
||||
}
|
||||
|
||||
private static void AddUIServices(ServiceCollection services)
|
||||
@@ -201,6 +204,7 @@ public partial class App : Application
|
||||
|
||||
// ViewModels
|
||||
services.AddSingleton<ShellViewModel>();
|
||||
services.AddSingleton<DockViewModel>();
|
||||
services.AddSingleton<IPageViewModelFactoryService, CommandPalettePageViewModelFactory>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<Grid
|
||||
x:Name="IconRoot"
|
||||
Margin="3,0,-5,0"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<Button
|
||||
x:Name="StatusMessagesButton"
|
||||
x:Uid="StatusMessagesButton"
|
||||
@@ -135,7 +135,7 @@
|
||||
x:Uid="SettingsButton"
|
||||
Click="SettingsIcon_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
@@ -154,7 +154,7 @@
|
||||
Text="{x:Bind CurrentPageViewModel.Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap"
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsNested, Mode=OneWay}" />
|
||||
Visibility="{x:Bind CurrentPageViewModel.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
Padding="0,0,4,0"
|
||||
|
||||
@@ -40,18 +40,27 @@ public sealed partial class CommandBar : UserControl,
|
||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(CommandBarViewModel.SelectedItem))
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = ViewModel.SelectedItem;
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(OpenContextMenuMessage message)
|
||||
{
|
||||
if (!ViewModel.ShouldShowContextMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Element is null)
|
||||
{
|
||||
// This is invoked from the "More" button on the command bar
|
||||
if (!ViewModel.ShouldShowContextMenu)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
@@ -66,7 +75,6 @@ public sealed partial class CommandBar : UserControl,
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is invoked from a specific element
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
@@ -128,7 +136,7 @@ public sealed partial class CommandBar : UserControl,
|
||||
|
||||
private void SettingsIcon_Clicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
}
|
||||
|
||||
private void MoreCommandsButton_Clicked(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
Separator="{StaticResource SeparatorContextMenuViewModelTemplate}" />
|
||||
|
||||
<!-- Template for context items in the context item menu -->
|
||||
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:CommandContextItemViewModel">
|
||||
<DataTemplate x:Key="DefaultContextMenuViewModelTemplate" x:DataType="coreViewModels:ICommandContextItemViewModel">
|
||||
<Grid Padding="{StaticResource DefaultContextMenuItemPadding}" AutomationProperties.Name="{x:Bind Title, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ContextMenu : UserControl,
|
||||
IRecipient<OpenContextMenuMessage>,
|
||||
IRecipient<UpdateCommandBarMessage>,
|
||||
IRecipient<TryCommandKeybindingMessage>
|
||||
{
|
||||
public ContextMenuViewModel ViewModel { get; } = new();
|
||||
@@ -29,7 +28,6 @@ public sealed partial class ContextMenu : UserControl,
|
||||
|
||||
// RegisterAll isn't AOT compatible
|
||||
WeakReferenceMessenger.Default.Register<OpenContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<TryCommandKeybindingMessage>(this);
|
||||
|
||||
if (ViewModel is not null)
|
||||
@@ -46,11 +44,6 @@ public sealed partial class ContextMenu : UserControl,
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
public void Receive(UpdateCommandBarMessage message)
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
public void Receive(TryCommandKeybindingMessage msg)
|
||||
{
|
||||
var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
|
||||
@@ -138,7 +131,8 @@ public sealed partial class ContextMenu : UserControl,
|
||||
{
|
||||
var prop = e.PropertyName;
|
||||
|
||||
if (prop == nameof(ContextMenuViewModel.FilteredItems))
|
||||
if (prop == nameof(ContextMenuViewModel.FilteredItems) ||
|
||||
prop == nameof(ContextMenuViewModel.SelectedItem))
|
||||
{
|
||||
UpdateUiForStackChange();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Controls.ScrollContainer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="ScrollButtonStyle" TargetType="Button">
|
||||
<Setter Property="Background" Value="{ThemeResource FlipViewNextPreviousButtonBackground}" />
|
||||
<Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
|
||||
<Setter Property="FontWeight" Value="Normal" />
|
||||
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
|
||||
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
|
||||
<Setter Property="FocusVisualMargin" Value="-3" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<ContentPresenter
|
||||
x:Name="ContentPresenter"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
AnimatedIcon.State="Normal"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Background="{TemplateBinding Background}"
|
||||
BackgroundSizing="{TemplateBinding BackgroundSizing}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Content="{TemplateBinding Content}"
|
||||
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<ContentPresenter.BackgroundTransition>
|
||||
<BrushTransition Duration="0:0:0.083" />
|
||||
</ContentPresenter.BackgroundTransition>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPointerOver}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="PointerOver" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBackgroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousButtonBorderBrushPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource FlipViewNextPreviousArrowForegroundPressed}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Pressed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBackgroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonBorderBrushDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
<VisualState.Setters>
|
||||
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
|
||||
<Setter Target="ContentPresenter.(AnimatedIcon.State)" Value="Normal" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</ContentPresenter>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
Background="{x:Bind Background, Mode=OneWay}"
|
||||
BorderBrush="{x:Bind BorderBrush, Mode=OneWay}"
|
||||
BorderThickness="{x:Bind BorderThickness, Mode=OneWay}"
|
||||
CornerRadius="{x:Bind CornerRadius, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition x:Name="Row2" Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Action button - position controlled by visual states -->
|
||||
<ContentPresenter
|
||||
x:Name="ActionButtonPresenter"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="2"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Content="{x:Bind ActionButton, Mode=OneWay}"
|
||||
Visibility="{x:Bind ActionButtonVisibility, Mode=OneWay}" />
|
||||
|
||||
<Grid
|
||||
x:Name="ScrollerContainer"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1">
|
||||
<ScrollViewer
|
||||
x:Name="scroller"
|
||||
HorizontalScrollBarVisibility="Hidden"
|
||||
HorizontalScrollMode="Enabled"
|
||||
SizeChanged="Scroller_SizeChanged"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
VerticalScrollMode="Disabled"
|
||||
ViewChanging="Scroller_ViewChanging">
|
||||
<Grid x:Name="ContentGrid">
|
||||
<ContentPresenter Content="{x:Bind Source, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
<Button
|
||||
x:Name="ScrollBackBtn"
|
||||
Margin="8,0,0,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll left"
|
||||
Click="ScrollBackBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll left"
|
||||
Visibility="Collapsed">
|
||||
<FontIcon
|
||||
x:Name="ScrollBackIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="ScrollForwardBtn"
|
||||
Margin="0,0,8,0"
|
||||
Padding="2,8,2,8"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Scroll right"
|
||||
Click="ScrollForwardBtn_Click"
|
||||
Style="{StaticResource ScrollButtonStyle}"
|
||||
ToolTipService.ToolTip="Scroll right">
|
||||
<FontIcon
|
||||
x:Name="ScrollForwardIcon"
|
||||
FontSize="{ThemeResource FlipViewButtonFontSize}"
|
||||
Glyph="" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="OrientationStates">
|
||||
<VisualState x:Name="HorizontalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Enabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Disabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="8,0,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll left" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="4,12,4,12" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,8,0" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll right" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="VerticalState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="scroller.HorizontalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.HorizontalScrollMode" Value="Disabled" />
|
||||
<Setter Target="scroller.VerticalScrollBarVisibility" Value="Hidden" />
|
||||
<Setter Target="scroller.VerticalScrollMode" Value="Enabled" />
|
||||
<Setter Target="ScrollBackBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollBackBtn.Margin" Value="0,8,0,0" />
|
||||
<Setter Target="ScrollBackBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollBackBtn.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="ScrollBackBtn.(AutomationProperties.Name)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackBtn.(ToolTipService.ToolTip)" Value="Scroll up" />
|
||||
<Setter Target="ScrollBackIcon.Glyph" Value="" />
|
||||
<Setter Target="ScrollForwardBtn.Padding" Value="12,4,12,4" />
|
||||
<Setter Target="ScrollForwardBtn.Margin" Value="0,0,0,8" />
|
||||
<Setter Target="ScrollForwardBtn.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ScrollForwardBtn.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ScrollForwardBtn.(AutomationProperties.Name)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardBtn.(ToolTipService.ToolTip)" Value="Scroll down" />
|
||||
<Setter Target="ScrollForwardIcon.Glyph" Value="" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="LayoutStates">
|
||||
<!-- Horizontal + Start: button on right -->
|
||||
<VisualState x:Name="HorizontalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="4,0,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Horizontal + End: button on left -->
|
||||
<VisualState x:Name="HorizontalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,4,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Center" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + Start: button on bottom -->
|
||||
<VisualState x:Name="VerticalStartState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="2" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,4,0,0" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<!-- Vertical + End: button on top -->
|
||||
<VisualState x:Name="VerticalEndState">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ScrollerContainer.(Grid.Row)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ScrollerContainer.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ScrollerContainer.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Row)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButtonPresenter.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="ActionButtonPresenter.Margin" Value="0,0,0,4" />
|
||||
<Setter Target="ActionButtonPresenter.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="ActionButtonPresenter.VerticalAlignment" Value="Stretch" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,222 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Controls;
|
||||
|
||||
public sealed partial class ScrollContainer : UserControl
|
||||
{
|
||||
public enum ScrollContentAlignment
|
||||
{
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
|
||||
public ScrollContainer()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += ScrollContainer_Loaded;
|
||||
}
|
||||
|
||||
private void ScrollContainer_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
UpdateOrientationState();
|
||||
UpdateLayoutState();
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => (object)GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SourceProperty =
|
||||
DependencyProperty.Register(nameof(Source), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(ScrollContainer), new PropertyMetadata(Orientation.Horizontal, OnOrientationChanged));
|
||||
|
||||
public ScrollContentAlignment ContentAlignment
|
||||
{
|
||||
get => (ScrollContentAlignment)GetValue(ContentAlignmentProperty);
|
||||
set => SetValue(ContentAlignmentProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ContentAlignmentProperty =
|
||||
DependencyProperty.Register(nameof(ContentAlignment), typeof(ScrollContentAlignment), typeof(ScrollContainer), new PropertyMetadata(ScrollContentAlignment.Start, OnContentAlignmentChanged));
|
||||
|
||||
public object ActionButton
|
||||
{
|
||||
get => (object)GetValue(ActionButtonProperty);
|
||||
set => SetValue(ActionButtonProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonProperty =
|
||||
DependencyProperty.Register(nameof(ActionButton), typeof(object), typeof(ScrollContainer), new PropertyMetadata(null));
|
||||
|
||||
public Visibility ActionButtonVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(ActionButtonVisibilityProperty);
|
||||
set => SetValue(ActionButtonVisibilityProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ActionButtonVisibilityProperty =
|
||||
DependencyProperty.Register(nameof(ActionButtonVisibility), typeof(Visibility), typeof(ScrollContainer), new PropertyMetadata(Visibility.Collapsed));
|
||||
|
||||
private static void OnContentAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToAlignment()
|
||||
{
|
||||
// Reset button visibility
|
||||
ScrollBackBtn.Visibility = Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (ContentAlignment == ScrollContentAlignment.End)
|
||||
{
|
||||
// Scroll to the end
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.ScrollableWidth, null, null, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.ScrollableHeight, null, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Scroll to the beginning
|
||||
scroller.ChangeView(0, 0, null, true);
|
||||
}
|
||||
|
||||
// Defer visibility update until after layout
|
||||
void OnLayoutUpdated(object? sender, object args)
|
||||
{
|
||||
scroller.LayoutUpdated -= OnLayoutUpdated;
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
scroller.LayoutUpdated += OnLayoutUpdated;
|
||||
}
|
||||
|
||||
private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ScrollContainer control)
|
||||
{
|
||||
control.UpdateOrientationState();
|
||||
control.UpdateLayoutState();
|
||||
control.ScrollToAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateOrientationState()
|
||||
{
|
||||
var stateName = Orientation == Orientation.Horizontal ? "HorizontalState" : "VerticalState";
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void UpdateLayoutState()
|
||||
{
|
||||
var isHorizontal = Orientation == Orientation.Horizontal;
|
||||
var isStart = ContentAlignment == ScrollContentAlignment.Start;
|
||||
|
||||
var stateName = (isHorizontal, isStart) switch
|
||||
{
|
||||
(true, true) => "HorizontalStartState",
|
||||
(true, false) => "HorizontalEndState",
|
||||
(false, true) => "VerticalStartState",
|
||||
(false, false) => "VerticalEndState",
|
||||
};
|
||||
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void Scroller_ViewChanging(object sender, ScrollViewerViewChangingEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility(e.FinalView.HorizontalOffset, e.FinalView.VerticalOffset);
|
||||
}
|
||||
|
||||
private void ScrollBackBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset - scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset - scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollForwardBtn since this button disappears after scrolling to the end.
|
||||
ScrollForwardBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void ScrollForwardBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
scroller.ChangeView(scroller.HorizontalOffset + scroller.ViewportWidth, null, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
scroller.ChangeView(null, scroller.VerticalOffset + scroller.ViewportHeight, null);
|
||||
}
|
||||
|
||||
// Manually focus to ScrollBackBtn since this button disappears after scrolling to the end.
|
||||
ScrollBackBtn.Focus(FocusState.Programmatic);
|
||||
}
|
||||
|
||||
private void Scroller_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
UpdateScrollButtonsVisibility();
|
||||
}
|
||||
|
||||
private void UpdateScrollButtonsVisibility(double? horizontalOffset = null, double? verticalOffset = null)
|
||||
{
|
||||
var hOffset = horizontalOffset ?? scroller.HorizontalOffset;
|
||||
var vOffset = verticalOffset ?? scroller.VerticalOffset;
|
||||
|
||||
if (Orientation == Orientation.Horizontal)
|
||||
{
|
||||
ScrollBackBtn.Visibility = hOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableWidth > 0 && hOffset < scroller.ScrollableWidth - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrollBackBtn.Visibility = vOffset > 1 ? Visibility.Visible : Visibility.Collapsed;
|
||||
ScrollForwardBtn.Visibility = scroller.ScrollableHeight > 0 && vOffset < scroller.ScrollableHeight - 1
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
449
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal file
449
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml
Normal file
@@ -0,0 +1,449 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<UserControl
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:coreVm="using:Microsoft.CmdPal.Core.ViewModels"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<StackLayout
|
||||
x:Key="ItemsOrientationLayout"
|
||||
Orientation="{x:Bind ItemsOrientation, Mode=OneWay}"
|
||||
Spacing="4" />
|
||||
<ItemsPanelTemplate x:Key="HorizontalItemsPanel">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
<ItemsPanelTemplate x:Key="VerticalItemsPanel">
|
||||
<StackPanel Orientation="Vertical" Spacing="4" />
|
||||
</ItemsPanelTemplate>
|
||||
|
||||
<DataTemplate x:Key="DockBandTemplate" x:DataType="dockVm:DockBandViewModel">
|
||||
<ItemsRepeater ItemsSource="{x:Bind Items, Mode=OneWay}" Layout="{StaticResource ItemsOrientationLayout}">
|
||||
<ItemsRepeater.Transitions>
|
||||
<TransitionCollection />
|
||||
</ItemsRepeater.Transitions>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="dockVm:DockItemViewModel">
|
||||
<local:DockItemControl
|
||||
Title="{x:Bind Title, Mode=OneWay}"
|
||||
RightTapped="BandItem_RightTapped"
|
||||
Subtitle="{x:Bind Subtitle, Mode=OneWay}"
|
||||
Tag="{x:Bind}"
|
||||
Tapped="BandItem_Tapped"
|
||||
ToolTip="{x:Bind Tooltip, Mode=OneWay}">
|
||||
<local:DockItemControl.Icon>
|
||||
<cpcontrols:IconBox
|
||||
x:Name="IconBorder"
|
||||
Width="16"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
</local:DockItemControl.Icon>
|
||||
</local:DockItemControl>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</DataTemplate>
|
||||
|
||||
<Style x:Key="DockBandListViewStyle" TargetType="ListView">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="IsItemClickEnabled" Value="False" />
|
||||
<Setter Property="SelectionMode" Value="None" />
|
||||
<!-- Drag properties controlled by code-behind based on IsEditMode -->
|
||||
<Setter Property="CanDragItems" Value="False" />
|
||||
<Setter Property="CanReorderItems" Value="False" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DockBandListViewItemStyle" TargetType="ListViewItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0,0,4,0" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Name="ContextMenuFlyoutStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||
</Flyout>
|
||||
|
||||
<!-- Edit mode context menu for dock bands -->
|
||||
<MenuFlyout x:Name="EditModeContextMenu" ShouldConstrainToRootBounds="False">
|
||||
<MenuFlyoutSubItem x:Name="LabelsSubMenu" Text="Labels">
|
||||
<MenuFlyoutSubItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutSubItem.Icon>
|
||||
<ToggleMenuFlyoutItem
|
||||
x:Name="ShowTitlesMenuItem"
|
||||
Click="ShowTitlesMenuItem_Click"
|
||||
Text="Show titles" />
|
||||
<ToggleMenuFlyoutItem
|
||||
x:Name="ShowSubtitlesMenuItem"
|
||||
Click="ShowSubtitlesMenuItem_Click"
|
||||
Text="Show subtitles" />
|
||||
</MenuFlyoutSubItem>
|
||||
<MenuFlyoutSeparator />
|
||||
<MenuFlyoutItem
|
||||
x:Name="UnpinBandMenuItem"
|
||||
Click="UnpinBandMenuItem_Click"
|
||||
Text="Unpin">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<FontIcon Glyph="" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
|
||||
<!-- Add band flyout - used in edit mode to add bands to dock sections -->
|
||||
<Flyout
|
||||
x:Name="AddBandFlyout"
|
||||
Placement="Bottom"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
<StackPanel Width="320">
|
||||
<TextBlock
|
||||
x:Name="NoAvailableBandsText"
|
||||
Padding="12"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="No commands available to pin"
|
||||
TextAlignment="Center"
|
||||
Visibility="Collapsed" />
|
||||
<ListView
|
||||
x:Name="AddBandListView"
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="AddBandListView_ItemClick"
|
||||
SelectionMode="None">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TopLevelViewModel">
|
||||
<Grid Padding="4" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<cpcontrols:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind IconViewModel, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="{x:Bind Title, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid
|
||||
x:Name="RootGrid"
|
||||
BorderThickness="0,0,0,1"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<!-- Edit Mode Overlay - shown when in edit mode -->
|
||||
<Grid
|
||||
x:Name="ContentGrid"
|
||||
Margin="4"
|
||||
Padding="4,0,4,0"
|
||||
Background="Transparent"
|
||||
RightTapped="RootGrid_RightTapped">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="StartScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Stretch">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="StartAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="Start"
|
||||
ToolTipService.ToolTip="Add band to Start">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="StartListView"
|
||||
HorizontalAlignment="Stretch"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="StartListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.StartItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}" />
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="CenterScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1"
|
||||
MinWidth="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Stretch">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="CenterAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="Center"
|
||||
ToolTipService.ToolTip="Add band to Center">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="CenterListView"
|
||||
HorizontalAlignment="Stretch"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="CenterListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.CenterItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}" />
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
|
||||
<cpcontrols:ScrollContainer
|
||||
x:Name="EndScroller"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
ContentAlignment="End">
|
||||
<cpcontrols:ScrollContainer.ActionButton>
|
||||
<Button
|
||||
x:Name="EndAddButton"
|
||||
Click="AddBandButton_Click"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Tag="End"
|
||||
ToolTipService.ToolTip="Add band to End">
|
||||
<FontIcon FontSize="12" Glyph="" />
|
||||
</Button>
|
||||
</cpcontrols:ScrollContainer.ActionButton>
|
||||
<cpcontrols:ScrollContainer.Source>
|
||||
<ListView
|
||||
x:Name="EndListView"
|
||||
DragItemsCompleted="BandListView_DragItemsCompleted"
|
||||
DragItemsStarting="BandListView_DragItemsStarting"
|
||||
DragOver="BandListView_DragOver"
|
||||
Drop="EndListView_Drop"
|
||||
ItemContainerStyle="{StaticResource DockBandListViewItemStyle}"
|
||||
ItemTemplate="{StaticResource DockBandTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalItemsPanel}"
|
||||
ItemsSource="{x:Bind ViewModel.EndItems, Mode=OneWay}"
|
||||
SelectionMode="None"
|
||||
Style="{StaticResource DockBandListViewStyle}">
|
||||
<ListView.ItemContainerTransitions>
|
||||
<TransitionCollection />
|
||||
</ListView.ItemContainerTransitions>
|
||||
</ListView>
|
||||
</cpcontrols:ScrollContainer.Source>
|
||||
</cpcontrols:ScrollContainer>
|
||||
<TeachingTip
|
||||
x:Name="EditButtonsTeachingTip"
|
||||
MinWidth="0"
|
||||
PreferredPlacement="Bottom"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
Style="{StaticResource TeachingTipWithoutCloseButtonStyle}"
|
||||
Target="{x:Bind ContentGrid}">
|
||||
|
||||
<TeachingTip.Content>
|
||||
<StackPanel
|
||||
x:Name="EditButtonsPanel"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="4">
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="DoneEditingButton_Click"
|
||||
Content="Save"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
Click="DiscardEditingButton_Click"
|
||||
Content="Discard" />
|
||||
</StackPanel>
|
||||
</TeachingTip.Content>
|
||||
</TeachingTip>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="DockOrientation">
|
||||
<VisualState x:Name="DockOnTop">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Top" />
|
||||
</VisualState.StateTriggers>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnBottom">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Bottom" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,1,0,0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnLeft">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Left" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="EndScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
|
||||
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="0,0,1,0" />
|
||||
|
||||
<Setter Target="StartListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="CenterListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="EndListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="DockOnRight">
|
||||
<VisualState.StateTriggers>
|
||||
<ui:IsEqualStateTrigger Value="{x:Bind DockSide, Mode=OneWay}" To="Right" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.(Grid.Row)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="StartScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="StartScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="StartScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="StartScroller.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="StartScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="CenterScroller.(Grid.Row)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="CenterScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CenterScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="CenterScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="CenterScroller.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="CenterScroller.Orientation" Value="Vertical" />
|
||||
|
||||
<Setter Target="EndScroller.Orientation" Value="Vertical" />
|
||||
<Setter Target="EndScroller.(Grid.Row)" Value="2" />
|
||||
<Setter Target="EndScroller.(Grid.RowSpan)" Value="1" />
|
||||
<Setter Target="EndScroller.(Grid.Column)" Value="0" />
|
||||
<Setter Target="EndScroller.(Grid.ColumnSpan)" Value="3" />
|
||||
<Setter Target="EndScroller.HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Target="EndScroller.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="ContentGrid.Padding" Value="4,8,4,8" />
|
||||
<Setter Target="RootGrid.BorderThickness" Value="1,0,0,0" />
|
||||
|
||||
<Setter Target="StartListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="CenterListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
<Setter Target="EndListView.ItemsPanel" Value="{StaticResource VerticalItemsPanel}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
|
||||
<!-- Edit Mode Visual States -->
|
||||
<VisualStateGroup x:Name="EditModeStates">
|
||||
<VisualState x:Name="EditModeOff" />
|
||||
<VisualState x:Name="EditModeOn">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="StartScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="StartScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="StartScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="StartScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="CenterScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="CenterScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="CenterScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="CenterScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="EndScroller.BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Target="EndScroller.Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
|
||||
<Setter Target="EndScroller.BorderThickness" Value="1" />
|
||||
<Setter Target="EndScroller.CornerRadius" Value="{StaticResource OverlayCornerRadius}" />
|
||||
<Setter Target="StartScroller.ActionButtonVisibility" Value="Visible" />
|
||||
<Setter Target="CenterScroller.ActionButtonVisibility" Value="Visible" />
|
||||
<Setter Target="EndScroller.ActionButtonVisibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
520
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal file
520
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockControl.xaml.cs
Normal file
@@ -0,0 +1,520 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
public sealed partial class DockControl : UserControl, IRecipient<CloseContextMenuMessage>, IRecipient<EnterDockEditModeMessage>
|
||||
{
|
||||
private DockViewModel _viewModel;
|
||||
|
||||
internal DockViewModel ViewModel => _viewModel;
|
||||
|
||||
public static readonly DependencyProperty ItemsOrientationProperty =
|
||||
DependencyProperty.Register(nameof(ItemsOrientation), typeof(Orientation), typeof(DockControl), new PropertyMetadata(Orientation.Horizontal));
|
||||
|
||||
public Orientation ItemsOrientation
|
||||
{
|
||||
get => (Orientation)GetValue(ItemsOrientationProperty);
|
||||
set => SetValue(ItemsOrientationProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DockSideProperty =
|
||||
DependencyProperty.Register(nameof(DockSide), typeof(DockSide), typeof(DockControl), new PropertyMetadata(DockSide.Top));
|
||||
|
||||
public DockSide DockSide
|
||||
{
|
||||
get => (DockSide)GetValue(DockSideProperty);
|
||||
set => SetValue(DockSideProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsEditModeProperty =
|
||||
DependencyProperty.Register(nameof(IsEditMode), typeof(bool), typeof(DockControl), new PropertyMetadata(false, OnIsEditModeChanged));
|
||||
|
||||
public bool IsEditMode
|
||||
{
|
||||
get => (bool)GetValue(IsEditModeProperty);
|
||||
set => SetValue(IsEditModeProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIsEditModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockControl control && e.NewValue is bool isEditMode)
|
||||
{
|
||||
control.UpdateEditMode(isEditMode);
|
||||
}
|
||||
}
|
||||
|
||||
internal DockControl(DockViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
InitializeComponent();
|
||||
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<EnterDockEditModeMessage>(this);
|
||||
|
||||
// Start with edit mode disabled - normal click behavior
|
||||
UpdateEditMode(false);
|
||||
}
|
||||
|
||||
public void Receive(EnterDockEditModeMessage message)
|
||||
{
|
||||
// Message may arrive from a background thread, dispatch to UI thread
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
EnterEditMode();
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateEditMode(bool isEditMode)
|
||||
{
|
||||
// Enable/disable drag-and-drop based on edit mode
|
||||
StartListView.CanDragItems = isEditMode;
|
||||
StartListView.CanReorderItems = isEditMode;
|
||||
StartListView.AllowDrop = isEditMode;
|
||||
|
||||
CenterListView.CanDragItems = isEditMode;
|
||||
CenterListView.CanReorderItems = isEditMode;
|
||||
CenterListView.AllowDrop = isEditMode;
|
||||
|
||||
EndListView.CanDragItems = isEditMode;
|
||||
EndListView.CanReorderItems = isEditMode;
|
||||
EndListView.AllowDrop = isEditMode;
|
||||
|
||||
if (isEditMode)
|
||||
{
|
||||
EditButtonsTeachingTip.PreferredPlacement = DockSide switch
|
||||
{
|
||||
DockSide.Left => TeachingTipPlacementMode.Right,
|
||||
DockSide.Right => TeachingTipPlacementMode.Left,
|
||||
DockSide.Top => TeachingTipPlacementMode.Bottom,
|
||||
DockSide.Bottom => TeachingTipPlacementMode.Top,
|
||||
_ => TeachingTipPlacementMode.Auto,
|
||||
};
|
||||
}
|
||||
|
||||
EditButtonsTeachingTip.IsOpen = isEditMode;
|
||||
|
||||
// Update visual state
|
||||
VisualStateManager.GoToState(this, isEditMode ? "EditModeOn" : "EditModeOff", true);
|
||||
}
|
||||
|
||||
internal void EnterEditMode()
|
||||
{
|
||||
// Snapshot current state so we can restore on discard
|
||||
ViewModel.SnapshotBandOrder();
|
||||
IsEditMode = true;
|
||||
}
|
||||
|
||||
internal void ExitEditMode()
|
||||
{
|
||||
IsEditMode = false;
|
||||
|
||||
// Save all changes when exiting edit mode
|
||||
ViewModel.SaveBandOrder();
|
||||
}
|
||||
|
||||
internal void DiscardEditMode()
|
||||
{
|
||||
IsEditMode = false;
|
||||
|
||||
// Restore the original band order from snapshot
|
||||
ViewModel.RestoreBandOrder();
|
||||
}
|
||||
|
||||
private void DoneEditingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ExitEditMode();
|
||||
}
|
||||
|
||||
private void DiscardEditingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DiscardEditMode();
|
||||
}
|
||||
|
||||
internal void UpdateSettings(DockSettings settings)
|
||||
{
|
||||
DockSide = settings.Side;
|
||||
|
||||
var isHorizontal = settings.Side == DockSide.Top || settings.Side == DockSide.Bottom;
|
||||
|
||||
ItemsOrientation = isHorizontal ? Orientation.Horizontal : Orientation.Vertical;
|
||||
|
||||
if (settings.Backdrop == DockBackdrop.Transparent)
|
||||
{
|
||||
RootGrid.BorderBrush = new SolidColorBrush(Colors.Transparent);
|
||||
}
|
||||
}
|
||||
|
||||
private void BandItem_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
|
||||
{
|
||||
// Ignore clicks when in edit mode - allow drag behavior instead
|
||||
if (IsEditMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockBandViewModel band && dockItem.Tag is DockItemViewModel item)
|
||||
{
|
||||
// Use the center of the border as the point to open at
|
||||
// var borderPos = dockItem.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
// var borderCenter = new Point(
|
||||
// borderPos.X + (dockItem.ActualWidth / 2),
|
||||
// borderPos.Y + (dockItem.ActualHeight / 2));
|
||||
//
|
||||
// // borderCenter is in DIPs, relative to the dock window.
|
||||
// // we need screen DIPs
|
||||
// var windowPos = dockItem.XamlRoot.Content.XamlRoot.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
// var screenPos = new Point(
|
||||
// borderCenter.X + windowPos.X,
|
||||
// borderCenter.Y + windowPos.Y);
|
||||
InvokeItem(item, dockItem);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Stores the band that was right-clicked for edit mode context menu
|
||||
private DockBandViewModel? _editModeContextBand;
|
||||
|
||||
private void BandItem_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
if (sender is DockItemControl dockItem && dockItem.DataContext is DockBandViewModel band && dockItem.Tag is DockItemViewModel item)
|
||||
{
|
||||
// In edit mode, show the edit mode context menu (show/hide labels)
|
||||
if (IsEditMode)
|
||||
{
|
||||
// Find the parent DockBandViewModel for this item
|
||||
_editModeContextBand = band;
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
// Update toggle menu item checked state based on current settings
|
||||
ShowTitlesMenuItem.IsChecked = _editModeContextBand.ShowTitles;
|
||||
ShowSubtitlesMenuItem.IsChecked = _editModeContextBand.ShowSubtitles;
|
||||
|
||||
EditModeContextMenu.ShowAt(
|
||||
dockItem,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal mode - show the command context menu
|
||||
if (item.HasMoreCommands)
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextMenuFlyout.ShowAt(
|
||||
dockItem,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowTitlesMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
_editModeContextBand.ShowTitles = ShowTitlesMenuItem.IsChecked;
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowSubtitlesMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
_editModeContextBand.ShowSubtitles = ShowSubtitlesMenuItem.IsChecked;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnpinBandMenuItem_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_editModeContextBand != null)
|
||||
{
|
||||
ViewModel.UnpinBand(_editModeContextBand);
|
||||
_editModeContextBand = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void InvokeItem(DockItemViewModel item, object flyoutUiContext)
|
||||
{
|
||||
var command = item.Command;
|
||||
try
|
||||
{
|
||||
// TODO! This is where we need to decide whether to open the command
|
||||
// as a context menu or as a full page.
|
||||
//
|
||||
// It might be the case that we should just have like... a
|
||||
// PerformDockCommandMessage like PerformCommandMessage but with the
|
||||
// context that we should be opening the command as a flyout.
|
||||
var m = PerformCommandMessage.CreateFlyoutMessage(command.Model, flyoutUiContext);
|
||||
WeakReferenceMessenger.Default.Send(m);
|
||||
|
||||
// var isPage = command.Model.Unsafe is not IInvokableCommand invokable;
|
||||
// if (isPage)
|
||||
// {
|
||||
// WeakReferenceMessenger.Default.Send<RequestShowPaletteAtMessage>(new(pos));
|
||||
// }
|
||||
}
|
||||
catch (COMException e)
|
||||
{
|
||||
Logger.LogError("Error invoking dock command", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
// We need to wait until our flyout is opened to try and toss focus
|
||||
// at its search box. The control isn't in the UI tree before that
|
||||
ContextControl.FocusSearchBox();
|
||||
}
|
||||
|
||||
public void Receive(CloseContextMenuMessage message)
|
||||
{
|
||||
if (ContextMenuFlyout.IsOpen)
|
||||
{
|
||||
ContextMenuFlyout.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void RootGrid_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(null);
|
||||
var item = this.ViewModel.GetContextMenuForDock();
|
||||
if (item.HasMoreCommands)
|
||||
{
|
||||
ContextControl.ViewModel.SelectedItem = item;
|
||||
ContextMenuFlyout.ShowAt(
|
||||
this.RootGrid,
|
||||
new FlyoutShowOptions()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
Placement = FlyoutPlacementMode.TopEdgeAlignedRight,
|
||||
Position = pos,
|
||||
});
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private DockBandViewModel? _draggedBand;
|
||||
|
||||
private void BandListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
|
||||
{
|
||||
if (e.Items.Count > 0 && e.Items[0] is DockBandViewModel band)
|
||||
{
|
||||
_draggedBand = band;
|
||||
e.Data.RequestedOperation = DataPackageOperation.Move;
|
||||
}
|
||||
}
|
||||
|
||||
private void BandListView_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
if (_draggedBand != null)
|
||||
{
|
||||
e.AcceptedOperation = DataPackageOperation.Move;
|
||||
}
|
||||
}
|
||||
|
||||
private void BandListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
// Reordering within the same list is handled automatically by ListView
|
||||
// We just need to sync the ViewModel order without saving
|
||||
if (args.DropResult == DataPackageOperation.Move && _draggedBand != null)
|
||||
{
|
||||
DockPinSide targetSide;
|
||||
ObservableCollection<DockBandViewModel> targetCollection;
|
||||
|
||||
if (sender == StartListView)
|
||||
{
|
||||
targetSide = DockPinSide.Start;
|
||||
targetCollection = ViewModel.StartItems;
|
||||
}
|
||||
else if (sender == CenterListView)
|
||||
{
|
||||
targetSide = DockPinSide.Center;
|
||||
targetCollection = ViewModel.CenterItems;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetSide = DockPinSide.End;
|
||||
targetCollection = ViewModel.EndItems;
|
||||
}
|
||||
|
||||
// Find the new index and sync ViewModel (without saving)
|
||||
var newIndex = targetCollection.IndexOf(_draggedBand);
|
||||
if (newIndex >= 0)
|
||||
{
|
||||
ViewModel.SyncBandPosition(_draggedBand, targetSide, newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
_draggedBand = null;
|
||||
}
|
||||
|
||||
private void StartListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.Start, e);
|
||||
}
|
||||
|
||||
private void CenterListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.Center, e);
|
||||
}
|
||||
|
||||
private void EndListView_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
HandleCrossListDrop(DockPinSide.End, e);
|
||||
}
|
||||
|
||||
private void HandleCrossListDrop(DockPinSide targetSide, DragEventArgs e)
|
||||
{
|
||||
if (_draggedBand == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check which list the band is currently in
|
||||
var isInStart = ViewModel.StartItems.Contains(_draggedBand);
|
||||
var isInCenter = ViewModel.CenterItems.Contains(_draggedBand);
|
||||
var isInEnd = ViewModel.EndItems.Contains(_draggedBand);
|
||||
|
||||
DockPinSide sourceSide;
|
||||
if (isInStart)
|
||||
{
|
||||
sourceSide = DockPinSide.Start;
|
||||
}
|
||||
else if (isInCenter)
|
||||
{
|
||||
sourceSide = DockPinSide.Center;
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceSide = DockPinSide.End;
|
||||
}
|
||||
|
||||
// Only handle cross-list drops here; same-list reorders are handled in DragItemsCompleted
|
||||
if (sourceSide != targetSide)
|
||||
{
|
||||
// Calculate drop index based on drop position
|
||||
var targetListView = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => StartListView,
|
||||
DockPinSide.Center => CenterListView,
|
||||
_ => EndListView,
|
||||
};
|
||||
var targetCollection = targetSide switch
|
||||
{
|
||||
DockPinSide.Start => ViewModel.StartItems,
|
||||
DockPinSide.Center => ViewModel.CenterItems,
|
||||
_ => ViewModel.EndItems,
|
||||
};
|
||||
|
||||
var dropIndex = GetDropIndex(targetListView, e, targetCollection.Count);
|
||||
|
||||
// Move the band to the new side (without saving - save happens on Done)
|
||||
ViewModel.MoveBandWithoutSaving(_draggedBand, targetSide, dropIndex);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private int GetDropIndex(ListView listView, DragEventArgs e, int itemCount)
|
||||
{
|
||||
var position = e.GetPosition(listView);
|
||||
|
||||
// Find the item at the drop position
|
||||
for (var i = 0; i < itemCount; i++)
|
||||
{
|
||||
if (listView.ContainerFromIndex(i) is ListViewItem container)
|
||||
{
|
||||
var itemBounds = container.TransformToVisual(listView).TransformBounds(
|
||||
new Rect(0, 0, container.ActualWidth, container.ActualHeight));
|
||||
|
||||
if (ItemsOrientation == Orientation.Horizontal)
|
||||
{
|
||||
// For horizontal layout, check X position
|
||||
if (position.X < itemBounds.X + (itemBounds.Width / 2))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For vertical layout, check Y position
|
||||
if (position.Y < itemBounds.Y + (itemBounds.Height / 2))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're past all items, insert at the end
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
// Tracks which section (Start/Center/End) the add button was clicked for
|
||||
private DockPinSide _addBandTargetSide;
|
||||
|
||||
private void AddBandButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is Button button && button.Tag is string sideTag)
|
||||
{
|
||||
_addBandTargetSide = sideTag switch
|
||||
{
|
||||
"Start" => DockPinSide.Start,
|
||||
"Center" => DockPinSide.Center,
|
||||
"End" => DockPinSide.End,
|
||||
_ => DockPinSide.Center,
|
||||
};
|
||||
|
||||
// Populate the list with available bands (not already in the dock)
|
||||
var availableBands = ViewModel.GetAvailableBandsToAdd().ToList();
|
||||
AddBandListView.ItemsSource = availableBands;
|
||||
|
||||
// Show/hide empty state text based on whether there are bands to add
|
||||
var hasAvailableBands = availableBands.Count > 0;
|
||||
NoAvailableBandsText.Visibility = hasAvailableBands ? Visibility.Collapsed : Visibility.Visible;
|
||||
AddBandListView.Visibility = hasAvailableBands ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// Show the flyout
|
||||
AddBandFlyout.ShowAt(button);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBandListView_ItemClick(object sender, ItemClickEventArgs e)
|
||||
{
|
||||
if (e.ClickedItem is TopLevelViewModel topLevel)
|
||||
{
|
||||
// Add the band to the target section
|
||||
ViewModel.AddBandToSection(topLevel, _addBandTargetSide);
|
||||
|
||||
// Close the flyout
|
||||
AddBandFlyout.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
178
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml
Normal file
178
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockItemControl.xaml
Normal file
@@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock">
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#0FFFFFFF" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#0BFFFFFF" />
|
||||
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#0FFFFFFF" />
|
||||
<GradientStop Offset="1.0" Color="#19FFFFFF" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#0BFFFFFF" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="Light">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="#80FFFFFF" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="#4DFFFFFF" />
|
||||
<LinearGradientBrush x:Key="DockItemBorderBrushPointerOver" MappingMode="Absolute" StartPoint="0,0" EndPoint="0,3">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0.33" Color="#08000000" />
|
||||
<GradientStop Offset="1.0" Color="#17000000" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="#05000000" />
|
||||
</ResourceDictionary>
|
||||
<ResourceDictionary x:Key="HighContrast">
|
||||
<SolidColorBrush x:Key="DockItemBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrush" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPointerOver" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBackgroundPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPointerOver" Color="{StaticResource SystemColorHighlightColor}" />
|
||||
<SolidColorBrush x:Key="DockItemBorderBrushPressed" Color="{StaticResource SystemColorHighlightTextColor}" />
|
||||
</ResourceDictionary>
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<CornerRadius x:Key="DockItemCornerRadius">4</CornerRadius>
|
||||
<Thickness x:Key="DockItemPadding">4,0,4,0</Thickness>
|
||||
|
||||
<Style BasedOn="{StaticResource DefaultDockItemControlStyle}" TargetType="local:DockItemControl" />
|
||||
|
||||
<Style x:Key="DefaultDockItemControlStyle" TargetType="local:DockItemControl">
|
||||
<Style.Setters>
|
||||
<Setter Property="Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DockItemBorderBrush}" />
|
||||
<Setter Property="Padding" Value="{StaticResource DockItemPadding}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
|
||||
<Setter Property="CornerRadius" Value="{StaticResource DockItemCornerRadius}" />
|
||||
<Setter Property="TextVisibility" Value="Visible" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="local:DockItemControl">
|
||||
<Grid
|
||||
x:Name="PART_RootGrid"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
ToolTipService.ToolTip="{TemplateBinding ToolTip}">
|
||||
<Grid
|
||||
x:Name="ContentGrid"
|
||||
AutomationProperties.Name="{TemplateBinding Title}"
|
||||
Background="Transparent"
|
||||
ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Icon -->
|
||||
<ContentPresenter
|
||||
x:Name="IconPresenter"
|
||||
VerticalAlignment="Center"
|
||||
Content="{TemplateBinding Icon}" />
|
||||
|
||||
<!-- Text (Title + Subtitle) -->
|
||||
<StackPanel
|
||||
x:Name="TextPanel"
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{TemplateBinding TextVisibility}">
|
||||
<TextBlock
|
||||
x:Name="TitleText"
|
||||
MinWidth="24"
|
||||
MaxWidth="100"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="12"
|
||||
Text="{TemplateBinding Title}"
|
||||
TextAlignment="Left"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
<TextBlock
|
||||
x:Name="SubtitleText"
|
||||
MaxWidth="100"
|
||||
Margin="0,-4,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Segoe UI"
|
||||
FontSize="10"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{TemplateBinding Subtitle}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="Normal" />
|
||||
<VisualState x:Name="PointerOver">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPointerOver}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Pressed">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackgroundPointerOver}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBorderBrushPressed}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Disabled">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource DockItemBackground}" />
|
||||
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="TitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
<Setter Target="SubtitleText.Foreground" Value="{ThemeResource ButtonForegroundDisabled}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="TextVisibilityStates">
|
||||
<VisualState x:Name="TextVisible" />
|
||||
<VisualState x:Name="TitleOnly">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SubtitleText.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="SubtitleOnly">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitleText.Visibility" Value="Collapsed" />
|
||||
<Setter Target="SubtitleText.Margin" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TextHidden">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
|
||||
<Setter Target="TextPanel.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="IconVisibilityStates">
|
||||
<VisualState x:Name="IconVisible" />
|
||||
<VisualState x:Name="IconHidden">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="ContentGrid.ColumnSpacing" Value="0" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,213 @@
|
||||
// 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.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
[ContentProperty(Name = nameof(Icon))]
|
||||
public sealed partial class DockItemControl : Control
|
||||
{
|
||||
public DockItemControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(DockItemControl);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ToolTipProperty =
|
||||
DependencyProperty.Register(nameof(ToolTip), typeof(string), typeof(DockItemControl), new PropertyMetadata(null));
|
||||
|
||||
public string ToolTip
|
||||
{
|
||||
get => (string)GetValue(ToolTipProperty);
|
||||
set => SetValue(ToolTipProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TitleProperty =
|
||||
DependencyProperty.Register(nameof(Title), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => (string)GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SubtitleProperty =
|
||||
DependencyProperty.Register(nameof(Subtitle), typeof(string), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public string Subtitle
|
||||
{
|
||||
get => (string)GetValue(SubtitleProperty);
|
||||
set => SetValue(SubtitleProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(nameof(Icon), typeof(object), typeof(DockItemControl), new PropertyMetadata(null, OnIconPropertyChanged));
|
||||
|
||||
public object Icon
|
||||
{
|
||||
get => GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty TextVisibilityProperty =
|
||||
DependencyProperty.Register(nameof(TextVisibility), typeof(Visibility), typeof(DockItemControl), new PropertyMetadata(null, OnTextPropertyChanged));
|
||||
|
||||
public Visibility TextVisibility
|
||||
{
|
||||
get => (Visibility)GetValue(TextVisibilityProperty);
|
||||
set => SetValue(TextVisibilityProperty, value);
|
||||
}
|
||||
|
||||
private const string IconPresenterName = "IconPresenter";
|
||||
|
||||
private FrameworkElement? _iconPresenter;
|
||||
|
||||
private static void OnTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockItemControl control)
|
||||
{
|
||||
control.UpdateTextVisibility();
|
||||
control.UpdateAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnIconPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is DockItemControl control)
|
||||
{
|
||||
control.UpdateIconVisibility();
|
||||
control.UpdateAlignment();
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasTitle => !string.IsNullOrEmpty(Title);
|
||||
|
||||
internal bool HasSubtitle => !string.IsNullOrEmpty(Subtitle);
|
||||
|
||||
internal bool HasText => HasTitle || HasSubtitle;
|
||||
|
||||
private void UpdateTextVisibility()
|
||||
{
|
||||
UpdateTextVisibilityState();
|
||||
}
|
||||
|
||||
private void UpdateTextVisibilityState()
|
||||
{
|
||||
// Determine which visual state to use based on title/subtitle presence
|
||||
var stateName = (HasTitle, HasSubtitle) switch
|
||||
{
|
||||
(true, true) => "TextVisible",
|
||||
(true, false) => "TitleOnly",
|
||||
(false, true) => "SubtitleOnly",
|
||||
(false, false) => "TextHidden",
|
||||
};
|
||||
|
||||
VisualStateManager.GoToState(this, stateName, true);
|
||||
}
|
||||
|
||||
private void UpdateIconVisibility()
|
||||
{
|
||||
if (Icon is IconBox icon)
|
||||
{
|
||||
var dt = icon.DataContext;
|
||||
var src = icon.Source;
|
||||
|
||||
if (_iconPresenter is not null)
|
||||
{
|
||||
// n.b. this might be wrong - I think we always have an Icon (an IconBox),
|
||||
// we need to check if the box has an icon
|
||||
_iconPresenter.Visibility = Icon is null ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
UpdateIconVisibilityState();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateIconVisibilityState()
|
||||
{
|
||||
var hasIcon = Icon is not null;
|
||||
VisualStateManager.GoToState(this, hasIcon ? "IconVisible" : "IconHidden", true);
|
||||
}
|
||||
|
||||
private void UpdateAlignment()
|
||||
{
|
||||
// If this item has both an icon and a label, left align so that the
|
||||
// icons don't wobble if the text changes.
|
||||
//
|
||||
// Otherwise, center align.
|
||||
var requestedTheme = ActualTheme;
|
||||
var isLight = requestedTheme == ElementTheme.Light;
|
||||
var showText = HasText;
|
||||
if (Icon is IconBox icoBox &&
|
||||
icoBox.DataContext is DockItemViewModel item &&
|
||||
item.Icon is IconInfoViewModel icon)
|
||||
{
|
||||
var showIcon = icon is not null && icon.HasIcon(isLight);
|
||||
if (showText && showIcon)
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Left;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalAlignment = HorizontalAlignment.Center;
|
||||
}
|
||||
|
||||
private void UpdateAllVisibility()
|
||||
{
|
||||
UpdateTextVisibility();
|
||||
UpdateIconVisibility();
|
||||
UpdateAlignment();
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
IsEnabledChanged -= OnIsEnabledChanged;
|
||||
|
||||
PointerEntered -= Control_PointerEntered;
|
||||
PointerExited -= Control_PointerExited;
|
||||
|
||||
PointerEntered += Control_PointerEntered;
|
||||
PointerExited += Control_PointerExited;
|
||||
|
||||
IsEnabledChanged += OnIsEnabledChanged;
|
||||
|
||||
// Get template children for visibility updates
|
||||
_iconPresenter = GetTemplateChild(IconPresenterName) as FrameworkElement;
|
||||
|
||||
// Set initial visibility
|
||||
UpdateAllVisibility();
|
||||
}
|
||||
|
||||
private void Control_PointerEntered(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "PointerOver", true);
|
||||
}
|
||||
|
||||
private void Control_PointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, "Normal", true);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerRoutedEventArgs e)
|
||||
{
|
||||
if (IsEnabled)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
VisualStateManager.GoToState(this, "Pressed", true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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.UI.ViewModels.Settings;
|
||||
using Windows.Win32;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
internal static class DockSettingsToViews
|
||||
{
|
||||
public static double WidthForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 128,
|
||||
DockSize.Medium => 192,
|
||||
DockSize.Large => 256,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double HeightForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 32,
|
||||
DockSize.Medium => 54,
|
||||
DockSize.Large => 76,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static double IconSizeForSize(DockSize size)
|
||||
{
|
||||
return size switch
|
||||
{
|
||||
DockSize.Small => 32 / 2,
|
||||
DockSize.Medium => 54 / 2,
|
||||
DockSize.Large => 76 / 2,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static Microsoft.UI.Xaml.Media.SystemBackdrop? GetSystemBackdrop(DockBackdrop backdrop)
|
||||
{
|
||||
return backdrop switch
|
||||
{
|
||||
DockBackdrop.Transparent => new TransparentTintBackdrop(),
|
||||
DockBackdrop.Acrylic => null, // new DesktopAcrylicBackdrop(),
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static uint GetAppBarEdge(DockSide side)
|
||||
{
|
||||
return side switch
|
||||
{
|
||||
DockSide.Left => PInvoke.ABE_LEFT,
|
||||
DockSide.Top => PInvoke.ABE_TOP,
|
||||
DockSide.Right => PInvoke.ABE_RIGHT,
|
||||
DockSide.Bottom => PInvoke.ABE_BOTTOM,
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
}
|
||||
}
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
74
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal file
74
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml
Normal file
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.CmdPal.UI.Dock.DockWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Dock"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:vm="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="PowerDock"
|
||||
Closed="DockWindow_Closed"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid
|
||||
x:Name="Root"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
|
||||
<Grid.Resources>
|
||||
<Style
|
||||
x:Name="ContextMenuFlyoutStyle"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextMenuControl" MaxHeight="512" />
|
||||
</Flyout>
|
||||
</Grid.Resources>
|
||||
|
||||
<!-- Colorization overlay for transparent backdrop -->
|
||||
<Border
|
||||
x:Name="ColorizationOverlay"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
IsHitTestVisible="False"
|
||||
Opacity="{x:Bind WindowViewModel.ColorizationOpacity, Mode=OneWay}"
|
||||
Visibility="{x:Bind WindowViewModel.ShowColorizationOverlay, Mode=OneWay}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind WindowViewModel.ColorizationColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
|
||||
<cpcontrols:BlurImageControl
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
BlurAmount="{x:Bind WindowViewModel.BackgroundImageBlurAmount, Mode=OneWay}"
|
||||
ImageBrightness="{x:Bind WindowViewModel.BackgroundImageBrightness, Mode=OneWay}"
|
||||
ImageOpacity="{x:Bind WindowViewModel.BackgroundImageOpacity, Mode=OneWay}"
|
||||
ImageSource="{x:Bind WindowViewModel.BackgroundImageSource, Mode=OneWay}"
|
||||
ImageStretch="{x:Bind WindowViewModel.BackgroundImageStretch, Mode=OneWay}"
|
||||
IsHitTestVisible="False"
|
||||
IsHoldingEnabled="False"
|
||||
TintColor="{x:Bind WindowViewModel.BackgroundImageTint, Mode=OneWay}"
|
||||
TintIntensity="{x:Bind WindowViewModel.BackgroundImageTintIntensity, Mode=OneWay}"
|
||||
Visibility="{x:Bind WindowViewModel.ShowBackgroundImage, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
878
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal file
878
src/modules/cmdpal/Microsoft.CmdPal.UI/Dock/DockWindow.xaml.cs
Normal file
@@ -0,0 +1,878 @@
|
||||
// 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.InteropServices;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Composition.SystemBackdrops;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Windows.Foundation;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Accessibility;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
using WinRT;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Dock;
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
|
||||
public sealed partial class DockWindow : WindowEx,
|
||||
IRecipient<BringToTopMessage>,
|
||||
IRecipient<RequestShowPaletteAtMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IRecipient<ShowCommandInContextMenuMessage>,
|
||||
IRecipient<CloseContextMenuMessage>,
|
||||
IDisposable
|
||||
{
|
||||
#pragma warning disable SA1306 // Field names should begin with lower-case letter
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private readonly uint WM_TASKBAR_RESTART;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
#pragma warning restore SA1306 // Field names should begin with lower-case letter
|
||||
|
||||
private readonly IThemeService _themeService;
|
||||
private readonly DockWindowViewModel _windowViewModel;
|
||||
|
||||
private HWND _hwnd = HWND.Null;
|
||||
private APPBARDATA _appBarData;
|
||||
private uint _callbackMessageId;
|
||||
|
||||
private DockSettings _settings;
|
||||
private DockViewModel viewModel;
|
||||
private DockControl _dock;
|
||||
private DesktopAcrylicController? _acrylicController;
|
||||
private SystemBackdropConfiguration? _configurationSource;
|
||||
private DockSize _lastSize;
|
||||
|
||||
// Store the original WndProc
|
||||
private WNDPROC? _originalWndProc;
|
||||
private WNDPROC? _customWndProc;
|
||||
|
||||
// internal Settings CurrentSettings => _settings;
|
||||
public DockWindow()
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var mainSettings = serviceProvider.GetService<SettingsModel>()!;
|
||||
mainSettings.SettingsChanged += SettingsChangedHandler;
|
||||
_settings = mainSettings.DockSettings;
|
||||
_lastSize = _settings.DockSize;
|
||||
|
||||
viewModel = serviceProvider.GetService<DockViewModel>()!;
|
||||
_themeService = serviceProvider.GetRequiredService<IThemeService>();
|
||||
_themeService.ThemeChanged += ThemeService_ThemeChanged;
|
||||
_windowViewModel = new DockWindowViewModel(_themeService);
|
||||
_dock = new DockControl(viewModel);
|
||||
|
||||
InitializeComponent();
|
||||
Root.Children.Add(_dock);
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
AppWindow.TitleBar.PreferredHeightOption = TitleBarHeightOption.Collapsed;
|
||||
AppWindow.IsShownInSwitchers = false;
|
||||
if (AppWindow.Presenter is OverlappedPresenter overlappedPresenter)
|
||||
{
|
||||
overlappedPresenter.SetBorderAndTitleBar(false, false);
|
||||
overlappedPresenter.IsResizable = false;
|
||||
}
|
||||
|
||||
this.Activated += DockWindow_Activated;
|
||||
|
||||
WeakReferenceMessenger.Default.Register<BringToTopMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<RequestShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ShowCommandInContextMenuMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<CloseContextMenuMessage>(this);
|
||||
|
||||
_hwnd = GetWindowHandle(this);
|
||||
|
||||
// Subclass the window to intercept messages
|
||||
//
|
||||
// Set up custom window procedure to listen for display changes
|
||||
// LOAD BEARING: If you don't stick the pointer to HotKeyPrc into a
|
||||
// member (and instead like, use a local), then the pointer we marshal
|
||||
// into the WindowLongPtr will be useless after we leave this function,
|
||||
// and our **WindProc will explode**.
|
||||
_customWndProc = CustomWndProc;
|
||||
|
||||
_callbackMessageId = PInvoke.RegisterWindowMessage($"CmdPal_ABM_{_hwnd}");
|
||||
|
||||
// TaskbarCreated is the message that's broadcast when explorer.exe
|
||||
// restarts. We need to know when that happens to be able to bring our
|
||||
// appbar back
|
||||
// And this apparently happens on lock screens / hibernates, too
|
||||
WM_TASKBAR_RESTART = PInvoke.RegisterWindowMessage("TaskbarCreated");
|
||||
|
||||
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(_customWndProc);
|
||||
_originalWndProc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(PInvoke.SetWindowLongPtr(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
|
||||
|
||||
// Disable minimize and maximize box
|
||||
var style = (WINDOW_STYLE)PInvoke.GetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE);
|
||||
style &= ~WINDOW_STYLE.WS_MINIMIZEBOX; // Remove WS_MINIMIZEBOX
|
||||
style &= ~WINDOW_STYLE.WS_MAXIMIZEBOX; // Remove WS_MAXIMIZEBOX
|
||||
_ = PInvoke.SetWindowLong(_hwnd, WINDOW_LONG_PTR_INDEX.GWL_STYLE, (int)style);
|
||||
|
||||
ShowDesktop.AddHook(this);
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
private void SettingsChangedHandler(SettingsModel sender, object? args)
|
||||
{
|
||||
_settings = sender.DockSettings;
|
||||
UpdateSettings();
|
||||
}
|
||||
|
||||
private void DockWindow_Activated(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
// These are used for removing the very subtle shadow/border that we get from Windows 11
|
||||
HwndExtensions.ToggleWindowStyle(_hwnd, false, WindowStyle.TiledWindow);
|
||||
unsafe
|
||||
{
|
||||
BOOL value = false;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_WINDOW_CORNER_PREFERENCE, &value, (uint)sizeof(BOOL));
|
||||
}
|
||||
}
|
||||
|
||||
private HWND GetWindowHandle(Window window)
|
||||
{
|
||||
var hwnd = WindowNative.GetWindowHandle(window);
|
||||
return new HWND(hwnd);
|
||||
}
|
||||
|
||||
private void UpdateSettings()
|
||||
{
|
||||
this.viewModel.UpdateSettings(_settings);
|
||||
|
||||
SystemBackdrop = DockSettingsToViews.GetSystemBackdrop(_settings.Backdrop);
|
||||
|
||||
// If the backdrop is acrylic, things are more complicated
|
||||
if (_settings.Backdrop == DockBackdrop.Acrylic)
|
||||
{
|
||||
SetAcrylic();
|
||||
}
|
||||
|
||||
_dock.UpdateSettings(_settings);
|
||||
var side = DockSettingsToViews.GetAppBarEdge(_settings.Side);
|
||||
|
||||
if (_appBarData.hWnd != IntPtr.Zero)
|
||||
{
|
||||
var sameEdge = _appBarData.uEdge == side;
|
||||
var sameSize = _lastSize == _settings.DockSize;
|
||||
if (sameEdge && sameSize)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DestroyAppBar(_hwnd);
|
||||
}
|
||||
|
||||
CreateAppBar(_hwnd);
|
||||
}
|
||||
|
||||
// We want to use DesktopAcrylicKind.Thin and custom colors as this is the default material
|
||||
// other Shell surfaces are using, this cannot be set in XAML however.
|
||||
private void SetAcrylic()
|
||||
{
|
||||
if (DesktopAcrylicController.IsSupported())
|
||||
{
|
||||
// Hooking up the policy object.
|
||||
_configurationSource = new SystemBackdropConfiguration
|
||||
{
|
||||
// Initial configuration state.
|
||||
IsInputActive = true,
|
||||
};
|
||||
UpdateAcrylic();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAcrylic()
|
||||
{
|
||||
if (_acrylicController != null)
|
||||
{
|
||||
_acrylicController.RemoveAllSystemBackdropTargets();
|
||||
_acrylicController.Dispose();
|
||||
}
|
||||
|
||||
var backdrop = _themeService.CurrentDockTheme.BackdropParameters;
|
||||
_acrylicController = new DesktopAcrylicController
|
||||
{
|
||||
Kind = DesktopAcrylicKind.Thin,
|
||||
TintColor = backdrop.TintColor,
|
||||
TintOpacity = backdrop.TintOpacity,
|
||||
FallbackColor = backdrop.FallbackColor,
|
||||
LuminosityOpacity = backdrop.LuminosityOpacity,
|
||||
};
|
||||
|
||||
// Enable the system backdrop.
|
||||
// Note: Be sure to have "using WinRT;" to support the Window.As<...>() call.
|
||||
_acrylicController.AddSystemBackdropTarget(this.As<ICompositionSupportsSystemBackdrop>());
|
||||
_acrylicController.SetSystemBackdropConfiguration(_configurationSource);
|
||||
}
|
||||
|
||||
private void DisposeAcrylic()
|
||||
{
|
||||
if (_acrylicController is not null)
|
||||
{
|
||||
_acrylicController.Dispose();
|
||||
_acrylicController = null!;
|
||||
_configurationSource = null!;
|
||||
}
|
||||
}
|
||||
|
||||
private void ThemeService_ThemeChanged(object? sender, ThemeChangedEventArgs e)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
// We only need to handle acrylic here.
|
||||
// Transparent background is handled directly in XAML by binding to
|
||||
// the DockWindowViewModel's ColorizationColor properties.
|
||||
if (_settings.Backdrop == DockBackdrop.Acrylic)
|
||||
{
|
||||
UpdateAcrylic();
|
||||
}
|
||||
|
||||
// ActualTheme / RequestedTheme sync,
|
||||
// as pilfered from WindowThemeSynchronizer
|
||||
// LOAD BEARING: Changing the RequestedTheme to Dark then Light then target forces
|
||||
// a refresh of the theme.
|
||||
Root.RequestedTheme = ElementTheme.Dark;
|
||||
Root.RequestedTheme = ElementTheme.Light;
|
||||
Root.RequestedTheme = _themeService.CurrentDockTheme.Theme;
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateAppBar(HWND hwnd)
|
||||
{
|
||||
_appBarData = new APPBARDATA
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf<APPBARDATA>(),
|
||||
hWnd = hwnd,
|
||||
uCallbackMessage = _callbackMessageId,
|
||||
};
|
||||
|
||||
// Register this window as an appbar
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_NEW, ref _appBarData);
|
||||
|
||||
// Stash the last size we created the bar at, so we know when to hot-
|
||||
// reload it
|
||||
_lastSize = _settings.DockSize;
|
||||
|
||||
UpdateWindowPosition();
|
||||
}
|
||||
|
||||
private void DestroyAppBar(HWND hwnd)
|
||||
{
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_REMOVE, ref _appBarData);
|
||||
_appBarData = default;
|
||||
}
|
||||
|
||||
private void UpdateWindowPosition()
|
||||
{
|
||||
Logger.LogDebug("UpdateWindowPosition");
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
|
||||
// Get system border metrics
|
||||
var borderWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXBORDER);
|
||||
var edgeWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXEDGE);
|
||||
var frameWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXFRAME);
|
||||
|
||||
UpdateAppBarDataForEdge(_settings.Side, _settings.DockSize, dpi / 96.0);
|
||||
|
||||
// Query and set position
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_QUERYPOS, ref _appBarData);
|
||||
PInvoke.SHAppBarMessage(PInvoke.ABM_SETPOS, ref _appBarData);
|
||||
|
||||
// TODO: investigate ABS_AUTOHIDE and autohide bars.
|
||||
// I think it's something like this, but I don't totally know
|
||||
// // _appBarData.lParam = ABS_ALWAYSONTOP;
|
||||
// _appBarData.lParam = (LPARAM)(int)PInvoke.ABS_AUTOHIDE;
|
||||
// PInvoke.SHAppBarMessage(ABM_SETSTATE, ref _appBarData);
|
||||
// PInvoke.SHAppBarMessage(PInvoke.ABM_SETAUTOHIDEBAR, ref _appBarData);
|
||||
|
||||
// Account for system borders when moving the window
|
||||
// Adjust position to account for window frame/border
|
||||
var adjustedLeft = _appBarData.rc.left - frameWidth;
|
||||
var adjustedTop = _appBarData.rc.top - frameWidth;
|
||||
var adjustedWidth = (_appBarData.rc.right - _appBarData.rc.left) + (2 * frameWidth);
|
||||
var adjustedHeight = (_appBarData.rc.bottom - _appBarData.rc.top) + (2 * frameWidth);
|
||||
|
||||
// Move the actual window
|
||||
PInvoke.MoveWindow(
|
||||
_hwnd,
|
||||
adjustedLeft,
|
||||
adjustedTop,
|
||||
adjustedWidth,
|
||||
adjustedHeight,
|
||||
true);
|
||||
}
|
||||
|
||||
private void UpdateAppBarDataForEdge(DockSide side, DockSize size, double scaleFactor)
|
||||
{
|
||||
Logger.LogDebug("UpdateAppBarDataForEdge");
|
||||
var horizontalHeightDips = DockSettingsToViews.HeightForSize(size);
|
||||
var verticalWidthDips = DockSettingsToViews.WidthForSize(size);
|
||||
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
|
||||
if (side == DockSide.Top)
|
||||
{
|
||||
_appBarData.uEdge = PInvoke.ABE_TOP;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = (int)(horizontalHeightDips * scaleFactor);
|
||||
}
|
||||
else if (side == DockSide.Bottom)
|
||||
{
|
||||
var heightPixels = (int)(horizontalHeightDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_BOTTOM;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = screenHeight - heightPixels;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else if (side == DockSide.Left)
|
||||
{
|
||||
var widthPixels = (int)(verticalWidthDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_LEFT;
|
||||
_appBarData.rc.left = 0;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = widthPixels;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else if (side == DockSide.Right)
|
||||
{
|
||||
var widthPixels = (int)(verticalWidthDips * scaleFactor);
|
||||
|
||||
_appBarData.uEdge = PInvoke.ABE_RIGHT;
|
||||
_appBarData.rc.left = screenWidth - widthPixels;
|
||||
_appBarData.rc.top = 0;
|
||||
_appBarData.rc.right = screenWidth;
|
||||
_appBarData.rc.bottom = screenHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private LRESULT CustomWndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// check settings changed
|
||||
if (msg == PInvoke.WM_SETTINGCHANGE)
|
||||
{
|
||||
var isFullscreen = IsWindowFullscreen();
|
||||
|
||||
Logger.LogDebug($"WM_SETTINGCHANGE ({isFullscreen})");
|
||||
|
||||
if (isFullscreen)
|
||||
{
|
||||
this.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Show();
|
||||
}
|
||||
|
||||
if (wParam == (uint)SYSTEM_PARAMETERS_INFO_ACTION.SPI_SETWORKAREA)
|
||||
{
|
||||
Logger.LogDebug($"WM_SETTINGCHANGE(SPI_SETWORKAREA)");
|
||||
|
||||
// Use debounced call to throttle rapid successive calls
|
||||
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
|
||||
}
|
||||
}
|
||||
else if (msg == PInvoke.WM_DISPLAYCHANGE)
|
||||
{
|
||||
Logger.LogDebug("WM_DISPLAYCHANGE");
|
||||
|
||||
// Use dispatcher to ensure we're on the UI thread
|
||||
DispatcherQueue.TryEnqueue(() => UpdateWindowPosition());
|
||||
}
|
||||
|
||||
// Intercept WM_SYSCOMMAND to prevent minimize and maximize
|
||||
else if (msg == PInvoke.WM_SYSCOMMAND)
|
||||
{
|
||||
var command = (int)(wParam.Value & 0xFFF0);
|
||||
if (command == PInvoke.SC_MINIMIZE || command == PInvoke.SC_MAXIMIZE)
|
||||
{
|
||||
// Block minimize and maximize commands
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop min/max on WM_WINDOWPOSCHANGING too
|
||||
else if (msg == PInvoke.WM_WINDOWPOSCHANGING)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var pWindowPos = (WINDOWPOS*)lParam.Value;
|
||||
|
||||
// Check if the window is being hidden (minimized) or if flags suggest minimize/maximize
|
||||
if ((pWindowPos->flags & SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW) != 0)
|
||||
{
|
||||
// Prevent hiding the window (minimize)
|
||||
pWindowPos->flags &= ~SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW;
|
||||
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW;
|
||||
}
|
||||
|
||||
// Additional check: if the window position suggests it's being minimized or maximized
|
||||
// by checking for dramatic size changes
|
||||
if (pWindowPos->cx <= 0 || pWindowPos->cy <= 0)
|
||||
{
|
||||
// Prevent zero or negative size changes (minimize)
|
||||
pWindowPos->flags |= SET_WINDOW_POS_FLAGS.SWP_NOSIZE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_SIZE to prevent minimize/maximize state changes
|
||||
else if (msg == PInvoke.WM_SIZE)
|
||||
{
|
||||
var sizeType = (int)wParam.Value;
|
||||
if (sizeType == PInvoke.SIZE_MINIMIZED || sizeType == PInvoke.SIZE_MAXIMIZED)
|
||||
{
|
||||
// Block the size change by not calling the original window procedure
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_SHOWWINDOW to prevent hiding (minimize)
|
||||
else if (msg == PInvoke.WM_SHOWWINDOW)
|
||||
{
|
||||
var isBeingShown = wParam.Value != 0;
|
||||
if (!isBeingShown)
|
||||
{
|
||||
// Prevent hiding the window
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle double-click on title bar (non-client area)
|
||||
else if (msg == PInvoke.WM_NCLBUTTONDBLCLK)
|
||||
{
|
||||
var hitTest = (int)wParam.Value;
|
||||
if (hitTest == PInvoke.HTCAPTION)
|
||||
{
|
||||
// Block double-click on title bar to prevent maximize
|
||||
return new LRESULT(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle WM_GETMINMAXINFO to control window size limits
|
||||
else if (msg == PInvoke.WM_GETMINMAXINFO)
|
||||
{
|
||||
// We can modify the min/max tracking info here if needed
|
||||
// For now, let it pass through but we could restrict max size
|
||||
}
|
||||
|
||||
// Handle the AppBarMessage message
|
||||
// This is needed to update the position when the work area changes.
|
||||
// (notably, when the user toggles auto-hide taskbars)
|
||||
else if (msg == _callbackMessageId)
|
||||
{
|
||||
if (wParam.Value == PInvoke.ABN_POSCHANGED)
|
||||
{
|
||||
UpdateWindowPosition();
|
||||
}
|
||||
}
|
||||
else if (msg == WM_TASKBAR_RESTART)
|
||||
{
|
||||
Logger.LogDebug("WM_TASKBAR_RESTART");
|
||||
|
||||
DispatcherQueue.TryEnqueue(() => CreateAppBar(_hwnd));
|
||||
|
||||
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(false));
|
||||
}
|
||||
|
||||
// Call the original window procedure for all other messages
|
||||
return PInvoke.CallWindowProc(_originalWndProc, hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
void IRecipient<BringToTopMessage>.Receive(BringToTopMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
var onTop = message.OnTop ? HWND.HWND_TOPMOST : HWND.HWND_NOTOPMOST;
|
||||
PInvoke.SetWindowPos(_hwnd, onTop, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
PInvoke.SetWindowPos(_hwnd, HWND.HWND_NOTOPMOST, 0, 0, 0, 0, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOSIZE);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsWindowFullscreen()
|
||||
{
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
|
||||
if (Marshal.GetExceptionForHR(PInvoke.SHQueryUserNotificationState(out var state)) is null)
|
||||
{
|
||||
if (state == QUERY_USER_NOTIFICATION_STATE.QUNS_RUNNING_D3D_FULL_SCREEN ||
|
||||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_BUSY ||
|
||||
state == QUERY_USER_NOTIFICATION_STATE.QUNS_PRESENTATION_MODE)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ContextMenuFlyout_Opened(object sender, object e)
|
||||
{
|
||||
ContextMenuControl.FocusSearchBox();
|
||||
}
|
||||
|
||||
public void Receive(QuitMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
DestroyAppBar(_hwnd);
|
||||
|
||||
this.Close();
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(ShowCommandInContextMenuMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
ShowFlyoutOnUiThread(message.Context, message.UiContext);
|
||||
});
|
||||
}
|
||||
|
||||
public void Receive(CloseContextMenuMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (ContextMenuFlyout.IsOpen)
|
||||
{
|
||||
ContextMenuFlyout.Hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IRecipient<RequestShowPaletteAtMessage>.Receive(RequestShowPaletteAtMessage message)
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => RequestShowPaletteOnUiThread(message.PosDips));
|
||||
}
|
||||
|
||||
private void RequestShowPaletteOnUiThread(Point posDips)
|
||||
{
|
||||
// TODO!!!! posDips was originally relative to the dock window.
|
||||
// It's now relative to the screen.
|
||||
// this method wasn't yet updated for that.
|
||||
|
||||
// pos is relative to our root. We need to convert to screen coords.
|
||||
var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
var screenPosDips = new Point(rootPosDips.X + posDips.X, rootPosDips.Y + posDips.Y);
|
||||
|
||||
var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
var scaleFactor = dpi / 96.0;
|
||||
var screenPosPixels = new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);
|
||||
|
||||
var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
|
||||
// Now we're going to find the best position for the palette.
|
||||
|
||||
// We want to anchor the palette on the dock side.
|
||||
// on the top:
|
||||
// - anchor to the top, left if we're on the left half of the screen
|
||||
// - anchor to the top, right if we're on the right half of the screen
|
||||
// On the left:
|
||||
// - anchor to the top, left if we're on the top half of the screen
|
||||
// - anchor to the bottom, left if we're on the bottom half of the screen
|
||||
// On the right:
|
||||
// - anchor to the top, right if we're on the top half of the screen
|
||||
// - anchor to the bottom, right if we're on the bottom half of the screen
|
||||
// On the bottom:
|
||||
// - anchor to the bottom, left if we're on the left half of the screen
|
||||
// - anchor to the bottom, right if we're on the right half of the screen
|
||||
var onTopHalf = screenPosPixels.Y < screenHeight / 2;
|
||||
var onLeftHalf = screenPosPixels.X < screenWidth / 2;
|
||||
var onRightHalf = !onLeftHalf;
|
||||
var onBottomHalf = !onTopHalf;
|
||||
|
||||
var anchorPoint = _settings.Side switch
|
||||
{
|
||||
DockSide.Top => onLeftHalf ? AnchorPoint.TopLeft : AnchorPoint.TopRight,
|
||||
DockSide.Bottom => onLeftHalf ? AnchorPoint.BottomLeft : AnchorPoint.BottomRight,
|
||||
DockSide.Left => onTopHalf ? AnchorPoint.TopLeft : AnchorPoint.BottomLeft,
|
||||
DockSide.Right => onTopHalf ? AnchorPoint.TopRight : AnchorPoint.BottomRight,
|
||||
_ => AnchorPoint.TopLeft,
|
||||
};
|
||||
|
||||
// we also need to slide the anchor point a bit away from the dock
|
||||
var paddingDips = 8;
|
||||
var paddingPixels = paddingDips * scaleFactor;
|
||||
PInvoke.GetWindowRect(_hwnd, out var ourRect);
|
||||
|
||||
// Depending on the side we're on, we need to offset differently
|
||||
switch (_settings.Side)
|
||||
{
|
||||
case DockSide.Top:
|
||||
screenPosPixels.Y = ourRect.bottom + paddingPixels;
|
||||
break;
|
||||
case DockSide.Bottom:
|
||||
screenPosPixels.Y = ourRect.top - paddingPixels;
|
||||
break;
|
||||
case DockSide.Left:
|
||||
screenPosPixels.X = ourRect.right + paddingPixels;
|
||||
break;
|
||||
case DockSide.Right:
|
||||
screenPosPixels.X = ourRect.left - paddingPixels;
|
||||
break;
|
||||
}
|
||||
|
||||
// Now that we know the anchor corner, and where to attempt to place it, we can
|
||||
// ask the palette to show itself there.
|
||||
WeakReferenceMessenger.Default.Send<ShowPaletteAtMessage>(new(screenPosPixels, anchorPoint));
|
||||
}
|
||||
|
||||
private void ShowFlyoutOnUiThread(IContextMenuContext context, object? uiContext)
|
||||
{
|
||||
ContextMenuControl.ViewModel.SelectedItem = context;
|
||||
|
||||
FlyoutShowOptions opts = new()
|
||||
{
|
||||
ShowMode = FlyoutShowMode.Standard,
|
||||
};
|
||||
|
||||
// Depending on the side we're on, change the direction of the results
|
||||
switch (_settings.Side)
|
||||
{
|
||||
case DockSide.Top:
|
||||
case DockSide.Left:
|
||||
case DockSide.Right:
|
||||
ContextMenuControl.ViewModel.FilterOnTop = true;
|
||||
break;
|
||||
case DockSide.Bottom:
|
||||
ContextMenuControl.ViewModel.FilterOnTop = false;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (_settings.Side)
|
||||
{
|
||||
case DockSide.Top:
|
||||
opts.Placement = FlyoutPlacementMode.Bottom;
|
||||
break;
|
||||
case DockSide.Left:
|
||||
opts.Placement = FlyoutPlacementMode.RightEdgeAlignedTop;
|
||||
break;
|
||||
case DockSide.Right:
|
||||
opts.Placement = FlyoutPlacementMode.LeftEdgeAlignedTop;
|
||||
break;
|
||||
case DockSide.Bottom:
|
||||
opts.Placement = FlyoutPlacementMode.Top;
|
||||
break;
|
||||
}
|
||||
|
||||
if (uiContext is UIElement uiElement)
|
||||
{
|
||||
ContextMenuFlyout.ShowAt(
|
||||
uiElement,
|
||||
opts);
|
||||
}
|
||||
|
||||
//// position is relative to our root. We need to convert to screen coords.
|
||||
////
|
||||
//// Root.TransformToVisual(null) will always return 0,0, because Root is at the root of our window.
|
||||
////
|
||||
//// we instead need:
|
||||
|
||||
//// var rootPosDips = Root.TransformToVisual(null).TransformPoint(new Point(0, 0));
|
||||
|
||||
//// var screenPosDips = new Point(rootPosDips.X + position.X, rootPosDips.Y + position.Y);
|
||||
|
||||
//// TODO! deal with DPI - I'm sure this is wrong
|
||||
// var dpi = PInvoke.GetDpiForWindow(_hwnd);
|
||||
// PInvoke.GetWindowRect(_hwnd, out var ourRect);
|
||||
// var scaleFactor = dpi / 96.0;
|
||||
|
||||
// var positionPixels = new Point(position.X * scaleFactor, position.Y * scaleFactor);
|
||||
|
||||
// var screenPosPixels = // new Point(screenPosDips.X * scaleFactor, screenPosDips.Y * scaleFactor);
|
||||
// new Point(ourRect.left + positionPixels.X, ourRect.top + positionPixels.Y);
|
||||
|
||||
// var screenWidth = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CXSCREEN);
|
||||
// var screenHeight = PInvoke.GetSystemMetrics(SYSTEM_METRICS_INDEX.SM_CYSCREEN);
|
||||
// var onTopHalf = screenPosPixels.Y < screenHeight / 2;
|
||||
// var onLeftHalf = screenPosPixels.X < screenWidth / 2;
|
||||
// var onRightHalf = !onLeftHalf;
|
||||
// var onBottomHalf = !onTopHalf;
|
||||
// var anchorPoint = _settings.Side switch
|
||||
// {
|
||||
// DockSide.Top => onLeftHalf ? AnchorPoint.TopLeft : AnchorPoint.TopRight,
|
||||
// DockSide.Bottom => onLeftHalf ? AnchorPoint.BottomLeft : AnchorPoint.BottomRight,
|
||||
// DockSide.Left => onTopHalf ? AnchorPoint.TopLeft : AnchorPoint.BottomLeft,
|
||||
// DockSide.Right => onTopHalf ? AnchorPoint.TopRight : AnchorPoint.BottomRight,
|
||||
// _ => AnchorPoint.TopLeft,
|
||||
// };
|
||||
|
||||
//// we also need to slide the anchor point a bit away from the dock
|
||||
// var paddingDips = 8;
|
||||
// var paddingPixels = paddingDips * scaleFactor;
|
||||
|
||||
//// Depending on the side we're on, we need to offset differently
|
||||
// switch (_settings.Side)
|
||||
// {
|
||||
// case DockSide.Top:
|
||||
// screenPosPixels.Y += paddingPixels;
|
||||
// break;
|
||||
// case DockSide.Bottom:
|
||||
// screenPosPixels.Y -= paddingPixels;
|
||||
// break;
|
||||
// case DockSide.Left:
|
||||
// screenPosPixels.X += paddingPixels;
|
||||
// break;
|
||||
// case DockSide.Right:
|
||||
// screenPosPixels.X -= paddingPixels;
|
||||
// break;
|
||||
// }
|
||||
|
||||
//// var finalPosDips = new Point((screenPosPixels.X / scaleFactor) - rootPosDips.X, (screenPosPixels.Y / scaleFactor) - rootPosDips.Y);
|
||||
// var finalPosDips = new Point(
|
||||
// (screenPosPixels.X - ourRect.left) / scaleFactor,
|
||||
// (screenPosPixels.Y - ourRect.top) / scaleFactor);
|
||||
|
||||
// ContextMenuFlyout.ShowAt(
|
||||
// Root,
|
||||
// new FlyoutShowOptions()
|
||||
// {
|
||||
// ShowMode = FlyoutShowMode.Standard,
|
||||
// Position = finalPosDips,
|
||||
// });
|
||||
}
|
||||
|
||||
public DockWindowViewModel WindowViewModel => _windowViewModel;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAcrylic();
|
||||
viewModel.Dispose();
|
||||
_windowViewModel.Dispose();
|
||||
}
|
||||
|
||||
private void DockWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
var serviceProvider = App.Current.Services;
|
||||
var settings = serviceProvider.GetService<SettingsModel>();
|
||||
settings?.SettingsChanged -= SettingsChangedHandler;
|
||||
_themeService.ThemeChanged -= ThemeService_ThemeChanged;
|
||||
DisposeAcrylic();
|
||||
|
||||
// Remove our appbar registration
|
||||
DestroyAppBar(_hwnd);
|
||||
|
||||
// Unhook the window procedure
|
||||
ShowDesktop.RemoveHook();
|
||||
}
|
||||
}
|
||||
|
||||
// Thank you to https://stackoverflow.com/a/35422795/1481137
|
||||
internal static class ShowDesktop
|
||||
{
|
||||
private const string WORKERW = "WorkerW";
|
||||
private const string PROGMAN = "Progman";
|
||||
|
||||
private static WINEVENTPROC? _hookProc;
|
||||
private static IntPtr _hookHandle = IntPtr.Zero;
|
||||
|
||||
public static void AddHook(Window window)
|
||||
{
|
||||
if (IsHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsHooked = true;
|
||||
|
||||
_hookProc = (WINEVENTPROC)WinEventCallback;
|
||||
_hookHandle = PInvoke.SetWinEventHook(PInvoke.EVENT_SYSTEM_FOREGROUND, PInvoke.EVENT_SYSTEM_FOREGROUND, HMODULE.Null, _hookProc, 0, 0, PInvoke.WINEVENT_OUTOFCONTEXT);
|
||||
}
|
||||
|
||||
public static void RemoveHook()
|
||||
{
|
||||
if (!IsHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsHooked = false;
|
||||
|
||||
PInvoke.UnhookWinEvent((HWINEVENTHOOK)_hookHandle);
|
||||
_hookProc = null;
|
||||
_hookHandle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static string GetWindowClass(HWND hwnd)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (char* c = new char[32])
|
||||
{
|
||||
_ = PInvoke.GetClassName(hwnd, (PWSTR)c, 32);
|
||||
return new string(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||
|
||||
private static void WinEventCallback(
|
||||
HWINEVENTHOOK hWinEventHook,
|
||||
uint eventType,
|
||||
HWND hwnd,
|
||||
int idObject,
|
||||
int idChild,
|
||||
uint dwEventThread,
|
||||
uint dwmsEventTime)
|
||||
{
|
||||
if (eventType == PInvoke.EVENT_SYSTEM_FOREGROUND)
|
||||
{
|
||||
var @class = GetWindowClass(hwnd);
|
||||
if (string.Equals(@class, WORKERW, StringComparison.Ordinal) || string.Equals(@class, PROGMAN, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("ShowDesktop invoked. Bring us back");
|
||||
WeakReferenceMessenger.Default.Send<BringToTopMessage>(new(true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsHooked { get; private set; }
|
||||
}
|
||||
|
||||
internal sealed record BringToTopMessage(bool OnTop);
|
||||
|
||||
internal sealed record RequestShowPaletteAtMessage(Point PosDips);
|
||||
|
||||
internal sealed record ShowPaletteAtMessage(Point PosPixels, AnchorPoint Anchor);
|
||||
|
||||
internal enum AnchorPoint
|
||||
{
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
@@ -587,7 +587,7 @@ public sealed partial class ListPage : Page,
|
||||
var shouldUpdateSelection = false;
|
||||
|
||||
// If it's a top level list update we force the reset to the top useful item
|
||||
if (!sender.IsNested)
|
||||
if (sender.IsRootPage)
|
||||
{
|
||||
shouldUpdateSelection = true;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// 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.
|
||||
|
||||
@@ -152,7 +152,7 @@ internal sealed partial class TrayIconService
|
||||
{
|
||||
if (wParam == PInvoke.WM_USER + 1)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>();
|
||||
WeakReferenceMessenger.Default.Send<OpenSettingsMessage>(new());
|
||||
}
|
||||
else if (wParam == PInvoke.WM_USER + 2)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ using Microsoft.CmdPal.Core.Common.Services;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Messages;
|
||||
using Microsoft.CmdPal.UI.Controls;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
@@ -32,7 +33,6 @@ using Windows.ApplicationModel.Activation;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.System;
|
||||
using Windows.UI;
|
||||
using Windows.UI.WindowManagement;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
@@ -50,6 +50,7 @@ namespace Microsoft.CmdPal.UI;
|
||||
public sealed partial class MainWindow : WindowEx,
|
||||
IRecipient<DismissMessage>,
|
||||
IRecipient<ShowWindowMessage>,
|
||||
IRecipient<ShowPaletteAtMessage>,
|
||||
IRecipient<HideWindowMessage>,
|
||||
IRecipient<QuitMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
@@ -134,6 +135,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<DismissMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<ShowPaletteAtMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<HideWindowMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigationDepthMessage>(this);
|
||||
@@ -332,6 +334,77 @@ public sealed partial class MainWindow : WindowEx,
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, MonitorBehavior target)
|
||||
{
|
||||
var positionWindowForTargetMonitor = (HWND hwnd) =>
|
||||
{
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
{
|
||||
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
}
|
||||
};
|
||||
ShowHwnd(hwndValue, positionWindowForTargetMonitor);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, Point anchorInPixels, AnchorPoint anchorCorner)
|
||||
{
|
||||
var positionWindowForAnchor = (HWND hwnd) =>
|
||||
{
|
||||
PInvoke.GetWindowRect(hwnd, out var bounds);
|
||||
var swpFlags = SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER;
|
||||
switch (anchorCorner)
|
||||
{
|
||||
case AnchorPoint.TopLeft:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)anchorInPixels.X,
|
||||
(int)anchorInPixels.Y,
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.TopRight:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)(anchorInPixels.X - bounds.Width),
|
||||
(int)anchorInPixels.Y,
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.BottomLeft:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)anchorInPixels.X,
|
||||
(int)(anchorInPixels.Y - bounds.Height),
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
case AnchorPoint.BottomRight:
|
||||
PInvoke.SetWindowPos(
|
||||
hwnd,
|
||||
HWND.HWND_TOP,
|
||||
(int)(anchorInPixels.X - bounds.Width),
|
||||
(int)(anchorInPixels.Y - bounds.Height),
|
||||
0,
|
||||
0,
|
||||
swpFlags);
|
||||
break;
|
||||
}
|
||||
};
|
||||
ShowHwnd(hwndValue, positionWindowForAnchor);
|
||||
}
|
||||
|
||||
private void ShowHwnd(IntPtr hwndValue, Action<HWND>? positionWindow)
|
||||
{
|
||||
StopAutoGoHome();
|
||||
|
||||
@@ -350,15 +423,9 @@ public sealed partial class MainWindow : WindowEx,
|
||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_RESTORE);
|
||||
}
|
||||
|
||||
if (target == MonitorBehavior.ToLast)
|
||||
if (positionWindow is not null)
|
||||
{
|
||||
var newRect = EnsureWindowIsVisible(_currentWindowPosition.ToPhysicalWindowRectangle(), new SizeInt32(_currentWindowPosition.ScreenWidth, _currentWindowPosition.ScreenHeight), _currentWindowPosition.Dpi);
|
||||
AppWindow.MoveAndResize(newRect);
|
||||
}
|
||||
else
|
||||
{
|
||||
var display = GetScreen(hwnd, target);
|
||||
PositionCentered(display);
|
||||
positionWindow(hwnd);
|
||||
}
|
||||
|
||||
// Check if the debugger is attached. If it is, we don't want to apply the tool window style,
|
||||
@@ -548,6 +615,11 @@ public sealed partial class MainWindow : WindowEx,
|
||||
ShowHwnd(message.Hwnd, settings.SummonOn);
|
||||
}
|
||||
|
||||
internal void Receive(ShowPaletteAtMessage message)
|
||||
{
|
||||
ShowHwnd(HWND.Null, message.PosPixels, message.Anchor);
|
||||
}
|
||||
|
||||
public void Receive(HideWindowMessage message)
|
||||
{
|
||||
// This might come in off the UI thread. Make sure to hop back.
|
||||
@@ -658,6 +730,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// Sure, it's not ideal, but at least it's not visible.
|
||||
}
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
|
||||
|
||||
// Start auto-go-home timer
|
||||
RestartAutoGoHome();
|
||||
}
|
||||
@@ -1070,6 +1144,7 @@ public sealed partial class MainWindow : WindowEx,
|
||||
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
|
||||
Cloak();
|
||||
this.Hide();
|
||||
WeakReferenceMessenger.Default.Send(new WindowHiddenMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -1120,6 +1195,8 @@ public sealed partial class MainWindow : WindowEx,
|
||||
DisposeAcrylic();
|
||||
}
|
||||
|
||||
void IRecipient<ShowPaletteAtMessage>.Receive(ShowPaletteAtMessage message) => Receive(message);
|
||||
|
||||
public void Receive(DragStartedMessage message)
|
||||
{
|
||||
_preventHideWhenDeactivated = true;
|
||||
@@ -1134,6 +1211,27 @@ public sealed partial class MainWindow : WindowEx,
|
||||
});
|
||||
}
|
||||
|
||||
// public void Receive(ShowCommandInContextMenuMessage message)
|
||||
// {
|
||||
// DispatcherQueue.TryEnqueue(() =>
|
||||
// {
|
||||
// ContextMenuControl.ViewModel.SelectedItem = message.Context;
|
||||
// ContextMenuFlyout.ShouldConstrainToRootBounds = false;
|
||||
// ContextMenuFlyout.ShowMode = FlyoutShowMode.Standard;
|
||||
|
||||
// // message.Position is in dips,
|
||||
|
||||
// ContextMenuFlyout.ShowAt(RootElement);
|
||||
|
||||
// // ContextMenuFlyout.ShowAt(
|
||||
// // RootElement,
|
||||
// // new FlyoutShowOptions()
|
||||
// // {
|
||||
// // ShowMode = FlyoutShowMode.Standard,
|
||||
// // Position = message.Position,
|
||||
// // });
|
||||
// });
|
||||
// }
|
||||
private unsafe void StealForeground()
|
||||
{
|
||||
var foregroundWindow = PInvoke.GetForegroundWindow();
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
<Version>$(CmdPalVersion)</Version>
|
||||
|
||||
<!-- For MVVM Toolkit Partial Properties/AOT support -->
|
||||
<LangVersion>preview</LangVersion>
|
||||
|
||||
<!-- OutputPath is set in CmdPal.Branding.props -->
|
||||
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
@@ -27,10 +30,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
|
||||
<!--<PropertyGroup>
|
||||
<!-- <PropertyGroup>
|
||||
<EnableCmdPalAOT>true</EnableCmdPalAOT>
|
||||
<GeneratePackageLocally>true</GeneratePackageLocally>
|
||||
</PropertyGroup>-->
|
||||
</PropertyGroup> -->
|
||||
|
||||
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -39,13 +42,13 @@
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
|
||||
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(CIBuild)' == 'true' or '$(GeneratePackageLocally)' == 'true'">
|
||||
<GenerateAppxPackageOnBuild>true</GenerateAppxPackageOnBuild>
|
||||
<AppxBundle>Never</AppxBundle>
|
||||
<AppxPackageTestDir>$(OutputPath)\AppPackages\Microsoft.CmdPal.UI_$(Version)_Test\</AppxPackageTestDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<!-- This lets us actually reference types from Microsoft.Terminal.UI and CmdPalKeyboardService -->
|
||||
<CsWinRTIncludes>Microsoft.Terminal.UI;CmdPalKeyboardService</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
@@ -74,6 +77,7 @@
|
||||
<None Remove="Controls\FallbackRankerDialog.xaml" />
|
||||
<None Remove="Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
<None Remove="Controls\ScreenPreview.xaml" />
|
||||
<None Remove="Controls\ScrollContainer.xaml" />
|
||||
<None Remove="Controls\SearchBar.xaml" />
|
||||
<None Remove="IsEnabledTextBlock.xaml" />
|
||||
<None Remove="ListDetailPage.xaml" />
|
||||
@@ -84,8 +88,8 @@
|
||||
<None Remove="SettingsWindow.xaml" />
|
||||
<None Remove="Settings\AppearancePage.xaml" />
|
||||
<None Remove="ShellPage.xaml" />
|
||||
<None Remove="Styles\Colors.xaml" />
|
||||
<None Remove="Styles\Settings.xaml" />
|
||||
<None Remove="Styles\TeachingTip.xaml" />
|
||||
<None Remove="Styles\TextBox.xaml" />
|
||||
<None Remove="Styles\Theme.Normal.xaml" />
|
||||
</ItemGroup>
|
||||
@@ -140,6 +144,7 @@
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Calc\Microsoft.CmdPal.Ext.Calc.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.PerformanceMonitor\Microsoft.CmdPal.Ext.PerformanceMonitor.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Registry\Microsoft.CmdPal.Ext.Registry.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
|
||||
<ProjectReference Include="..\ext\Microsoft.CmdPal.Ext.TimeDate\Microsoft.CmdPal.Ext.TimeDate.csproj" />
|
||||
@@ -214,6 +219,24 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\TeachingTip.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Dock\DockItemControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\ScrollContainer.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\CommandPalettePreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -264,11 +287,6 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Colors.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Styles\Settings.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
@@ -59,11 +59,52 @@ GetModuleHandle
|
||||
GetWindowLong
|
||||
SetWindowLong
|
||||
WINDOW_EX_STYLE
|
||||
|
||||
CreateWindowEx
|
||||
WNDCLASSEXW
|
||||
RegisterClassEx
|
||||
GetStockObject
|
||||
GetModuleHandle
|
||||
|
||||
MoveWindow
|
||||
GetSystemMetrics
|
||||
SHAppBarMessage
|
||||
ABM_NEW
|
||||
ABM_QUERYPOS
|
||||
ABM_SETPOS
|
||||
ABM_REMOVE
|
||||
ABM_SETAUTOHIDEBAR
|
||||
ABS_AUTOHIDE
|
||||
ABN_POSCHANGED
|
||||
APPBARDATA
|
||||
ABE_TOP
|
||||
ABE_BOTTOM
|
||||
ABE_LEFT
|
||||
ABE_RIGHT
|
||||
SYSTEM_METRICS_INDEX
|
||||
GetDpiForWindow
|
||||
SHQueryUserNotificationState
|
||||
SYSTEM_PARAMETERS_INFO_ACTION
|
||||
WINDOWPOS
|
||||
WM_DISPLAYCHANGE
|
||||
WM_SYSCOMMAND
|
||||
WM_SETTINGCHANGE
|
||||
WM_WINDOWPOSCHANGING
|
||||
WM_SHOWWINDOW
|
||||
WM_SIZE
|
||||
WM_GETMINMAXINFO
|
||||
SetWinEventHook
|
||||
WINDOW_STYLE
|
||||
SC_MINIMIZE
|
||||
SC_MAXIMIZE
|
||||
SET_WINDOW_POS_FLAGS
|
||||
SIZE_MAXIMIZED
|
||||
SIZE_MINIMIZED
|
||||
HWND_NOTOPMOST
|
||||
HWND_TOP
|
||||
HTCAPTION
|
||||
GetClassName
|
||||
EVENT_SYSTEM_FOREGROUND
|
||||
WINEVENT_OUTOFCONTEXT
|
||||
GetWindowThreadProcessId
|
||||
AttachThreadInput
|
||||
AttachThreadInput
|
||||
|
||||
@@ -200,14 +200,19 @@
|
||||
|
||||
<!-- Back button -->
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<!--
|
||||
This border is to hold a bit of padding we need when
|
||||
the back button is hidden
|
||||
-->
|
||||
<Border Margin="20,0,0,0" Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}" />
|
||||
<Image
|
||||
Width="20"
|
||||
Margin="20,0,6,0"
|
||||
Margin="0,0,6,0"
|
||||
HorizontalAlignment="Center"
|
||||
ui:VisualExtensions.NormalizedCenterPoint="0.5,0.5"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
Source="ms-appx:///Assets/icon.svg"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
@@ -250,7 +255,7 @@
|
||||
FontSize=14}"
|
||||
FontSize="16"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.HasBackButton, Mode=OneWay}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
EasingMode="EaseIn"
|
||||
@@ -297,7 +302,7 @@
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
SourceKey="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}"
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsNested, Mode=OneWay}">
|
||||
Visibility="{x:Bind ViewModel.CurrentPage.IsRootPage, Mode=OneWay, Converter={StaticResource BoolToInvertedVisibilityConverter}}">
|
||||
<animations:Implicit.ShowAnimations>
|
||||
<animations:OpacityAnimation
|
||||
From="0"
|
||||
|
||||
@@ -10,11 +10,13 @@ using CommunityToolkit.WinUI;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Dock;
|
||||
using Microsoft.CmdPal.UI.Events;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.Settings;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
@@ -25,6 +27,7 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Windows.UI.Core;
|
||||
using WinUIEx;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
using VirtualKey = Windows.System.VirtualKey;
|
||||
|
||||
@@ -47,6 +50,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
IRecipient<ShowConfirmationMessage>,
|
||||
IRecipient<ShowToastMessage>,
|
||||
IRecipient<NavigateToPageMessage>,
|
||||
IRecipient<ShowHideDockMessage>,
|
||||
INotifyPropertyChanged,
|
||||
IDisposable
|
||||
{
|
||||
@@ -64,6 +68,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
private readonly CompositeFormat _pageNavigatedAnnouncement;
|
||||
|
||||
private SettingsWindow? _settingsWindow;
|
||||
private DockWindow? _dockWindow;
|
||||
|
||||
private CancellationTokenSource? _focusAfterLoadedCts;
|
||||
private WeakReference<Page>? _lastNavigatedPageRef;
|
||||
@@ -94,6 +99,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Register<ShowToastMessage>(this);
|
||||
WeakReferenceMessenger.Default.Register<NavigateToPageMessage>(this);
|
||||
|
||||
WeakReferenceMessenger.Default.Register<ShowHideDockMessage>(this);
|
||||
|
||||
AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true);
|
||||
AddHandler(KeyDownEvent, new KeyEventHandler(ShellPage_OnKeyDown), false);
|
||||
AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true);
|
||||
@@ -102,6 +109,12 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
var pageAnnouncementFormat = ResourceLoaderInstance.GetString("ScreenReader_Announcement_NavigatedToPage0");
|
||||
_pageNavigatedAnnouncement = CompositeFormat.Parse(pageAnnouncementFormat);
|
||||
|
||||
if (App.Current.Services.GetService<SettingsModel>()!.EnableDock)
|
||||
{
|
||||
_dockWindow = new DockWindow();
|
||||
_dockWindow.Show();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -143,7 +156,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
public void Receive(NavigateToPageMessage message)
|
||||
{
|
||||
// TODO GH #526 This needs more better locking too
|
||||
_ = _queue.TryEnqueue(() =>
|
||||
_ = _queue.TryEnqueue(DispatcherQueuePriority.High, () =>
|
||||
{
|
||||
// Also hide our details pane about here, if we had one
|
||||
HideDetails();
|
||||
@@ -248,26 +261,29 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeConfirmationDialog(ConfirmResultViewModel vm)
|
||||
{
|
||||
vm.SafeInitializePropertiesSynchronous();
|
||||
}
|
||||
private void InitializeConfirmationDialog(ConfirmResultViewModel vm) => vm.SafeInitializePropertiesSynchronous();
|
||||
|
||||
public void Receive(OpenSettingsMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
OpenSettings();
|
||||
OpenSettings(message.Page);
|
||||
});
|
||||
}
|
||||
|
||||
public void OpenSettings()
|
||||
public void OpenSettings(string? page = null)
|
||||
{
|
||||
if (_settingsWindow is null)
|
||||
{
|
||||
_settingsWindow = new SettingsWindow();
|
||||
}
|
||||
|
||||
if (page is not null)
|
||||
{
|
||||
_settingsWindow.OpenToPage = page;
|
||||
_settingsWindow.Navigate(page);
|
||||
}
|
||||
|
||||
_settingsWindow.Activate();
|
||||
_settingsWindow.BringToFront();
|
||||
}
|
||||
@@ -328,10 +344,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
|
||||
public void Receive(ClearSearchMessage message) => SearchBox.ClearSearch();
|
||||
|
||||
public void Receive(HotkeySummonMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
|
||||
}
|
||||
public void Receive(HotkeySummonMessage message) => _ = DispatcherQueue.TryEnqueue(() => SummonOnUiThread(message));
|
||||
|
||||
public void Receive(SettingsWindowClosedMessage message) => _settingsWindow = null;
|
||||
|
||||
@@ -400,10 +413,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
WeakReferenceMessenger.Default.Send<FocusSearchBoxMessage>();
|
||||
}
|
||||
|
||||
public void Receive(GoBackMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
||||
}
|
||||
public void Receive(GoBackMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoBack(message.WithAnimation, message.FocusSearch));
|
||||
|
||||
private void GoBack(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
@@ -444,10 +454,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(GoHomeMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
||||
}
|
||||
public void Receive(GoHomeMessage message) => _ = DispatcherQueue.TryEnqueue(() => GoHome(withAnimation: message.WithAnimation, focusSearch: message.FocusSearch));
|
||||
|
||||
private void GoHome(bool withAnimation = true, bool focusSearch = true)
|
||||
{
|
||||
@@ -465,6 +472,27 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(ShowHideDockMessage message)
|
||||
{
|
||||
_ = DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
if (message.ShowDock)
|
||||
{
|
||||
if (_dockWindow is null)
|
||||
{
|
||||
_dockWindow = new DockWindow();
|
||||
}
|
||||
|
||||
_dockWindow.Show();
|
||||
}
|
||||
else if (_dockWindow is not null)
|
||||
{
|
||||
_dockWindow.Close();
|
||||
_dockWindow = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void BackButton_Clicked(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) => WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
|
||||
|
||||
private void RootFrame_Navigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)
|
||||
@@ -721,5 +749,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
|
||||
_focusAfterLoadedCts?.Cancel();
|
||||
_focusAfterLoadedCts?.Dispose();
|
||||
_focusAfterLoadedCts = null;
|
||||
|
||||
_dockWindow?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -36,11 +37,14 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
private bool _isInitialized;
|
||||
private bool _disposed;
|
||||
private InternalThemeState _currentState;
|
||||
private DockThemeSnapshot _currentDockState;
|
||||
|
||||
public event EventHandler<ThemeChangedEventArgs>? ThemeChanged;
|
||||
|
||||
public ThemeSnapshot Current => Volatile.Read(ref _currentState).Snapshot;
|
||||
|
||||
public DockThemeSnapshot CurrentDockTheme => Volatile.Read(ref _currentDockState);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the theme service. Must be called after the application window is activated and on UI thread.
|
||||
/// </summary>
|
||||
@@ -134,6 +138,60 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
// Atomic swap
|
||||
Interlocked.Exchange(ref _currentState, newState);
|
||||
|
||||
// Compute DockThemeSnapshot from DockSettings
|
||||
var dockSettings = _settings.DockSettings;
|
||||
var dockIntensity = Math.Clamp(dockSettings.CustomThemeColorIntensity, 0, 100);
|
||||
IThemeProvider dockProvider = dockIntensity > 0 && dockSettings.ColorizationMode is ColorizationMode.CustomColor or ColorizationMode.WindowsAccentColor or ColorizationMode.Image
|
||||
? _colorfulThemeProvider
|
||||
: _normalThemeProvider;
|
||||
|
||||
var dockTint = dockSettings.ColorizationMode switch
|
||||
{
|
||||
ColorizationMode.CustomColor => dockSettings.CustomThemeColor,
|
||||
ColorizationMode.WindowsAccentColor => _uiSettings.GetColorValue(UIColorType.Accent),
|
||||
ColorizationMode.Image => dockSettings.CustomThemeColor,
|
||||
_ => Colors.Transparent,
|
||||
};
|
||||
var dockEffectiveTheme = GetElementTheme((ElementTheme)dockSettings.Theme);
|
||||
var dockImageSource = dockSettings.ColorizationMode == ColorizationMode.Image
|
||||
? LoadImageSafe(dockSettings.BackgroundImagePath)
|
||||
: null;
|
||||
var dockStretch = dockSettings.BackgroundImageFit switch
|
||||
{
|
||||
BackgroundImageFit.Fill => Stretch.Fill,
|
||||
_ => Stretch.UniformToFill,
|
||||
};
|
||||
var dockOpacity = Math.Clamp(dockSettings.BackgroundImageOpacity, 0, 100) / 100.0;
|
||||
|
||||
var dockContext = new ThemeContext
|
||||
{
|
||||
Tint = dockTint,
|
||||
ColorIntensity = dockIntensity,
|
||||
Theme = dockEffectiveTheme,
|
||||
BackgroundImageSource = dockImageSource,
|
||||
BackgroundImageStretch = dockStretch,
|
||||
BackgroundImageOpacity = dockOpacity,
|
||||
};
|
||||
var dockBackdrop = dockProvider.GetAcrylicBackdrop(dockContext);
|
||||
var dockBlur = dockSettings.BackgroundImageBlurAmount;
|
||||
var dockBrightness = dockSettings.BackgroundImageBrightness;
|
||||
|
||||
var dockSnapshot = new DockThemeSnapshot
|
||||
{
|
||||
Tint = dockTint,
|
||||
TintIntensity = dockIntensity / 100f,
|
||||
Theme = dockEffectiveTheme,
|
||||
Backdrop = dockSettings.Backdrop,
|
||||
BackgroundImageSource = dockImageSource,
|
||||
BackgroundImageStretch = dockStretch,
|
||||
BackgroundImageOpacity = dockOpacity,
|
||||
BackdropParameters = dockBackdrop,
|
||||
BlurAmount = dockBlur,
|
||||
BackgroundBrightness = dockBrightness / 100f,
|
||||
};
|
||||
|
||||
Interlocked.Exchange(ref _currentDockState, dockSnapshot);
|
||||
|
||||
_resourceSwapper.TryActivateTheme(provider.ThemeKey);
|
||||
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs());
|
||||
}
|
||||
@@ -205,6 +263,20 @@ internal sealed partial class ThemeService : IThemeService, IDisposable
|
||||
},
|
||||
Provider = _normalThemeProvider,
|
||||
};
|
||||
|
||||
_currentDockState = new DockThemeSnapshot
|
||||
{
|
||||
Tint = Colors.Transparent,
|
||||
TintIntensity = 1.0f,
|
||||
Theme = ElementTheme.Light,
|
||||
Backdrop = DockBackdrop.Acrylic,
|
||||
BackdropParameters = new AcrylicBackdropParameters(Colors.Black, Colors.Black, 0.5f, 0.5f),
|
||||
BackgroundImageOpacity = 1,
|
||||
BackgroundImageSource = null,
|
||||
BackgroundImageStretch = Stretch.Fill,
|
||||
BlurAmount = 0,
|
||||
BackgroundBrightness = 0,
|
||||
};
|
||||
}
|
||||
|
||||
private void RequestReload()
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.CmdPal.UI.Settings.DockSettingsPage"
|
||||
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:cpControls="using:Microsoft.CmdPal.UI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:dockVm="using:Microsoft.CmdPal.UI.ViewModels.Dock"
|
||||
xmlns:helpers="using:Microsoft.CmdPal.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.CmdPal.UI.Settings"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="using:CommunityToolkit.WinUI"
|
||||
xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<Grid Padding="16">
|
||||
<StackPanel
|
||||
MaxWidth="1000"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="{StaticResource SettingsCardSpacing}">
|
||||
|
||||
<!--
|
||||
I got these from the samples, but they break XAML hot-reloading,
|
||||
so I commented them out.
|
||||
-->
|
||||
|
||||
<!--<StackPanel.ChildrenTransitions>
|
||||
<EntranceThemeTransition FromVerticalOffset="50" />
|
||||
<RepositionThemeTransition IsStaggeringEnabled="False" />
|
||||
</StackPanel.ChildrenTransitions>-->
|
||||
|
||||
<!-- Enable Dock -->
|
||||
<controls:SettingsCard x:Uid="Settings_GeneralPage_EnableDock_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ToggleSwitch IsOn="{x:Bind ViewModel.EnableDock, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Appearance Section -->
|
||||
<TextBlock x:Uid="DockAppearanceSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<!-- Dock Position -->
|
||||
<controls:SettingsExpander
|
||||
Description="Choose where the dock appears on your screen"
|
||||
Header="Dock position and appearance"
|
||||
IsExpanded="True">
|
||||
<controls:SettingsExpander.HeaderIcon>
|
||||
<SymbolIcon Symbol="MoveToFolder" />
|
||||
</controls:SettingsExpander.HeaderIcon>
|
||||
<ComboBox
|
||||
x:Name="DockPositionComboBox"
|
||||
MinWidth="120"
|
||||
SelectedIndex="{x:Bind SelectedSideIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem Content="Left" />
|
||||
<ComboBoxItem Content="Top" />
|
||||
<ComboBoxItem Content="Right" />
|
||||
<ComboBoxItem Content="Bottom" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
|
||||
<!-- Show Labels -->
|
||||
<controls:SettingsCard ContentAlignment="Left">
|
||||
<cpControls:CheckBoxWithDescriptionControl
|
||||
Description="Choose whether to show labels for dock items by default"
|
||||
Header="Show labels"
|
||||
IsChecked="{x:Bind ShowLabels, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- Theme Section -->
|
||||
<TextBlock x:Uid="DockAppearance_ThemeSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<controls:SettingsCard x:Uid="DockAppearance_AppTheme_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ThemeIndex, Mode=TwoWay}">
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_System_Automation" Tag="Default">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_System" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Light_Automation" Tag="Light">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Light" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark_Automation" Tag="Dark">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Settings_GeneralPage_AppTheme_Mode_Dark" />
|
||||
</StackPanel>
|
||||
</ComboBoxItem>
|
||||
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Backdrop Style -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_Backdrop_SettingsCard" HeaderIcon="{ui:FontIcon Glyph=}">
|
||||
<ComboBox
|
||||
x:Name="BackdropComboBox"
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
SelectedIndex="{x:Bind ViewModel.DockAppearance.BackdropIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Transparent" />
|
||||
<ComboBoxItem x:Uid="DockAppearance_Backdrop_Acrylic" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background / Colorization Section -->
|
||||
<controls:SettingsExpander
|
||||
x:Uid="DockAppearance_Background_SettingsExpander"
|
||||
HeaderIcon="{ui:FontIcon Glyph=}"
|
||||
IsExpanded="{x:Bind ViewModel.DockAppearance.IsColorizationDetailsExpanded, Mode=TwoWay}">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind ViewModel.DockAppearance.ColorizationModeIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_None" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_WindowsAccent" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_CustomColor" />
|
||||
<ComboBoxItem x:Uid="Settings_GeneralPage_ColorizationMode_Image" />
|
||||
</ComboBox>
|
||||
<controls:SettingsExpander.Items>
|
||||
<!-- none -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="DockAppearance_NoBackground_SettingsCard"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ContentAlignment="Vertical"
|
||||
Visibility="{x:Bind ViewModel.DockAppearance.IsNoBackgroundVisible, Mode=OneWay}">
|
||||
<TextBlock
|
||||
x:Uid="DockAppearance_NoBackground_DescriptionTextBlock"
|
||||
Margin="24"
|
||||
HorizontalAlignment="Stretch"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
HorizontalTextAlignment="Center"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- system accent color -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_WindowsAccentColor_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsAccentColorControlsVisible, Mode=OneWay}">
|
||||
<controls:SettingsCard.Description>
|
||||
<TextBlock>
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_SettingsCard_Description1" />
|
||||
<Hyperlink
|
||||
Click="OpenWindowsColorsSettings_Click"
|
||||
TextDecorations="None"
|
||||
UnderlineStyle="None">
|
||||
<Run x:Uid="Settings_GeneralPage_WindowsAccentColor_OpenWindowsColorsLinkText" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</controls:SettingsCard.Description>
|
||||
<controls:SettingsCard.Content>
|
||||
<Border
|
||||
MinWidth="32"
|
||||
MinHeight="32"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{x:Bind ViewModel.DockAppearance.EffectiveThemeColor, Mode=OneWay}" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</controls:SettingsCard.Content>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- background image -->
|
||||
<controls:SettingsCard
|
||||
x:Uid="DockAppearance_BackgroundImage_SettingsCard"
|
||||
Description="{x:Bind ViewModel.DockAppearance.BackgroundImagePath, Mode=OneWay}"
|
||||
Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Button x:Uid="Settings_GeneralPage_BackgroundImage_ChooseImageButton" Click="PickBackgroundImage_Click" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBrightness_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="-100"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBrightness, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageBlur_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="50"
|
||||
Minimum="0"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.BackgroundImageBlurAmount, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImageFit_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<ComboBox SelectedIndex="{x:Bind ViewModel.DockAppearance.BackgroundImageFitIndex, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Fill" />
|
||||
<ComboBoxItem x:Uid="BackgroundImageFit_ComboBoxItem_Stretch" />
|
||||
</ComboBox>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Background tint color and intensity -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTint_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintVisible, Mode=OneWay}">
|
||||
<cpControls:ColorPickerButton
|
||||
HasSelectedColor="True"
|
||||
IsAlphaEnabled="False"
|
||||
PaletteColors="{x:Bind ViewModel.DockAppearance.Swatches}"
|
||||
SelectedColor="{x:Bind ViewModel.DockAppearance.ThemeColor, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundTintIntensity_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsCustomTintIntensityVisible, Mode=OneWay}">
|
||||
<Slider
|
||||
MinWidth="{StaticResource SettingActionControlMinWidth}"
|
||||
Maximum="100"
|
||||
Minimum="1"
|
||||
StepFrequency="1"
|
||||
Value="{x:Bind ViewModel.DockAppearance.ColorIntensity, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
<!-- Reset background image properties -->
|
||||
<controls:SettingsCard x:Uid="DockAppearance_BackgroundImage_ResetProperties_SettingsCard" Visibility="{x:Bind ViewModel.DockAppearance.IsBackgroundControlsVisible, Mode=OneWay}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Uid="Settings_GeneralPage_Background_ResetImagePropertiesButton" Command="{x:Bind ViewModel.DockAppearance.ResetBackgroundImagePropertiesCommand}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
|
||||
<!-- Bands Section -->
|
||||
<TextBlock x:Uid="DockBandsSettingsHeader" Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" />
|
||||
|
||||
<ItemsRepeater ItemsSource="{x:Bind AllDockBandItems, Mode=OneWay}">
|
||||
<ItemsRepeater.Layout>
|
||||
<StackLayout Spacing="{StaticResource SettingsCardSpacing}" />
|
||||
</ItemsRepeater.Layout>
|
||||
<ItemsRepeater.ItemTemplate>
|
||||
<DataTemplate x:DataType="dockVm:DockBandSettingsViewModel">
|
||||
<controls:SettingsCard
|
||||
Description="{x:Bind Description, Mode=OneWay}"
|
||||
Header="{x:Bind Title, Mode=OneWay}"
|
||||
IsClickEnabled="False">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<cpControls:ContentIcon>
|
||||
<cpControls:ContentIcon.Content>
|
||||
<cpControls:IconBox
|
||||
Width="20"
|
||||
Height="20"
|
||||
AutomationProperties.AccessibilityView="Raw"
|
||||
SourceKey="{x:Bind Icon, Mode=OneWay}"
|
||||
SourceRequested="{x:Bind helpers:IconCacheProvider.SourceRequested}" />
|
||||
</cpControls:ContentIcon.Content>
|
||||
</cpControls:ContentIcon>
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<ToggleSwitch IsOn="{x:Bind IsPinned, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
</DataTemplate>
|
||||
</ItemsRepeater.ItemTemplate>
|
||||
</ItemsRepeater>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Dock;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Settings;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.Windows.Storage.Pickers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Settings;
|
||||
|
||||
public sealed partial class DockSettingsPage : Page
|
||||
{
|
||||
private readonly TaskScheduler _mainTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
|
||||
|
||||
internal SettingsViewModel ViewModel { get; }
|
||||
|
||||
public List<DockBandSettingsViewModel> AllDockBandItems => GetAllBandSettings();
|
||||
|
||||
public DockSettingsPage()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
|
||||
var settings = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var themeService = App.Current.Services.GetService<IThemeService>()!;
|
||||
var topLevelCommandManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
ViewModel = new SettingsViewModel(settings, topLevelCommandManager, _mainTaskScheduler, themeService);
|
||||
|
||||
// Initialize UI state
|
||||
InitializeSettings();
|
||||
}
|
||||
|
||||
private void InitializeSettings()
|
||||
{
|
||||
// Initialize UI controls to match current settings
|
||||
DockPositionComboBox.SelectedIndex = SelectedSideIndex;
|
||||
BackdropComboBox.SelectedIndex = SelectedBackdropIndex;
|
||||
}
|
||||
|
||||
private async void PickBackgroundImage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (XamlRoot?.ContentIslandEnvironment is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var windowId = XamlRoot?.ContentIslandEnvironment?.AppWindowId ?? new Microsoft.UI.WindowId(0);
|
||||
|
||||
var picker = new FileOpenPicker(windowId)
|
||||
{
|
||||
CommitButtonText = ViewModels.Properties.Resources.builtin_settings_appearance_pick_background_image_title!,
|
||||
SuggestedStartLocation = PickerLocationId.PicturesLibrary,
|
||||
ViewMode = PickerViewMode.Thumbnail,
|
||||
};
|
||||
|
||||
string[] extensions = [".png", ".bmp", ".jpg", ".jpeg", ".jfif", ".gif", ".tiff", ".tif", ".webp", ".jxr"];
|
||||
foreach (var ext in extensions)
|
||||
{
|
||||
picker.FileTypeFilter!.Add(ext);
|
||||
}
|
||||
|
||||
var file = await picker.PickSingleFileAsync()!;
|
||||
if (file != null)
|
||||
{
|
||||
ViewModel.DockAppearance.BackgroundImagePath = file.Path ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to pick background image file for dock", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenWindowsColorsSettings_Click(Hyperlink sender, HyperlinkClickEventArgs args)
|
||||
{
|
||||
// LOAD BEARING (or BEAR LOADING?): Process.Start with UseShellExecute inside a XAML input event can trigger WinUI reentrancy
|
||||
// and cause FailFast crashes. Task.Run moves the call off the UI thread to prevent hard process termination.
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = Process.Start(new ProcessStartInfo("ms-settings:colors") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to open Windows Settings", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Property bindings for ComboBoxes
|
||||
public int SelectedDockSizeIndex
|
||||
{
|
||||
get => DockSizeToSelectedIndex(ViewModel.Dock_DockSize);
|
||||
set => ViewModel.Dock_DockSize = SelectedIndexToDockSize(value);
|
||||
}
|
||||
|
||||
public int SelectedSideIndex
|
||||
{
|
||||
get => SideToSelectedIndex(ViewModel.Dock_Side);
|
||||
set => ViewModel.Dock_Side = SelectedIndexToSide(value);
|
||||
}
|
||||
|
||||
public int SelectedBackdropIndex
|
||||
{
|
||||
get => BackdropToSelectedIndex(ViewModel.Dock_Backdrop);
|
||||
set => ViewModel.Dock_Backdrop = SelectedIndexToBackdrop(value);
|
||||
}
|
||||
|
||||
public bool ShowLabels
|
||||
{
|
||||
get => ViewModel.Dock_ShowLabels;
|
||||
set => ViewModel.Dock_ShowLabels = value;
|
||||
}
|
||||
|
||||
// Conversion methods for ComboBox bindings
|
||||
private static int DockSizeToSelectedIndex(DockSize size) => size switch
|
||||
{
|
||||
DockSize.Small => 0,
|
||||
DockSize.Medium => 1,
|
||||
DockSize.Large => 2,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private static DockSize SelectedIndexToDockSize(int index) => index switch
|
||||
{
|
||||
0 => DockSize.Small,
|
||||
1 => DockSize.Medium,
|
||||
2 => DockSize.Large,
|
||||
_ => DockSize.Small,
|
||||
};
|
||||
|
||||
private static int SideToSelectedIndex(DockSide side) => side switch
|
||||
{
|
||||
DockSide.Left => 0,
|
||||
DockSide.Top => 1,
|
||||
DockSide.Right => 2,
|
||||
DockSide.Bottom => 3,
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
private static DockSide SelectedIndexToSide(int index) => index switch
|
||||
{
|
||||
0 => DockSide.Left,
|
||||
1 => DockSide.Top,
|
||||
2 => DockSide.Right,
|
||||
3 => DockSide.Bottom,
|
||||
_ => DockSide.Top,
|
||||
};
|
||||
|
||||
private static int BackdropToSelectedIndex(DockBackdrop backdrop) => backdrop switch
|
||||
{
|
||||
DockBackdrop.Transparent => 0,
|
||||
DockBackdrop.Acrylic => 1,
|
||||
_ => 2,
|
||||
};
|
||||
|
||||
private static DockBackdrop SelectedIndexToBackdrop(int index) => index switch
|
||||
{
|
||||
0 => DockBackdrop.Transparent,
|
||||
1 => DockBackdrop.Acrylic,
|
||||
_ => DockBackdrop.Acrylic,
|
||||
};
|
||||
|
||||
private List<TopLevelViewModel> GetAllBands()
|
||||
{
|
||||
var allBands = new List<TopLevelViewModel>();
|
||||
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
|
||||
foreach (var item in tlcManager.DockBands)
|
||||
{
|
||||
if (item.IsDockBand)
|
||||
{
|
||||
allBands.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return allBands;
|
||||
}
|
||||
|
||||
private List<DockBandSettingsViewModel> GetAllBandSettings()
|
||||
{
|
||||
var allSettings = new List<DockBandSettingsViewModel>();
|
||||
|
||||
// var allBands = GetAllBands();
|
||||
var tlcManager = App.Current.Services.GetService<TopLevelCommandManager>()!;
|
||||
var settingsModel = App.Current.Services.GetService<SettingsModel>()!;
|
||||
var dockViewModel = App.Current.Services.GetService<DockViewModel>()!;
|
||||
var allBands = tlcManager.DockBands;
|
||||
foreach (var band in allBands)
|
||||
{
|
||||
var setting = band.DockBandSettings;
|
||||
if (setting is not null)
|
||||
{
|
||||
var bandVm = dockViewModel.FindBandByTopLevel(band);
|
||||
allSettings.Add(new(
|
||||
dockSettingsModel: setting,
|
||||
topLevelAdapter: band,
|
||||
bandViewModel: bandVm,
|
||||
settingsModel: settingsModel
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return allSettings;
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,12 @@
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Extensions"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Extensions" />
|
||||
<!-- xF596 is HolePunchLandscapeTop -->
|
||||
<NavigationViewItem
|
||||
x:Name="DockSettingsPageNavItem"
|
||||
x:Uid="Settings_GeneralPage_NavigationViewItem_Dock"
|
||||
Icon="{ui:FontIcon Glyph=}"
|
||||
Tag="Dock" />
|
||||
</NavigationView.MenuItems>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
|
||||
@@ -33,7 +33,9 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
|
||||
|
||||
// Gets or sets optional action invoked after NavigationView is loaded.
|
||||
public Action NavigationViewLoaded { get; set; } = () => { };
|
||||
public Action? NavigationViewLoaded { get; set; }
|
||||
|
||||
internal string? OpenToPage { get; set; }
|
||||
|
||||
public SettingsWindow()
|
||||
{
|
||||
@@ -69,7 +71,9 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
NavView.SelectedItem = NavView.MenuItems[0];
|
||||
Navigate("General");
|
||||
|
||||
Navigate(OpenToPage);
|
||||
OpenToPage = null;
|
||||
|
||||
if (sender is NavigationView navigationView)
|
||||
{
|
||||
@@ -96,19 +100,36 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
Navigate((selectedItem.Tag as string)!);
|
||||
}
|
||||
|
||||
private void Navigate(string page)
|
||||
internal void Navigate(string? page)
|
||||
{
|
||||
var pageType = page switch
|
||||
{
|
||||
null => typeof(GeneralPage),
|
||||
"General" => typeof(GeneralPage),
|
||||
"Appearance" => typeof(AppearancePage),
|
||||
"Extensions" => typeof(ExtensionsPage),
|
||||
"Dock" => typeof(DockSettingsPage),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
var actualPage = page ?? "General";
|
||||
if (pageType is not null)
|
||||
{
|
||||
// BreadCrumbs.Clear();
|
||||
// BreadCrumbs.Add(new(actualPage, actualPage));
|
||||
NavFrame.Navigate(pageType);
|
||||
|
||||
// Now, make sure to actually select the correct menu item too
|
||||
foreach (var obj in NavView.MenuItems)
|
||||
{
|
||||
if (obj is NavigationViewItem item)
|
||||
{
|
||||
if (item.Tag is string s && s == page)
|
||||
{
|
||||
NavView.SelectedItem = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +282,12 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
var pageType = RS_.GetString("Settings_PageTitles_ExtensionsPage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(DockSettingsPage))
|
||||
{
|
||||
NavView.SelectedItem = DockSettingsPageNavItem;
|
||||
var pageType = RS_.GetString("Settings_PageTitles_DockPage");
|
||||
BreadCrumbs.Add(new(pageType, pageType));
|
||||
}
|
||||
else if (e.SourcePageType == typeof(ExtensionPage) && e.Parameter is ProviderSettingsViewModel vm)
|
||||
{
|
||||
NavView.SelectedItem = ExtensionPageNavItem;
|
||||
|
||||
@@ -389,6 +389,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_NavigationViewItem_Extensions.Content" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_NavigationViewItem_Dock.Content" xml:space="preserve">
|
||||
<value>Dock (Preview)</value>
|
||||
</data>
|
||||
<data name="SettingsButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Open Command Palette settings</value>
|
||||
</data>
|
||||
@@ -398,6 +401,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="BehaviorSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Behavior</value>
|
||||
</data>
|
||||
<data name="DockAppearanceSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Appearance</value>
|
||||
</data>
|
||||
<data name="DockBandsSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Bands</value>
|
||||
</data>
|
||||
<data name="ContextFilterBox.PlaceholderText" xml:space="preserve">
|
||||
<value>Search commands...</value>
|
||||
</data>
|
||||
@@ -412,6 +421,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_DisableAnimations_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Disable animations when switching between pages</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Enable Dock</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EnableDock_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Enable a toolbar with quick access to commands</value>
|
||||
</data>
|
||||
<data name="BackButton.[using:Microsoft.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
|
||||
<value>Back</value>
|
||||
@@ -616,6 +631,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_PageTitles_ExtensionsPage" xml:space="preserve">
|
||||
<value>Extensions</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_DockPage" xml:space="preserve">
|
||||
<value>Dock</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_EscapeKeyBehavior_Option_DismissEmptySearchOrGoBack.Content" xml:space="preserve">
|
||||
<value>Clear search first, then go back</value>
|
||||
</data>
|
||||
@@ -727,4 +745,73 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
||||
<value>Version {0}</value>
|
||||
</data>
|
||||
<data name="Settings_NavigationViewItem_DockAppearance.Content" xml:space="preserve">
|
||||
<value>Dock Appearance</value>
|
||||
</data>
|
||||
<data name="Settings_PageTitles_DockAppearancePage" xml:space="preserve">
|
||||
<value>Dock Appearance</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ThemeSettingsHeader.Text" xml:space="preserve">
|
||||
<value>Theme</value>
|
||||
</data>
|
||||
<data name="DockAppearance_AppTheme_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Dock theme mode</value>
|
||||
</data>
|
||||
<data name="DockAppearance_AppTheme_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Select which theme to display for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Backdrop style</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_SettingsCard.Description" xml:space="preserve">
|
||||
<value>Choose the background effect for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Mica.Content" xml:space="preserve">
|
||||
<value>Mica</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Transparent.Content" xml:space="preserve">
|
||||
<value>Transparent</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Backdrop_Acrylic.Content" xml:space="preserve">
|
||||
<value>Acrylic</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Background_SettingsExpander.Header" xml:space="preserve">
|
||||
<value>Background</value>
|
||||
</data>
|
||||
<data name="DockAppearance_Background_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>Choose a custom background color or image for the dock</value>
|
||||
</data>
|
||||
<data name="DockAppearance_ColorizationMode.Header" xml:space="preserve">
|
||||
<value>Colorization mode</value>
|
||||
</data>
|
||||
<data name="DockAppearance_NoBackground_SettingsCard.Header" xml:space="preserve">
|
||||
<value>No background</value>
|
||||
</data>
|
||||
<data name="DockAppearance_NoBackground_DescriptionTextBlock.Text" xml:space="preserve">
|
||||
<value>No settings</value>
|
||||
</data>
|
||||
<data name="DockAppearance_WindowsAccentColor_SettingsCard.Header" xml:space="preserve">
|
||||
<value>System accent color</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImage_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageBrightness_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image brightness</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageBlur_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image blur</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImageFit_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Background image fit</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundTint_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color tint</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundTintIntensity_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Color intensity</value>
|
||||
</data>
|
||||
<data name="DockAppearance_BackgroundImage_ResetProperties_SettingsCard.Header" xml:space="preserve">
|
||||
<value>Restore defaults</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
442
src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TeachingTip.xaml
Normal file
442
src/modules/cmdpal/Microsoft.CmdPal.UI/Styles/TeachingTip.xaml
Normal file
@@ -0,0 +1,442 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- TeachingTip doesn't have a simple way to get rid of the close button. It requires a full custom style :( -->
|
||||
<Style x:Key="TeachingTipWithoutCloseButtonStyle" TargetType="TeachingTip">
|
||||
<Setter Property="Background" Value="{ThemeResource TeachingTipBackgroundBrush}" />
|
||||
<Setter Property="Foreground" Value="{ThemeResource TeachingTipForegroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource TeachingTipBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="{ThemeResource TeachingTipContentBorderThicknessUntargeted}" />
|
||||
<Setter Property="CornerRadius" Value="{ThemeResource OverlayCornerRadius}" />
|
||||
<Setter Property="ActionButtonStyle" Value="{ThemeResource DefaultButtonStyle}" />
|
||||
<Setter Property="CloseButtonStyle" Value="{ThemeResource DefaultButtonStyle}" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TeachingTip">
|
||||
<Border
|
||||
x:Name="Container"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Background="Transparent">
|
||||
<Grid
|
||||
MinWidth="{ThemeResource TeachingTipMinWidth}"
|
||||
MinHeight="{ThemeResource TeachingTipMinHeight}"
|
||||
MaxWidth="{ThemeResource TeachingTipMaxWidth}"
|
||||
MaxHeight="{ThemeResource TeachingTipMaxHeight}"
|
||||
AutomationProperties.Name="{TemplateBinding AutomationProperties.Name}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
x:Name="TailOcclusionGrid"
|
||||
Grid.RowSpan="5"
|
||||
Grid.ColumnSpan="5"
|
||||
MinWidth="{ThemeResource TeachingTipMinWidth}"
|
||||
MinHeight="{ThemeResource TeachingTipMinHeight}"
|
||||
MaxWidth="{ThemeResource TeachingTipMaxWidth}"
|
||||
MaxHeight="{ThemeResource TeachingTipMaxHeight}"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailMargin}" />
|
||||
<ColumnDefinition Width="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailMargin}" />
|
||||
<RowDefinition Height="{StaticResource TeachingTipTailShortSideLength}" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid
|
||||
x:Name="ContentRootGrid"
|
||||
Grid.Row="1"
|
||||
Grid.RowSpan="3"
|
||||
Grid.Column="1"
|
||||
Grid.ColumnSpan="3"
|
||||
AutomationProperties.LandmarkType="Custom"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}"
|
||||
FlowDirection="{TemplateBinding FlowDirection}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Border
|
||||
x:Name="HeroContentBorder"
|
||||
Grid.Row="0"
|
||||
Background="{TemplateBinding Background}"
|
||||
Child="{TemplateBinding HeroContent}" />
|
||||
<Grid x:Name="NonHeroContentRootGrid" Grid.Row="1">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="{StaticResource TeachingTipContentMargin}">
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter
|
||||
x:Name="IconPresenter"
|
||||
Grid.Column="0"
|
||||
Foreground="{TemplateBinding Foreground}">
|
||||
<Border Child="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.IconElement}" />
|
||||
</ContentPresenter>
|
||||
<StackPanel x:Name="TitlesStackPanel" Grid.Column="1">
|
||||
<TextBlock
|
||||
x:Name="TitleTextBlock"
|
||||
Grid.Column="0"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TeachingTipTitleForegroundBrush}"
|
||||
Text="{TemplateBinding Title}"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="Collapsed" />
|
||||
<TextBlock
|
||||
x:Name="SubtitleTextBlock"
|
||||
Grid.Row="1"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
Foreground="{ThemeResource TeachingTipSubtitleForegroundBrush}"
|
||||
Text="{TemplateBinding Subtitle}"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<ContentPresenter
|
||||
x:Name="MainContentPresenter"
|
||||
Grid.Row="1"
|
||||
Background="{TemplateBinding Background}"
|
||||
Content="{TemplateBinding Content}"
|
||||
FontFamily="{TemplateBinding FontFamily}"
|
||||
FontSize="{TemplateBinding FontSize}"
|
||||
FontStretch="{TemplateBinding FontStretch}"
|
||||
FontStyle="{TemplateBinding FontStyle}"
|
||||
FontWeight="{TemplateBinding FontWeight}"
|
||||
Foreground="{TemplateBinding Foreground}" />
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
x:Name="ActionButton"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{TemplateBinding ActionButtonCommand}"
|
||||
CommandParameter="{TemplateBinding ActionButtonCommandParameter}"
|
||||
Style="{TemplateBinding ActionButtonStyle}">
|
||||
<ContentPresenter Content="{TemplateBinding ActionButtonContent}" TextWrapping="WrapWholeWords" />
|
||||
</Button>
|
||||
<Button
|
||||
x:Name="CloseButton"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{TemplateBinding CloseButtonCommand}"
|
||||
CommandParameter="{TemplateBinding CloseButtonCommandParameter}"
|
||||
Style="{TemplateBinding CloseButtonStyle}">
|
||||
<ContentPresenter Content="{TemplateBinding CloseButtonContent}" TextWrapping="WrapWholeWords" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Button
|
||||
x:Name="AlternateCloseButton"
|
||||
Style="{ThemeResource AlternateCloseButtonStyle}"
|
||||
Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Polygon
|
||||
x:Name="TailPolygon"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Fill="{TemplateBinding Background}"
|
||||
Stroke="{TemplateBinding BorderBrush}"
|
||||
StrokeThickness="{StaticResource TeachingTipBorderThickness}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<VisualStateManager.VisualStateGroups>
|
||||
<VisualStateGroup x:Name="LightDismissStates">
|
||||
<VisualState x:Name="LightDismiss">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Fill" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
<Setter Target="ContentRootGrid.Background" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
<Setter Target="MainContentPresenter.Background" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
<Setter Target="HeroContentBorder.Background" Value="{ThemeResource TeachingTipTransientBackground}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NormalDismiss" />
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="ButtonsStates">
|
||||
<VisualState x:Name="NoButtonsVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Collapsed" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="ActionButtonVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Collapsed" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Visible" />
|
||||
<Setter Target="ActionButton.(Grid.ColumnSpan)" Value="2" />
|
||||
<Setter Target="ActionButton.Margin" Value="{ThemeResource TeachingTipButtonPanelMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CloseButtonVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Visible" />
|
||||
<Setter Target="CloseButton.Margin" Value="{ThemeResource TeachingTipButtonPanelMargin}" />
|
||||
<Setter Target="CloseButton.(Grid.Column)" Value="0" />
|
||||
<Setter Target="CloseButton.(Grid.ColumnSpan)" Value="2" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="BothButtonsVisible">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="CloseButton.Visibility" Value="Visible" />
|
||||
<Setter Target="CloseButton.Margin" Value="{ThemeResource TeachingTipRightButtonMargin}" />
|
||||
<Setter Target="ActionButton.Visibility" Value="Visible" />
|
||||
<Setter Target="ActionButton.(Grid.Column)" Value="0" />
|
||||
<Setter Target="ActionButton.Margin" Value="{ThemeResource TeachingTipLeftButtonMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="ContentStates">
|
||||
<VisualState x:Name="Content">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="MainContentPresenter.Margin" Value="{StaticResource TeachingTipMainContentPresentMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NoContent">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="MainContentPresenter.Margin" Value="{StaticResource TeachingTipMainContentAbsentMargin}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="CloseButtonLocations">
|
||||
<VisualState x:Name="HeaderCloseButton">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitlesStackPanel.Margin" Value="{StaticResource TeachingTipTitleStackPanelMarginWithHeaderCloseButton}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="FooterCloseButton">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitlesStackPanel.Margin" Value="{StaticResource TeachingTipTitleStackPanelMarginWithFooterCloseButton}" />
|
||||
<Setter Target="AlternateCloseButton.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="IconStates">
|
||||
<VisualState x:Name="Icon">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="IconPresenter.Margin" Value="{StaticResource TeachingTipIconPresenterMarginWithIcon}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="NoIcon">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="IconPresenter.Margin" Value="{StaticResource TeachingTipIconPresenterMarginWithoutIcon}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="HeroContentPlacementStates">
|
||||
<VisualState x:Name="HeroContentTop">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="HeroContentBorder.(Grid.Row)" Value="0" />
|
||||
<Setter Target="HeroContentBorder.CornerRadius" Value="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource TopCornerRadiusFilterConverter}, FallbackValue=0}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="HeroContentBottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="HeroContentBorder.(Grid.Row)" Value="2" />
|
||||
<Setter Target="HeroContentBorder.CornerRadius" Value="{Binding CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BottomCornerRadiusFilterConverter}, FallbackValue=0}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="PlacementStates">
|
||||
<VisualState x:Name="Top">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10, 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Bottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,10 10,0 20,10" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="0" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginBottom}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Left">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 0,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="4" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginLeft}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Right">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="10,0 0,10 10,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="0" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginRight}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TopRight">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="TopLeft">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="BottomRight">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,10 10,0 20,10" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="0" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginBottom}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="BottomLeft">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,10 10,0 20,10" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="0" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginBottom}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="LeftTop">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 0,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="4" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginLeft}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="LeftBottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10 0,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="4" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Right" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginLeft}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="RightTop">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="10,0 0,10 10,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="0" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginRight}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="RightBottom">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="10,0 0,10 10,20" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="2" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="0" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Left" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Top" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginRight}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Center">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Visible" />
|
||||
<Setter Target="TailPolygon.Points" Value="0,0 10,10, 20,0" />
|
||||
<Setter Target="TailPolygon.(Grid.Row)" Value="4" />
|
||||
<Setter Target="TailPolygon.(Grid.Column)" Value="2" />
|
||||
<Setter Target="TailPolygon.HorizontalAlignment" Value="Center" />
|
||||
<Setter Target="TailPolygon.VerticalAlignment" Value="Bottom" />
|
||||
<Setter Target="TailPolygon.Margin" Value="{StaticResource TeachingTipTailPolygonMarginTop}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Untargeted">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TailPolygon.Visibility" Value="Collapsed" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="TitleBlockStates">
|
||||
<VisualState x:Name="ShowTitleTextBlock">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="TitleTextBlock.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CollapseTitleTextBlock" />
|
||||
</VisualStateGroup>
|
||||
<VisualStateGroup x:Name="SubtitleBlockStates">
|
||||
<VisualState x:Name="ShowSubtitleTextBlock">
|
||||
<VisualState.Setters>
|
||||
<Setter Target="SubtitleTextBlock.Visibility" Value="Visible" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="CollapseSubtitleTextBlock" />
|
||||
</VisualStateGroup>
|
||||
</VisualStateManager.VisualStateGroups>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -5,7 +5,7 @@
|
||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||
<VersionMajor>0</VersionMajor>
|
||||
<VersionMinor>8</VersionMinor>
|
||||
<VersionMinor>9</VersionMinor>
|
||||
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -75,6 +75,8 @@ functionality.
|
||||
- [Advanced scenarios](#advanced-scenarios)
|
||||
- [Status messages](#status-messages)
|
||||
- [Rendering of ICommandItems in Lists and Menus](#rendering-of-icommanditems-in-lists-and-menus)
|
||||
- [Addenda I: API additions (ICommandProvider2)](#addenda-i-api-additions-icommandprovider2)
|
||||
- [Addenda IV: Dock bands](#addenda-iv-dock-bands)
|
||||
- [Class diagram](#class-diagram)
|
||||
- [Future considerations](#future-considerations)
|
||||
- [Arbitrary parameters and arguments](#arbitrary-parameters-and-arguments)
|
||||
@@ -2045,6 +2047,87 @@ Fortunately, we can put all of that (`GetApiExtensionStubs`,
|
||||
developers won't have to do anything. The toolkit will just do the right thing
|
||||
for them.
|
||||
|
||||
## Addenda IV: Dock bands
|
||||
|
||||
The "dock" is another way to surface commands to the user. This is a
|
||||
toolbar-like window that can be docked to the side of the screen, or floated as
|
||||
its own window. It enables another surface for extensions to display real-time
|
||||
information and shortcuts to users.
|
||||
|
||||
Bands are powered by the same interfaces as DevPal itself. Extensions can provide
|
||||
bands via the new `DockBand` property on `ICommandProvider3`.
|
||||
|
||||
```csharp
|
||||
interface ICommandProvider3 requires ICommandProvider2
|
||||
{
|
||||
ICommandItem[] GetDockBands();
|
||||
};
|
||||
```
|
||||
|
||||
A **Dock Band** is one "strip of items" in the dock. Each band can have multiple
|
||||
items. This allows an extension to create a strip of buttons that should all be
|
||||
treated as a single unit. For example, a media player band will want probably
|
||||
four items:
|
||||
* one for the previous track
|
||||
* one for play/pause
|
||||
* one for next track
|
||||
* and one to display the album art and track title
|
||||
|
||||
`GetDockBands` returns an array of `ICommandItem`s. Each `ICommandItem`
|
||||
represents one band in the dock. These represent all of the bands that an
|
||||
extension would allow the user to add to their dock.
|
||||
|
||||
All of the `ICommandItem`s returned from `GetDockBands` **must** have a
|
||||
`Command` with a non-empty `Id` set. If the `Id` is null or empty, DevPal will
|
||||
ignore that band.
|
||||
|
||||
Bands are not automatically added to the dock. Instead, the user must choose
|
||||
which bands they want to add. This is done via the DevPal settings page.
|
||||
Furthermore, bands are not displayed in the list of commands in DevPal itself.
|
||||
This allows extension authors to create objects that are only intended for the
|
||||
dock, without cluttering up the main DevPal UI, and vice versa.
|
||||
|
||||
DevPal will then create UI in the dock for each band the user has chosen to add.
|
||||
What that looks like will depend on the `Command` in the `ICommandItem`:
|
||||
* A `IInvokableCommand` will be rendered as a single button. Think "the
|
||||
time/date" button on the taskbar, that opens the notification center.
|
||||
* A `IListPage` will be rendered as a strip of buttons, one for each `IListItem`
|
||||
in the list. Think "media controls" for a music player.
|
||||
* A `IContentPage` will be rendered as a single button. Clicking that button
|
||||
will open a flyout with that content rendered in it. Think "weather" or "news"
|
||||
flyouts.
|
||||
|
||||
If the `Command` in the `IListItem`s of a band are pages, then clicking those
|
||||
buttons will open DevPal to that page, as if it were a flyout from the dock.
|
||||
|
||||
The `.Title` property of the top-level `ICommandItem` representing the band will
|
||||
be used as the name of the band in the settings. So a media player band might
|
||||
want to set the `Title` to "Contoso Music Player", even if the individual
|
||||
buttons in the band don't show that title.
|
||||
|
||||
Users may also "pin" a top-level command from DevPal into the dock. DevPal will
|
||||
take care of creating a new band (owned by devpal) with that command in it. This
|
||||
allows users to add quick shortcuts to their favorite commands in the dock.
|
||||
Think: pinning an app, or pinning a particular GitHub query.
|
||||
|
||||
Bands are added via ID. An extension may choose to have a TopLevelCommand and a
|
||||
DockBand with the same `Id`. In this case, if the user pins the TopLevelCommand
|
||||
to the dock, DevPal will pin the band from `GetDockBands`, rather than creating
|
||||
a simple pinned command. This allows extension authors to seamlessly have a
|
||||
top-level command present a palette-specific experience, while also having a
|
||||
dock-specific experience. In our ongoing media player example, the top-level
|
||||
command might open DevPal to a full-featured music control page, while the dock
|
||||
band has simpler buttons on it (without a title/subtitle).
|
||||
|
||||
Users may choose to have:
|
||||
* the orientation of the dock: vertical or horizontal
|
||||
* the size of the dock
|
||||
* which bands are shown in the dock
|
||||
* whether the "labels" (read: `Title` & `Subtitle`) of individual bands are
|
||||
shown or hidden.
|
||||
- Dock bands will still display the `Title` & `Subtitle` of each item in the
|
||||
band as the tooltip on those items, even when the "labels" are hidden.
|
||||
|
||||
## Class diagram
|
||||
|
||||
This is a diagram attempting to show the relationships between the various types we've defined for the SDK. Some elements are omitted for clarity. (Notably, `IconData` and `IPropChanged`, which are used in many places.)
|
||||
|
||||
@@ -17,25 +17,12 @@ public sealed partial class AppListItem : ListItem
|
||||
{
|
||||
private readonly AppCommand _appCommand;
|
||||
private readonly AppItem _app;
|
||||
private readonly Lazy<Details> _details;
|
||||
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
|
||||
private readonly Lazy<Task<Details>> _detailsLoadTask;
|
||||
|
||||
private InterlockedBoolean _isLoadingIcon;
|
||||
private InterlockedBoolean _isLoadingDetails;
|
||||
|
||||
public override IDetails? Details
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isLoadingDetails.Set())
|
||||
{
|
||||
_ = LoadDetailsAsync();
|
||||
}
|
||||
|
||||
return base.Details;
|
||||
}
|
||||
set => base.Details = value;
|
||||
}
|
||||
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
|
||||
|
||||
public override IIconInfo? Icon
|
||||
{
|
||||
@@ -65,20 +52,14 @@ public sealed partial class AppListItem : ListItem
|
||||
|
||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||
|
||||
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
|
||||
}
|
||||
_details = new Lazy<Details>(() =>
|
||||
{
|
||||
var t = BuildDetails();
|
||||
t.Wait();
|
||||
return t.Result;
|
||||
});
|
||||
|
||||
private async Task LoadDetailsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
Details = await _detailsLoadTask.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning($"Failed to load details for {AppIdentifier}\n{ex}");
|
||||
}
|
||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
|
||||
}
|
||||
|
||||
private async Task LoadIconAsync()
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
@@ -34,7 +33,6 @@ internal sealed partial class RunAsAdminCommand : InvokableCommand
|
||||
{
|
||||
if (packaged)
|
||||
{
|
||||
// For UWP/packaged apps, use shell:AppsFolder which works from packaged context
|
||||
var command = "shell:AppsFolder\\" + target;
|
||||
command = Environment.ExpandEnvironmentVariables(command.Trim());
|
||||
|
||||
@@ -45,37 +43,9 @@ internal sealed partial class RunAsAdminCommand : InvokableCommand
|
||||
}
|
||||
else
|
||||
{
|
||||
// For Win32 apps, use ActionRunner helper process to work around WinUI3/MSIX packaging limitation.
|
||||
// When running from a packaged app, ShellExecute with "runas" verb may fail for certain apps
|
||||
// (e.g., apps launched via .lnk shortcuts). ActionRunner runs outside the MSIX container,
|
||||
// so it can properly invoke the UAC dialog.
|
||||
var actionRunnerPath = ActionRunnerHelper.GetActionRunnerPath();
|
||||
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.Administrator);
|
||||
|
||||
if (string.IsNullOrEmpty(actionRunnerPath))
|
||||
{
|
||||
// Fallback to direct Process.Start if ActionRunner is not found
|
||||
ExtensionHost.LogMessage($"ActionRunner not found, falling back to direct Process.Start for '{target}'");
|
||||
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.Administrator);
|
||||
Process.Start(info);
|
||||
return;
|
||||
}
|
||||
|
||||
var args = $"-run-as-admin -target \"{target}\"";
|
||||
if (!string.IsNullOrEmpty(parentDir))
|
||||
{
|
||||
args += $" -workingDir \"{parentDir}\"";
|
||||
}
|
||||
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = actionRunnerPath,
|
||||
Arguments = args,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
|
||||
ExtensionHost.LogMessage($"Launching '{target}' as administrator via ActionRunner");
|
||||
Process.Start(processInfo);
|
||||
Process.Start(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user