mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-28 23:17:03 +01:00
Compare commits
1 Commits
issue/4483
...
dev/crutka
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8090a990f5 |
64
.github/actions/spell-check/expect.txt
vendored
64
.github/actions/spell-check/expect.txt
vendored
@@ -22,6 +22,7 @@ ADate
|
||||
ADDSTRING
|
||||
ADDUNDORECORD
|
||||
ADifferent
|
||||
adjacents
|
||||
ADMINS
|
||||
adml
|
||||
admx
|
||||
@@ -102,6 +103,7 @@ ASYNCWINDOWPLACEMENT
|
||||
ASYNCWINDOWPOS
|
||||
atl
|
||||
ATRIOX
|
||||
ATX
|
||||
aumid
|
||||
authenticode
|
||||
AUTOBUDDY
|
||||
@@ -218,11 +220,10 @@ CIELCh
|
||||
cim
|
||||
CImage
|
||||
cla
|
||||
claude
|
||||
CLASSDC
|
||||
classguid
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
claude
|
||||
CLEARTYPE
|
||||
clickable
|
||||
clickonce
|
||||
@@ -261,6 +262,7 @@ colorhistory
|
||||
colorhistorylimit
|
||||
COLORKEY
|
||||
colorref
|
||||
Convs
|
||||
comctl
|
||||
comdlg
|
||||
comexp
|
||||
@@ -281,7 +283,6 @@ CONTEXTHELP
|
||||
CONTEXTMENUHANDLER
|
||||
contractversion
|
||||
CONTROLPARENT
|
||||
Convs
|
||||
copiedcolorrepresentation
|
||||
coppied
|
||||
copyable
|
||||
@@ -297,6 +298,7 @@ cpcontrols
|
||||
cph
|
||||
cplusplus
|
||||
CPower
|
||||
cppcoreguidelines
|
||||
cpptools
|
||||
cppvsdbg
|
||||
cppwinrt
|
||||
@@ -325,7 +327,7 @@ CURRENTDIR
|
||||
CURSORINFO
|
||||
cursorpos
|
||||
CURSORSHOWING
|
||||
cursorwrap
|
||||
CURSORWRAP
|
||||
customaction
|
||||
CUSTOMACTIONTEST
|
||||
CUSTOMFORMATPLACEHOLDER
|
||||
@@ -348,14 +350,12 @@ datareader
|
||||
datatracker
|
||||
dataversion
|
||||
Dayof
|
||||
dbcc
|
||||
DBID
|
||||
DBLCLKS
|
||||
DBLEPSILON
|
||||
DBPROP
|
||||
DBPROPIDSET
|
||||
DBPROPSET
|
||||
DBT
|
||||
DCBA
|
||||
DCOM
|
||||
DComposition
|
||||
@@ -373,7 +373,8 @@ DEFAULTICON
|
||||
defaultlib
|
||||
DEFAULTONLY
|
||||
DEFAULTSIZE
|
||||
defaulttonearest
|
||||
DEFAULTTONEAREST
|
||||
Defaulttonearest
|
||||
DEFAULTTONULL
|
||||
DEFAULTTOPRIMARY
|
||||
DEFERERASE
|
||||
@@ -395,19 +396,14 @@ DESKTOPVERTRES
|
||||
devblogs
|
||||
devdocs
|
||||
devenv
|
||||
DEVICEINTERFACE
|
||||
devicetype
|
||||
DEVINTERFACE
|
||||
devmgmt
|
||||
DEVMODE
|
||||
DEVMODEW
|
||||
DEVNODES
|
||||
devpal
|
||||
DEVTYP
|
||||
dfx
|
||||
DIALOGEX
|
||||
diffs
|
||||
digicert
|
||||
diffs
|
||||
DINORMAL
|
||||
DISABLEASACTIONKEY
|
||||
DISABLENOSCROLL
|
||||
@@ -550,6 +546,7 @@ fdx
|
||||
FErase
|
||||
fesf
|
||||
FFFF
|
||||
FInc
|
||||
Figma
|
||||
FILEEXPLORER
|
||||
fileexploreraddons
|
||||
@@ -570,7 +567,6 @@ FILESYSPATH
|
||||
Filetime
|
||||
FILEVERSION
|
||||
FILTERMODE
|
||||
FInc
|
||||
findfast
|
||||
findmymouse
|
||||
FIXEDFILEINFO
|
||||
@@ -672,14 +668,13 @@ HCRYPTPROV
|
||||
hcursor
|
||||
hcwhite
|
||||
hdc
|
||||
HDEVNOTIFY
|
||||
hdr
|
||||
hdrop
|
||||
hdwwiz
|
||||
Helpline
|
||||
helptext
|
||||
hgdiobj
|
||||
HGFE
|
||||
hgdiobj
|
||||
hglobal
|
||||
hhk
|
||||
HHmmssfff
|
||||
@@ -755,9 +750,9 @@ HWNDPARENT
|
||||
HWNDPREV
|
||||
hyjiacan
|
||||
IAI
|
||||
icf
|
||||
ICONERROR
|
||||
ICONLOCATION
|
||||
icf
|
||||
IDCANCEL
|
||||
IDD
|
||||
idk
|
||||
@@ -848,8 +843,8 @@ jeli
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
jjw
|
||||
jobject
|
||||
JOBOBJECT
|
||||
jobject
|
||||
jpe
|
||||
jpnime
|
||||
Jsons
|
||||
@@ -893,7 +888,6 @@ Ldr
|
||||
LEFTALIGN
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
@@ -936,9 +930,9 @@ LOWORD
|
||||
lparam
|
||||
LPBITMAPINFOHEADER
|
||||
LPCFHOOKPROC
|
||||
lpch
|
||||
LPCITEMIDLIST
|
||||
LPCLSID
|
||||
lpch
|
||||
lpcmi
|
||||
LPCMINVOKECOMMANDINFO
|
||||
LPCREATESTRUCT
|
||||
@@ -954,7 +948,6 @@ LPMONITORINFO
|
||||
LPOSVERSIONINFOEXW
|
||||
LPQUERY
|
||||
lprc
|
||||
LPrivate
|
||||
LPSAFEARRAY
|
||||
lpstr
|
||||
lpsz
|
||||
@@ -964,6 +957,7 @@ lptpm
|
||||
LPTR
|
||||
LPTSTR
|
||||
lpv
|
||||
LPrivate
|
||||
LPW
|
||||
lpwcx
|
||||
lpwndpl
|
||||
@@ -1007,19 +1001,18 @@ mber
|
||||
MBM
|
||||
MBR
|
||||
Mbuttondown
|
||||
mcp
|
||||
MDICHILD
|
||||
MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
meme
|
||||
mcp
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
metafile
|
||||
@@ -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
|
||||
@@ -1765,13 +1760,15 @@ SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
sut
|
||||
swe
|
||||
svchost
|
||||
SVGIn
|
||||
SVGIO
|
||||
svgz
|
||||
SVSI
|
||||
SWFO
|
||||
swp
|
||||
SWP
|
||||
Swp
|
||||
SWPNOSIZE
|
||||
SWPNOZORDER
|
||||
SWRESTORE
|
||||
@@ -1790,7 +1787,8 @@ SYSKEY
|
||||
syskeydown
|
||||
SYSKEYUP
|
||||
SYSLIB
|
||||
sysmenu
|
||||
SYSMENU
|
||||
Sysmenu
|
||||
systemai
|
||||
SYSTEMAPPS
|
||||
SYSTEMMODAL
|
||||
@@ -1894,9 +1892,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'
|
||||
---
|
||||
|
||||
|
||||
@@ -30,11 +30,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
- [C++ events](https://github.com/search?q=repo%3Amicrosoft%2FPowerToys+ProjectTelemetryPrivacyDataTag&type=code)
|
||||
|
||||
### General
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -47,18 +43,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.GeneralSettingsChanged</td>
|
||||
<td>Logs changes made to general settings within PowerToys.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Install_Fail</td>
|
||||
<td>Triggered when the PowerToys installation process encounters an error and fails to complete.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Repair_Cancel</td>
|
||||
<td>Triggered when a PowerToys repair operation is cancelled by the user before completion.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Repair_Fail</td>
|
||||
<td>Triggered when the PowerToys repair operation fails to complete successfully due to an error.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Runner_Launch</td>
|
||||
<td>Indicates when the PowerToys Runner is launched.</td>
|
||||
@@ -75,18 +59,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.ScoobeStartedEvent</td>
|
||||
<td>Triggered when SCOOBE (Secondary Out-of-box experience) starts.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ShortcutConflictControlClickedEvent</td>
|
||||
<td>Triggered when a user clicks on the Shortcut Conflict Control button in the PowerToys Settings UI Dashboard.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ShortcutConflictDetectedEvent</td>
|
||||
<td>Triggered when keyboard shortcut conflicts are detected in the PowerToys Settings UI Dashboard.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ShortcutConflictResolvedEvent</td>
|
||||
<td>Triggered when a keyboard shortcut conflict is resolved in the PowerToys Settings UI.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.TrayFlyoutActivatedEvent</td>
|
||||
<td>Indicates when the tray flyout menu is activated.</td>
|
||||
@@ -95,14 +67,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.TrayFlyoutModuleRunEvent</td>
|
||||
<td>Logs when a utility from the tray flyout menu is run.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.UnInstall_Cancel</td>
|
||||
<td>Triggered when the PowerToys uninstallation process is cancelled by the user before completion.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.UnInstall_Fail</td>
|
||||
<td>Triggered when the PowerToys uninstallation process fails to complete successfully due to an error. </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Uninstall_Success</td>
|
||||
<td>Logs when PowerToys is successfully uninstalled (who would do such a thing!).</td>
|
||||
@@ -118,19 +82,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### OOBE (Out-of-box experience)
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.OobeModuleRunEvent</td>
|
||||
<td>Triggered when a user clicks to run or launch a PowerToys module directly from the OOBE (out-of-box experience) interface.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.OobeSectionEvent</td>
|
||||
<td>Occurs when OOBE is shown to the user.</td>
|
||||
@@ -143,18 +99,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.OobeStartedEvent</td>
|
||||
<td>Indicates when the out-of-box experience has been initiated.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.OobeVariantAssignmentEvent</td>
|
||||
<td>This event logs A/B testing assignments for experimental features, helping track which users are in control or alternate groups for feature experiments. </td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Advanced Paste
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -222,11 +170,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Always on Top
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -246,11 +190,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Awake
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -278,11 +218,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Color Picker
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -299,14 +235,18 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.ColorPicker_Settings</td>
|
||||
<td>Triggered when the settings for the Color Picker are accessed or modified.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ColorPickerCancelledEvent</td>
|
||||
<td>Occurs when a color picking action is cancelled by the user.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ColorPickerShowEvent</td>
|
||||
<td>Triggered when the Color Picker UI is displayed on the screen.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Command Not Found
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -319,6 +259,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.CmdNotFoundInstallEvent</td>
|
||||
<td>Triggered when a Command Not Found is installed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdNotFoundInstanceCreatedEvent</td>
|
||||
<td>Occurs when an instance of a Command Not Found is created.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CmdNotFoundUninstallEvent</td>
|
||||
<td>Triggered when Command Not Found is uninstalled after being previously installed.</td>
|
||||
@@ -327,11 +271,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
|
||||
### Command Palette
|
||||
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -395,11 +335,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Crop And Lock
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -408,26 +344,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.CropAndLock_ActivateReparent</td>
|
||||
<td>Triggered when the cropping interface is activated for reparenting the cropped content.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CropAndLock_ActivateScreenshot</td>
|
||||
<td>Triggered when the screenshot mode is activated in Crop and Lock.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CropAndLock_ActivateThumbnail</td>
|
||||
<td>Occurs when the thumbnail view for cropped content is activated.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CropAndLock_CreateReparentWindow</td>
|
||||
<td>Triggered when a reparent window is created in Crop and Lock mode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CropAndLock_CreateScreenshotWindow</td>
|
||||
<td>Triggered when a screenshot window is created in Crop and Lock mode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CropAndLock_CreateThumbnailWindow</td>
|
||||
<td>Triggered when a thumbnail window is created in Crop and Lock mode.<-/td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CropAndLock_EnableCropAndLock</td>
|
||||
<td>Triggered when Crop and Lock is enabled.</td>
|
||||
@@ -438,28 +358,8 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Cursor Wrap
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.CursorWrap_EnableCursorWrap</td>
|
||||
<td>Triggered when Cursor Wrap is enabled or disabled.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Environment Variables
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -487,11 +387,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### FancyZones
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -508,10 +404,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.FancyZones_EnableFancyZones</td>
|
||||
<td>Occurs when FancyZones is enabled.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZones_Error</td>
|
||||
<td>Triggered when an error occurs within the FancyZones module. This event logs critical errors to help diagnose and troubleshoot issues with FancyZones functionality, such as failures to set up Windows hooks or other system-level operations required for window management.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZones_KeyboardSnapWindowToZone</td>
|
||||
<td>Triggered when a window is snapped to a zone using the keyboard.</td>
|
||||
@@ -524,6 +416,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.FancyZones_MoveOrResizeStarted</td>
|
||||
<td>Triggered when a window move or resize action is initiated.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZones_MoveSizeEnd</td>
|
||||
<td>Occurs when the moving or resizing of a window has ended.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZones_OnKeyDown</td>
|
||||
<td>Triggered when a key is pressed down while interacting with zones.</td>
|
||||
@@ -536,6 +432,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.FancyZones_Settings</td>
|
||||
<td>Triggered when FancyZones settings are accessed or modified.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZones_SettingsChanged</td>
|
||||
<td>Occurs when there is a change in the FancyZones settings.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZones_SnapNewWindowIntoZone</td>
|
||||
<td>Triggered when a new window is snapped into a zone.</td>
|
||||
@@ -556,50 +456,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.FancyZones_CLICommand</td>
|
||||
<td>Triggered when a FancyZones CLI command is executed, logging the command name and success status.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZonesEditorStartEvent</td>
|
||||
<td>Triggered when the FancyZones Editor application starts. This logs the initialization of the editor UI, which is used to create and configure custom zone layouts.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FancyZonesEditorStartFinishEvent</td>
|
||||
<td>Triggered when the FancyZones Editor has completed loading and is ready for user interaction.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### File Locksmith
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_EnableFileLocksmith</td>
|
||||
<td>Triggered when File Locksmith is enabled.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_Invoked</td>
|
||||
<td>Occurs when File Locksmith is invoked.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_InvokedRet</td>
|
||||
<td>Triggered when File Locksmith invocation returns a result.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_QueryContextMenuError</td>
|
||||
<td>Occurs when there is an error querying the context menu for File Locksmith.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### FileExplorerAddOns
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -636,10 +496,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.MarkdownFilePreviewed</td>
|
||||
<td>Triggered when a Markdown file is previewed in File Explorer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.MarkdownFilePreviewError</td>
|
||||
<td>Triggered when there is an error previewing a Markdown file in File Explorer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.PdfFileHandlerLoaded</td>
|
||||
<td>Occurs when a PDF file handler is loaded.</td>
|
||||
@@ -648,10 +504,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.PdfFilePreviewed</td>
|
||||
<td>Triggered when a PDF file is previewed in File Explorer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.PdfFilePreviewError</td>
|
||||
<td>Triggered when there is an error previewing a PDF file in File Explorer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.PowerPreview_Enabled</td>
|
||||
<td>Occurs when preview is enabled.</td>
|
||||
@@ -668,10 +520,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.PowerPreview_TweakUISettings_InitSet__ErrorLoadingFile</td>
|
||||
<td>Triggered when there is an error loading a file during Tweak UI settings initialization.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.PowerPreview_TweakUISettings_SetConfig__InvalidJSONGiven</td>
|
||||
<td>Triggered when invalid JSON is provided to the Power Preview settings configuration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.PowerPreview_TweakUISettings_SuccessfullyUpdatedSettings</td>
|
||||
<td>Occurs when the Tweak UI settings for Power Preview are successfully updated.</td>
|
||||
@@ -680,10 +528,6 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.QoiFilePreviewed</td>
|
||||
<td>Triggered when a QOI file is previewed in File Explorer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.QoiFilePreviewError</td>
|
||||
<td>Triggered when there is an error previewing a QOI (Quite OK Image) file in File Explorer.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.SvgFileHandlerLoaded</td>
|
||||
<td>Occurs when an SVG file handler is loaded.</td>
|
||||
@@ -698,12 +542,32 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### File Locksmith
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_EnableFileLocksmith</td>
|
||||
<td>Triggered when File Locksmith is enabled.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_Invoked</td>
|
||||
<td>Occurs when File Locksmith is invoked.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_InvokedRet</td>
|
||||
<td>Triggered when File Locksmith invocation returns a result.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.FileLocksmith_QueryContextMenuError</td>
|
||||
<td>Occurs when there is an error querying the context menu for File Locksmith.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Find My Mouse
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -719,11 +583,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Hosts File Editor
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -740,22 +600,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.HostsFileEditorOpenedEvent</td>
|
||||
<td>Fires when Hosts File Editor is opened.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.HostEditorStartEvent</td>
|
||||
<td>Triggered when the Hosts File Editor application starts. This logs the initialization of the Hosts File Editor UI with a timestamp.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.HostEditorStartFinishEvent</td>
|
||||
<td>Triggered when the Hosts File Editor has completed loading and is ready for user interaction.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Image Resizer
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -772,18 +620,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.ImageResizer_InvokedRet</td>
|
||||
<td>Fires when the Image Resizer operation is completed and returns a result.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.ImageResizer_QueryContextMenuError</td>
|
||||
<td>Triggered when there is an error querying the context menu for Image Resizer.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Keyboard Manager
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -796,22 +636,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.KeyboardManager_AppSpecificShortcutRemapCount</td>
|
||||
<td>Logs the number of application-specific shortcut remaps configured by the user.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.KeyboardManager_AppSpecificShortcutToKeyRemapInvoked</td>
|
||||
<td>Logs each instance when an application-specific shortcut-to-key remap is used.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.KeyboardManager_AppSpecificShortcutToShortcutRemapInvoked</td>
|
||||
<td>Logs each instance when an application-specific shortcut-to-shortcut remap is used.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.KeyboardManager_Error</td>
|
||||
<td>Triggered when an error occurs in Keyboard Manager. This logs the error code, error message, and the method name where the error occurred.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.KeyboardManager_ErrorSendingKeyAndShortcutRemapLoadedConfiguration</td>
|
||||
<td>Triggered when there is an error sending remapping configuration telemetry. This occurs when Keyboard Manager fails to report the loaded key and shortcut remap configurations</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.KeyboardManager_DailyAppSpecificShortcutToKeyRemapInvoked</td>
|
||||
<td>Logs the daily count of application-specific shortcut-to-key remaps executed by the user.</td>
|
||||
@@ -875,11 +703,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Light Switch
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -903,11 +727,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Mouse Highlighter
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -920,18 +740,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.MouseHighlighter_StartHighlightingSession</td>
|
||||
<td>Occurs when a new highlighting session is started.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.MouseHighlighter_StartSpotlightSession</td>
|
||||
<td>Triggered when a spotlight session is started in Mouse Highlighter. This occurs when the user activates the spotlight mode.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Mouse Jump
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -955,11 +767,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Mouse Pointer Crosshairs
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -975,11 +783,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Mouse Without Borders
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1031,11 +835,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### New+
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1067,11 +867,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Peek
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1104,18 +900,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.Peek_Settings</td>
|
||||
<td>Triggered when the settings for Peek are modified.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Peek_SpaceModeEnabled</td>
|
||||
<td>Triggered when the Space key activation mode is enabled or disabled in Peek</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### PowerRename
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1147,11 +935,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### PowerToys Run
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1192,14 +976,14 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.RunPluginsSettingsEvent</td>
|
||||
<td>Triggered when the settings for PowerToys Run plugins are accessed or modified.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.WindowWalker_EnableWindowWalker</td>
|
||||
<td>Triggered when the Window Walker plugin is enabled.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Quick Accent
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1215,11 +999,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Registry Preview
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1232,22 +1012,10 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Microsoft.PowerToys.RegistryPreview_EnableRegistryPreview</td>
|
||||
<td>Occurs when Registry Preview is enabled.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.RegistryPreviewEditorStartEvent</td>
|
||||
<td>Triggered when the Registry Preview application starts. This logs the initialization of the Registry Preview UI with a timestamp.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.RegistryPreviewEditorStartFinishEvent</td>
|
||||
<td>Triggered when the Registry Preview application has completed loading and is ready for user interaction.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Screen Ruler
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1267,11 +1035,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Shortcut Guide
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1287,11 +1051,7 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Text Extractor
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1315,15 +1075,15 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Workspaces
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Projects_CLIUsage</td>
|
||||
<td>Logs usage of command-line arguments for launching apps.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.Workspaces_CreateEvent</td>
|
||||
<td>Triggered when a new workspace is created.</td>
|
||||
@@ -1345,21 +1105,13 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
<td>Triggered when a workspace is launched.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.WorkspacesEditorStartEvent</td>
|
||||
<td>Triggered when the Workspaces Editor application starts. This logs the initialization of the Workspaces Editor UI with a timestamp.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Microsoft.PowerToys.WorkspacesEditorStartFinishEvent</td>
|
||||
<td>Triggered when the Workspaces Editor has completed loading and is ready for user interaction.</td>
|
||||
<td>Microsoft.PowerToys.Workspaces_Settings</td>
|
||||
<td>Logs changes to workspaces settings.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### ZoomIt
|
||||
<table style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<table style="width:100%">
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="Module_CmdPal" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<RemoveFile Id="RemoveOldCmdPalMsix" Name="Microsoft.CmdPal.UI_*.msix" On="install" />
|
||||
<?if $(sys.BUILDARCH) = x64 ?>
|
||||
<File Id="Microsoft.CmdPal.UI___var.CmdPalVersion_._x64.msix" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_x64.msix" />
|
||||
<?else?>
|
||||
|
||||
@@ -104,66 +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();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -16,50 +16,13 @@ DWORD WINAPI _checkTheme(LPVOID lpParam)
|
||||
|
||||
void ThemeListener::AddChangedHandler(THEME_HANDLE handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||
handles.push_back(handle);
|
||||
}
|
||||
|
||||
void ThemeListener::DelChangedHandler(THEME_HANDLE handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||
auto it = std::find(handles.begin(), handles.end(), handle);
|
||||
if (it != handles.end())
|
||||
{
|
||||
handles.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void ThemeListener::AddAppThemeChangedHandler(THEME_HANDLE handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||
appThemeHandles.push_back(handle);
|
||||
}
|
||||
|
||||
void ThemeListener::DelAppThemeChangedHandler(THEME_HANDLE handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||
auto it = std::find(appThemeHandles.begin(), appThemeHandles.end(), handle);
|
||||
if (it != appThemeHandles.end())
|
||||
{
|
||||
appThemeHandles.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void ThemeListener::AddSystemThemeChangedHandler(THEME_HANDLE handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||
systemThemeHandles.push_back(handle);
|
||||
}
|
||||
|
||||
void ThemeListener::DelSystemThemeChangedHandler(THEME_HANDLE handle)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||
auto it = std::find(systemThemeHandles.begin(), systemThemeHandles.end(), handle);
|
||||
if (it != systemThemeHandles.end())
|
||||
{
|
||||
systemThemeHandles.erase(it);
|
||||
}
|
||||
handles.erase(it);
|
||||
}
|
||||
|
||||
void ThemeListener::CheckTheme()
|
||||
@@ -85,51 +48,13 @@ void ThemeListener::CheckTheme()
|
||||
|
||||
WaitForSingleObject(hEvent, INFINITE);
|
||||
|
||||
auto _appTheme = ThemeHelpers::GetAppTheme();
|
||||
auto _systemTheme = ThemeHelpers::GetSystemTheme();
|
||||
|
||||
bool appThemeChanged = (AppTheme != _appTheme);
|
||||
bool systemThemeChanged = (SystemTheme != _systemTheme);
|
||||
|
||||
if (appThemeChanged || systemThemeChanged)
|
||||
auto _theme = ThemeHelpers::GetAppTheme();
|
||||
if (AppTheme != _theme)
|
||||
{
|
||||
AppTheme = _appTheme;
|
||||
SystemTheme = _systemTheme;
|
||||
|
||||
// Copy handlers under lock, then invoke outside lock to avoid deadlock
|
||||
std::vector<THEME_HANDLE> handlesCopy;
|
||||
std::vector<THEME_HANDLE> appThemeHandlesCopy;
|
||||
std::vector<THEME_HANDLE> systemThemeHandlesCopy;
|
||||
|
||||
AppTheme = _theme;
|
||||
for (int i = 0; i < handles.size(); i++)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||
handlesCopy = handles;
|
||||
if (appThemeChanged)
|
||||
{
|
||||
appThemeHandlesCopy = appThemeHandles;
|
||||
}
|
||||
if (systemThemeChanged)
|
||||
{
|
||||
systemThemeHandlesCopy = systemThemeHandles;
|
||||
}
|
||||
}
|
||||
|
||||
// Call generic handlers (backward compatible)
|
||||
for (const auto& handler : handlesCopy)
|
||||
{
|
||||
handler();
|
||||
}
|
||||
|
||||
// Call app theme specific handlers
|
||||
for (const auto& handler : appThemeHandlesCopy)
|
||||
{
|
||||
handler();
|
||||
}
|
||||
|
||||
// Call system theme specific handlers
|
||||
for (const auto& handler : systemThemeHandlesCopy)
|
||||
{
|
||||
handler();
|
||||
handles[i]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <windows.h>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
typedef void (*THEME_HANDLE)();
|
||||
DWORD WINAPI _checkTheme(LPVOID lpParam);
|
||||
@@ -15,7 +14,6 @@ public:
|
||||
ThemeListener()
|
||||
{
|
||||
AppTheme = ThemeHelpers::GetAppTheme();
|
||||
SystemTheme = ThemeHelpers::GetSystemTheme();
|
||||
dwThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_checkTheme, this, 0, &dwThreadId);
|
||||
}
|
||||
~ThemeListener()
|
||||
@@ -25,20 +23,12 @@ public:
|
||||
}
|
||||
|
||||
Theme AppTheme;
|
||||
Theme SystemTheme;
|
||||
void ThemeListener::AddChangedHandler(THEME_HANDLE handle);
|
||||
void ThemeListener::DelChangedHandler(THEME_HANDLE handle);
|
||||
void ThemeListener::AddAppThemeChangedHandler(THEME_HANDLE handle);
|
||||
void ThemeListener::DelAppThemeChangedHandler(THEME_HANDLE handle);
|
||||
void ThemeListener::AddSystemThemeChangedHandler(THEME_HANDLE handle);
|
||||
void ThemeListener::DelSystemThemeChangedHandler(THEME_HANDLE handle);
|
||||
void CheckTheme();
|
||||
|
||||
private:
|
||||
HANDLE dwThreadHandle;
|
||||
DWORD dwThreadId;
|
||||
std::vector<THEME_HANDLE> handles;
|
||||
std::vector<THEME_HANDLE> appThemeHandles;
|
||||
std::vector<THEME_HANDLE> systemThemeHandles;
|
||||
mutable std::mutex handlesMutex;
|
||||
};
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <appxpackaging.h>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
@@ -338,30 +337,6 @@ namespace package
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by package version in descending order (newest first)
|
||||
std::sort(matchedFiles.begin(), matchedFiles.end(), [](const std::wstring& a, const std::wstring& b) {
|
||||
std::wstring nameA, nameB;
|
||||
PACKAGE_VERSION versionA{}, versionB{};
|
||||
|
||||
bool gotA = GetPackageNameAndVersionFromAppx(a, nameA, versionA);
|
||||
bool gotB = GetPackageNameAndVersionFromAppx(b, nameB, versionB);
|
||||
|
||||
// Files that failed to parse go to the end
|
||||
if (!gotA)
|
||||
return false;
|
||||
if (!gotB)
|
||||
return true;
|
||||
|
||||
// Compare versions: Major, Minor, Build, Revision (descending)
|
||||
if (versionA.Major != versionB.Major)
|
||||
return versionA.Major > versionB.Major;
|
||||
if (versionA.Minor != versionB.Minor)
|
||||
return versionA.Minor > versionB.Minor;
|
||||
if (versionA.Build != versionB.Build)
|
||||
return versionA.Build > versionB.Build;
|
||||
return versionA.Revision > versionB.Revision;
|
||||
});
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
|
||||
@@ -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
@@ -53,7 +53,7 @@ public partial class ContextMenuViewModel : ObservableObject,
|
||||
{
|
||||
if (SelectedItem is not null)
|
||||
{
|
||||
if (SelectedItem.PrimaryCommand is not null || SelectedItem.HasMoreCommands)
|
||||
if (SelectedItem.MoreCommands.Count() > 1)
|
||||
{
|
||||
ContextMenuStack.Clear();
|
||||
PushContextStack(SelectedItem.AllCommands);
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -44,14 +44,13 @@ public sealed partial class CommandBar : UserControl,
|
||||
|
||||
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 +65,6 @@ public sealed partial class CommandBar : UserControl,
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is invoked from a specific element
|
||||
_ = DispatcherQueue.TryEnqueue(
|
||||
() =>
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
@@ -117,16 +98,18 @@ public sealed partial class AppListItem : ListItem
|
||||
}
|
||||
else
|
||||
{
|
||||
// do nothing if we fail to load an icon.
|
||||
// Logging it would be too NOISY, there's really no need.
|
||||
if (!string.IsNullOrEmpty(_app.IcoPath))
|
||||
try
|
||||
{
|
||||
heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false);
|
||||
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath, true);
|
||||
if (stream is not null)
|
||||
{
|
||||
heroImage = IconInfo.FromStream(stream);
|
||||
}
|
||||
}
|
||||
|
||||
if (heroImage == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||
catch (Exception)
|
||||
{
|
||||
heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
|
||||
// do nothing if we fail to load an icon.
|
||||
// Logging it would be too NOISY, there's really no need.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,18 +132,25 @@ public sealed partial class AppListItem : ListItem
|
||||
|
||||
if (useThumbnails)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_app.IcoPath))
|
||||
try
|
||||
{
|
||||
icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
|
||||
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
|
||||
if (stream is not null)
|
||||
{
|
||||
icon = IconInfo.FromStream(stream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug($"Failed to load icon for {AppIdentifier}:\n{ex}");
|
||||
}
|
||||
|
||||
if (icon == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||
{
|
||||
icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true);
|
||||
}
|
||||
icon = icon ?? new IconInfo(_app.IcoPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
icon = new IconInfo(_app.IcoPath);
|
||||
}
|
||||
|
||||
icon ??= new IconInfo(_app.IcoPath);
|
||||
|
||||
return icon;
|
||||
}
|
||||
@@ -193,25 +183,4 @@ public sealed partial class AppListItem : ListItem
|
||||
|
||||
return newCommands.ToArray();
|
||||
}
|
||||
|
||||
private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo);
|
||||
if (stream is not null)
|
||||
{
|
||||
return IconInfo.FromStream(stream);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logOnFailure)
|
||||
{
|
||||
Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -30,38 +29,9 @@ internal sealed partial class RunAsUserCommand : InvokableCommand
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// Use ActionRunner helper process to work around WinUI3/MSIX packaging limitation.
|
||||
// When running from a packaged app, ShellExecute with the "runas user" verb causes
|
||||
// CredentialUIBroker.exe to spawn infinitely without showing the credential dialog.
|
||||
// ActionRunner runs outside the MSIX container, so it can properly invoke the credential UI.
|
||||
var actionRunnerPath = ActionRunnerHelper.GetActionRunnerPath();
|
||||
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.OtherUser);
|
||||
|
||||
if (string.IsNullOrEmpty(actionRunnerPath))
|
||||
{
|
||||
// Fallback to direct Process.Start if ActionRunner is not found
|
||||
// This may not work in packaged context, but provides a fallback for development
|
||||
ExtensionHost.LogMessage($"ActionRunner not found, falling back to direct Process.Start for '{target}'");
|
||||
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.OtherUser);
|
||||
Process.Start(info);
|
||||
return;
|
||||
}
|
||||
|
||||
var args = $"-run-as-user -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 different user via ActionRunner");
|
||||
Process.Start(processInfo);
|
||||
Process.Start(info);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1052,6 +1052,9 @@ public class Win32Program : IProgram
|
||||
app.FullPath) :
|
||||
app.IcoPath;
|
||||
|
||||
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
|
||||
app.FullPath :
|
||||
icoPath;
|
||||
return new AppItem()
|
||||
{
|
||||
Name = app.Name,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to locate and invoke the PowerToys ActionRunner executable.
|
||||
/// ActionRunner is used to work around WinUI3/MSIX packaging limitations where
|
||||
/// certain shell operations (like "Run as different user" or "Run as administrator")
|
||||
/// don't work properly from within a packaged app context.
|
||||
/// </summary>
|
||||
internal static class ActionRunnerHelper
|
||||
{
|
||||
private const string ActionRunnerExeName = "PowerToys.ActionRunner.exe";
|
||||
|
||||
private static string? _cachedPath;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path to the ActionRunner executable.
|
||||
/// </summary>
|
||||
/// <returns>The full path to ActionRunner.exe, or null if not found.</returns>
|
||||
public static string? GetActionRunnerPath()
|
||||
{
|
||||
if (_cachedPath != null)
|
||||
{
|
||||
return _cachedPath;
|
||||
}
|
||||
|
||||
_cachedPath = FindActionRunnerPath();
|
||||
return _cachedPath;
|
||||
}
|
||||
|
||||
private static string? FindActionRunnerPath()
|
||||
{
|
||||
// Use the standard PowerToys path resolver to find the installation directory.
|
||||
// This handles registry lookups for installed versions and debug builds correctly.
|
||||
var installPath = PowerToysPathResolver.GetPowerToysInstallPath();
|
||||
if (!string.IsNullOrEmpty(installPath))
|
||||
{
|
||||
var actionRunnerPath = Path.Combine(installPath, ActionRunnerExeName);
|
||||
if (File.Exists(actionRunnerPath))
|
||||
{
|
||||
return actionRunnerPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -197,10 +197,16 @@ namespace Peek.UI
|
||||
|
||||
ViewModel.Initialize(selectedItem);
|
||||
|
||||
// If no files were found (e.g., user is typing in rename/search box, or in virtual folders),
|
||||
// don't show anything - just return silently to avoid stealing focus
|
||||
// If no files were found (e.g., in virtual folders like Home/Recent), show an error
|
||||
if (ViewModel.CurrentItem == null)
|
||||
{
|
||||
Logger.LogInfo("Peek: No files found to preview, showing error.");
|
||||
var errorMessage = ResourceLoaderInstance.ResourceLoader.GetString("NoFilesSelected");
|
||||
ViewModel.ShowError(errorMessage);
|
||||
|
||||
// Still show the window so user can see the warning
|
||||
this.Show();
|
||||
WindowHelpers.BringToForeground(this.GetWindowHandle());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,27 +33,23 @@ static std::wstring SanitizeAndNormalize(const std::wstring& input)
|
||||
|
||||
// Normalize to NFC (Precomposed).
|
||||
// Get the size needed for the normalized string, including null terminator.
|
||||
int sizeEstimate = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
|
||||
if (sizeEstimate <= 0)
|
||||
int size = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
|
||||
if (size <= 0)
|
||||
{
|
||||
return sanitized; // Return unaltered if normalization fails.
|
||||
}
|
||||
|
||||
// Perform the normalization.
|
||||
std::wstring normalized;
|
||||
normalized.resize(sizeEstimate);
|
||||
int actualSize = NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], sizeEstimate);
|
||||
normalized.resize(size);
|
||||
NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], size);
|
||||
|
||||
if (actualSize <= 0)
|
||||
// Remove the explicit null terminator added by NormalizeString.
|
||||
if (!normalized.empty() && normalized.back() == L'\0')
|
||||
{
|
||||
// Normalization failed, return sanitized string.
|
||||
return sanitized;
|
||||
normalized.pop_back();
|
||||
}
|
||||
|
||||
// Resize to actual size minus the null terminator.
|
||||
// actualSize includes the null terminator when input length is -1.
|
||||
normalized.resize(static_cast<size_t>(actualSize) - 1);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
|
||||
@@ -695,38 +695,6 @@ TEST_METHOD(VerifyUnicodeAndWhitespaceNormalizationRegex)
|
||||
VerifyNormalizationHelper(UseRegularExpressions);
|
||||
}
|
||||
|
||||
TEST_METHOD(VerifyRegexMetacharacterDollarSign)
|
||||
{
|
||||
CComPtr<IPowerRenameRegEx> renameRegEx;
|
||||
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
|
||||
DWORD flags = UseRegularExpressions;
|
||||
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
|
||||
|
||||
PWSTR result = nullptr;
|
||||
Assert::IsTrue(renameRegEx->PutSearchTerm(L"$") == S_OK);
|
||||
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"_end") == S_OK);
|
||||
unsigned long index = {};
|
||||
Assert::IsTrue(renameRegEx->Replace(L"test.txt", &result, index) == S_OK);
|
||||
Assert::AreEqual(L"test.txt_end", result);
|
||||
CoTaskMemFree(result);
|
||||
}
|
||||
|
||||
TEST_METHOD(VerifyRegexMetacharacterCaret)
|
||||
{
|
||||
CComPtr<IPowerRenameRegEx> renameRegEx;
|
||||
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
|
||||
DWORD flags = UseRegularExpressions;
|
||||
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
|
||||
|
||||
PWSTR result = nullptr;
|
||||
Assert::IsTrue(renameRegEx->PutSearchTerm(L"^") == S_OK);
|
||||
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"start_") == S_OK);
|
||||
unsigned long index = {};
|
||||
Assert::IsTrue(renameRegEx->Replace(L"test.txt", &result, index) == S_OK);
|
||||
Assert::AreEqual(L"start_test.txt", result);
|
||||
CoTaskMemFree(result);
|
||||
}
|
||||
|
||||
#ifndef TESTS_PARTIAL
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,4 @@
|
||||
namespace cmdArg
|
||||
{
|
||||
const inline wchar_t* RUN_NONELEVATED = L"-run-non-elevated";
|
||||
const inline wchar_t* RUN_AS_USER = L"-run-as-user";
|
||||
const inline wchar_t* RUN_AS_ADMIN = L"-run-as-admin";
|
||||
}
|
||||
|
||||
@@ -173,10 +173,6 @@
|
||||
<value>Settings\tDouble-click</value>
|
||||
<comment>Don't localize "\t" as that is what separates the click portion to be right aligned in the menu.</comment>
|
||||
</data>
|
||||
<data name="SETTINGS_MENU_TEXT_LEFTCLICK" xml:space="preserve">
|
||||
<value>Settings\tLeft-click</value>
|
||||
<comment>Don't localize "\t" as that is what separates the click portion to be right aligned in the menu. This is shown when Quick Access is disabled.</comment>
|
||||
</data>
|
||||
<data name="DOCUMENTATION_MENU_TEXT" xml:space="preserve">
|
||||
<value>Documentation</value>
|
||||
</data>
|
||||
|
||||
@@ -81,36 +81,3 @@ void Trace::UpdateDownloadCompleted(bool success, const std::wstring& version)
|
||||
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
|
||||
void Trace::TrayIconLeftClick(bool quickAccessEnabled)
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"TrayIcon_LeftClick",
|
||||
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
|
||||
void Trace::TrayIconDoubleClick(bool quickAccessEnabled)
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"TrayIcon_DoubleClick",
|
||||
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
|
||||
void Trace::TrayIconRightClick(bool quickAccessEnabled)
|
||||
{
|
||||
TraceLoggingWriteWrapper(
|
||||
g_hProvider,
|
||||
"TrayIcon_RightClick",
|
||||
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
|
||||
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||
}
|
||||
|
||||
@@ -13,9 +13,4 @@ public:
|
||||
// Auto-update telemetry
|
||||
static void UpdateCheckCompleted(bool success, bool updateAvailable, const std::wstring& fromVersion, const std::wstring& toVersion);
|
||||
static void UpdateDownloadCompleted(bool success, const std::wstring& version);
|
||||
|
||||
// Tray icon interaction telemetry
|
||||
static void TrayIconLeftClick(bool quickAccessEnabled);
|
||||
static void TrayIconDoubleClick(bool quickAccessEnabled);
|
||||
static void TrayIconRightClick(bool quickAccessEnabled);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "centralized_kb_hook.h"
|
||||
#include "quick_access_host.h"
|
||||
#include "hotkey_conflict_detector.h"
|
||||
#include "trace.h"
|
||||
#include <Windows.h>
|
||||
|
||||
#include <common/utils/resources.h>
|
||||
@@ -15,7 +14,6 @@
|
||||
#include <common/logger/logger.h>
|
||||
#include <common/utils/elevation.h>
|
||||
#include <common/Themes/theme_listener.h>
|
||||
#include <common/Themes/theme_helpers.h>
|
||||
#include "bug_report.h"
|
||||
|
||||
namespace
|
||||
@@ -41,7 +39,6 @@ namespace
|
||||
bool double_click_timer_running = false;
|
||||
bool double_clicked = false;
|
||||
POINT tray_icon_click_point;
|
||||
std::optional<bool> last_quick_access_state; // Track the last known Quick Access state
|
||||
|
||||
static ThemeListener theme_listener;
|
||||
static bool theme_adaptive_enabled = false;
|
||||
@@ -132,9 +129,6 @@ void click_timer_elapsed()
|
||||
double_click_timer_running = false;
|
||||
if (!double_clicked)
|
||||
{
|
||||
// Log telemetry for single click (confirmed it's not a double click)
|
||||
Trace::TrayIconLeftClick(get_general_settings().enableQuickAccess);
|
||||
|
||||
if (get_general_settings().enableQuickAccess)
|
||||
{
|
||||
open_quick_access_flyout_window();
|
||||
@@ -200,21 +194,6 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
|
||||
case WM_RBUTTONUP:
|
||||
case WM_CONTEXTMENU:
|
||||
{
|
||||
bool quick_access_enabled = get_general_settings().enableQuickAccess;
|
||||
|
||||
// Log telemetry
|
||||
Trace::TrayIconRightClick(quick_access_enabled);
|
||||
|
||||
// Reload menu if Quick Access state has changed or is first time
|
||||
if (h_menu && (!last_quick_access_state.has_value() || quick_access_enabled != last_quick_access_state.value()))
|
||||
{
|
||||
DestroyMenu(h_menu);
|
||||
h_menu = nullptr;
|
||||
h_sub_menu = nullptr;
|
||||
}
|
||||
|
||||
last_quick_access_state = quick_access_enabled;
|
||||
|
||||
if (!h_menu)
|
||||
{
|
||||
h_menu = LoadMenu(reinterpret_cast<HINSTANCE>(&__ImageBase), MAKEINTRESOURCE(ID_TRAY_MENU));
|
||||
@@ -222,39 +201,17 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
|
||||
if (h_menu)
|
||||
{
|
||||
static std::wstring settings_menuitem_label = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT);
|
||||
static std::wstring settings_menuitem_label_leftclick = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT_LEFTCLICK);
|
||||
static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_MENU_TEXT);
|
||||
static std::wstring submit_bug_menuitem_label = GET_RESOURCE_STRING(IDS_SUBMIT_BUG_TEXT);
|
||||
static std::wstring documentation_menuitem_label = GET_RESOURCE_STRING(IDS_DOCUMENTATION_MENU_TEXT);
|
||||
static std::wstring quick_access_menuitem_label = GET_RESOURCE_STRING(IDS_QUICK_ACCESS_MENU_TEXT);
|
||||
|
||||
// Update Settings menu text based on Quick Access state
|
||||
if (quick_access_enabled)
|
||||
{
|
||||
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
|
||||
}
|
||||
else
|
||||
{
|
||||
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label_leftclick.data());
|
||||
}
|
||||
|
||||
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
|
||||
change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_menuitem_label.data());
|
||||
change_menu_item_text(ID_REPORT_BUG_COMMAND, submit_bug_menuitem_label.data());
|
||||
bool bug_report_disabled = is_bug_report_running();
|
||||
EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (bug_report_disabled ? MF_GRAYED : MF_ENABLED));
|
||||
change_menu_item_text(ID_DOCUMENTATION_MENU_COMMAND, documentation_menuitem_label.data());
|
||||
change_menu_item_text(ID_QUICK_ACCESS_MENU_COMMAND, quick_access_menuitem_label.data());
|
||||
|
||||
// Hide or show Quick Access menu item based on setting
|
||||
if (!h_sub_menu)
|
||||
{
|
||||
h_sub_menu = GetSubMenu(h_menu, 0);
|
||||
}
|
||||
if (!quick_access_enabled)
|
||||
{
|
||||
// Remove Quick Access menu item when disabled
|
||||
DeleteMenu(h_sub_menu, ID_QUICK_ACCESS_MENU_COMMAND, MF_BYCOMMAND);
|
||||
}
|
||||
}
|
||||
if (!h_sub_menu)
|
||||
{
|
||||
@@ -285,9 +242,6 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
|
||||
}
|
||||
case WM_LBUTTONDBLCLK:
|
||||
{
|
||||
// Log telemetry
|
||||
Trace::TrayIconDoubleClick(get_general_settings().enableQuickAccess);
|
||||
|
||||
double_clicked = true;
|
||||
open_settings_window(std::nullopt);
|
||||
break;
|
||||
@@ -339,7 +293,7 @@ static void handle_theme_change()
|
||||
{
|
||||
if (theme_adaptive_enabled)
|
||||
{
|
||||
tray_icon_data.hIcon = get_icon(ThemeHelpers::GetSystemTheme());
|
||||
tray_icon_data.hIcon = get_icon(theme_listener.AppTheme);
|
||||
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
||||
}
|
||||
}
|
||||
@@ -356,7 +310,7 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive)
|
||||
{
|
||||
theme_adaptive_enabled = theme_adaptive;
|
||||
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
HICON const icon = theme_adaptive ? get_icon(ThemeHelpers::GetSystemTheme()) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
if (icon)
|
||||
{
|
||||
UINT id_tray_icon = 1;
|
||||
@@ -403,7 +357,7 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive)
|
||||
ChangeWindowMessageFilterEx(hwnd, WM_COMMAND, MSGFLT_ALLOW, nullptr);
|
||||
|
||||
tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE;
|
||||
theme_listener.AddSystemThemeChangedHandler(&handle_theme_change);
|
||||
theme_listener.AddChangedHandler(&handle_theme_change);
|
||||
|
||||
// Register callback to update bug report menu item status
|
||||
BugReportManager::instance().register_callback([](bool isRunning) {
|
||||
@@ -435,7 +389,7 @@ void set_tray_icon_theme_adaptive(bool theme_adaptive)
|
||||
|
||||
if (theme_adaptive)
|
||||
{
|
||||
icon = get_icon(ThemeHelpers::GetSystemTheme());
|
||||
icon = get_icon(theme_listener.AppTheme);
|
||||
if (!icon)
|
||||
{
|
||||
Logger::warn(L"set_tray_icon_theme_adaptive: Failed to load theme adaptive icon, falling back to default");
|
||||
|
||||
@@ -34,21 +34,21 @@ public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteA
|
||||
public AdvancedPasteAdditionalAction PasteAsTxtFile
|
||||
{
|
||||
get => _pasteAsTxtFile;
|
||||
init => Set(ref _pasteAsTxtFile, value ?? new());
|
||||
init => Set(ref _pasteAsTxtFile, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.PasteAsPngFile)]
|
||||
public AdvancedPasteAdditionalAction PasteAsPngFile
|
||||
{
|
||||
get => _pasteAsPngFile;
|
||||
init => Set(ref _pasteAsPngFile, value ?? new());
|
||||
init => Set(ref _pasteAsPngFile, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.PasteAsHtmlFile)]
|
||||
public AdvancedPasteAdditionalAction PasteAsHtmlFile
|
||||
{
|
||||
get => _pasteAsHtmlFile;
|
||||
init => Set(ref _pasteAsHtmlFile, value ?? new());
|
||||
init => Set(ref _pasteAsHtmlFile, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
|
||||
@@ -93,11 +93,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
|
||||
[JsonPropertyName("custom-actions")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteCustomActions CustomActions { get; set; }
|
||||
public AdvancedPasteCustomActions CustomActions { get; init; }
|
||||
|
||||
[JsonPropertyName("additional-actions")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
public AdvancedPasteAdditionalActions AdditionalActions { get; set; }
|
||||
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
||||
|
||||
[JsonPropertyName("paste-ai-configuration")]
|
||||
[CmdConfigureIgnoreAttribute]
|
||||
|
||||
@@ -32,14 +32,14 @@ public sealed class AdvancedPasteTranscodeAction : Observable, IAdvancedPasteAct
|
||||
public AdvancedPasteAdditionalAction TranscodeToMp3
|
||||
{
|
||||
get => _transcodeToMp3;
|
||||
init => Set(ref _transcodeToMp3, value ?? new());
|
||||
init => Set(ref _transcodeToMp3, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
|
||||
public AdvancedPasteAdditionalAction TranscodeToMp4
|
||||
{
|
||||
get => _transcodeToMp4;
|
||||
init => Set(ref _transcodeToMp4, value ?? new());
|
||||
init => Set(ref _transcodeToMp4, value);
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
|
||||
259
src/settings-ui/Settings.UI.Library/AsyncSettingsRepository`1.cs
Normal file
259
src/settings-ui/Settings.UI.Library/AsyncSettingsRepository`1.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
/// <summary>
|
||||
/// Async settings repository implementation with caching and thread-safe access.
|
||||
/// Provides non-blocking settings operations suitable for UI applications.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
public sealed class AsyncSettingsRepository<T> : IAsyncSettingsRepository<T>, IDisposable
|
||||
where T : class, ISettingsConfig, new()
|
||||
{
|
||||
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
private readonly string _moduleName;
|
||||
private readonly string _fileName;
|
||||
|
||||
private T _cachedSettings;
|
||||
private FileSystemWatcher _watcher;
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<T> SettingsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AsyncSettingsRepository{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="settingsUtils">The settings utilities instance.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
public AsyncSettingsRepository(ISettingsUtils settingsUtils, string fileName = "settings.json")
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
_fileName = fileName;
|
||||
|
||||
// Get module name from type
|
||||
var settingsItem = new T();
|
||||
_moduleName = settingsItem.GetModuleName();
|
||||
|
||||
InitializeWatcher();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T SettingsConfig
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_cachedSettings == null)
|
||||
{
|
||||
_semaphore.Wait();
|
||||
try
|
||||
{
|
||||
if (_cachedSettings == null)
|
||||
{
|
||||
_cachedSettings = LoadSettingsInternal();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
return _cachedSettings;
|
||||
}
|
||||
|
||||
private set => _cachedSettings = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<T> GetSettingsAsync(bool forceRefresh = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!forceRefresh && _cachedSettings != null)
|
||||
{
|
||||
return _cachedSettings;
|
||||
}
|
||||
|
||||
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!forceRefresh && _cachedSettings != null)
|
||||
{
|
||||
return _cachedSettings;
|
||||
}
|
||||
|
||||
_cachedSettings = await Task.Run(() => LoadSettingsInternal(), cancellationToken).ConfigureAwait(false);
|
||||
return _cachedSettings;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask SaveSettingsAsync(T settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
// Temporarily stop watching to avoid self-triggered events
|
||||
StopWatching();
|
||||
|
||||
await Task.Run(
|
||||
() => _settingsUtils.SaveSettings(settings.ToJsonString(), _moduleName, _fileName),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_cachedSettings = settings;
|
||||
}
|
||||
finally
|
||||
{
|
||||
StartWatching();
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ReloadSettings()
|
||||
{
|
||||
_semaphore.Wait();
|
||||
try
|
||||
{
|
||||
var newSettings = LoadSettingsInternal();
|
||||
if (newSettings != null)
|
||||
{
|
||||
_cachedSettings = newSettings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to reload settings for {_moduleName}", ex);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> ReloadSettingsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var newSettings = await Task.Run(() => LoadSettingsInternal(), cancellationToken).ConfigureAwait(false);
|
||||
if (newSettings != null)
|
||||
{
|
||||
_cachedSettings = newSettings;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to reload settings for {_moduleName}", ex);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void StopWatching()
|
||||
{
|
||||
if (_watcher != null)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void StartWatching()
|
||||
{
|
||||
if (_watcher != null)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
}
|
||||
|
||||
private T LoadSettingsInternal()
|
||||
{
|
||||
try
|
||||
{
|
||||
return _settingsUtils.GetSettingsOrDefault<T>(_moduleName, _fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load settings for {_moduleName}", ex);
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeWatcher()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = _settingsUtils.GetSettingsFilePath(_moduleName, _fileName);
|
||||
var directory = Path.GetDirectoryName(filePath);
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
_watcher = new FileSystemWatcher(directory, fileName);
|
||||
_watcher.NotifyFilter = NotifyFilters.LastWrite;
|
||||
_watcher.Changed += OnWatcherChanged;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to initialize settings watcher for {typeof(T).Name}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnWatcherChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
// Wait a bit for the file write to complete and retry if needed
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
if (await ReloadSettingsAsync().ConfigureAwait(false))
|
||||
{
|
||||
SettingsChanged?.Invoke(_cachedSettings);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_watcher?.Dispose();
|
||||
_semaphore?.Dispose();
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,15 +22,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("disable_wrap_during_drag")]
|
||||
public BoolProperty DisableWrapDuringDrag { get; set; }
|
||||
|
||||
[JsonPropertyName("wrap_mode")]
|
||||
public IntProperty WrapMode { get; set; }
|
||||
|
||||
public CursorWrapProperties()
|
||||
{
|
||||
ActivationShortcut = DefaultActivationShortcut;
|
||||
AutoActivate = new BoolProperty(false);
|
||||
DisableWrapDuringDrag = new BoolProperty(true);
|
||||
WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,16 +47,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
// This can be utilized in the future if the settings.json file is to be modified/deleted.
|
||||
public bool UpgradeSettingsConfiguration()
|
||||
{
|
||||
bool settingsUpgraded = false;
|
||||
|
||||
// Add WrapMode property if it doesn't exist (for users upgrading from older versions)
|
||||
if (Properties.WrapMode == null)
|
||||
{
|
||||
Properties.WrapMode = new IntProperty(0); // Default to Both
|
||||
settingsUpgraded = true;
|
||||
}
|
||||
|
||||
return settingsUpgraded;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for asynchronous settings repository operations.
|
||||
/// Provides non-blocking access to settings with caching support.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
public interface IAsyncSettingsRepository<T>
|
||||
where T : class, ISettingsConfig, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when the settings have been externally changed (e.g., by another process).
|
||||
/// </summary>
|
||||
event Action<T> SettingsChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current cached settings synchronously.
|
||||
/// Returns the cached value immediately, or loads if not cached.
|
||||
/// </summary>
|
||||
T SettingsConfig { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the settings asynchronously with optional refresh from disk.
|
||||
/// </summary>
|
||||
/// <param name="forceRefresh">If true, bypasses cache and reads from disk.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The settings object.</returns>
|
||||
ValueTask<T> GetSettingsAsync(bool forceRefresh = false, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the settings asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to save.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task representing the save operation.</returns>
|
||||
ValueTask SaveSettingsAsync(T settings, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the settings from disk, updating the cache.
|
||||
/// </summary>
|
||||
/// <returns>True if the reload was successful.</returns>
|
||||
bool ReloadSettings();
|
||||
|
||||
/// <summary>
|
||||
/// Reloads the settings from disk asynchronously, updating the cache.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>True if the reload was successful.</returns>
|
||||
ValueTask<bool> ReloadSettingsAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Stops watching for external file changes.
|
||||
/// </summary>
|
||||
void StopWatching();
|
||||
|
||||
/// <summary>
|
||||
/// Starts watching for external file changes.
|
||||
/// </summary>
|
||||
void StartWatching();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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.PowerToys.Settings.UI.Library.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for settings utility operations to enable dependency injection and testability.
|
||||
/// </summary>
|
||||
public interface ISettingsUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a settings file exists.
|
||||
/// </summary>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>True if the settings file exists.</returns>
|
||||
bool SettingsExists(string powertoy = "", string fileName = "settings.json");
|
||||
|
||||
/// <summary>
|
||||
/// Deletes settings for the specified module.
|
||||
/// </summary>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
void DeleteSettings(string powertoy = "");
|
||||
|
||||
/// <summary>
|
||||
/// Gets settings for the specified module.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>The deserialized settings object.</returns>
|
||||
T GetSettings<T>(string powertoy = "", string fileName = "settings.json")
|
||||
where T : ISettingsConfig, new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets settings for the specified module, or returns default if not found.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The settings type.</typeparam>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>The deserialized settings object or default.</returns>
|
||||
T GetSettingsOrDefault<T>(string powertoy = "", string fileName = "settings.json")
|
||||
where T : ISettingsConfig, new();
|
||||
|
||||
/// <summary>
|
||||
/// Saves settings to a JSON file.
|
||||
/// </summary>
|
||||
/// <param name="jsonSettings">The JSON settings string.</param>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
void SaveSettings(string jsonSettings, string powertoy = "", string fileName = "settings.json");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file path to the settings file.
|
||||
/// </summary>
|
||||
/// <param name="powertoy">The module name.</param>
|
||||
/// <param name="fileName">The settings file name.</param>
|
||||
/// <returns>The full path to the settings file.</returns>
|
||||
string GetSettingsFilePath(string powertoy = "", string fileName = "settings.json");
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
@@ -77,12 +78,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
}
|
||||
}
|
||||
|
||||
private void Watcher_Changed(object sender, FileSystemEventArgs e)
|
||||
private async void Watcher_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
// Wait a bit for the file write to complete and retry if needed
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
if (ReloadSettings())
|
||||
{
|
||||
SettingsChanged?.Invoke(SettingsConfig);
|
||||
|
||||
@@ -16,7 +16,7 @@ using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
// Some functions are marked as virtual to allow mocking in unit tests.
|
||||
public class SettingsUtils
|
||||
public class SettingsUtils : ISettingsUtils
|
||||
{
|
||||
public const string DefaultFileName = "settings.json";
|
||||
private const string DefaultModuleName = "";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when backup/restore operations complete.
|
||||
/// </summary>
|
||||
public sealed class BackupRestoreCompletedMessage : ValueChangedMessage<BackupRestoreCompletedMessage.ResultData>
|
||||
{
|
||||
public BackupRestoreCompletedMessage(bool success, string message, bool isBackup)
|
||||
: base(new ResultData(success, message, isBackup))
|
||||
{
|
||||
}
|
||||
|
||||
public record ResultData(bool Success, string Message, bool IsBackup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Messaging.Messages;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when general settings are updated.
|
||||
/// </summary>
|
||||
public sealed class GeneralSettingsUpdatedMessage : ValueChangedMessage<GeneralSettings>
|
||||
{
|
||||
public GeneralSettingsUpdatedMessage(GeneralSettings settings)
|
||||
: base(settings)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when hotkey conflicts are detected.
|
||||
/// </summary>
|
||||
public sealed class HotkeyConflictDetectedMessage : ValueChangedMessage<HotkeyConflictDetectedMessage.ConflictData>
|
||||
{
|
||||
public HotkeyConflictDetectedMessage(string moduleName, string hotkeyDescription, bool isSystemConflict)
|
||||
: base(new ConflictData(moduleName, hotkeyDescription, isSystemConflict))
|
||||
{
|
||||
}
|
||||
|
||||
public record ConflictData(string ModuleName, string HotkeyDescription, bool IsSystemConflict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when an IPC message is received from the PowerToys runner.
|
||||
/// </summary>
|
||||
public sealed class IPCMessageReceivedMessage : ValueChangedMessage<string>
|
||||
{
|
||||
public IPCMessageReceivedMessage(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when a module's enabled state changes.
|
||||
/// </summary>
|
||||
public sealed class ModuleEnabledChangedMessage : ValueChangedMessage<ModuleEnabledChangedMessage.ModuleStateData>
|
||||
{
|
||||
public ModuleEnabledChangedMessage(string moduleName, bool isEnabled)
|
||||
: base(new ModuleStateData(moduleName, isEnabled))
|
||||
{
|
||||
}
|
||||
|
||||
public record ModuleStateData(string ModuleName, bool IsEnabled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent to request navigation to a specific page.
|
||||
/// </summary>
|
||||
public sealed class NavigateToPageMessage : ValueChangedMessage<System.Type>
|
||||
{
|
||||
public NavigateToPageMessage(System.Type pageType)
|
||||
: base(pageType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent to request a restart of PowerToys.
|
||||
/// </summary>
|
||||
public sealed class RestartRequestedMessage
|
||||
{
|
||||
public bool MaintainElevation { get; }
|
||||
|
||||
public RestartRequestedMessage(bool maintainElevation = true)
|
||||
{
|
||||
MaintainElevation = maintainElevation;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/settings-ui/Settings.UI/Messages/SettingsSavedMessage.cs
Normal file
21
src/settings-ui/Settings.UI/Messages/SettingsSavedMessage.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when settings are saved.
|
||||
/// </summary>
|
||||
public sealed class SettingsSavedMessage : ValueChangedMessage<SettingsSavedMessage.SettingsSaveData>
|
||||
{
|
||||
public SettingsSavedMessage(string moduleName)
|
||||
: base(new SettingsSaveData(moduleName))
|
||||
{
|
||||
}
|
||||
|
||||
public record SettingsSaveData(string ModuleName);
|
||||
}
|
||||
}
|
||||
19
src/settings-ui/Settings.UI/Messages/ThemeChangedMessage.cs
Normal file
19
src/settings-ui/Settings.UI/Messages/ThemeChangedMessage.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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.Messaging.Messages;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Message sent when the application theme changes.
|
||||
/// </summary>
|
||||
public sealed class ThemeChangedMessage : ValueChangedMessage<string>
|
||||
{
|
||||
public ThemeChangedMessage(string themeName)
|
||||
: base(themeName)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.OpacityMaskView" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Segmented" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.Primitives" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
|
||||
|
||||
131
src/settings-ui/Settings.UI/Services/INavigationService.cs
Normal file
131
src/settings-ui/Settings.UI/Services/INavigationService.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media.Animation;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for navigation services to enable testability and abstraction
|
||||
/// from the static NavigationService class.
|
||||
/// </summary>
|
||||
public interface INavigationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when navigation has completed.
|
||||
/// </summary>
|
||||
event NavigatedEventHandler Navigated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when navigation has failed.
|
||||
/// </summary>
|
||||
event NavigationFailedEventHandler NavigationFailed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the navigation frame.
|
||||
/// </summary>
|
||||
Frame Frame { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether navigation can go back.
|
||||
/// </summary>
|
||||
bool CanGoBack { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether navigation can go forward.
|
||||
/// </summary>
|
||||
bool CanGoForward { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Navigates back in the navigation stack.
|
||||
/// </summary>
|
||||
/// <returns>True if navigation was successful.</returns>
|
||||
bool GoBack();
|
||||
|
||||
/// <summary>
|
||||
/// Navigates forward in the navigation stack.
|
||||
/// </summary>
|
||||
void GoForward();
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the specified page type.
|
||||
/// </summary>
|
||||
/// <param name="pageType">The type of page to navigate to.</param>
|
||||
/// <param name="parameter">Optional navigation parameter.</param>
|
||||
/// <param name="infoOverride">Optional navigation transition override.</param>
|
||||
/// <returns>True if navigation was successful.</returns>
|
||||
bool Navigate(Type pageType, object parameter = null, NavigationTransitionInfo infoOverride = null);
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the specified page type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of page to navigate to.</typeparam>
|
||||
/// <param name="parameter">Optional navigation parameter.</param>
|
||||
/// <param name="infoOverride">Optional navigation transition override.</param>
|
||||
/// <returns>True if navigation was successful.</returns>
|
||||
bool Navigate<T>(object parameter = null, NavigationTransitionInfo infoOverride = null)
|
||||
where T : Page;
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a page of the specified type is selected.
|
||||
/// </summary>
|
||||
/// <param name="pageType">The type of page to ensure is selected.</param>
|
||||
void EnsurePageIsSelected(Type pageType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper around the static NavigationService to implement INavigationService.
|
||||
/// Allows for gradual migration and testability.
|
||||
/// </summary>
|
||||
public class NavigationServiceWrapper : INavigationService
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public event NavigatedEventHandler Navigated
|
||||
{
|
||||
add => NavigationService.Navigated += value;
|
||||
remove => NavigationService.Navigated -= value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event NavigationFailedEventHandler NavigationFailed
|
||||
{
|
||||
add => NavigationService.NavigationFailed += value;
|
||||
remove => NavigationService.NavigationFailed -= value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Frame Frame
|
||||
{
|
||||
get => NavigationService.Frame;
|
||||
set => NavigationService.Frame = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanGoBack => NavigationService.CanGoBack;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool CanGoForward => NavigationService.CanGoForward;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool GoBack() => NavigationService.GoBack();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void GoForward() => NavigationService.GoForward();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Navigate(Type pageType, object parameter = null, NavigationTransitionInfo infoOverride = null)
|
||||
=> NavigationService.Navigate(pageType, parameter, infoOverride);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Navigate<T>(object parameter = null, NavigationTransitionInfo infoOverride = null)
|
||||
where T : Page
|
||||
=> NavigationService.Navigate<T>(parameter, infoOverride);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void EnsurePageIsSelected(Type pageType) => NavigationService.EnsurePageIsSelected(pageType);
|
||||
}
|
||||
}
|
||||
@@ -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.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for configuring services in the dependency injection container.
|
||||
/// </summary>
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds core Settings UI services to the service collection.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddSettingsServices(this IServiceCollection services)
|
||||
{
|
||||
// Register singleton services
|
||||
services.AddSingleton<ISettingsUtils>(SettingsUtils.Default);
|
||||
services.AddSingleton<ThemeService>(App.ThemeService);
|
||||
services.AddSingleton<INavigationService, NavigationServiceWrapper>();
|
||||
|
||||
// Register settings repositories as singletons
|
||||
services.AddSingleton(sp =>
|
||||
{
|
||||
var settingsUtils = sp.GetRequiredService<ISettingsUtils>();
|
||||
return SettingsRepository<GeneralSettings>.GetInstance((SettingsUtils)settingsUtils);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds ViewModel registrations to the service collection.
|
||||
/// ViewModels are registered as transient to create new instances per request.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection.</param>
|
||||
/// <returns>The service collection for chaining.</returns>
|
||||
public static IServiceCollection AddViewModels(this IServiceCollection services)
|
||||
{
|
||||
// Register ViewModels as transient for fresh instances
|
||||
// These can be migrated incrementally as each ViewModel is updated
|
||||
|
||||
// Tier 1 ViewModels (low complexity) - to be migrated first
|
||||
// services.AddTransient<FileLocksmithViewModel>();
|
||||
// services.AddTransient<RegistryPreviewViewModel>();
|
||||
// services.AddTransient<CropAndLockViewModel>();
|
||||
|
||||
// Tier 2 ViewModels (medium complexity)
|
||||
// services.AddTransient<ColorPickerViewModel>();
|
||||
// services.AddTransient<AlwaysOnTopViewModel>();
|
||||
// services.AddTransient<PowerOcrViewModel>();
|
||||
// services.AddTransient<HostsViewModel>();
|
||||
|
||||
// Tier 3 ViewModels (medium-high complexity)
|
||||
// services.AddTransient<FancyZonesViewModel>();
|
||||
// services.AddTransient<PowerLauncherViewModel>();
|
||||
// services.AddTransient<KeyboardManagerViewModel>();
|
||||
|
||||
// Tier 4 ViewModels (high complexity) - migrate last
|
||||
// services.AddTransient<GeneralViewModel>();
|
||||
// services.AddTransient<DashboardViewModel>();
|
||||
// services.AddTransient<ShellViewModel>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/settings-ui/Settings.UI/Services/ServiceProvider.cs
Normal file
113
src/settings-ui/Settings.UI/Services/ServiceProvider.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a static wrapper around <see cref="Microsoft.Extensions.DependencyInjection.ServiceProvider"/>
|
||||
/// to enable gradual migration to dependency injection.
|
||||
/// </summary>
|
||||
public static class ServiceProvider
|
||||
{
|
||||
private static readonly object _lock = new object();
|
||||
private static IServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the service provider has been initialized.
|
||||
/// </summary>
|
||||
public static bool IsInitialized => _serviceProvider != null;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the service provider with the specified service collection.
|
||||
/// This should be called once during application startup.
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection containing all registered services.</param>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the service provider is already initialized.</exception>
|
||||
public static void Initialize(IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_serviceProvider != null)
|
||||
{
|
||||
throw new InvalidOperationException("ServiceProvider is already initialized.");
|
||||
}
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a service of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to get.</typeparam>
|
||||
/// <returns>The service instance, or null if not registered.</returns>
|
||||
public static T GetService<T>()
|
||||
where T : class
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a required service of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of service to get.</typeparam>
|
||||
/// <returns>The service instance.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the service is not registered.</exception>
|
||||
public static T GetRequiredService<T>()
|
||||
where T : class
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetRequiredService<T>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a service of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The type of service to get.</param>
|
||||
/// <returns>The service instance, or null if not registered.</returns>
|
||||
public static object GetService(Type serviceType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serviceType);
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetService(serviceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a required service of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The type of service to get.</param>
|
||||
/// <returns>The service instance.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the service is not registered.</exception>
|
||||
public static object GetRequiredService(Type serviceType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(serviceType);
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.GetRequiredService(serviceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new scope for scoped services.
|
||||
/// </summary>
|
||||
/// <returns>A new service scope.</returns>
|
||||
public static IServiceScope CreateScope()
|
||||
{
|
||||
EnsureInitialized();
|
||||
return _serviceProvider.CreateScope();
|
||||
}
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (_serviceProvider == null)
|
||||
{
|
||||
throw new InvalidOperationException("ServiceProvider has not been initialized. Call Initialize() first.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Telemetry.Events;
|
||||
@@ -83,6 +84,9 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
|
||||
}
|
||||
|
||||
// Initialize dependency injection
|
||||
InitializeServices();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
UnhandledException += App_UnhandledException;
|
||||
@@ -95,6 +99,14 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
});
|
||||
}
|
||||
|
||||
private static void InitializeServices()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSettingsServices();
|
||||
services.AddViewModels();
|
||||
Services.ServiceProvider.Initialize(services);
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception", e.Exception);
|
||||
|
||||
@@ -22,15 +22,19 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx
|
||||
{
|
||||
private bool _isFullyInitialized;
|
||||
private bool _createHidden;
|
||||
|
||||
public MainWindow(bool createHidden = false)
|
||||
{
|
||||
_createHidden = createHidden;
|
||||
|
||||
var bootTime = new System.Diagnostics.Stopwatch();
|
||||
bootTime.Start();
|
||||
|
||||
this.Activated += Window_Activated_SetIcon;
|
||||
|
||||
App.ThemeService.ThemeChanged += OnThemeChanged;
|
||||
App.ThemeService.ApplyTheme();
|
||||
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
|
||||
@@ -43,8 +47,13 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
{
|
||||
placement.ShowCmd = NativeMethods.SW_HIDE;
|
||||
|
||||
// Restore the last known placement on the first activation
|
||||
this.Activated += Window_Activated;
|
||||
// Defer full initialization until window is shown
|
||||
this.Activated += Window_Activated_LazyInit;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Full initialization for visible windows
|
||||
CompleteInitialization();
|
||||
}
|
||||
|
||||
NativeMethods.SetWindowPlacement(hWnd, ref placement);
|
||||
@@ -52,6 +61,16 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Title = App.IsElevated ? loader.GetString("SettingsWindow_AdminTitle") : loader.GetString("SettingsWindow_Title");
|
||||
|
||||
// IPC callbacks must be set up immediately so messages can be received even when hidden
|
||||
SetupIPCCallbacks();
|
||||
|
||||
bootTime.Stop();
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
|
||||
}
|
||||
|
||||
private void SetupIPCCallbacks()
|
||||
{
|
||||
// send IPC Message
|
||||
ShellPage.SetDefaultSndMessageCallback(msg =>
|
||||
{
|
||||
@@ -128,13 +147,10 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
App.GetScoobeWindow().Activate();
|
||||
});
|
||||
|
||||
this.InitializeComponent();
|
||||
SetAppTitleBar();
|
||||
|
||||
// receive IPC Message
|
||||
App.IPCMessageReceivedCallback = (string msg) =>
|
||||
{
|
||||
if (ShellPage.ShellHandler.IPCResponseHandleList != null)
|
||||
if (ShellPage.ShellHandler?.IPCResponseHandleList != null)
|
||||
{
|
||||
var success = JsonObject.TryParse(msg, out JsonObject json);
|
||||
if (success)
|
||||
@@ -150,10 +166,33 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
bootTime.Stop();
|
||||
private void CompleteInitialization()
|
||||
{
|
||||
if (_isFullyInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new SettingsBootEvent() { BootTimeMs = bootTime.ElapsedMilliseconds });
|
||||
_isFullyInitialized = true;
|
||||
|
||||
App.ThemeService.ApplyTheme();
|
||||
this.InitializeComponent();
|
||||
SetAppTitleBar();
|
||||
}
|
||||
|
||||
private void Window_Activated_LazyInit(object sender, WindowActivatedEventArgs args)
|
||||
{
|
||||
if (args.WindowActivationState != WindowActivationState.Deactivated && !_isFullyInitialized)
|
||||
{
|
||||
CompleteInitialization();
|
||||
|
||||
// After lazy init, also restore placement
|
||||
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
|
||||
var placement = WindowHelper.DeserializePlacementOrDefault(hWnd);
|
||||
NativeMethods.SetWindowPlacement(hWnd, ref placement);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAppTitleBar()
|
||||
@@ -165,6 +204,8 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
public void NavigateToSection(System.Type type)
|
||||
{
|
||||
// Ensure full initialization before navigation
|
||||
CompleteInitialization();
|
||||
ShellPage.Navigate(type);
|
||||
}
|
||||
|
||||
@@ -222,6 +263,8 @@ namespace Microsoft.PowerToys.Settings.UI
|
||||
|
||||
internal void EnsurePageIsSelected()
|
||||
{
|
||||
// Ensure full initialization before page selection
|
||||
CompleteInitialization();
|
||||
ShellPage.EnsurePageIsSelected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
Action stateUpdatingAction = () =>
|
||||
{
|
||||
this.DispatcherQueue.TryEnqueue(() =>
|
||||
this.DispatcherQueue.TryEnqueue(async () =>
|
||||
{
|
||||
ViewModel.RefreshUpdatingState();
|
||||
await ViewModel.RefreshUpdatingStateAsync().ConfigureAwait(true);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -92,9 +92,16 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
CheckBugReportStatus();
|
||||
|
||||
doRefreshBackupRestoreStatus(100);
|
||||
this.Loaded += (s, e) =>
|
||||
{
|
||||
ViewModel.OnPageLoaded();
|
||||
|
||||
this.Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
// Defer backup status check to after page is loaded with low priority
|
||||
this.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () =>
|
||||
{
|
||||
doRefreshBackupRestoreStatus(100);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private void OpenColorsSettings_Click(object sender, RoutedEventArgs e)
|
||||
@@ -123,11 +130,11 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private void RefreshBackupRestoreStatus(int delayMs = 0)
|
||||
{
|
||||
Task.Run(() =>
|
||||
Task.Run(async () =>
|
||||
{
|
||||
if (delayMs > 0)
|
||||
{
|
||||
Thread.Sleep(delayMs);
|
||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var settingsBackupAndRestoreUtils = SettingsBackupAndRestoreUtils.Instance;
|
||||
@@ -171,7 +178,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private async void ViewDiagnosticData_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
await Task.Run(ViewModel.ViewDiagnosticData);
|
||||
await ViewModel.ViewDiagnosticDataAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void BugReportToolClicked(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -47,13 +47,6 @@
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
|
||||
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableWrapDuringDrag" IsChecked="{x:Bind ViewModel.CursorWrapDisableWrapDuringDrag, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="MouseUtilsCursorWrapWrapMode" x:Uid="MouseUtils_CursorWrap_WrapMode">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.CursorWrapWrapMode, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_Both" />
|
||||
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_VerticalOnly" />
|
||||
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_HorizontalOnly" />
|
||||
</ComboBox>
|
||||
</tkcontrols:SettingsCard>
|
||||
</tkcontrols:SettingsExpander.Items>
|
||||
</tkcontrols:SettingsExpander>
|
||||
</controls:SettingsGroup>
|
||||
|
||||
@@ -2728,18 +2728,6 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="MouseUtils_CursorWrap_AutoActivate.Content" xml:space="preserve">
|
||||
<value>Automatically activate on utility startup</value>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_WrapMode.Header" xml:space="preserve">
|
||||
<value>Wrap mode</value>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_WrapMode_VerticalOnly.Content" xml:space="preserve">
|
||||
<value>Vertical only</value>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_WrapMode_HorizontalOnly.Content" xml:space="preserve">
|
||||
<value>Horizontal only</value>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_WrapMode_Both.Content" xml:space="preserve">
|
||||
<value>Vertical and horizontal</value>
|
||||
</data>
|
||||
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Text" xml:space="preserve">
|
||||
<value>Mouse Pointer Crosshairs</value>
|
||||
<comment>Mouse as in the hardware peripheral.</comment>
|
||||
|
||||
@@ -76,24 +76,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||
|
||||
// To obtain the settings configurations of Advanced Paste.
|
||||
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
||||
// To obtain the settings configurations of Fancy zones.
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
|
||||
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig ?? throw new ArgumentException("SettingsConfig cannot be null", nameof(advancedPasteSettingsRepository));
|
||||
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
||||
|
||||
if (_advancedPasteSettings.Properties is null)
|
||||
{
|
||||
throw new ArgumentException("AdvancedPasteSettings.Properties cannot be null", nameof(advancedPasteSettingsRepository));
|
||||
}
|
||||
|
||||
// Ensure AdditionalActions and CustomActions are initialized to prevent null reference exceptions
|
||||
// This handles legacy settings files that may be missing these properties
|
||||
_advancedPasteSettings.Properties.AdditionalActions ??= new AdvancedPasteAdditionalActions();
|
||||
_advancedPasteSettings.Properties.CustomActions ??= new AdvancedPasteCustomActions();
|
||||
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
|
||||
|
||||
AttachConfigurationHandlers();
|
||||
|
||||
@@ -101,7 +93,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value ?? new ObservableCollection<AdvancedPasteCustomAction>();
|
||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
|
||||
|
||||
SetupSettingsFileWatcher();
|
||||
|
||||
@@ -477,13 +469,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public PasteAIConfiguration PasteAIConfiguration
|
||||
{
|
||||
get
|
||||
{
|
||||
// Ensure PasteAIConfiguration is never null for XAML binding
|
||||
_advancedPasteSettings.Properties.PasteAIConfiguration ??= new PasteAIConfiguration();
|
||||
return _advancedPasteSettings.Properties.PasteAIConfiguration;
|
||||
}
|
||||
|
||||
get => _advancedPasteSettings.Properties.PasteAIConfiguration;
|
||||
set
|
||||
{
|
||||
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))
|
||||
|
||||
@@ -1,43 +1,92 @@
|
||||
// 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;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
public partial class FileLocksmithViewModel : Observable
|
||||
/// <summary>
|
||||
/// ViewModel for the File Locksmith settings page.
|
||||
/// Uses CommunityToolkit.Mvvm for MVVM pattern implementation.
|
||||
/// </summary>
|
||||
public partial class FileLocksmithViewModel : ObservableObject
|
||||
{
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private readonly ISettingsUtils _settingsUtils;
|
||||
|
||||
private FileLocksmithSettings Settings { get; set; }
|
||||
|
||||
private const string ModuleName = FileLocksmithSettings.ModuleName;
|
||||
private const string ModuleNameConst = FileLocksmithSettings.ModuleName;
|
||||
|
||||
private string _settingsConfigFileFolder = string.Empty;
|
||||
|
||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsEnabledGpoConfigured))]
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isFileLocksmithEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _enabledOnContextExtendedMenu;
|
||||
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLocksmithViewModel"/> class
|
||||
/// using dependency injection (for new code).
|
||||
/// </summary>
|
||||
/// <param name="settingsUtils">The settings utilities.</param>
|
||||
/// <param name="generalSettingsRepository">The general settings repository.</param>
|
||||
/// <param name="sendConfigMSG">The IPC message callback.</param>
|
||||
public FileLocksmithViewModel(
|
||||
ISettingsUtils settingsUtils,
|
||||
ISettingsRepository<GeneralSettings> generalSettingsRepository,
|
||||
Func<string, int> sendConfigMSG)
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
ArgumentNullException.ThrowIfNull(generalSettingsRepository);
|
||||
|
||||
GeneralSettingsConfig = generalSettingsRepository.SettingsConfig;
|
||||
SendConfigMSG = sendConfigMSG ?? throw new ArgumentNullException(nameof(sendConfigMSG));
|
||||
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileLocksmithViewModel"/> class
|
||||
/// (backward compatible constructor for existing code).
|
||||
/// </summary>
|
||||
public FileLocksmithViewModel(SettingsUtils settingsUtils, ISettingsRepository<GeneralSettings> settingsRepository, Func<string, int> ipcMSGCallBackFunc, string configFileSubfolder = "")
|
||||
{
|
||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||
|
||||
// To obtain the general settings configurations of PowerToys Settings.
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
|
||||
_settingsConfigFileFolder = configFileSubfolder;
|
||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
LoadSettings();
|
||||
}
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
FileLocksmithLocalProperties localSettings = _settingsUtils.GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
|
||||
FileLocksmithLocalProperties localSettings = ((SettingsUtils)_settingsUtils).GetSettingsOrDefault<FileLocksmithLocalProperties>(GetSettingsSubPath(), "file-locksmith-settings.json");
|
||||
Settings = new FileLocksmithSettings(localSettings);
|
||||
}
|
||||
catch (Exception)
|
||||
@@ -48,16 +97,12 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
InitializeEnabledValue();
|
||||
|
||||
// set the callback functions value to handle outgoing IPC message.
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
_fileLocksmithEnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
|
||||
EnabledOnContextExtendedMenu = Settings.Properties.ExtendedContextMenuOnly.Value;
|
||||
}
|
||||
|
||||
public string GetSettingsSubPath()
|
||||
{
|
||||
return _settingsConfigFileFolder + "\\" + ModuleName;
|
||||
return _settingsConfigFileFolder + "\\" + ModuleNameConst;
|
||||
}
|
||||
|
||||
private void InitializeEnabledValue()
|
||||
@@ -66,65 +111,40 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
|
||||
{
|
||||
// Get the enabled state from GPO.
|
||||
_enabledStateIsGPOConfigured = true;
|
||||
_isFileLocksmithEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
||||
EnabledStateIsGPOConfigured = true;
|
||||
IsFileLocksmithEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
_isFileLocksmithEnabled = GeneralSettingsConfig.Enabled.FileLocksmith;
|
||||
IsFileLocksmithEnabled = GeneralSettingsConfig.Enabled.FileLocksmith;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFileLocksmithEnabled
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the enabled state is configured by GPO.
|
||||
/// </summary>
|
||||
public bool IsEnabledGpoConfigured => EnabledStateIsGPOConfigured;
|
||||
|
||||
partial void OnIsFileLocksmithEnabledChanged(bool value)
|
||||
{
|
||||
get => _isFileLocksmithEnabled;
|
||||
set
|
||||
if (EnabledStateIsGPOConfigured)
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured)
|
||||
{
|
||||
// If it's GPO configured, shouldn't be able to change this state.
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isFileLocksmithEnabled != value)
|
||||
{
|
||||
_isFileLocksmithEnabled = value;
|
||||
|
||||
GeneralSettingsConfig.Enabled.FileLocksmith = value;
|
||||
OnPropertyChanged(nameof(IsFileLocksmithEnabled));
|
||||
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
|
||||
// TODO: Implement when this module has properties.
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
// If it's GPO configured, shouldn't be able to change this state.
|
||||
return;
|
||||
}
|
||||
|
||||
GeneralSettingsConfig.Enabled.FileLocksmith = value;
|
||||
|
||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
||||
SendConfigMSG(outgoing.ToString());
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
public bool EnabledOnContextExtendedMenu
|
||||
partial void OnEnabledOnContextExtendedMenuChanged(bool value)
|
||||
{
|
||||
get
|
||||
{
|
||||
return _fileLocksmithEnabledOnContextExtendedMenu;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _fileLocksmithEnabledOnContextExtendedMenu)
|
||||
{
|
||||
_fileLocksmithEnabledOnContextExtendedMenu = value;
|
||||
Settings.Properties.ExtendedContextMenuOnly.Value = value;
|
||||
OnPropertyChanged(nameof(EnabledOnContextExtendedMenu));
|
||||
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabledGpoConfigured
|
||||
{
|
||||
get => _enabledStateIsGPOConfigured;
|
||||
Settings.Properties.ExtendedContextMenuOnly.Value = value;
|
||||
NotifySettingsChanged();
|
||||
}
|
||||
|
||||
private void NotifySettingsChanged()
|
||||
@@ -138,13 +158,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
JsonSerializer.Serialize(Settings, SourceGenerationContextContext.Default.FileLocksmithSettings)));
|
||||
}
|
||||
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
private bool _isFileLocksmithEnabled;
|
||||
private bool _fileLocksmithEnabledOnContextExtendedMenu;
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the enabled state by re-reading GPO configuration.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
InitializeEnabledValue();
|
||||
|
||||
@@ -213,14 +213,20 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
_fileWatcher = Helper.GetFileWatcher(string.Empty, UpdatingSettings.SettingsFile, dispatcherAction);
|
||||
}
|
||||
|
||||
// Diagnostic data retention policy
|
||||
// Defer diagnostic data cleanup to background task to avoid blocking UI
|
||||
_ = Task.Run(CleanupDiagnosticDataAsync);
|
||||
|
||||
InitializeLanguages();
|
||||
}
|
||||
|
||||
private void CleanupDiagnosticDataAsync()
|
||||
{
|
||||
// Diagnostic data retention policy - runs on background thread
|
||||
string etwDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\etw");
|
||||
DeleteDiagnosticDataOlderThan28Days(etwDirPath);
|
||||
|
||||
string localLowEtwDirPath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw");
|
||||
DeleteDiagnosticDataOlderThan28Days(localLowEtwDirPath);
|
||||
|
||||
InitializeLanguages();
|
||||
}
|
||||
|
||||
// Supported languages. Taken from Resources.wxs + default + en-US
|
||||
@@ -1356,52 +1362,54 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
NotifyAllBackupAndRestoreProperties();
|
||||
}
|
||||
|
||||
public void RefreshUpdatingState()
|
||||
public async Task RefreshUpdatingStateAsync()
|
||||
{
|
||||
object oLock = new object();
|
||||
lock (oLock)
|
||||
// Load settings with retry on background thread
|
||||
var config = await Task.Run(async () =>
|
||||
{
|
||||
var config = UpdatingSettings.LoadSettings();
|
||||
var loadedConfig = UpdatingSettings.LoadSettings();
|
||||
|
||||
// Retry loading if failed
|
||||
for (int i = 0; i < 3 && config == null; i++)
|
||||
for (int i = 0; i < 3 && loadedConfig == null; i++)
|
||||
{
|
||||
System.Threading.Thread.Sleep(100);
|
||||
config = UpdatingSettings.LoadSettings();
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
loadedConfig = UpdatingSettings.LoadSettings();
|
||||
}
|
||||
|
||||
if (config == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
return loadedConfig;
|
||||
}).ConfigureAwait(true);
|
||||
|
||||
UpdatingSettingsConfig = config;
|
||||
|
||||
if (PowerToysUpdatingState != config.State)
|
||||
{
|
||||
IsNewVersionDownloading = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool dateChanged = UpdateCheckedDate == UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
bool fileDownloaded = string.IsNullOrEmpty(UpdatingSettingsConfig.DownloadedInstallerFilename);
|
||||
IsNewVersionDownloading = !(dateChanged || fileDownloaded);
|
||||
}
|
||||
|
||||
PowerToysUpdatingState = UpdatingSettingsConfig.State;
|
||||
PowerToysNewAvailableVersion = UpdatingSettingsConfig.NewVersion;
|
||||
PowerToysNewAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
|
||||
UpdateCheckedDate = UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
|
||||
_isNoNetwork = PowerToysUpdatingState == UpdatingSettings.UpdatingState.NetworkError;
|
||||
NotifyPropertyChanged(nameof(IsNoNetwork));
|
||||
NotifyPropertyChanged(nameof(IsNewVersionDownloading));
|
||||
NotifyPropertyChanged(nameof(IsUpdatePanelVisible));
|
||||
_isNewVersionChecked = PowerToysUpdatingState == UpdatingSettings.UpdatingState.UpToDate && !IsNewVersionDownloading;
|
||||
NotifyPropertyChanged(nameof(IsNewVersionCheckedAndUpToDate));
|
||||
|
||||
NotifyPropertyChanged(nameof(IsDownloadAllowed));
|
||||
if (config == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdatingSettingsConfig = config;
|
||||
|
||||
if (PowerToysUpdatingState != config.State)
|
||||
{
|
||||
IsNewVersionDownloading = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool dateChanged = UpdateCheckedDate == UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
bool fileDownloaded = string.IsNullOrEmpty(UpdatingSettingsConfig.DownloadedInstallerFilename);
|
||||
IsNewVersionDownloading = !(dateChanged || fileDownloaded);
|
||||
}
|
||||
|
||||
PowerToysUpdatingState = UpdatingSettingsConfig.State;
|
||||
PowerToysNewAvailableVersion = UpdatingSettingsConfig.NewVersion;
|
||||
PowerToysNewAvailableVersionLink = UpdatingSettingsConfig.ReleasePageLink;
|
||||
UpdateCheckedDate = UpdatingSettingsConfig.LastCheckedDateLocalized;
|
||||
|
||||
_isNoNetwork = PowerToysUpdatingState == UpdatingSettings.UpdatingState.NetworkError;
|
||||
NotifyPropertyChanged(nameof(IsNoNetwork));
|
||||
NotifyPropertyChanged(nameof(IsNewVersionDownloading));
|
||||
NotifyPropertyChanged(nameof(IsUpdatePanelVisible));
|
||||
_isNewVersionChecked = PowerToysUpdatingState == UpdatingSettings.UpdatingState.UpToDate && !IsNewVersionDownloading;
|
||||
NotifyPropertyChanged(nameof(IsNewVersionCheckedAndUpToDate));
|
||||
|
||||
NotifyPropertyChanged(nameof(IsDownloadAllowed));
|
||||
}
|
||||
|
||||
private void InitializeLanguages()
|
||||
@@ -1489,7 +1497,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
internal void ViewDiagnosticData()
|
||||
internal async Task ViewDiagnosticDataAsync()
|
||||
{
|
||||
string localLowEtwDirPath = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE"), "AppData", "LocalLow", "Microsoft", "PowerToys", "etw");
|
||||
string etwDirPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft\\PowerToys\\etw");
|
||||
@@ -1522,7 +1530,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
string tracerptPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "system32");
|
||||
|
||||
ETLConverter converter = new ETLConverter(etwDirPath, tracerptPath);
|
||||
Task.Run(() => converter.ConvertDiagnosticsETLsAsync()).Wait();
|
||||
await converter.ConvertDiagnosticsETLsAsync().ConfigureAwait(false);
|
||||
|
||||
if (Directory.Exists(etwDirPath))
|
||||
{
|
||||
|
||||
242
src/settings-ui/Settings.UI/ViewModels/ModuleViewModelBase.cs
Normal file
242
src/settings-ui/Settings.UI/ViewModels/ModuleViewModelBase.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for module-specific ViewModels that provides common functionality
|
||||
/// such as enabled state, GPO management, and hotkey conflict handling.
|
||||
/// </summary>
|
||||
public abstract partial class ModuleViewModelBase : ViewModelBase
|
||||
{
|
||||
private readonly Dictionary<string, bool> _hotkeyConflictStatus = new Dictionary<string, bool>();
|
||||
private readonly Dictionary<string, string> _hotkeyConflictTooltips = new Dictionary<string, string>();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsEnabledGpoConfigured))]
|
||||
private bool _isEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isGpoManaged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModuleViewModelBase"/> class.
|
||||
/// </summary>
|
||||
protected ModuleViewModelBase()
|
||||
{
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ModuleViewModelBase"/> class with a custom messenger.
|
||||
/// </summary>
|
||||
/// <param name="messenger">The messenger instance to use.</param>
|
||||
protected ModuleViewModelBase(IMessenger messenger)
|
||||
: base(messenger)
|
||||
{
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated += OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the module name used for settings and conflict detection.
|
||||
/// </summary>
|
||||
protected abstract string ModuleName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the enabled state is configured by GPO.
|
||||
/// </summary>
|
||||
public bool IsEnabledGpoConfigured => IsGpoManaged;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnPageLoaded()
|
||||
{
|
||||
base.OnPageLoaded();
|
||||
Debug.WriteLine($"=== PAGE LOADED: {ModuleName} ===");
|
||||
GlobalHotkeyConflictManager.Instance?.RequestAllConflicts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all hotkey settings for this module.
|
||||
/// Override in derived classes to return module-specific hotkey settings.
|
||||
/// </summary>
|
||||
/// <returns>A dictionary of module names to hotkey settings arrays.</returns>
|
||||
public virtual Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles updates to hotkey conflicts for the module.
|
||||
/// </summary>
|
||||
protected virtual void OnConflictsUpdated(object sender, AllHotkeyConflictsEventArgs e)
|
||||
{
|
||||
UpdateHotkeyConflictStatus(e.Conflicts);
|
||||
var allHotkeySettings = GetAllHotkeySettings();
|
||||
|
||||
void UpdateConflictProperties()
|
||||
{
|
||||
if (allHotkeySettings != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, HotkeySettings[]> kvp in allHotkeySettings)
|
||||
{
|
||||
var module = kvp.Key;
|
||||
var hotkeySettingsList = kvp.Value;
|
||||
|
||||
for (int i = 0; i < hotkeySettingsList.Length; i++)
|
||||
{
|
||||
var key = $"{module.ToLowerInvariant()}_{i}";
|
||||
hotkeySettingsList[i].HasConflict = GetHotkeyConflictStatus(key);
|
||||
hotkeySettingsList[i].ConflictDescription = GetHotkeyConflictTooltip(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var settingsWindow = App.GetSettingsWindow();
|
||||
settingsWindow.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, UpdateConflictProperties);
|
||||
}
|
||||
catch
|
||||
{
|
||||
UpdateConflictProperties();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets module-related conflicts from all conflicts data.
|
||||
/// </summary>
|
||||
protected ModuleConflictsData GetModuleRelatedConflicts(AllHotkeyConflictsData allConflicts)
|
||||
{
|
||||
var moduleConflicts = new ModuleConflictsData();
|
||||
|
||||
if (allConflicts.InAppConflicts != null)
|
||||
{
|
||||
foreach (var conflict in allConflicts.InAppConflicts)
|
||||
{
|
||||
if (IsModuleInvolved(conflict))
|
||||
{
|
||||
moduleConflicts.InAppConflicts.Add(conflict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allConflicts.SystemConflicts != null)
|
||||
{
|
||||
foreach (var conflict in allConflicts.SystemConflicts)
|
||||
{
|
||||
if (IsModuleInvolved(conflict))
|
||||
{
|
||||
moduleConflicts.SystemConflicts.Add(conflict);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moduleConflicts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the hotkey conflict status based on all conflicts data.
|
||||
/// </summary>
|
||||
protected virtual void UpdateHotkeyConflictStatus(AllHotkeyConflictsData allConflicts)
|
||||
{
|
||||
_hotkeyConflictStatus.Clear();
|
||||
_hotkeyConflictTooltips.Clear();
|
||||
|
||||
if (allConflicts.InAppConflicts.Count > 0)
|
||||
{
|
||||
foreach (var conflictGroup in allConflicts.InAppConflicts)
|
||||
{
|
||||
foreach (var conflict in conflictGroup.Modules)
|
||||
{
|
||||
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
|
||||
_hotkeyConflictStatus[keyName] = true;
|
||||
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("InAppHotkeyConflictTooltipText");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allConflicts.SystemConflicts.Count > 0)
|
||||
{
|
||||
foreach (var conflictGroup in allConflicts.SystemConflicts)
|
||||
{
|
||||
foreach (var conflict in conflictGroup.Modules)
|
||||
{
|
||||
if (string.Equals(conflict.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var keyName = $"{conflict.ModuleName.ToLowerInvariant()}_{conflict.HotkeyID}";
|
||||
_hotkeyConflictStatus[keyName] = true;
|
||||
_hotkeyConflictTooltips[keyName] = ResourceLoaderInstance.ResourceLoader.GetString("SysHotkeyConflictTooltipText");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether a specific hotkey has a conflict.
|
||||
/// </summary>
|
||||
protected virtual bool GetHotkeyConflictStatus(string key)
|
||||
{
|
||||
return _hotkeyConflictStatus.ContainsKey(key) && _hotkeyConflictStatus[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the conflict tooltip for a specific hotkey.
|
||||
/// </summary>
|
||||
protected virtual string GetHotkeyConflictTooltip(string key)
|
||||
{
|
||||
return _hotkeyConflictTooltips.TryGetValue(key, out string value) ? value : null;
|
||||
}
|
||||
|
||||
private bool IsModuleInvolved(HotkeyConflictGroupData conflict)
|
||||
{
|
||||
if (conflict.Modules == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return conflict.Modules.Any(module =>
|
||||
string.Equals(module.ModuleName, ModuleName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (GlobalHotkeyConflictManager.Instance != null)
|
||||
{
|
||||
GlobalHotkeyConflictManager.Instance.ConflictsUpdated -= OnConflictsUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,9 +113,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Null-safe access in case property wasn't upgraded yet - default to TRUE
|
||||
_cursorWrapDisableWrapDuringDrag = CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag?.Value ?? true;
|
||||
|
||||
// Null-safe access in case property wasn't upgraded yet - default to 0 (Both)
|
||||
_cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0;
|
||||
|
||||
int isEnabled = 0;
|
||||
|
||||
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
|
||||
@@ -1086,34 +1083,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public int CursorWrapWrapMode
|
||||
{
|
||||
get
|
||||
{
|
||||
return _cursorWrapWrapMode;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _cursorWrapWrapMode)
|
||||
{
|
||||
_cursorWrapWrapMode = value;
|
||||
|
||||
// Ensure the property exists before setting value
|
||||
if (CursorWrapSettingsConfig.Properties.WrapMode == null)
|
||||
{
|
||||
CursorWrapSettingsConfig.Properties.WrapMode = new IntProperty(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CursorWrapSettingsConfig.Properties.WrapMode.Value = value;
|
||||
}
|
||||
|
||||
NotifyCursorWrapPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
OnPropertyChanged(propertyName);
|
||||
@@ -1185,6 +1154,5 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
private bool _isCursorWrapEnabled;
|
||||
private bool _cursorWrapAutoActivate;
|
||||
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
|
||||
private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly
|
||||
}
|
||||
}
|
||||
|
||||
108
src/settings-ui/Settings.UI/ViewModels/ViewModelBase.cs
Normal file
108
src/settings-ui/Settings.UI/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all ViewModels using CommunityToolkit.Mvvm.
|
||||
/// Provides lifecycle methods and messaging support.
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : ObservableRecipient, IDisposable
|
||||
{
|
||||
private bool _isDisposed;
|
||||
private bool _isActive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ViewModelBase"/> class.
|
||||
/// </summary>
|
||||
protected ViewModelBase()
|
||||
: base(WeakReferenceMessenger.Default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ViewModelBase"/> class with a custom messenger.
|
||||
/// </summary>
|
||||
/// <param name="messenger">The messenger instance to use.</param>
|
||||
protected ViewModelBase(IMessenger messenger)
|
||||
: base(messenger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this ViewModel is currently active (visible).
|
||||
/// </summary>
|
||||
public bool IsViewModelActive => _isActive;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the view associated with this ViewModel is navigated to.
|
||||
/// Override this method to perform initialization when the page is displayed.
|
||||
/// </summary>
|
||||
public virtual void OnNavigatedTo()
|
||||
{
|
||||
_isActive = true;
|
||||
IsActive = true; // Activates message subscriptions in ObservableRecipient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the view associated with this ViewModel is navigated to.
|
||||
/// Override this method to perform async initialization when the page is displayed.
|
||||
/// </summary>
|
||||
/// <returns>A task representing the async operation.</returns>
|
||||
public virtual Task OnNavigatedToAsync()
|
||||
{
|
||||
OnNavigatedTo();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the view associated with this ViewModel is navigated away from.
|
||||
/// Override this method to perform cleanup when the page is no longer displayed.
|
||||
/// </summary>
|
||||
public virtual void OnNavigatedFrom()
|
||||
{
|
||||
_isActive = false;
|
||||
IsActive = false; // Deactivates message subscriptions in ObservableRecipient
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the page is loaded. This is triggered from the Page's Loaded event.
|
||||
/// </summary>
|
||||
public virtual void OnPageLoaded()
|
||||
{
|
||||
// Default implementation does nothing.
|
||||
// Override in derived classes to perform actions when the page is loaded.
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing">True to release both managed and unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Deactivate message subscriptions
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user