mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-23 04:27:14 +01:00
Compare commits
21 Commits
user/yeela
...
update-eve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c133c16db6 | ||
|
|
6804cb3399 | ||
|
|
1d37ed8752 | ||
|
|
6a4ea500fc | ||
|
|
a6b8cea7cd | ||
|
|
5f61057b38 | ||
|
|
0314a709f5 | ||
|
|
a246789719 | ||
|
|
af401dd6e9 | ||
|
|
6c2a99dfd6 | ||
|
|
7cf32bf204 | ||
|
|
ae9ba62a40 | ||
|
|
d48338bad3 | ||
|
|
fd19168883 | ||
|
|
db9f8d555e | ||
|
|
be90b587da | ||
|
|
a2cd47f36c | ||
|
|
3167145d42 | ||
|
|
0899961e56 | ||
|
|
8a7503e7dc | ||
|
|
4704e3edb8 |
6
.github/actions/spell-check/expect.txt
vendored
6
.github/actions/spell-check/expect.txt
vendored
@@ -215,6 +215,7 @@ cim
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
CLEARTYPE
|
||||
clickable
|
||||
@@ -1602,7 +1603,7 @@ sharpfuzz
|
||||
SHCNE
|
||||
SHCNF
|
||||
SHCONTF
|
||||
Shcore
|
||||
shcore
|
||||
shellapi
|
||||
SHELLDETAILS
|
||||
SHELLDLL
|
||||
@@ -1701,6 +1702,7 @@ srw
|
||||
srwlock
|
||||
sse
|
||||
ssf
|
||||
sszzz
|
||||
STACKFRAME
|
||||
stackoverflow
|
||||
STARTF
|
||||
@@ -1711,6 +1713,7 @@ STARTUPINFOW
|
||||
startupscreen
|
||||
STATFLAG
|
||||
STATICEDGE
|
||||
staticmethod
|
||||
STATSTG
|
||||
stdafx
|
||||
STDAPI
|
||||
@@ -1877,6 +1880,7 @@ uild
|
||||
uitests
|
||||
UITo
|
||||
ULONGLONG
|
||||
Ultrawide
|
||||
ums
|
||||
UMax
|
||||
UMin
|
||||
|
||||
50
.github/prompts/create-commit-title.prompt.md
vendored
50
.github/prompts/create-commit-title.prompt.md
vendored
@@ -6,13 +6,45 @@ description: 'Generate an 80-character git commit title for the local diff'
|
||||
|
||||
# Generate Commit Title
|
||||
|
||||
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
|
||||
## Purpose
|
||||
Provide a single-line, ready-to-paste git commit title (<= 80 characters) that reflects the most important local changes since `HEAD`.
|
||||
|
||||
**Workflow:**
|
||||
1. Run a single command to view the local diff since the last commit:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
2. From that diff, identify the dominant area (reference key paths like `src/modules/*`, `doc/devdocs/**`, etc.), the type of change (bug fix, docs update, config tweak), and any notable impact.
|
||||
3. Draft a concise, imperative commit title summarizing the dominant change. Keep it plain ASCII, <= 80 characters, and avoid trailing punctuation. Mention the primary component when obvious (for example `FancyZones:` or `Docs:`).
|
||||
4. Respond with only the final commit title on a single line so it can be pasted directly into `git commit`.
|
||||
## Input to collect
|
||||
- Run exactly one command to view the local diff:
|
||||
```@terminal
|
||||
git diff HEAD
|
||||
```
|
||||
|
||||
## How to decide the title
|
||||
1. From the diff, find the dominant area (e.g., `src/modules/*`, `doc/devdocs/**`) and the change type (bug fix, docs update, config tweak).
|
||||
2. Draft an imperative, plain-ASCII title that:
|
||||
- Mentions the primary component when obvious (e.g., `FancyZones:` or `Docs:`)
|
||||
- Stays within 80 characters and has no trailing punctuation
|
||||
|
||||
## Final output
|
||||
- Reply with only the commit title on a single line—no extra text.
|
||||
|
||||
## PR title convention (when asked)
|
||||
Use Conventional Commits style:
|
||||
|
||||
`<type>(<scope>): <summary>`
|
||||
|
||||
**Allowed types**
|
||||
- feat, fix, docs, refactor, perf, test, build, ci, chore
|
||||
|
||||
**Scope rules**
|
||||
- Use a short, PowerToys-focused scope (one word preferred). Common scopes:
|
||||
- Core: `runner`, `settings-ui`, `common`, `docs`, `build`, `ci`, `installer`, `gpo`, `dsc`
|
||||
- Modules: `fancyzones`, `powerrename`, `awake`, `colorpicker`, `imageresizer`, `keyboardmanager`, `mouseutils`, `peek`, `hosts`, `file-locksmith`, `screen-ruler`, `text-extractor`, `cropandlock`, `paste`, `powerlauncher`
|
||||
- If unclear, pick the closest module or subsystem; omit only if unavoidable
|
||||
|
||||
**Summary rules**
|
||||
- Imperative, present tense (“add”, “update”, “remove”, “fix”)
|
||||
- Keep it <= 72 characters when possible; be specific, avoid “misc changes”
|
||||
|
||||
**Examples**
|
||||
- `feat(fancyzones): add canvas template duplication`
|
||||
- `fix(mouseutils): guard crosshair toggle when dpi info missing`
|
||||
- `docs(runner): document tray icon states`
|
||||
- `build(installer): align wix v5 suffix flag`
|
||||
- `ci(ci): cache pipeline artifacts for x64`
|
||||
|
||||
1
.github/prompts/create-pr-summary.prompt.md
vendored
1
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -22,3 +22,4 @@ description: 'Generate a PowerToys-ready pull request description from the local
|
||||
5. Confirm validation: list tests executed with results or state why tests were skipped in line with repo guidance.
|
||||
6. Load `.github/pull_request_template.md`, mirror its section order, and populate it with the gathered facts. Include only relevant checklist entries, marking them `[x]/[ ]` and noting any intentional omissions as "N/A".
|
||||
7. Present the filled template inside a fenced ```markdown code block with no extra commentary so it is ready to paste into a PR, clearly flagging any placeholders that still need user input.
|
||||
8. Prepend the PR title above the filled template, applying the Conventional Commit type/scope rules from `.github/prompts/create-commit-title.prompt.md`; pick the dominant component from the diff and keep the title concise and imperative.
|
||||
|
||||
9
.github/prompts/fix-spelling.prompt.md
vendored
9
.github/prompts/fix-spelling.prompt.md
vendored
@@ -10,8 +10,8 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
|
||||
**Guardrails:**
|
||||
- Update only discussion threads authored by `github-actions` or `github-actions[bot]` that mention `Code scanning results / check-spelling`.
|
||||
- Resolve findings solely by editing `.github/actions/spell-check/expect.txt`; reuse existing entries.
|
||||
- Leave all other files and topics untouched.
|
||||
- Prefer improving the wording in the originally flagged file when it clarifies intent without changing meaning; if the wording is already clear/standard for the context, handle it via `.github/actions/spell-check/expect.txt` and reuse existing entries.
|
||||
- Limit edits to the flagged text and `.github/actions/spell-check/expect.txt`; leave all other files and topics untouched.
|
||||
|
||||
**Prerequisites:**
|
||||
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
|
||||
@@ -20,5 +20,6 @@ description: 'Resolve Code scanning / check-spelling comments on the active PR'
|
||||
**Workflow:**
|
||||
1. Determine the active pull request with a single `gh pr view --json number` call (default to the current branch).
|
||||
2. Fetch all PR discussion data once via `gh pr view --json comments,reviews` and filter to check-spelling comments authored by `github-actions` or `github-actions[bot]` that are not minimized; when several remain, process only the most recent comment body.
|
||||
3. For each flagged token, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
|
||||
4. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
|
||||
3. For each flagged token, first consider tightening or rephrasing the original text to avoid the false positive while keeping the meaning intact; if the existing wording is already normal and professional for the context, proceed to allowlisting instead of changing it.
|
||||
4. When allowlisting, review `.github/actions/spell-check/expect.txt` for an equivalent term (for example an existing lowercase variant); when found, reuse that normalized term rather than adding a new entry, even if the flagged token differs only by casing. Only add a new entry after confirming no equivalent already exists.
|
||||
5. Add any remaining missing token to `.github/actions/spell-check/expect.txt`, keeping surrounding formatting intact.
|
||||
@@ -125,6 +125,10 @@
|
||||
"WinUI3Apps\\Powertoys.Peek.UI.exe",
|
||||
"WinUI3Apps\\Powertoys.Peek.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.QuickAccess.dll",
|
||||
"WinUI3Apps\\PowerToys.QuickAccess.exe",
|
||||
"WinUI3Apps\\PowerToys.Settings.UI.Controls.dll",
|
||||
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariablesModuleInterface.dll",
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariablesUILib.dll",
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
|
||||
|
||||
17
.vscode/settings.json
vendored
Normal file
17
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"github.copilot.chat.reviewSelection.instructions": [
|
||||
{
|
||||
"file": ".github/prompts/review-pr.prompt.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.commitMessageGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/prompts/create-commit-title.prompt.md"
|
||||
}
|
||||
],
|
||||
"github.copilot.chat.pullRequestDescriptionGeneration.instructions": [
|
||||
{
|
||||
"file": ".github/prompts/create-pr-summary.prompt.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -30,7 +30,11 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -43,6 +47,18 @@ _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>
|
||||
@@ -59,6 +75,18 @@ _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>
|
||||
@@ -67,6 +95,14 @@ _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>
|
||||
@@ -74,11 +110,19 @@ _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 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.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>
|
||||
@@ -91,10 +135,18 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -162,7 +214,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Always on Top
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -182,7 +238,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Awake
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -210,7 +270,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Color Picker
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -227,18 +291,14 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -251,10 +311,6 @@ _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>
|
||||
@@ -263,7 +319,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
|
||||
### Command Palette
|
||||
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -327,7 +387,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Crop And Lock
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -336,10 +400,26 @@ _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>
|
||||
@@ -350,8 +430,28 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -379,7 +479,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### FancyZones
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -396,6 +500,10 @@ _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>
|
||||
@@ -408,10 +516,6 @@ _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>
|
||||
@@ -424,10 +528,6 @@ _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>
|
||||
@@ -448,10 +548,50 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -488,6 +628,10 @@ _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>
|
||||
@@ -496,6 +640,10 @@ _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>
|
||||
@@ -512,6 +660,10 @@ _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>
|
||||
@@ -520,6 +672,10 @@ _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>
|
||||
@@ -534,32 +690,12 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -575,7 +711,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Hosts File Editor
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -592,10 +732,22 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -612,10 +764,18 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -628,10 +788,22 @@ _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>
|
||||
@@ -695,7 +867,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Light Switch
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -719,7 +895,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Mouse Highlighter
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -732,10 +912,18 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -759,7 +947,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Mouse Pointer Crosshairs
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -775,7 +967,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Mouse Without Borders
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -827,7 +1023,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### New+
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -859,7 +1059,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Peek
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -892,10 +1096,18 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -927,7 +1139,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### PowerToys Run
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -968,14 +1184,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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -991,7 +1207,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Registry Preview
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -1004,10 +1224,22 @@ _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 style="width:100%; table-layout:fixed">
|
||||
<colgroup>
|
||||
<col style="width:40%">
|
||||
<col style="width:60%">
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th>Event Name</th>
|
||||
<th>Description</th>
|
||||
@@ -1027,7 +1259,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Shortcut Guide
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -1043,7 +1279,11 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Text Extractor
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
@@ -1067,15 +1307,15 @@ _If you want to find diagnostic data events in the source code, these two links
|
||||
</table>
|
||||
|
||||
### Workspaces
|
||||
<table style="width:100%">
|
||||
<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.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>
|
||||
@@ -1097,13 +1337,21 @@ _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.Workspaces_Settings</td>
|
||||
<td>Logs changes to workspaces settings.</td>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### ZoomIt
|
||||
<table style="width:100%">
|
||||
<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>
|
||||
|
||||
@@ -66,5 +66,10 @@ namespace PowerToys.GPOWrapperProjection
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
|
||||
}
|
||||
|
||||
public static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue()
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredLightSwitchEnabledValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
#include "resource.h"
|
||||
#include <windows.h>
|
||||
#include "../../../common/version/version.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
#include "winres.h"
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION FILE_VERSION
|
||||
PRODUCTVERSION PRODUCT_VERSION
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS 0x40004L
|
||||
FILETYPE 0x1L
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", COMPANY_NAME
|
||||
VALUE "FileDescription", "File Locksmith CLI"
|
||||
VALUE "FileVersion", FILE_VERSION_STRING
|
||||
VALUE "InternalName", "FileLocksmithCLI.exe"
|
||||
VALUE "LegalCopyright", COPYRIGHT_NOTE
|
||||
VALUE "OriginalFilename", "FileLocksmithCLI.exe"
|
||||
VALUE "ProductName", PRODUCT_NAME
|
||||
VALUE "ProductVersion", PRODUCT_VERSION_STRING
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Validating/Testing Cursor Wrap.
|
||||
|
||||
If a user determines that CursorWrap isn't working on their PC there are some steps you can take to determine why CursorWrap functionality might not be working as expected.
|
||||
|
||||
Note that for a single monitor cursor wrap should always work since all monitor edges are not touching/overlapping with other monitors - the cursor should always wrap to the opposite edge of the same monitor.
|
||||
|
||||
Multi-monitor is supported through building a polygon shape for the outer edges of all monitors, inner monitor edges are ignored, movement of the cursor from one monitor to an adjacent monitor is handled by Windows - CursorWrap doesn't get involved in monitor-to-monitor movement, only outer-edges.
|
||||
|
||||
We have seen a couple of computer setups that have multi-monitors where CursorWrap doesn't work as expected, this appears to be due to a monitor not being 'snapped' to the edge of an adjacent monitor - If you use Display Settings in Windows you can move monitors around, these appear to 'snap' to an edge of an existing monitor.
|
||||
|
||||
What to do if Cursor Wrapping isn't working as expected ?
|
||||
|
||||
1. in the CursorWrapTests folder there's a PowerShell script called `Capture-MonitorLayout.ps1` - this will generate a .json file in the form `"$($env:USERNAME)_monitor_layout.json` - the .json file contains an array of monitors, their position, size, dpi, and scaling.
|
||||
2. Use `CursorWrapTests/monitor_layout_tests.py` to validate the monitor layout/wrapping behavior (uses the json file from point 1 above).
|
||||
3. Use `analyze_test_results.py` to analyze the monitor layout test output and provide information about why wrapping might not be working
|
||||
|
||||
To run `monitor_layout_tests.py` you will need Python installed on your PC.
|
||||
|
||||
Run `python monitor_layout_tests.py --layout-file <path to json file>` you can also add an optional `--verbose` to view verbose output.
|
||||
|
||||
monitor_layout_tests.py will produce an output file called `test_report.json` - the contents of the file will look like this (this is from a single monitor test).
|
||||
|
||||
```json
|
||||
{
|
||||
"summary": {
|
||||
"total_configs": 1,
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"total_issues": 0,
|
||||
"pass_rate": "100.00%"
|
||||
},
|
||||
"failures": [],
|
||||
"recommendations": [
|
||||
"All tests passed - edge detection logic is working correctly!"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If there are failures (the failures array is not empty) you can run the second python application called `analyze_test_results.py`
|
||||
|
||||
Supported options include:
|
||||
```text
|
||||
-h, --help show this help message and exit
|
||||
--report REPORT Path to test report JSON file
|
||||
--detailed Show detailed failure listing
|
||||
--copilot Generate GitHub Copilot-friendly fix prompt
|
||||
```
|
||||
|
||||
Running the analyze_test_results.py script against our single monitor test results produces the following:
|
||||
|
||||
```text
|
||||
python .\analyze_test_results.py --detailed
|
||||
================================================================================
|
||||
CURSORWRAP TEST RESULTS ANALYSIS
|
||||
================================================================================
|
||||
|
||||
Total Configurations Tested: 1
|
||||
Passed: 1 (100.00%)
|
||||
Failed: 0
|
||||
Total Issues: 0
|
||||
|
||||
✓ ALL TESTS PASSED! Edge detection logic is working correctly.
|
||||
|
||||
✓ No failures to analyze!
|
||||
```
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env pwsh
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Captures the current monitor layout configuration for CursorWrap testing.
|
||||
|
||||
.DESCRIPTION
|
||||
Queries Windows for all connected monitors and saves their configuration
|
||||
(position, size, DPI, primary status) to a JSON file that can be used
|
||||
for testing the CursorWrap module.
|
||||
|
||||
.PARAMETER OutputPath
|
||||
Path where the JSON file will be saved. Default: monitor_layout.json
|
||||
|
||||
.EXAMPLE
|
||||
.\Capture-MonitorLayout.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Capture-MonitorLayout.ps1 -OutputPath "my_setup.json"
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$OutputPath = "$($env:USERNAME)_monitor_layout.json"
|
||||
)
|
||||
|
||||
# Add Windows Forms for screen enumeration
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
function Get-MonitorDPI {
|
||||
param([System.Windows.Forms.Screen]$Screen)
|
||||
|
||||
# Try to get DPI using P/Invoke with multiple methods
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class DisplayConfig {
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr MonitorFromPoint(POINT pt, uint dwFlags);
|
||||
|
||||
[DllImport("shcore.dll")]
|
||||
public static extern int GetDpiForMonitor(IntPtr hmonitor, int dpiType, out uint dpiX, out uint dpiY);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
public static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT {
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
public struct MONITORINFOEX {
|
||||
public int cbSize;
|
||||
public RECT rcMonitor;
|
||||
public RECT rcWork;
|
||||
public uint dwFlags;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string szDevice;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT {
|
||||
public int Left;
|
||||
public int Top;
|
||||
public int Right;
|
||||
public int Bottom;
|
||||
}
|
||||
|
||||
public const uint MONITOR_DEFAULTTOPRIMARY = 1;
|
||||
public const int MDT_EFFECTIVE_DPI = 0;
|
||||
public const int MDT_ANGULAR_DPI = 1;
|
||||
public const int MDT_RAW_DPI = 2;
|
||||
public const int LOGPIXELSX = 88;
|
||||
public const int LOGPIXELSY = 90;
|
||||
}
|
||||
"@ -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
$point = New-Object DisplayConfig+POINT
|
||||
$point.X = $Screen.Bounds.Left + ($Screen.Bounds.Width / 2)
|
||||
$point.Y = $Screen.Bounds.Top + ($Screen.Bounds.Height / 2)
|
||||
|
||||
$hMonitor = [DisplayConfig]::MonitorFromPoint($point, 1)
|
||||
|
||||
# Method 1: Try GetDpiForMonitor (Windows 8.1+)
|
||||
[uint]$dpiX = 0
|
||||
[uint]$dpiY = 0
|
||||
$result = [DisplayConfig]::GetDpiForMonitor($hMonitor, 0, [ref]$dpiX, [ref]$dpiY)
|
||||
|
||||
if ($result -eq 0 -and $dpiX -gt 0) {
|
||||
Write-Verbose "DPI detected via GetDpiForMonitor: $dpiX"
|
||||
return $dpiX
|
||||
}
|
||||
|
||||
# Method 2: Try RAW DPI
|
||||
$result = [DisplayConfig]::GetDpiForMonitor($hMonitor, 2, [ref]$dpiX, [ref]$dpiY)
|
||||
if ($result -eq 0 -and $dpiX -gt 0) {
|
||||
Write-Verbose "DPI detected via RAW DPI: $dpiX"
|
||||
return $dpiX
|
||||
}
|
||||
|
||||
# Method 3: Try getting device context DPI (legacy method)
|
||||
$hdc = [DisplayConfig]::GetDC([IntPtr]::Zero)
|
||||
if ($hdc -ne [IntPtr]::Zero) {
|
||||
$dpiValue = [DisplayConfig]::GetDeviceCaps($hdc, 88) # LOGPIXELSX
|
||||
[DisplayConfig]::ReleaseDC([IntPtr]::Zero, $hdc)
|
||||
if ($dpiValue -gt 0) {
|
||||
Write-Verbose "DPI detected via GetDeviceCaps: $dpiValue"
|
||||
return $dpiValue
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Verbose "DPI detection error: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
Write-Warning "Could not detect DPI for $($Screen.DeviceName), using default 96 DPI"
|
||||
return 96 # Standard 96 DPI (100% scaling)
|
||||
}
|
||||
|
||||
function Capture-MonitorLayout {
|
||||
Write-Host "Capturing monitor layout..." -ForegroundColor Cyan
|
||||
Write-Host "=" * 80
|
||||
|
||||
$screens = [System.Windows.Forms.Screen]::AllScreens
|
||||
$monitors = @()
|
||||
|
||||
foreach ($screen in $screens) {
|
||||
$isPrimary = $screen.Primary
|
||||
$bounds = $screen.Bounds
|
||||
$dpi = Get-MonitorDPI -Screen $screen
|
||||
|
||||
$monitor = [ordered]@{
|
||||
left = $bounds.Left
|
||||
top = $bounds.Top
|
||||
right = $bounds.Right
|
||||
bottom = $bounds.Bottom
|
||||
width = $bounds.Width
|
||||
height = $bounds.Height
|
||||
dpi = $dpi
|
||||
scaling_percent = [math]::Round(($dpi / 96.0) * 100, 0)
|
||||
primary = $isPrimary
|
||||
device_name = $screen.DeviceName
|
||||
}
|
||||
|
||||
$monitors += $monitor
|
||||
|
||||
# Display info
|
||||
$primaryTag = if ($isPrimary) { " [PRIMARY]" } else { "" }
|
||||
$scaling = [math]::Round(($dpi / 96.0) * 100, 0)
|
||||
|
||||
Write-Host "`nMonitor $($monitors.Count)$primaryTag" -ForegroundColor Green
|
||||
Write-Host " Device: $($screen.DeviceName)"
|
||||
Write-Host " Position: ($($bounds.Left), $($bounds.Top))"
|
||||
Write-Host " Size: $($bounds.Width)x$($bounds.Height)"
|
||||
Write-Host " DPI: $dpi ($scaling% scaling)"
|
||||
Write-Host " Bounds: [$($bounds.Left), $($bounds.Top), $($bounds.Right), $($bounds.Bottom)]"
|
||||
}
|
||||
|
||||
# Create output object
|
||||
$output = [ordered]@{
|
||||
captured_at = (Get-Date -Format "yyyy-MM-ddTHH:mm:sszzz")
|
||||
computer_name = $env:COMPUTERNAME
|
||||
user_name = $env:USERNAME
|
||||
monitor_count = $monitors.Count
|
||||
monitors = $monitors
|
||||
}
|
||||
|
||||
# Save to JSON
|
||||
$json = $output | ConvertTo-Json -Depth 10
|
||||
Set-Content -Path $OutputPath -Value $json -Encoding UTF8
|
||||
|
||||
Write-Host "`n" + ("=" * 80)
|
||||
Write-Host "Monitor layout saved to: $OutputPath" -ForegroundColor Green
|
||||
Write-Host "Total monitors captured: $($monitors.Count)" -ForegroundColor Cyan
|
||||
Write-Host "`nYou can now use this file with the test script:" -ForegroundColor Yellow
|
||||
Write-Host " python monitor_layout_tests.py --layout-file $OutputPath" -ForegroundColor White
|
||||
|
||||
return $output
|
||||
}
|
||||
|
||||
# Main execution
|
||||
try {
|
||||
$layout = Capture-MonitorLayout
|
||||
|
||||
# Display summary
|
||||
Write-Host "`n" + ("=" * 80)
|
||||
Write-Host "SUMMARY" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80)
|
||||
Write-Host "Configuration Name: $($layout.computer_name)"
|
||||
Write-Host "Captured: $($layout.captured_at)"
|
||||
Write-Host "Monitors: $($layout.monitor_count)"
|
||||
|
||||
# Calculate desktop dimensions
|
||||
$widths = @($layout.monitors | ForEach-Object { $_.width })
|
||||
$heights = @($layout.monitors | ForEach-Object { $_.height })
|
||||
|
||||
$totalWidth = ($widths | Measure-Object -Sum).Sum
|
||||
$maxHeight = ($heights | Measure-Object -Maximum).Maximum
|
||||
|
||||
Write-Host "Total Desktop Width: $totalWidth pixels"
|
||||
Write-Host "Max Desktop Height: $maxHeight pixels"
|
||||
|
||||
# Analyze potential coordinate issues
|
||||
Write-Host "`n" + ("=" * 80)
|
||||
Write-Host "COORDINATE ANALYSIS" -ForegroundColor Cyan
|
||||
Write-Host ("=" * 80)
|
||||
|
||||
# Check for gaps between monitors
|
||||
if ($layout.monitor_count -gt 1) {
|
||||
$hasGaps = $false
|
||||
for ($i = 0; $i -lt $layout.monitor_count - 1; $i++) {
|
||||
$m1 = $layout.monitors[$i]
|
||||
for ($j = $i + 1; $j -lt $layout.monitor_count; $j++) {
|
||||
$m2 = $layout.monitors[$j]
|
||||
|
||||
# Check horizontal gap
|
||||
$hGap = [Math]::Min([Math]::Abs($m1.right - $m2.left), [Math]::Abs($m2.right - $m1.left))
|
||||
# Check vertical overlap
|
||||
$vOverlapStart = [Math]::Max($m1.top, $m2.top)
|
||||
$vOverlapEnd = [Math]::Min($m1.bottom, $m2.bottom)
|
||||
$vOverlap = $vOverlapEnd - $vOverlapStart
|
||||
|
||||
if ($hGap -gt 50 -and $vOverlap -gt 0) {
|
||||
Write-Host "⚠ Gap detected between Monitor $($i+1) and Monitor $($j+1): ${hGap}px horizontal gap" -ForegroundColor Yellow
|
||||
Write-Host " Vertical overlap: ${vOverlap}px" -ForegroundColor Yellow
|
||||
Write-Host " This may indicate a Windows coordinate bug if monitors appear snapped in Display Settings" -ForegroundColor Yellow
|
||||
$hasGaps = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $hasGaps) {
|
||||
Write-Host "✓ No unexpected gaps detected" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# DPI/Scaling notes
|
||||
Write-Host "`nDPI/Scaling Impact on Coordinates:" -ForegroundColor Cyan
|
||||
Write-Host "• Coordinate values (left, top, right, bottom) are in LOGICAL PIXELS"
|
||||
Write-Host "• These are DPI-independent virtual coordinates"
|
||||
Write-Host "• Physical pixels = Logical pixels × (DPI / 96)"
|
||||
Write-Host "• Example: 1920 logical pixels at 150% scaling = 1920 × 1.5 = 2880 physical pixels"
|
||||
Write-Host "• Windows snaps monitors using logical pixel coordinates"
|
||||
Write-Host "• If monitors appear snapped but coordinates show gaps, this is a Windows bug"
|
||||
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Host "`nError capturing monitor layout:" -ForegroundColor Red
|
||||
Write-Host $_.Exception.Message -ForegroundColor Red
|
||||
Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
"""
|
||||
Test Results Analyzer for CursorWrap Monitor Layout Tests
|
||||
|
||||
Analyzes test_report.json and provides detailed explanations of failures,
|
||||
patterns, and recommendations.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
||||
class TestResultAnalyzer:
|
||||
"""Analyzes test results and provides insights"""
|
||||
|
||||
def __init__(self, report_path: str = "test_report.json"):
|
||||
with open(report_path, 'r') as f:
|
||||
self.report = json.load(f)
|
||||
|
||||
self.failures = self.report.get('failures', [])
|
||||
self.summary = self.report.get('summary', {})
|
||||
self.recommendations = self.report.get('recommendations', [])
|
||||
|
||||
def print_overview(self):
|
||||
"""Print test overview"""
|
||||
print("=" * 80)
|
||||
print("CURSORWRAP TEST RESULTS ANALYSIS")
|
||||
print("=" * 80)
|
||||
print(f"\nTotal Configurations Tested: {self.summary.get('total_configs', 0)}")
|
||||
print(f"Passed: {self.summary.get('passed', 0)} ({self.summary.get('pass_rate', 'N/A')})")
|
||||
print(f"Failed: {self.summary.get('failed', 0)}")
|
||||
print(f"Total Issues: {self.summary.get('total_issues', 0)}")
|
||||
|
||||
if self.summary.get('passed', 0) == self.summary.get('total_configs', 0):
|
||||
print("\n✓ ALL TESTS PASSED! Edge detection logic is working correctly.")
|
||||
return
|
||||
|
||||
print(f"\n⚠ {self.summary.get('total_issues', 0)} issues detected\n")
|
||||
|
||||
def analyze_failure_patterns(self):
|
||||
"""Analyze and categorize failure patterns"""
|
||||
print("=" * 80)
|
||||
print("FAILURE PATTERN ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
# Group by test type
|
||||
by_test_type = defaultdict(list)
|
||||
for failure in self.failures:
|
||||
by_test_type[failure['test_name']].append(failure)
|
||||
|
||||
# Group by configuration
|
||||
by_config = defaultdict(list)
|
||||
for failure in self.failures:
|
||||
by_config[failure['monitor_config']].append(failure)
|
||||
|
||||
print(f"\n1. Failures by Test Type:")
|
||||
for test_type, failures in sorted(by_test_type.items(), key=lambda x: len(x[1]), reverse=True):
|
||||
print(f" • {test_type}: {len(failures)} failures")
|
||||
|
||||
print(f"\n2. Configurations with Failures:")
|
||||
for config, failures in sorted(by_config.items(), key=lambda x: len(x[1]), reverse=True):
|
||||
print(f" • {config}")
|
||||
print(f" {len(failures)} issues")
|
||||
|
||||
return by_test_type, by_config
|
||||
|
||||
def analyze_wrap_calculation_failures(self, failures: List[Dict[str, Any]]):
|
||||
"""Detailed analysis of wrap calculation failures"""
|
||||
print("\n" + "=" * 80)
|
||||
print("WRAP CALCULATION FAILURE ANALYSIS")
|
||||
print("=" * 80)
|
||||
|
||||
# Analyze cursor positions
|
||||
positions = []
|
||||
configs = set()
|
||||
|
||||
for failure in failures:
|
||||
configs.add(failure['monitor_config'])
|
||||
# Extract position from expected message
|
||||
if 'test_point' in failure.get('details', {}):
|
||||
pos = failure['details']['test_point']
|
||||
positions.append(pos)
|
||||
|
||||
print(f"\nAffected Configurations: {len(configs)}")
|
||||
for config in sorted(configs):
|
||||
print(f" • {config}")
|
||||
|
||||
if positions:
|
||||
print(f"\nFailed Test Points: {len(positions)}")
|
||||
# Analyze if failures are at edges
|
||||
edge_positions = defaultdict(int)
|
||||
for x, y in positions:
|
||||
# Simplified edge detection
|
||||
if x <= 10:
|
||||
edge_positions['left edge'] += 1
|
||||
elif y <= 10:
|
||||
edge_positions['top edge'] += 1
|
||||
else:
|
||||
edge_positions['other'] += 1
|
||||
|
||||
if edge_positions:
|
||||
print("\nPosition Distribution:")
|
||||
for pos_type, count in edge_positions.items():
|
||||
print(f" • {pos_type}: {count}")
|
||||
|
||||
def explain_common_issues(self):
|
||||
"""Explain common issues found in results"""
|
||||
print("\n" + "=" * 80)
|
||||
print("COMMON ISSUE EXPLANATIONS")
|
||||
print("=" * 80)
|
||||
|
||||
has_wrap_failures = any(f['test_name'] == 'wrap_calculation' for f in self.failures)
|
||||
has_edge_failures = any(f['test_name'] == 'single_monitor_edges' for f in self.failures)
|
||||
has_touching_failures = any(f['test_name'] == 'touching_monitors' for f in self.failures)
|
||||
|
||||
if has_wrap_failures:
|
||||
print("\n⚠ WRAP CALCULATION FAILURES")
|
||||
print("-" * 80)
|
||||
print("Issue: Cursor is on an outer edge but wrapping is not occurring.")
|
||||
print("\nLikely Causes:")
|
||||
print(" 1. Partial Overlap Problem:")
|
||||
print(" • When monitors have different sizes (e.g., 4K + 1080p)")
|
||||
print(" • Only part of an edge is actually adjacent to another monitor")
|
||||
print(" • Current code marks the ENTIRE edge as non-outer if ANY part is adjacent")
|
||||
print(" • This prevents wrapping even in regions where it should occur")
|
||||
print("\n 2. Edge Detection Logic:")
|
||||
print(" • Check IdentifyOuterEdges() in MonitorTopology.cpp")
|
||||
print(" • Consider segmenting edges based on actual overlap regions")
|
||||
print("\n 3. Test Point Selection:")
|
||||
print(" • Failures may be at corners or quarter points")
|
||||
print(" • Indicates edge behavior varies along its length")
|
||||
|
||||
if has_edge_failures:
|
||||
print("\n⚠ SINGLE MONITOR EDGE FAILURES")
|
||||
print("-" * 80)
|
||||
print("Issue: Single monitor should have exactly 4 outer edges.")
|
||||
print("\nThis indicates a fundamental problem in edge detection baseline.")
|
||||
|
||||
if has_touching_failures:
|
||||
print("\n⚠ TOUCHING MONITORS FAILURES")
|
||||
print("-" * 80)
|
||||
print("Issue: Adjacent monitors not detected correctly.")
|
||||
print("\nCheck EdgesAreAdjacent() logic and 50px tolerance settings.")
|
||||
|
||||
def print_recommendations(self):
|
||||
"""Print recommendations from the report"""
|
||||
if not self.recommendations:
|
||||
return
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("RECOMMENDATIONS")
|
||||
print("=" * 80)
|
||||
|
||||
for i, rec in enumerate(self.recommendations, 1):
|
||||
print(f"\n{i}. {rec}")
|
||||
|
||||
def detailed_failure_dump(self):
|
||||
"""Print all failure details"""
|
||||
print("\n" + "=" * 80)
|
||||
print("DETAILED FAILURE LISTING")
|
||||
print("=" * 80)
|
||||
|
||||
for i, failure in enumerate(self.failures, 1):
|
||||
print(f"\n[{i}] {failure['test_name']}")
|
||||
print(f"Configuration: {failure['monitor_config']}")
|
||||
print(f"Expected: {failure['expected']}")
|
||||
print(f"Actual: {failure['actual']}")
|
||||
|
||||
if 'details' in failure:
|
||||
details = failure['details']
|
||||
if 'edge' in details:
|
||||
edge = details['edge']
|
||||
print(f"Edge: {edge.get('edge_type', 'N/A')} at position {edge.get('position', 'N/A')}, "
|
||||
f"range [{edge.get('range_start', 'N/A')}, {edge.get('range_end', 'N/A')}]")
|
||||
if 'test_point' in details:
|
||||
print(f"Test Point: {details['test_point']}")
|
||||
print("-" * 80)
|
||||
|
||||
def generate_github_copilot_prompt(self):
|
||||
"""Generate a prompt suitable for GitHub Copilot to fix the issues"""
|
||||
print("\n" + "=" * 80)
|
||||
print("GITHUB COPILOT FIX PROMPT")
|
||||
print("=" * 80)
|
||||
print("\n```markdown")
|
||||
print("# CursorWrap Edge Detection Bug Report")
|
||||
print()
|
||||
print("## Test Results Summary")
|
||||
print(f"- Total Configurations Tested: {self.summary.get('total_configs', 0)}")
|
||||
print(f"- Pass Rate: {self.summary.get('pass_rate', 'N/A')}")
|
||||
print(f"- Failed Tests: {self.summary.get('failed', 0)}")
|
||||
print(f"- Total Issues: {self.summary.get('total_issues', 0)}")
|
||||
print()
|
||||
|
||||
# Group failures
|
||||
by_test_type = defaultdict(list)
|
||||
for failure in self.failures:
|
||||
by_test_type[failure['test_name']].append(failure)
|
||||
|
||||
print("## Critical Issues Found")
|
||||
print()
|
||||
|
||||
# Analyze wrap calculation failures
|
||||
if 'wrap_calculation' in by_test_type:
|
||||
failures = by_test_type['wrap_calculation']
|
||||
configs = set(f['monitor_config'] for f in failures)
|
||||
|
||||
print("### 1. Wrap Calculation Failures (PARTIAL OVERLAP BUG)")
|
||||
print()
|
||||
print(f"**Count**: {len(failures)} failures across {len(configs)} configuration(s)")
|
||||
print()
|
||||
print("**Affected Configurations**:")
|
||||
for config in sorted(configs):
|
||||
print(f"- {config}")
|
||||
print()
|
||||
|
||||
print("**Root Cause Analysis**:")
|
||||
print()
|
||||
print("The current implementation in `MonitorTopology::IdentifyOuterEdges()` marks an")
|
||||
print("ENTIRE edge as non-outer if ANY portion of that edge is adjacent to another monitor.")
|
||||
print()
|
||||
print("**Problem Scenario**: 1080p monitor + 4K monitor at bottom-right")
|
||||
print("```")
|
||||
print("4K Monitor (3840x2160 at 0,0)")
|
||||
print("┌────────────────────────────────────────┐")
|
||||
print("│ │ <- Y: 0-1080 NO adjacent monitor")
|
||||
print("│ │ RIGHT EDGE SHOULD BE OUTER")
|
||||
print("│ │")
|
||||
print("│ │┌──────────┐")
|
||||
print("│ ││ 1080p │ <- Y: 1080-2160 HAS adjacent")
|
||||
print("└────────────────────────────────────────┘│ at │ RIGHT EDGE NOT OUTER")
|
||||
print(" │ (3840, │")
|
||||
print(" │ 1080) │")
|
||||
print(" └──────────┘")
|
||||
print("```")
|
||||
print()
|
||||
print("**Current Behavior**: Right edge of 4K monitor is marked as NON-OUTER for entire")
|
||||
print("range (Y: 0-2160) because it detects adjacency in the bottom portion (Y: 1080-2160).")
|
||||
print()
|
||||
print("**Expected Behavior**: Right edge should be:")
|
||||
print("- OUTER from Y: 0 to Y: 1080 (no adjacent monitor)")
|
||||
print("- NON-OUTER from Y: 1080 to Y: 2160 (adjacent to 1080p monitor)")
|
||||
print()
|
||||
|
||||
print("**Failed Test Examples**:")
|
||||
print()
|
||||
for i, failure in enumerate(failures[:3], 1): # Show first 3
|
||||
details = failure.get('details', {})
|
||||
test_point = details.get('test_point', 'N/A')
|
||||
edge = details.get('edge', {})
|
||||
edge_type = edge.get('edge_type', 'N/A')
|
||||
position = edge.get('position', 'N/A')
|
||||
range_start = edge.get('range_start', 'N/A')
|
||||
range_end = edge.get('range_end', 'N/A')
|
||||
|
||||
print(f"{i}. **Configuration**: {failure['monitor_config']}")
|
||||
print(f" - Test Point: {test_point}")
|
||||
print(f" - Edge: {edge_type} at X={position}, Y range=[{range_start}, {range_end}]")
|
||||
print(f" - Expected: Cursor wraps to opposite edge")
|
||||
print(f" - Actual: No wrap occurred (edge incorrectly marked as non-outer)")
|
||||
print()
|
||||
|
||||
if len(failures) > 3:
|
||||
print(f" ... and {len(failures) - 3} more similar failures")
|
||||
print()
|
||||
|
||||
# Other failure types
|
||||
if 'single_monitor_edges' in by_test_type:
|
||||
print("### 2. Single Monitor Edge Detection Failures")
|
||||
print()
|
||||
print(f"**Count**: {len(by_test_type['single_monitor_edges'])} failures")
|
||||
print()
|
||||
print("Single monitor configurations should have exactly 4 outer edges.")
|
||||
print("This indicates a fundamental problem in baseline edge detection.")
|
||||
print()
|
||||
|
||||
if 'touching_monitors' in by_test_type:
|
||||
print("### 3. Adjacent Monitor Detection Failures")
|
||||
print()
|
||||
print(f"**Count**: {len(by_test_type['touching_monitors'])} failures")
|
||||
print()
|
||||
print("Adjacent monitors not being detected correctly by EdgesAreAdjacent().")
|
||||
print()
|
||||
|
||||
print("## Required Code Changes")
|
||||
print()
|
||||
print("### File: `MonitorTopology.cpp`")
|
||||
print()
|
||||
print("**Change 1**: Modify `IdentifyOuterEdges()` to support partial edge adjacency")
|
||||
print()
|
||||
print("Instead of marking entire edges as outer/non-outer, the code needs to:")
|
||||
print()
|
||||
print("1. **Segment edges** based on actual overlap regions with adjacent monitors")
|
||||
print("2. Create **sub-edges** for portions of an edge that have different outer status")
|
||||
print("3. Update `IsOnOuterEdge()` to check if the **cursor's specific position** is on an outer portion")
|
||||
print()
|
||||
print("**Proposed Approach**:")
|
||||
print()
|
||||
print("```cpp")
|
||||
print("// Instead of: edge.isOuter = true/false for entire edge")
|
||||
print("// Use: Store list of outer ranges for each edge")
|
||||
print()
|
||||
print("struct MonitorEdge {")
|
||||
print(" // ... existing fields ...")
|
||||
print(" std::vector<std::pair<int, int>> outerRanges; // Ranges where edge is outer")
|
||||
print("};")
|
||||
print()
|
||||
print("// In IdentifyOuterEdges():")
|
||||
print("// For each edge, find ALL adjacent opposite edges")
|
||||
print("// Calculate which portions of the edge have NO adjacent opposite")
|
||||
print("// Store these as outer ranges")
|
||||
print()
|
||||
print("// In IsOnOuterEdge():")
|
||||
print("// Check if cursor position falls within any outer range")
|
||||
print("if (edge.type == EdgeType::Left || edge.type == EdgeType::Right) {")
|
||||
print(" // Check if cursorPos.y is in any outer range")
|
||||
print("} else {")
|
||||
print(" // Check if cursorPos.x is in any outer range")
|
||||
print("}")
|
||||
print("```")
|
||||
print()
|
||||
print("**Change 2**: Update `EdgesAreAdjacent()` validation")
|
||||
print()
|
||||
print("The 50px tolerance logic is correct but needs to return overlap range info:")
|
||||
print()
|
||||
print("```cpp")
|
||||
print("struct AdjacencyResult {")
|
||||
print(" bool isAdjacent;")
|
||||
print(" int overlapStart; // Where the adjacency begins")
|
||||
print(" int overlapEnd; // Where the adjacency ends")
|
||||
print("};")
|
||||
print()
|
||||
print("AdjacencyResult CheckEdgeAdjacency(const MonitorEdge& edge1, ")
|
||||
print(" const MonitorEdge& edge2, ")
|
||||
print(" int tolerance);")
|
||||
print("```")
|
||||
print()
|
||||
print("## Test Validation")
|
||||
print()
|
||||
print("After implementing changes, run:")
|
||||
print("```bash")
|
||||
print("python monitor_layout_tests.py --max-monitors 10")
|
||||
print("```")
|
||||
print()
|
||||
print("Expected results:")
|
||||
print("- All 21+ configurations should pass")
|
||||
print("- Specifically, the 4K+1080p configuration should pass all 5 test points per edge")
|
||||
print("- Wrap calculation should work correctly at partial overlap boundaries")
|
||||
print()
|
||||
print("## Files to Modify")
|
||||
print()
|
||||
print("1. `MonitorTopology.h` - Update MonitorEdge structure")
|
||||
print("2. `MonitorTopology.cpp` - Implement segmented edge detection")
|
||||
print(" - `IdentifyOuterEdges()` - Main logic change")
|
||||
print(" - `IsOnOuterEdge()` - Check position against ranges")
|
||||
print(" - `EdgesAreAdjacent()` - Optionally return range info")
|
||||
print()
|
||||
print("```")
|
||||
|
||||
def run_analysis(self, detailed: bool = False, copilot_mode: bool = False):
|
||||
"""Run complete analysis"""
|
||||
if copilot_mode:
|
||||
self.generate_github_copilot_prompt()
|
||||
return
|
||||
|
||||
self.print_overview()
|
||||
|
||||
if not self.failures:
|
||||
print("\n✓ No failures to analyze!")
|
||||
return
|
||||
|
||||
by_test_type, by_config = self.analyze_failure_patterns()
|
||||
|
||||
# Specific analysis for wrap calculation failures
|
||||
if 'wrap_calculation' in by_test_type:
|
||||
self.analyze_wrap_calculation_failures(by_test_type['wrap_calculation'])
|
||||
|
||||
self.explain_common_issues()
|
||||
self.print_recommendations()
|
||||
|
||||
if detailed:
|
||||
self.detailed_failure_dump()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze CursorWrap test results"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--report",
|
||||
default="test_report.json",
|
||||
help="Path to test report JSON file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--detailed",
|
||||
action="store_true",
|
||||
help="Show detailed failure listing"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--copilot",
|
||||
action="store_true",
|
||||
help="Generate GitHub Copilot-friendly fix prompt"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
analyzer = TestResultAnalyzer(args.report)
|
||||
analyzer.run_analysis(detailed=args.detailed, copilot_mode=args.copilot)
|
||||
|
||||
# Exit with error code if there were failures
|
||||
sys.exit(0 if not analyzer.failures else 1)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Could not find report file: {args.report}")
|
||||
print("\nRun monitor_layout_tests.py first to generate the report.")
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError:
|
||||
print(f"Error: Invalid JSON in report file: {args.report}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error analyzing report: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,892 @@
|
||||
"""
|
||||
Monitor Layout Edge Detection Test Suite for CursorWrap
|
||||
|
||||
This script validates the edge detection and wrapping logic across thousands of
|
||||
monitor configurations without requiring the full PowerToys build environment.
|
||||
|
||||
Tests:
|
||||
- 1-4 monitor configurations
|
||||
- Common resolutions and DPI scales
|
||||
- Various arrangements (horizontal, vertical, L-shape, grid)
|
||||
- Edge detection (touching vs. gap)
|
||||
- Wrap calculations
|
||||
|
||||
Output: JSON report with failures for GitHub Copilot analysis
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
from enum import Enum
|
||||
import sys
|
||||
|
||||
# ============================================================================
|
||||
# Data Structures (mirrors C++ implementation)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class MonitorInfo:
|
||||
"""Represents a physical monitor"""
|
||||
left: int
|
||||
top: int
|
||||
right: int
|
||||
bottom: int
|
||||
dpi: int = 96
|
||||
primary: bool = False
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
return self.right - self.left
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
return self.bottom - self.top
|
||||
|
||||
@property
|
||||
def center_x(self) -> int:
|
||||
return (self.left + self.right) // 2
|
||||
|
||||
@property
|
||||
def center_y(self) -> int:
|
||||
return (self.top + self.bottom) // 2
|
||||
|
||||
|
||||
class EdgeType(Enum):
|
||||
LEFT = "Left"
|
||||
RIGHT = "Right"
|
||||
TOP = "Top"
|
||||
BOTTOM = "Bottom"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Edge:
|
||||
"""Represents a monitor edge"""
|
||||
edge_type: EdgeType
|
||||
position: int # x for vertical, y for horizontal
|
||||
range_start: int
|
||||
range_end: int
|
||||
monitor_index: int
|
||||
|
||||
def overlaps(self, other: 'Edge', tolerance: int = 1) -> bool:
|
||||
"""Check if two edges overlap in their perpendicular range"""
|
||||
if self.edge_type != other.edge_type:
|
||||
return False
|
||||
if abs(self.position - other.position) > tolerance:
|
||||
return False
|
||||
return not (
|
||||
self.range_end <= other.range_start or other.range_end <= self.range_start)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestFailure:
|
||||
"""Records a test failure for analysis"""
|
||||
test_name: str
|
||||
monitor_config: str
|
||||
expected: str
|
||||
actual: str
|
||||
details: Dict
|
||||
|
||||
# ============================================================================
|
||||
# Edge Detection Logic (Python implementation of C++ logic)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class MonitorTopology:
|
||||
"""Implements the edge detection logic to be validated"""
|
||||
|
||||
ADJACENCY_TOLERANCE = 50 # Pixels - tolerance for detecting adjacent edges (matches C++ implementation)
|
||||
EDGE_THRESHOLD = 1 # Pixels - cursor must be within this distance to trigger wrap
|
||||
|
||||
def __init__(self, monitors: List[MonitorInfo]):
|
||||
self.monitors = monitors
|
||||
self.outer_edges: List[Edge] = []
|
||||
self._detect_outer_edges()
|
||||
|
||||
def _detect_outer_edges(self):
|
||||
"""Detect which edges are outer (can wrap)"""
|
||||
all_edges = self._collect_all_edges()
|
||||
|
||||
for edge in all_edges:
|
||||
if self._is_outer_edge(edge, all_edges):
|
||||
self.outer_edges.append(edge)
|
||||
|
||||
def _collect_all_edges(self) -> List[Edge]:
|
||||
"""Collect all edges from all monitors"""
|
||||
edges = []
|
||||
|
||||
for idx, mon in enumerate(self.monitors):
|
||||
edges.append(
|
||||
Edge(
|
||||
EdgeType.LEFT,
|
||||
mon.left,
|
||||
mon.top,
|
||||
mon.bottom,
|
||||
idx))
|
||||
edges.append(
|
||||
Edge(
|
||||
EdgeType.RIGHT,
|
||||
mon.right,
|
||||
mon.top,
|
||||
mon.bottom,
|
||||
idx))
|
||||
edges.append(Edge(EdgeType.TOP, mon.top, mon.left, mon.right, idx))
|
||||
edges.append(
|
||||
Edge(
|
||||
EdgeType.BOTTOM,
|
||||
mon.bottom,
|
||||
mon.left,
|
||||
mon.right,
|
||||
idx))
|
||||
|
||||
return edges
|
||||
|
||||
def _is_outer_edge(self, edge: Edge, all_edges: List[Edge]) -> bool:
|
||||
"""
|
||||
Determine if an edge is "outer" (can wrap)
|
||||
|
||||
Rules:
|
||||
1. If edge has an adjacent opposite edge (within 50px tolerance AND overlapping range), it's NOT outer
|
||||
2. Otherwise, edge IS outer
|
||||
Note: This matches C++ EdgesAreAdjacent() logic
|
||||
"""
|
||||
opposite_type = self._get_opposite_edge_type(edge.edge_type)
|
||||
|
||||
# Find opposite edges that overlap in perpendicular range
|
||||
opposite_edges = [e for e in all_edges
|
||||
if e.edge_type == opposite_type
|
||||
and e.monitor_index != edge.monitor_index
|
||||
and self._ranges_overlap(edge.range_start, edge.range_end,
|
||||
e.range_start, e.range_end)]
|
||||
|
||||
if not opposite_edges:
|
||||
return True # No opposite edges = outer edge
|
||||
|
||||
# Check if any opposite edge is adjacent (within tolerance)
|
||||
for opp in opposite_edges:
|
||||
distance = abs(edge.position - opp.position)
|
||||
if distance <= self.ADJACENCY_TOLERANCE:
|
||||
return False # Adjacent edge found = not outer
|
||||
|
||||
return True # No adjacent edges = outer
|
||||
|
||||
@staticmethod
|
||||
def _get_opposite_edge_type(edge_type: EdgeType) -> EdgeType:
|
||||
"""Get the opposite edge type"""
|
||||
opposites = {
|
||||
EdgeType.LEFT: EdgeType.RIGHT,
|
||||
EdgeType.RIGHT: EdgeType.LEFT,
|
||||
EdgeType.TOP: EdgeType.BOTTOM,
|
||||
EdgeType.BOTTOM: EdgeType.TOP
|
||||
}
|
||||
return opposites[edge_type]
|
||||
|
||||
@staticmethod
|
||||
def _ranges_overlap(
|
||||
a_start: int,
|
||||
a_end: int,
|
||||
b_start: int,
|
||||
b_end: int) -> bool:
|
||||
"""Check if two 1D ranges overlap"""
|
||||
return not (a_end <= b_start or b_end <= a_start)
|
||||
|
||||
def calculate_wrap_position(self, x: int, y: int) -> Tuple[int, int]:
|
||||
"""Calculate where cursor should wrap to"""
|
||||
# Find which outer edge was crossed and calculate wrap
|
||||
# At corners, multiple edges may match - try all and return first successful wrap
|
||||
for edge in self.outer_edges:
|
||||
if self._is_on_edge(x, y, edge):
|
||||
new_x, new_y = self._wrap_from_edge(x, y, edge)
|
||||
if (new_x, new_y) != (x, y):
|
||||
# Wrap succeeded
|
||||
return (new_x, new_y)
|
||||
|
||||
return (x, y) # No wrap
|
||||
|
||||
def _is_on_edge(self, x: int, y: int, edge: Edge) -> bool:
|
||||
"""Check if point is on the given edge"""
|
||||
tolerance = 2 # Pixels
|
||||
|
||||
if edge.edge_type in (EdgeType.LEFT, EdgeType.RIGHT):
|
||||
return (abs(x - edge.position) <= tolerance and
|
||||
edge.range_start <= y <= edge.range_end)
|
||||
else:
|
||||
return (abs(y - edge.position) <= tolerance and
|
||||
edge.range_start <= x <= edge.range_end)
|
||||
|
||||
def _wrap_from_edge(self, x: int, y: int, edge: Edge) -> Tuple[int, int]:
|
||||
"""Calculate wrap destination from an outer edge"""
|
||||
opposite_type = self._get_opposite_edge_type(edge.edge_type)
|
||||
|
||||
# Find opposite outer edges that overlap
|
||||
opposite_edges = [e for e in self.outer_edges
|
||||
if e.edge_type == opposite_type
|
||||
and self._point_in_range(x, y, e)]
|
||||
|
||||
if not opposite_edges:
|
||||
return (x, y) # No wrap destination
|
||||
|
||||
# Find closest opposite edge
|
||||
target_edge = min(opposite_edges,
|
||||
key=lambda e: abs(e.position - edge.position))
|
||||
|
||||
# Calculate new position
|
||||
if edge.edge_type in (EdgeType.LEFT, EdgeType.RIGHT):
|
||||
return (target_edge.position, y)
|
||||
else:
|
||||
return (x, target_edge.position)
|
||||
|
||||
@staticmethod
|
||||
def _point_in_range(x: int, y: int, edge: Edge) -> bool:
|
||||
"""Check if point's perpendicular coordinate is in edge's range"""
|
||||
if edge.edge_type in (EdgeType.LEFT, EdgeType.RIGHT):
|
||||
return edge.range_start <= y <= edge.range_end
|
||||
else:
|
||||
return edge.range_start <= x <= edge.range_end
|
||||
|
||||
# ============================================================================
|
||||
# Test Configuration Generators
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestConfigGenerator:
|
||||
"""Generates comprehensive test configurations"""
|
||||
|
||||
# Common resolutions
|
||||
RESOLUTIONS = [
|
||||
(1920, 1080), # 1080p
|
||||
(2560, 1440), # 1440p
|
||||
(3840, 2160), # 4K
|
||||
(3440, 1440), # Ultrawide
|
||||
(1920, 1200), # 16:10
|
||||
]
|
||||
|
||||
# DPI scales
|
||||
DPI_SCALES = [96, 120, 144, 192] # 100%, 125%, 150%, 200%
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls, filepath: str) -> List[List[MonitorInfo]]:
|
||||
"""Load monitor configuration from captured JSON file"""
|
||||
# Handle UTF-8 with BOM (PowerShell default)
|
||||
with open(filepath, 'r', encoding='utf-8-sig') as f:
|
||||
data = json.load(f)
|
||||
|
||||
monitors = []
|
||||
for mon in data.get('monitors', []):
|
||||
monitor = MonitorInfo(
|
||||
left=mon['left'],
|
||||
top=mon['top'],
|
||||
right=mon['right'],
|
||||
bottom=mon['bottom'],
|
||||
dpi=mon.get('dpi', 96),
|
||||
primary=mon.get('primary', False)
|
||||
)
|
||||
monitors.append(monitor)
|
||||
|
||||
return [monitors] if monitors else []
|
||||
|
||||
@classmethod
|
||||
def generate_all_configs(cls,
|
||||
max_monitors: int = 4) -> List[List[MonitorInfo]]:
|
||||
"""Generate all test configurations"""
|
||||
configs = []
|
||||
|
||||
# Single monitor (baseline)
|
||||
configs.extend(cls._single_monitor_configs())
|
||||
|
||||
# Two monitors (most common)
|
||||
if max_monitors >= 2:
|
||||
configs.extend(cls._two_monitor_configs())
|
||||
|
||||
# Three monitors
|
||||
if max_monitors >= 3:
|
||||
configs.extend(cls._three_monitor_configs())
|
||||
|
||||
# Four monitors
|
||||
if max_monitors >= 4:
|
||||
configs.extend(cls._four_monitor_configs())
|
||||
|
||||
# Five+ monitors
|
||||
if max_monitors >= 5:
|
||||
configs.extend(cls._five_plus_monitor_configs(max_monitors))
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _single_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Single monitor configurations"""
|
||||
configs = []
|
||||
|
||||
for width, height in cls.RESOLUTIONS[:3]: # Limit for single monitor
|
||||
for dpi in cls.DPI_SCALES[:2]: # Limit DPI variations
|
||||
mon = MonitorInfo(0, 0, width, height, dpi, True)
|
||||
configs.append([mon])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _two_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Two monitor configurations"""
|
||||
configs = []
|
||||
# Both 1080p for simplicity
|
||||
res1, res2 = cls.RESOLUTIONS[0], cls.RESOLUTIONS[0]
|
||||
|
||||
# Horizontal (touching)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res1[0], res1[1], primary=True),
|
||||
MonitorInfo(res1[0], 0, res1[0] + res2[0], res2[1])
|
||||
])
|
||||
|
||||
# Vertical (touching)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res1[0], res1[1], primary=True),
|
||||
MonitorInfo(0, res1[1], res2[0], res1[1] + res2[1])
|
||||
])
|
||||
|
||||
# Different resolutions
|
||||
res_big = cls.RESOLUTIONS[2] # 4K
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res1[0], res1[1], primary=True),
|
||||
MonitorInfo(res1[0], 0, res1[0] + res_big[0], res_big[1])
|
||||
])
|
||||
|
||||
# Offset alignment (common real-world scenario)
|
||||
offset = 200
|
||||
configs.append([
|
||||
MonitorInfo(0, offset, res1[0], offset + res1[1], primary=True),
|
||||
MonitorInfo(res1[0], 0, res1[0] + res2[0], res2[1])
|
||||
])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _three_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Three monitor configurations"""
|
||||
configs = []
|
||||
res = cls.RESOLUTIONS[0] # 1080p
|
||||
|
||||
# Linear horizontal
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(res[0] * 2, 0, res[0] * 3, res[1])
|
||||
])
|
||||
|
||||
# L-shape (common gaming setup)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(0, res[1], res[0], res[1] * 2)
|
||||
])
|
||||
|
||||
# Vertical stack
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(0, res[1], res[0], res[1] * 2),
|
||||
MonitorInfo(0, res[1] * 2, res[0], res[1] * 3)
|
||||
])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _four_monitor_configs(cls) -> List[List[MonitorInfo]]:
|
||||
"""Four monitor configurations"""
|
||||
configs = []
|
||||
res = cls.RESOLUTIONS[0] # 1080p
|
||||
|
||||
# 2x2 grid (classic)
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(0, res[1], res[0], res[1] * 2),
|
||||
MonitorInfo(res[0], res[1], res[0] * 2, res[1] * 2)
|
||||
])
|
||||
|
||||
# Linear horizontal
|
||||
configs.append([
|
||||
MonitorInfo(0, 0, res[0], res[1], primary=True),
|
||||
MonitorInfo(res[0], 0, res[0] * 2, res[1]),
|
||||
MonitorInfo(res[0] * 2, 0, res[0] * 3, res[1]),
|
||||
MonitorInfo(res[0] * 3, 0, res[0] * 4, res[1])
|
||||
])
|
||||
|
||||
return configs
|
||||
|
||||
@classmethod
|
||||
def _five_plus_monitor_configs(cls, max_count: int) -> List[List[MonitorInfo]]:
|
||||
"""Five to ten monitor configurations"""
|
||||
configs = []
|
||||
res = cls.RESOLUTIONS[0] # 1080p
|
||||
|
||||
# Linear horizontal (5-10 monitors)
|
||||
for count in range(5, min(max_count + 1, 11)):
|
||||
monitor_list = []
|
||||
for i in range(count):
|
||||
is_primary = (i == 0)
|
||||
monitor_list.append(
|
||||
MonitorInfo(res[0] * i, 0, res[0] * (i + 1), res[1], primary=is_primary)
|
||||
)
|
||||
configs.append(monitor_list)
|
||||
|
||||
return configs
|
||||
|
||||
# ============================================================================
|
||||
# Test Validators
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class EdgeDetectionValidator:
|
||||
"""Validates edge detection logic"""
|
||||
|
||||
@staticmethod
|
||||
def validate_single_monitor(
|
||||
monitors: List[MonitorInfo]) -> Optional[TestFailure]:
|
||||
"""Single monitor should have 4 outer edges"""
|
||||
topology = MonitorTopology(monitors)
|
||||
expected_count = 4
|
||||
actual_count = len(topology.outer_edges)
|
||||
|
||||
if actual_count != expected_count:
|
||||
return TestFailure(
|
||||
test_name="single_monitor_edges",
|
||||
monitor_config=EdgeDetectionValidator._describe_config(
|
||||
monitors),
|
||||
expected=f"{expected_count} outer edges",
|
||||
actual=f"{actual_count} outer edges",
|
||||
details={"edges": [asdict(e) for e in topology.outer_edges]}
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_touching_monitors(
|
||||
monitors: List[MonitorInfo]) -> Optional[TestFailure]:
|
||||
"""Touching monitors should have no gap between them"""
|
||||
topology = MonitorTopology(monitors)
|
||||
|
||||
# For 2 touching monitors horizontally, expect 6 outer edges (not 8)
|
||||
if len(monitors) == 2:
|
||||
# Check if they're aligned horizontally and touching
|
||||
m1, m2 = monitors
|
||||
if m1.right == m2.left and m1.top == m2.top and m1.bottom == m2.bottom:
|
||||
expected = 6 # 2 internal edges removed
|
||||
actual = len(topology.outer_edges)
|
||||
if actual != expected:
|
||||
return TestFailure(
|
||||
test_name="touching_monitors",
|
||||
monitor_config=EdgeDetectionValidator._describe_config(
|
||||
monitors),
|
||||
expected=f"{expected} outer edges (2 touching edges removed)",
|
||||
actual=f"{actual} outer edges",
|
||||
details={"edges": [asdict(e)
|
||||
for e in topology.outer_edges]}
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_wrap_calculation(
|
||||
monitors: List[MonitorInfo]) -> List[TestFailure]:
|
||||
"""Validate cursor wrap calculations"""
|
||||
failures = []
|
||||
topology = MonitorTopology(monitors)
|
||||
|
||||
# Test wrapping at each outer edge with multiple points
|
||||
for edge in topology.outer_edges:
|
||||
test_points = EdgeDetectionValidator._get_test_points_on_edge(
|
||||
edge, monitors)
|
||||
|
||||
for test_point in test_points:
|
||||
x, y = test_point
|
||||
|
||||
# Check if there's actually a valid wrap destination
|
||||
# (some outer edges may not have opposite edges due to partial overlap)
|
||||
opposite_type = topology._get_opposite_edge_type(edge.edge_type)
|
||||
has_opposite = any(
|
||||
e.edge_type == opposite_type and
|
||||
topology._point_in_range(x, y, e)
|
||||
for e in topology.outer_edges
|
||||
)
|
||||
|
||||
if not has_opposite:
|
||||
# No wrap destination available - this is OK for partial overlaps
|
||||
continue
|
||||
|
||||
new_x, new_y = topology.calculate_wrap_position(x, y)
|
||||
|
||||
# Verify wrap happened (position changed)
|
||||
if (new_x, new_y) == (x, y):
|
||||
# Should have wrapped but didn't
|
||||
failure = TestFailure(
|
||||
test_name="wrap_calculation",
|
||||
monitor_config=EdgeDetectionValidator._describe_config(
|
||||
monitors),
|
||||
expected=f"Cursor should wrap from ({x},{y})",
|
||||
actual=f"No wrap occurred",
|
||||
details={
|
||||
"edge": asdict(edge),
|
||||
"test_point": (x, y)
|
||||
}
|
||||
)
|
||||
failures.append(failure)
|
||||
|
||||
return failures
|
||||
|
||||
@staticmethod
|
||||
def _get_test_points_on_edge(
|
||||
edge: Edge, monitors: List[MonitorInfo]) -> List[Tuple[int, int]]:
|
||||
"""Get multiple test points on the given edge (5 points: top/left corner, quarter, center, three-quarter, bottom/right corner)"""
|
||||
monitor = monitors[edge.monitor_index]
|
||||
points = []
|
||||
|
||||
if edge.edge_type == EdgeType.LEFT:
|
||||
x = monitor.left
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
y = int(monitor.top + (monitor.height - 1) * ratio)
|
||||
points.append((x, y))
|
||||
elif edge.edge_type == EdgeType.RIGHT:
|
||||
x = monitor.right - 1
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
y = int(monitor.top + (monitor.height - 1) * ratio)
|
||||
points.append((x, y))
|
||||
elif edge.edge_type == EdgeType.TOP:
|
||||
y = monitor.top
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
x = int(monitor.left + (monitor.width - 1) * ratio)
|
||||
points.append((x, y))
|
||||
elif edge.edge_type == EdgeType.BOTTOM:
|
||||
y = monitor.bottom - 1
|
||||
for ratio in [0.0, 0.25, 0.5, 0.75, 1.0]:
|
||||
x = int(monitor.left + (monitor.width - 1) * ratio)
|
||||
points.append((x, y))
|
||||
|
||||
return points
|
||||
|
||||
@staticmethod
|
||||
def _describe_config(monitors: List[MonitorInfo]) -> str:
|
||||
"""Generate human-readable config description"""
|
||||
if len(monitors) == 1:
|
||||
m = monitors[0]
|
||||
return f"Single {m.width}x{m.height} @{m.dpi}DPI"
|
||||
|
||||
desc = f"{len(monitors)} monitors: "
|
||||
for i, m in enumerate(monitors):
|
||||
desc += f"M{i}({m.width}x{m.height} at {m.left},{m.top}) "
|
||||
return desc.strip()
|
||||
|
||||
# ============================================================================
|
||||
# Test Runner
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class TestRunner:
|
||||
"""Orchestrates the test execution"""
|
||||
|
||||
def __init__(self, max_monitors: int = 10, verbose: bool = False, layout_file: str = None):
|
||||
self.max_monitors = max_monitors
|
||||
self.verbose = verbose
|
||||
self.layout_file = layout_file
|
||||
self.failures: List[TestFailure] = []
|
||||
self.test_count = 0
|
||||
self.passed_count = 0
|
||||
|
||||
def _print_layout_diagram(self, monitors: List[MonitorInfo]):
|
||||
"""Print a text-based diagram of the monitor layout"""
|
||||
print("\n" + "=" * 80)
|
||||
print("MONITOR LAYOUT DIAGRAM")
|
||||
print("=" * 80)
|
||||
|
||||
# Find bounds of entire desktop
|
||||
min_x = min(m.left for m in monitors)
|
||||
min_y = min(m.top for m in monitors)
|
||||
max_x = max(m.right for m in monitors)
|
||||
max_y = max(m.bottom for m in monitors)
|
||||
|
||||
# Calculate scale to fit in ~70 chars wide
|
||||
desktop_width = max_x - min_x
|
||||
desktop_height = max_y - min_y
|
||||
|
||||
# Scale factor: target 70 chars width
|
||||
scale = desktop_width / 70.0
|
||||
if scale < 1:
|
||||
scale = 1
|
||||
|
||||
# Create grid (70 chars wide, proportional height)
|
||||
grid_width = 70
|
||||
grid_height = max(10, int(desktop_height / scale))
|
||||
grid_height = min(grid_height, 30) # Cap at 30 lines
|
||||
|
||||
# Initialize grid with spaces
|
||||
grid = [[' ' for _ in range(grid_width)] for _ in range(grid_height)]
|
||||
|
||||
# Draw each monitor
|
||||
for idx, mon in enumerate(monitors):
|
||||
# Convert monitor coords to grid coords
|
||||
x1 = int((mon.left - min_x) / scale)
|
||||
y1 = int((mon.top - min_y) / scale)
|
||||
x2 = int((mon.right - min_x) / scale)
|
||||
y2 = int((mon.bottom - min_y) / scale)
|
||||
|
||||
# Clamp to grid
|
||||
x1 = max(0, min(x1, grid_width - 1))
|
||||
x2 = max(0, min(x2, grid_width))
|
||||
y1 = max(0, min(y1, grid_height - 1))
|
||||
y2 = max(0, min(y2, grid_height))
|
||||
|
||||
# Draw monitor border and fill
|
||||
char = str(idx) if idx < 10 else chr(65 + idx - 10) # 0-9, then A-Z
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if y < grid_height and x < grid_width:
|
||||
# Draw borders
|
||||
if y == y1 or y == y2 - 1:
|
||||
grid[y][x] = '─'
|
||||
elif x == x1 or x == x2 - 1:
|
||||
grid[y][x] = '│'
|
||||
else:
|
||||
grid[y][x] = char
|
||||
|
||||
# Draw corners
|
||||
if y1 < grid_height and x1 < grid_width:
|
||||
grid[y1][x1] = '┌'
|
||||
if y1 < grid_height and x2 - 1 < grid_width:
|
||||
grid[y1][x2 - 1] = '┐'
|
||||
if y2 - 1 < grid_height and x1 < grid_width:
|
||||
grid[y2 - 1][x1] = '└'
|
||||
if y2 - 1 < grid_height and x2 - 1 < grid_width:
|
||||
grid[y2 - 1][x2 - 1] = '┘'
|
||||
|
||||
# Print grid
|
||||
print()
|
||||
for row in grid:
|
||||
print(''.join(row))
|
||||
|
||||
# Print legend
|
||||
print("\n" + "-" * 80)
|
||||
print("MONITOR DETAILS:")
|
||||
print("-" * 80)
|
||||
for idx, mon in enumerate(monitors):
|
||||
char = str(idx) if idx < 10 else chr(65 + idx - 10)
|
||||
primary = " [PRIMARY]" if mon.primary else ""
|
||||
scaling = int((mon.dpi / 96.0) * 100)
|
||||
print(f" [{char}] Monitor {idx}{primary}")
|
||||
print(f" Position: ({mon.left}, {mon.top})")
|
||||
print(f" Size: {mon.width}x{mon.height}")
|
||||
print(f" DPI: {mon.dpi} ({scaling}% scaling)")
|
||||
print(f" Bounds: [{mon.left}, {mon.top}, {mon.right}, {mon.bottom}]")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
def run_all_tests(self):
|
||||
"""Execute all test configurations"""
|
||||
print("=" * 80)
|
||||
print("CursorWrap Monitor Layout Edge Detection Test Suite")
|
||||
print("=" * 80)
|
||||
|
||||
# Load or generate configs
|
||||
if self.layout_file:
|
||||
print(f"\nLoading monitor layout from {self.layout_file}...")
|
||||
configs = TestConfigGenerator.load_from_file(self.layout_file)
|
||||
# Show visual diagram for captured layouts
|
||||
if configs:
|
||||
self._print_layout_diagram(configs[0])
|
||||
else:
|
||||
print("\nGenerating test configurations...")
|
||||
configs = TestConfigGenerator.generate_all_configs(self.max_monitors)
|
||||
|
||||
total_tests = len(configs)
|
||||
print(f"Testing {total_tests} configuration(s)")
|
||||
print("=" * 80)
|
||||
|
||||
# Run tests
|
||||
for i, config in enumerate(configs, 1):
|
||||
self._run_test_config(config, i, total_tests)
|
||||
|
||||
# Report results
|
||||
self._print_summary()
|
||||
self._save_report()
|
||||
|
||||
def _run_test_config(
|
||||
self,
|
||||
monitors: List[MonitorInfo],
|
||||
iteration: int,
|
||||
total: int):
|
||||
"""Run all validators on a single configuration"""
|
||||
desc = EdgeDetectionValidator._describe_config(monitors)
|
||||
|
||||
if not self.verbose:
|
||||
# Minimal output: just progress
|
||||
progress = (iteration / total) * 100
|
||||
print(
|
||||
f"\r[{iteration}/{total}] {progress:5.1f}% - Testing: {desc[:60]:<60}", end="", flush=True)
|
||||
else:
|
||||
print(f"\n[{iteration}/{total}] Testing: {desc}")
|
||||
|
||||
# Run validators
|
||||
self.test_count += 1
|
||||
config_passed = True
|
||||
|
||||
# Single monitor validation
|
||||
if len(monitors) == 1:
|
||||
failure = EdgeDetectionValidator.validate_single_monitor(monitors)
|
||||
if failure:
|
||||
self.failures.append(failure)
|
||||
config_passed = False
|
||||
|
||||
# Touching monitors validation (2+ monitors)
|
||||
if len(monitors) >= 2:
|
||||
failure = EdgeDetectionValidator.validate_touching_monitors(monitors)
|
||||
if failure:
|
||||
self.failures.append(failure)
|
||||
config_passed = False
|
||||
|
||||
# Wrap calculation validation
|
||||
wrap_failures = EdgeDetectionValidator.validate_wrap_calculation(monitors)
|
||||
if wrap_failures:
|
||||
self.failures.extend(wrap_failures)
|
||||
config_passed = False
|
||||
|
||||
if config_passed:
|
||||
self.passed_count += 1
|
||||
|
||||
if self.verbose and not config_passed:
|
||||
print(f" ? FAILED ({len([f for f in self.failures if desc in f.monitor_config])} issues)")
|
||||
elif self.verbose:
|
||||
print(" ? PASSED")
|
||||
|
||||
def _print_summary(self):
|
||||
"""Print test summary"""
|
||||
print("\n\n" + "=" * 80)
|
||||
print("TEST SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"Total Configurations: {self.test_count}")
|
||||
print(f"Passed: {self.passed_count} ({self.passed_count/self.test_count*100:.1f}%)")
|
||||
print(f"Failed: {self.test_count - self.passed_count} ({(self.test_count - self.passed_count)/self.test_count*100:.1f}%)")
|
||||
print(f"Total Issues Found: {len(self.failures)}")
|
||||
print("=" * 80)
|
||||
|
||||
if self.failures:
|
||||
print("\n?? FAILURES DETECTED - See test_report.json for details")
|
||||
print("\nTop 5 Failure Types:")
|
||||
failure_types = {}
|
||||
for f in self.failures:
|
||||
failure_types[f.test_name] = failure_types.get(f.test_name, 0) + 1
|
||||
|
||||
for test_name, count in sorted(failure_types.items(), key=lambda x: x[1], reverse=True)[:5]:
|
||||
print(f" - {test_name}: {count} failures")
|
||||
else:
|
||||
print("\n? ALL TESTS PASSED!")
|
||||
|
||||
def _save_report(self):
|
||||
"""Save detailed JSON report"""
|
||||
|
||||
# Helper to convert enums to strings
|
||||
def convert_for_json(obj):
|
||||
if isinstance(obj, dict):
|
||||
return {k: convert_for_json(v) for k, v in obj.items()}
|
||||
elif isinstance(obj, list):
|
||||
return [convert_for_json(item) for item in obj]
|
||||
elif isinstance(obj, Enum):
|
||||
return obj.value
|
||||
else:
|
||||
return obj
|
||||
|
||||
report = {
|
||||
"summary": {
|
||||
"total_configs": self.test_count,
|
||||
"passed": self.passed_count,
|
||||
"failed": self.test_count - self.passed_count,
|
||||
"total_issues": len(self.failures),
|
||||
"pass_rate": f"{self.passed_count/self.test_count*100:.2f}%"
|
||||
},
|
||||
"failures": convert_for_json([asdict(f) for f in self.failures]),
|
||||
"recommendations": self._generate_recommendations()
|
||||
}
|
||||
|
||||
output_file = "test_report.json"
|
||||
with open(output_file, "w") as f:
|
||||
json.dump(report, f, indent=2)
|
||||
|
||||
print(f"\n?? Detailed report saved to: {output_file}")
|
||||
|
||||
def _generate_recommendations(self) -> List[str]:
|
||||
"""Generate recommendations based on failures"""
|
||||
recommendations = []
|
||||
|
||||
failure_types = {}
|
||||
for f in self.failures:
|
||||
failure_types[f.test_name] = failure_types.get(f.test_name, 0) + 1
|
||||
|
||||
if "single_monitor_edges" in failure_types:
|
||||
recommendations.append(
|
||||
"Single monitor edge detection failing - verify baseline case in MonitorTopology::_detect_outer_edges()"
|
||||
)
|
||||
|
||||
if "touching_monitors" in failure_types:
|
||||
recommendations.append(
|
||||
f"Adjacent monitor detection failing ({failure_types['touching_monitors']} cases) - "
|
||||
"review ADJACENCY_TOLERANCE (50px) and edge overlap logic in EdgesAreAdjacent()"
|
||||
)
|
||||
|
||||
if "wrap_calculation" in failure_types:
|
||||
recommendations.append(
|
||||
f"Wrap calculation failing ({failure_types['wrap_calculation']} cases) - "
|
||||
"review CursorWrapCore::HandleMouseMove() wrap destination logic"
|
||||
)
|
||||
|
||||
if not recommendations:
|
||||
recommendations.append("All tests passed - edge detection logic is working correctly!")
|
||||
|
||||
return recommendations
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point
|
||||
# ============================================================================
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point
|
||||
# ============================================================================
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="CursorWrap Monitor Layout Edge Detection Test Suite"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-monitors",
|
||||
type=int,
|
||||
default=10,
|
||||
help="Maximum number of monitors to test (1-10)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Enable verbose output"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--layout-file",
|
||||
type=str,
|
||||
help="Use captured monitor layout JSON file instead of generated configs"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.layout_file:
|
||||
# Validate max_monitors only for generated configs
|
||||
if args.max_monitors < 1 or args.max_monitors > 10:
|
||||
print("Error: max-monitors must be between 1 and 10")
|
||||
sys.exit(1)
|
||||
|
||||
runner = TestRunner(
|
||||
max_monitors=args.max_monitors,
|
||||
verbose=args.verbose,
|
||||
layout_file=args.layout_file
|
||||
)
|
||||
runner.run_all_tests()
|
||||
|
||||
# Exit with error code if tests failed
|
||||
sys.exit(0 if not runner.failures else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -506,8 +506,58 @@ public:
|
||||
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
// Helper method to check if there's a monitor adjacent in coordinate space (not grid)
|
||||
bool HasAdjacentMonitorInCoordinateSpace(const RECT& currentMonitorRect, int direction)
|
||||
{
|
||||
// direction: 0=left, 1=right, 2=top, 3=bottom
|
||||
const int tolerance = 50; // Allow small gaps
|
||||
|
||||
for (const auto& monitor : m_monitors)
|
||||
{
|
||||
bool isAdjacent = false;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case 0: // Left - check if another monitor's right edge touches/overlaps our left edge
|
||||
isAdjacent = (abs(monitor.rect.right - currentMonitorRect.left) <= tolerance) &&
|
||||
(monitor.rect.bottom > currentMonitorRect.top + tolerance) &&
|
||||
(monitor.rect.top < currentMonitorRect.bottom - tolerance);
|
||||
break;
|
||||
|
||||
case 1: // Right - check if another monitor's left edge touches/overlaps our right edge
|
||||
isAdjacent = (abs(monitor.rect.left - currentMonitorRect.right) <= tolerance) &&
|
||||
(monitor.rect.bottom > currentMonitorRect.top + tolerance) &&
|
||||
(monitor.rect.top < currentMonitorRect.bottom - tolerance);
|
||||
break;
|
||||
|
||||
case 2: // Top - check if another monitor's bottom edge touches/overlaps our top edge
|
||||
isAdjacent = (abs(monitor.rect.bottom - currentMonitorRect.top) <= tolerance) &&
|
||||
(monitor.rect.right > currentMonitorRect.left + tolerance) &&
|
||||
(monitor.rect.left < currentMonitorRect.right - tolerance);
|
||||
break;
|
||||
|
||||
case 3: // Bottom - check if another monitor's top edge touches/overlaps our bottom edge
|
||||
isAdjacent = (abs(monitor.rect.top - currentMonitorRect.bottom) <= tolerance) &&
|
||||
(monitor.rect.right > currentMonitorRect.left + tolerance) &&
|
||||
(monitor.rect.left < currentMonitorRect.right - tolerance);
|
||||
break;
|
||||
}
|
||||
|
||||
if (isAdjacent)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: Found adjacent monitor in coordinate space (direction {})", direction);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// *** COMPLETELY REWRITTEN CURSOR WRAPPING LOGIC ***
|
||||
// Implements vertical scrolling to bottom/top of vertical stack as requested
|
||||
// Only wraps when there's NO adjacent monitor in the coordinate space
|
||||
POINT HandleMouseMove(const POINT& currentPos)
|
||||
{
|
||||
POINT newPos = currentPos;
|
||||
@@ -546,12 +596,22 @@ public:
|
||||
|
||||
// *** VERTICAL WRAPPING LOGIC - CONFIRMED WORKING ***
|
||||
// Move to bottom of vertical stack when hitting top edge
|
||||
// Only wrap if there's NO adjacent monitor in the coordinate space
|
||||
if (currentPos.y <= currentMonitorInfo.rcMonitor.top)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: TOP EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor above in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 2))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists above (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the bottom-most monitor in the vertical stack (same column)
|
||||
HMONITOR bottomMonitor = nullptr;
|
||||
|
||||
@@ -604,6 +664,15 @@ public:
|
||||
Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: BOTTOM EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor below in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 3))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists below (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the top-most monitor in the vertical stack (same column)
|
||||
HMONITOR topMonitor = nullptr;
|
||||
|
||||
@@ -653,13 +722,22 @@ public:
|
||||
|
||||
// *** FIXED HORIZONTAL WRAPPING LOGIC ***
|
||||
// Move to opposite end of horizontal stack when hitting left/right edge
|
||||
// Only handle horizontal wrapping if we haven't already wrapped vertically
|
||||
// Only wrap if there's NO adjacent monitor in the coordinate space (let Windows handle natural transitions)
|
||||
if (!wrapped && currentPos.x <= currentMonitorInfo.rcMonitor.left)
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: LEFT EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor to the left in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 0))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the left (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the right-most monitor in the horizontal stack (same row)
|
||||
HMONITOR rightMonitor = nullptr;
|
||||
|
||||
@@ -712,6 +790,15 @@ public:
|
||||
Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: RIGHT EDGE DETECTED =======");
|
||||
#endif
|
||||
|
||||
// Check if there's an adjacent monitor to the right in coordinate space
|
||||
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 1))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the right (Windows will handle)");
|
||||
#endif
|
||||
return currentPos; // Let Windows handle natural cursor movement
|
||||
}
|
||||
|
||||
// Find the left-most monitor in the horizontal stack (same row)
|
||||
HMONITOR leftMonitor = nullptr;
|
||||
|
||||
@@ -981,45 +1068,104 @@ void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
|
||||
}
|
||||
else
|
||||
{
|
||||
// For more than 2 monitors, use the general algorithm
|
||||
RECT totalBounds = monitors[0].rect;
|
||||
for (const auto& monitor : monitors)
|
||||
{
|
||||
totalBounds.left = min(totalBounds.left, monitor.rect.left);
|
||||
totalBounds.top = min(totalBounds.top, monitor.rect.top);
|
||||
totalBounds.right = max(totalBounds.right, monitor.rect.right);
|
||||
totalBounds.bottom = max(totalBounds.bottom, monitor.rect.bottom);
|
||||
// For more than 2 monitors, use edge-based alignment algorithm
|
||||
// This ensures monitors with aligned edges (e.g., top edges at same Y) are grouped in same row
|
||||
|
||||
// Helper lambda to check if two ranges overlap or are adjacent (with tolerance)
|
||||
auto rangesOverlapOrTouch = [](int start1, int end1, int start2, int end2, int tolerance = 50) -> bool {
|
||||
// Check if ranges overlap or are within tolerance distance
|
||||
return (start1 <= end2 + tolerance) && (start2 <= end1 + tolerance);
|
||||
};
|
||||
|
||||
// Sort monitors by horizontal position (left edge) for column assignment
|
||||
std::vector<const MonitorInfo*> monitorsByX;
|
||||
for (const auto& monitor : monitors) {
|
||||
monitorsByX.push_back(&monitor);
|
||||
}
|
||||
std::sort(monitorsByX.begin(), monitorsByX.end(), [](const MonitorInfo* a, const MonitorInfo* b) {
|
||||
return a->rect.left < b->rect.left;
|
||||
});
|
||||
|
||||
// Sort monitors by vertical position (top edge) for row assignment
|
||||
std::vector<const MonitorInfo*> monitorsByY;
|
||||
for (const auto& monitor : monitors) {
|
||||
monitorsByY.push_back(&monitor);
|
||||
}
|
||||
std::sort(monitorsByY.begin(), monitorsByY.end(), [](const MonitorInfo* a, const MonitorInfo* b) {
|
||||
return a->rect.top < b->rect.top;
|
||||
});
|
||||
|
||||
// Assign rows based on vertical overlap - monitors that overlap vertically should be in same row
|
||||
std::map<const MonitorInfo*, int> monitorToRow;
|
||||
int currentRow = 0;
|
||||
|
||||
for (size_t i = 0; i < monitorsByY.size(); i++) {
|
||||
const auto* monitor = monitorsByY[i];
|
||||
|
||||
// Check if this monitor overlaps vertically with any monitor already assigned to current row
|
||||
bool foundOverlap = false;
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
const auto* other = monitorsByY[j];
|
||||
if (monitorToRow[other] == currentRow) {
|
||||
// Check vertical overlap
|
||||
if (rangesOverlapOrTouch(monitor->rect.top, monitor->rect.bottom,
|
||||
other->rect.top, other->rect.bottom)) {
|
||||
monitorToRow[monitor] = currentRow;
|
||||
foundOverlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundOverlap) {
|
||||
// Start new row if no overlap found and we have room
|
||||
if (currentRow < 2 && i < monitorsByY.size() - 1) {
|
||||
currentRow++;
|
||||
}
|
||||
monitorToRow[monitor] = currentRow;
|
||||
}
|
||||
}
|
||||
|
||||
int totalWidth = totalBounds.right - totalBounds.left;
|
||||
int totalHeight = totalBounds.bottom - totalBounds.top;
|
||||
int gridWidth = max(1, totalWidth / 3);
|
||||
int gridHeight = max(1, totalHeight / 3);
|
||||
// Assign columns based on horizontal position (left-to-right order)
|
||||
// Monitors are already sorted by X coordinate (left edge)
|
||||
std::map<const MonitorInfo*, int> monitorToCol;
|
||||
|
||||
// Place monitors in the 3x3 grid based on their center points
|
||||
// For horizontal arrangement, distribute monitors evenly across columns
|
||||
if (monitorsByX.size() == 1) {
|
||||
// Single monitor - place in middle column
|
||||
monitorToCol[monitorsByX[0]] = 1;
|
||||
}
|
||||
else if (monitorsByX.size() == 2) {
|
||||
// Two monitors - place at opposite ends for wrapping
|
||||
monitorToCol[monitorsByX[0]] = 0; // Leftmost monitor
|
||||
monitorToCol[monitorsByX[1]] = 2; // Rightmost monitor
|
||||
}
|
||||
else {
|
||||
// Three or more monitors - distribute across grid
|
||||
for (size_t i = 0; i < monitorsByX.size() && i < 3; i++) {
|
||||
monitorToCol[monitorsByX[i]] = static_cast<int>(i);
|
||||
}
|
||||
// If more than 3 monitors, place extras in rightmost column
|
||||
for (size_t i = 3; i < monitorsByX.size(); i++) {
|
||||
monitorToCol[monitorsByX[i]] = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Place monitors in grid using the computed row/column assignments
|
||||
for (const auto& monitor : monitors)
|
||||
{
|
||||
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
// Calculate center point of monitor
|
||||
int centerX = (monitor.rect.left + monitor.rect.right) / 2;
|
||||
int centerY = (monitor.rect.top + monitor.rect.bottom) / 2;
|
||||
|
||||
// Map to grid position
|
||||
int col = (centerX - totalBounds.left) / gridWidth;
|
||||
int row = (centerY - totalBounds.top) / gridHeight;
|
||||
|
||||
// Ensure we stay within bounds
|
||||
col = max(0, min(2, col));
|
||||
row = max(0, min(2, row));
|
||||
int row = monitorToRow[&monitor];
|
||||
int col = monitorToCol[&monitor];
|
||||
|
||||
grid[row][col] = hMonitor;
|
||||
monitorToPosition[hMonitor] = {row, col, true};
|
||||
positionToMonitor[{row, col}] = hMonitor;
|
||||
|
||||
#ifdef _DEBUG
|
||||
Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}], center=({}, {})",
|
||||
monitor.monitorId, row, col, centerX, centerY);
|
||||
Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}] (left={}, top={}, right={}, bottom={})",
|
||||
monitor.monitorId, row, col,
|
||||
monitor.rect.left, monitor.rect.top, monitor.rect.right, monitor.rect.bottom);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { },
|
||||
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
|
||||
new CommandItem(_newExtension) { Title = _newExtension.Title },
|
||||
];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() =>
|
||||
|
||||
@@ -134,6 +134,9 @@
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
PreviewKeyDown="CommandsDropdown_PreviewKeyDown"
|
||||
SelectionMode="Single">
|
||||
<ListView.Resources>
|
||||
<x:Boolean x:Key="ListViewItemSelectionIndicatorVisualEnabled">False</x:Boolean>
|
||||
</ListView.Resources>
|
||||
<ListView.ItemContainerStyle>
|
||||
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
|
||||
@@ -99,8 +99,6 @@ public sealed partial class WrapPanel : Panel
|
||||
set { SetValue(HorizontalSpacingProperty, value); }
|
||||
}
|
||||
|
||||
private bool IsSectionItem(UIElement element) => element is FrameworkElement fe && fe.DataContext is ListItemViewModel item && item.IsSectionOrSeparator;
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the <see cref="HorizontalSpacing"/> dependency property.
|
||||
/// </summary>
|
||||
@@ -350,7 +348,7 @@ public sealed partial class WrapPanel : Panel
|
||||
return;
|
||||
}
|
||||
|
||||
var isFullLine = IsSectionItem(child);
|
||||
var isFullLine = GetIsFullLine(child);
|
||||
var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize);
|
||||
|
||||
if (isFullLine)
|
||||
|
||||
@@ -18,8 +18,19 @@ internal sealed partial class GridItemContainerStyleSelector : StyleSelector
|
||||
|
||||
public Style? Gallery { get; set; }
|
||||
|
||||
public Style? Section { get; set; }
|
||||
|
||||
public Style? Separator { get; set; }
|
||||
|
||||
protected override Style? SelectStyleCore(object item, DependencyObject container)
|
||||
{
|
||||
if (item is ListItemViewModel { IsSectionOrSeparator: true } listItem)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(listItem.Title)
|
||||
? Separator!
|
||||
: Section;
|
||||
}
|
||||
|
||||
return GridProperties switch
|
||||
{
|
||||
SmallGridPropertiesViewModel => Small,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Microsoft.CmdPal.UI;
|
||||
|
||||
internal sealed partial class ListItemContainerStyleSelector : StyleSelector
|
||||
{
|
||||
public Style? Default { get; set; }
|
||||
|
||||
public Style? Section { get; set; }
|
||||
|
||||
public Style? Separator { get; set; }
|
||||
|
||||
protected override Style? SelectStyleCore(object item, DependencyObject container)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
ListItemViewModel { IsSectionOrSeparator: true } listItemViewModel when string.IsNullOrWhiteSpace(listItemViewModel.Title) => Separator!,
|
||||
ListItemViewModel { IsSectionOrSeparator: true } => Section,
|
||||
_ => Default,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,10 @@
|
||||
<CornerRadius x:Key="SmallGridViewItemCornerRadius">8</CornerRadius>
|
||||
<CornerRadius x:Key="MediumGridViewItemCornerRadius">8</CornerRadius>
|
||||
|
||||
<x:Double x:Key="ListViewItemMinHeight">40</x:Double>
|
||||
<x:Double x:Key="ListViewSectionMinHeight">0</x:Double>
|
||||
<x:Double x:Key="ListViewSeparatorMinHeight">0</x:Double>
|
||||
|
||||
<Style x:Key="IconGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
@@ -94,6 +98,7 @@
|
||||
<Style x:Key="GalleryGridViewItemStyle" TargetType="GridViewItem">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="GridViewItem">
|
||||
@@ -155,6 +160,70 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="GridViewSectionItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="4,8,12,0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Bottom" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSectionMinHeight}" />
|
||||
<Setter Property="cpcontrols:WrapPanel.IsFullLine" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="GridViewSeparatorItemStyle"
|
||||
BasedOn="{StaticResource DefaultGridViewItemStyle}"
|
||||
TargetType="GridViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="4,4,12,4" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSeparatorMinHeight}" />
|
||||
<Setter Property="cpcontrols:WrapPanel.IsFullLine" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ListDefaultContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewItemMinHeight}" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ListSectionContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="16,8,12,0" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Bottom" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSectionMinHeight}" />
|
||||
<Setter Property="AllowDrop" Value="False" />
|
||||
</Style>
|
||||
|
||||
<Style
|
||||
x:Key="ListSeparatorContainerStyle"
|
||||
BasedOn="{StaticResource DefaultListViewItemStyle}"
|
||||
TargetType="ListViewItem">
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="IsTabStop" Value="False" />
|
||||
<Setter Property="IsHoldingEnabled" Value="False" />
|
||||
<Setter Property="Padding" Value="16,4,12,4" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="MinHeight" Value="{StaticResource ListViewSeparatorMinHeight}" />
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="TagTemplate" x:DataType="coreViewModels:TagViewModel">
|
||||
<cpcontrols:Tag
|
||||
AutomationProperties.Name="{x:Bind Text, Mode=OneWay}"
|
||||
@@ -166,16 +235,6 @@
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<cmdpalUI:GridItemTemplateSelector
|
||||
x:Key="GridItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
x:Key="ListItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
@@ -183,11 +242,29 @@
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:ListItemContainerStyleSelector
|
||||
x:Key="ListItemContainerStyleSelector"
|
||||
Default="{StaticResource ListDefaultContainerStyle}"
|
||||
Section="{StaticResource ListSectionContainerStyle}"
|
||||
Separator="{StaticResource ListSeparatorContainerStyle}" />
|
||||
|
||||
<cmdpalUI:GridItemTemplateSelector
|
||||
x:Key="GridItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
Gallery="{StaticResource GalleryGridItemViewModelTemplate}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource GridSeparatorViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:GridItemContainerStyleSelector
|
||||
x:Key="GridItemContainerStyleSelector"
|
||||
Gallery="{StaticResource GalleryGridViewItemStyle}"
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource IconGridViewItemStyle}"
|
||||
Section="{StaticResource GridViewSectionItemStyle}"
|
||||
Separator="{StaticResource GridViewSeparatorItemStyle}"
|
||||
Small="{StaticResource IconGridViewItemStyle}" />
|
||||
|
||||
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
|
||||
@@ -255,21 +332,21 @@
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid>
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Margin="0,2,0,2"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSectionViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid
|
||||
Margin="0"
|
||||
Margin="0,8,0,0"
|
||||
VerticalAlignment="Center"
|
||||
cpcontrols:WrapPanel.IsFullLine="True"
|
||||
ColumnSpacing="8"
|
||||
@@ -281,13 +358,9 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Foreground="{ThemeResource TextFillColorDisabled}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Section}" />
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -414,7 +487,7 @@
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="11"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
@@ -423,6 +496,10 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GridSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Rectangle Height="1" Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
@@ -448,6 +525,7 @@
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemContainerStyleSelector="{StaticResource ListItemContainerStyleSelector}"
|
||||
ItemTemplateSelector="{StaticResource ListItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
@@ -460,7 +538,7 @@
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="16,0"
|
||||
Padding="16,16"
|
||||
CanDragItems="True"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
@@ -477,7 +555,10 @@
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cpcontrols:WrapPanel HorizontalSpacing="8" Orientation="Horizontal" />
|
||||
<cpcontrols:WrapPanel
|
||||
HorizontalSpacing="8"
|
||||
Orientation="Horizontal"
|
||||
VerticalSpacing="8" />
|
||||
</ItemsPanelTemplate>
|
||||
</GridView.ItemsPanel>
|
||||
<GridView.ItemContainerTransitions>
|
||||
|
||||
@@ -18,6 +18,9 @@ internal static class BindTransformers
|
||||
public static Visibility EmptyOrWhitespaceToCollapsed(string? input)
|
||||
=> string.IsNullOrWhiteSpace(input) ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
public static Visibility EmptyOrWhitespaceToVisible(string? input)
|
||||
=> string.IsNullOrWhiteSpace(input) ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public static Visibility VisibleWhenAny(bool value1, bool value2)
|
||||
=> (value1 || value2) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
@@ -126,16 +126,16 @@
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="DetailsSeparatorTemplate" x:DataType="coreViewModels:DetailsSeparatorViewModel">
|
||||
<StackPanel Margin="0,8,8,0" Orientation="Vertical">
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
Style="{StaticResource SeparatorKeyTextBlockStyle}"
|
||||
Text="{x:Bind Key, Mode=OneWay}"
|
||||
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), Mode=OneWay, FallbackValue=Collapsed}" />
|
||||
<Border
|
||||
Margin="0,0,0,0"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,2">
|
||||
<TextBlock
|
||||
Margin="0,0,0,0"
|
||||
Style="{StaticResource SeparatorKeyTextBlockStyle}"
|
||||
Text="{x:Bind Key, Mode=OneWay}"
|
||||
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToCollapsed(Key), FallbackValue=Collapsed}" />
|
||||
</Border>
|
||||
BorderThickness="0,0,0,1"
|
||||
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToVisible(Key), Mode=OneWay, FallbackValue=Collapsed}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="DetailsTagsTemplate" x:DataType="coreViewModels:DetailsTagsViewModel">
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -30,8 +34,56 @@ public sealed partial class GeneralPage : Page
|
||||
{
|
||||
get
|
||||
{
|
||||
var version = Package.Current.Id.Version;
|
||||
return $"Version {version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
|
||||
var versionNo = ResourceLoaderInstance.GetString("Settings_GeneralPage_VersionNo");
|
||||
if (!TryGetPackagedVersion(out var version) && !TryGetAssemblyVersion(out version))
|
||||
{
|
||||
version = "?";
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.CurrentCulture, versionNo, version);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetPackagedVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
// Package.Current throws InvalidOperationException if the app is not packaged
|
||||
var v = Package.Current.Id.Version;
|
||||
version = $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}";
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get version from the package", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetAssemblyVersion(out string version)
|
||||
{
|
||||
version = string.Empty;
|
||||
try
|
||||
{
|
||||
var processPath = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(processPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var info = FileVersionInfo.GetVersionInfo(processPath);
|
||||
version = $"{info.FileMajorPart}.{info.FileMinorPart}.{info.FileBuildPart}.{info.FilePrivatePart}";
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get version from the executable", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,4 +724,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="Settings_ExtensionsPage_More_ReorderFallbacks_MenuFlyoutItem.Text" xml:space="preserve">
|
||||
<value>Manage fallback order</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_VersionNo" xml:space="preserve">
|
||||
<value>Version {0}</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -90,20 +90,5 @@ namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
|
||||
// Assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(displayName));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetTranslatedPluginDescriptionTest()
|
||||
{
|
||||
// Setup
|
||||
var provider = new TimeDateCommandsProvider();
|
||||
|
||||
// Act
|
||||
var commands = provider.TopLevelCommands();
|
||||
var subtitle = commands[0].Subtitle;
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(subtitle));
|
||||
Assert.IsTrue(subtitle.Contains("Show time and date values in different formats"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||
<VersionMajor>0</VersionMajor>
|
||||
<VersionMinor>7</VersionMinor>
|
||||
<VersionMinor>8</VersionMinor>
|
||||
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -15,7 +15,6 @@ public partial class CalculatorCommandProvider : CommandProvider
|
||||
private static ISettingsInterface settings = new SettingsManager();
|
||||
private readonly ListItem _listItem = new(new CalculatorListPage(settings))
|
||||
{
|
||||
Subtitle = Resources.calculator_top_level_subtitle,
|
||||
MoreCommands = [new CommandContextItem(((SettingsManager)settings).Settings.SettingsPage)],
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
|
||||
{
|
||||
Title = Properties.Resources.list_item_title,
|
||||
Subtitle = Properties.Resources.list_item_subtitle,
|
||||
Icon = Icons.ClipboardListIcon,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(_settingsManager.Settings.SettingsPage),
|
||||
|
||||
@@ -31,7 +31,6 @@ public partial class RemoteDesktopCommandProvider : CommandProvider
|
||||
|
||||
listPageCommand = new CommandItem(listPage)
|
||||
{
|
||||
Subtitle = Resources.remotedesktop_subtitle,
|
||||
Icon = Icons.RDPIcon,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(settingsManager.Settings.SettingsPage),
|
||||
|
||||
@@ -39,7 +39,6 @@ public partial class ShellCommandsProvider : CommandProvider
|
||||
{
|
||||
Icon = Icons.RunV2Icon,
|
||||
Title = Resources.shell_command_name,
|
||||
Subtitle = Resources.cmd_plugin_description,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(Settings.SettingsPage),
|
||||
],
|
||||
|
||||
@@ -28,7 +28,6 @@ public sealed partial class TimeDateCommandsProvider : CommandProvider
|
||||
{
|
||||
Icon = _timeDateExtensionPage.Icon,
|
||||
Title = Resources.Microsoft_plugin_timedate_plugin_name,
|
||||
Subtitle = GetTranslatedPluginDescription(),
|
||||
MoreCommands = [new CommandContextItem(_settingsManager.Settings.SettingsPage)],
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ public partial class WindowWalkerCommandsProvider : CommandProvider
|
||||
_windowWalkerPageItem = new CommandItem(new WindowWalkerListPage())
|
||||
{
|
||||
Title = Resources.window_walker_top_level_command_title,
|
||||
Subtitle = Resources.windowwalker_name,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(Settings.SettingsPage),
|
||||
],
|
||||
|
||||
@@ -30,7 +30,6 @@ public sealed partial class WindowsSettingsCommandsProvider : CommandProvider
|
||||
_searchSettingsListItem = new CommandItem(new WindowsSettingsListPage(_windowsSettings))
|
||||
{
|
||||
Title = Resources.settings_title,
|
||||
Subtitle = Resources.settings_subtitle,
|
||||
};
|
||||
_fallback = new(_windowsSettings);
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ internal sealed partial class SectionsIndexPage : ListPage
|
||||
{
|
||||
Title = "A Gallery grid page with sections",
|
||||
},
|
||||
new ListItem(new SampleListPageWithSections(new GalleryGridLayout() { ShowTitle = false, ShowSubtitle = false }))
|
||||
{
|
||||
Title = "A Gallery grid page without labels with sections",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -14,7 +15,7 @@ public partial class BaseObservable : INotifyPropChanged
|
||||
{
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -22,10 +23,37 @@ public partial class BaseObservable : INotifyPropChanged
|
||||
// this can crash as we try to invoke the handlers from that process.
|
||||
// However, just catching it seems to still raise the event on the
|
||||
// new host?
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(propertyName));
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(propertyName!));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the backing field to the specified value and raises a property changed
|
||||
/// notification if the value is different from the current one.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the property.</typeparam>
|
||||
/// <param name="field">A reference to the backing field for the property.</param>
|
||||
/// <param name="value">The new value to assign to the property.</param>
|
||||
/// <param name="propertyName">
|
||||
/// The name of the property. This is optional and is usually supplied
|
||||
/// automatically by the <see cref="CallerMemberNameAttribute"/>.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> if the field was updated and a property changed
|
||||
/// notification was raised; otherwise, <see langword="false"/>.
|
||||
/// </returns>
|
||||
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName!);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,31 +6,11 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Command : BaseObservable, ICommand
|
||||
{
|
||||
public virtual string Name
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
public virtual string Name { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual string Id { get; set; } = string.Empty;
|
||||
|
||||
public virtual IconInfo Icon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
= new();
|
||||
public virtual IconInfo Icon { get; set => SetProperty(ref field, value); } = new();
|
||||
|
||||
IIconInfo ICommand.Icon => Icon;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandContextItem : CommandItem, ICommandContextItem
|
||||
{
|
||||
public virtual bool IsCritical { get; set; }
|
||||
public virtual bool IsCritical { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual KeyChord RequestedShortcut { get; set; }
|
||||
public virtual KeyChord RequestedShortcut { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public CommandContextItem(ICommand command)
|
||||
: base(command)
|
||||
|
||||
@@ -19,44 +19,36 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
private DataPackage? _dataPackage;
|
||||
private DataPackageView? _dataPackageView;
|
||||
|
||||
public virtual IIconInfo? Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
public virtual IIconInfo? Icon { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual string Title
|
||||
{
|
||||
get => !string.IsNullOrEmpty(_title) ? _title : _command?.Name ?? string.Empty;
|
||||
|
||||
set
|
||||
{
|
||||
var oldTitle = Title;
|
||||
_title = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
if (Title != oldTitle)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Subtitle
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
public virtual string Subtitle { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual ICommand? Command
|
||||
{
|
||||
get => _command;
|
||||
set
|
||||
{
|
||||
if (EqualityComparer<ICommand?>.Default.Equals(value, _command))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldTitle = Title;
|
||||
|
||||
if (_commandListener is not null)
|
||||
{
|
||||
_commandListener.Detach();
|
||||
@@ -71,8 +63,8 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
value.PropChanged += _commandListener.OnEvent;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Command));
|
||||
if (string.IsNullOrEmpty(_title))
|
||||
OnPropertyChanged();
|
||||
if (string.IsNullOrEmpty(_title) && oldTitle != Title)
|
||||
{
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
@@ -88,17 +80,7 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IContextItem[] MoreCommands
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(MoreCommands));
|
||||
}
|
||||
}
|
||||
|
||||
= [];
|
||||
public virtual IContextItem[] MoreCommands { get; set => SetProperty(ref field, value); } = [];
|
||||
|
||||
public DataPackage? DataPackage
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandResult : ICommandResult
|
||||
{
|
||||
public ICommandResultArgs? Args { get; private set; }
|
||||
public ICommandResultArgs? Args { get; private init; }
|
||||
|
||||
public CommandResultKind Kind { get; private set; } = CommandResultKind.Dismiss;
|
||||
public CommandResultKind Kind { get; private init; } = CommandResultKind.Dismiss;
|
||||
|
||||
public static CommandResult Dismiss()
|
||||
{
|
||||
|
||||
@@ -10,17 +10,9 @@ public abstract partial class ContentPage : Page, IContentPage
|
||||
{
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual IDetails? Details
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Details));
|
||||
}
|
||||
}
|
||||
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IContextItem[] Commands { get; set; } = [];
|
||||
public virtual IContextItem[] Commands { get; set => SetProperty(ref field, value); } = [];
|
||||
|
||||
public abstract IContent[] GetContent();
|
||||
|
||||
|
||||
@@ -7,65 +7,15 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Details : BaseObservable, IDetails, IExtendedAttributesProvider
|
||||
{
|
||||
public virtual IIconInfo HeroImage
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(HeroImage));
|
||||
}
|
||||
}
|
||||
public virtual IIconInfo HeroImage { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
|
||||
= new IconInfo();
|
||||
public virtual string Title { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual string Title
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
public virtual string Body { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
= string.Empty;
|
||||
public virtual IDetailsElement[] Metadata { get; set => SetProperty(ref field, value); } = [];
|
||||
|
||||
public virtual string Body
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Body));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual IDetailsElement[] Metadata
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Metadata));
|
||||
}
|
||||
}
|
||||
|
||||
= [];
|
||||
|
||||
public virtual ContentSize Size
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Size));
|
||||
}
|
||||
}
|
||||
|
||||
= ContentSize.Small;
|
||||
public virtual ContentSize Size { get; set => SetProperty(ref field, value); } = ContentSize.Small;
|
||||
|
||||
public IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||
{
|
||||
|
||||
@@ -6,39 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Filter : BaseObservable, IFilter
|
||||
{
|
||||
public virtual IIconInfo Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
public virtual IIconInfo Icon { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
|
||||
= new IconInfo();
|
||||
public virtual string Id { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual string Id
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string Name
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
public virtual string Name { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -6,17 +6,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class Filters : BaseObservable, IFilters
|
||||
{
|
||||
public string CurrentFilterId
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(CurrentFilterId));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
public string CurrentFilterId { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
// This method should be overridden in derived classes to provide the actual filters.
|
||||
public abstract IFilterItem[] GetFilters();
|
||||
|
||||
@@ -6,41 +6,11 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class FormContent : BaseObservable, IFormContent
|
||||
{
|
||||
public virtual string DataJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(DataJson));
|
||||
}
|
||||
}
|
||||
public virtual string DataJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
= string.Empty;
|
||||
public virtual string StateJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual string StateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(StateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string TemplateJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(TemplateJson));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
public virtual string TemplateJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual ICommandResult SubmitForm(string inputs, string data) => SubmitForm(inputs);
|
||||
|
||||
|
||||
@@ -6,27 +6,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class GalleryGridLayout : BaseObservable, IGalleryGridLayout
|
||||
{
|
||||
public virtual bool ShowTitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
public virtual bool ShowTitle { get; set => SetProperty(ref field, value); } = true;
|
||||
|
||||
= true;
|
||||
|
||||
public virtual bool ShowSubtitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowSubtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
public virtual bool ShowSubtitle { get; set => SetProperty(ref field, value); } = true;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -6,51 +6,13 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ListItem : CommandItem, IListItem
|
||||
{
|
||||
private ITag[] _tags = [];
|
||||
private IDetails? _details;
|
||||
public virtual ITag[] Tags { get; set => SetProperty(ref field, value); } = [];
|
||||
|
||||
private string _section = string.Empty;
|
||||
private string _textToSuggest = string.Empty;
|
||||
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual ITag[] Tags
|
||||
{
|
||||
get => _tags;
|
||||
set
|
||||
{
|
||||
_tags = value;
|
||||
OnPropertyChanged(nameof(Tags));
|
||||
}
|
||||
}
|
||||
public virtual string Section { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual IDetails? Details
|
||||
{
|
||||
get => _details;
|
||||
set
|
||||
{
|
||||
_details = value;
|
||||
OnPropertyChanged(nameof(Details));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Section
|
||||
{
|
||||
get => _section;
|
||||
set
|
||||
{
|
||||
_section = value;
|
||||
OnPropertyChanged(nameof(Section));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string TextToSuggest
|
||||
{
|
||||
get => _textToSuggest;
|
||||
set
|
||||
{
|
||||
_textToSuggest = value;
|
||||
OnPropertyChanged(nameof(TextToSuggest));
|
||||
}
|
||||
}
|
||||
public virtual string TextToSuggest { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public ListItem(ICommand command)
|
||||
: base(command)
|
||||
|
||||
@@ -8,85 +8,23 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ListPage : Page, IListPage
|
||||
{
|
||||
private string _placeholderText = string.Empty;
|
||||
private string _searchText = string.Empty;
|
||||
private bool _showDetails;
|
||||
private bool _hasMore;
|
||||
private IFilters? _filters;
|
||||
private IGridProperties? _gridProperties;
|
||||
private ICommandItem? _emptyContent;
|
||||
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual string PlaceholderText
|
||||
{
|
||||
get => _placeholderText;
|
||||
set
|
||||
{
|
||||
_placeholderText = value;
|
||||
OnPropertyChanged(nameof(PlaceholderText));
|
||||
}
|
||||
}
|
||||
private string _searchText = string.Empty;
|
||||
|
||||
public virtual string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
_searchText = value;
|
||||
OnPropertyChanged(nameof(SearchText));
|
||||
}
|
||||
}
|
||||
public virtual string PlaceholderText { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual bool ShowDetails
|
||||
{
|
||||
get => _showDetails;
|
||||
set
|
||||
{
|
||||
_showDetails = value;
|
||||
OnPropertyChanged(nameof(ShowDetails));
|
||||
}
|
||||
}
|
||||
public virtual string SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
|
||||
|
||||
public virtual bool HasMoreItems
|
||||
{
|
||||
get => _hasMore;
|
||||
set
|
||||
{
|
||||
_hasMore = value;
|
||||
OnPropertyChanged(nameof(HasMoreItems));
|
||||
}
|
||||
}
|
||||
public virtual bool ShowDetails { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IFilters? Filters
|
||||
{
|
||||
get => _filters;
|
||||
set
|
||||
{
|
||||
_filters = value;
|
||||
OnPropertyChanged(nameof(Filters));
|
||||
}
|
||||
}
|
||||
public virtual bool HasMoreItems { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IGridProperties? GridProperties
|
||||
{
|
||||
get => _gridProperties;
|
||||
set
|
||||
{
|
||||
_gridProperties = value;
|
||||
OnPropertyChanged(nameof(GridProperties));
|
||||
}
|
||||
}
|
||||
public virtual IFilters? Filters { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual ICommandItem? EmptyContent
|
||||
{
|
||||
get => _emptyContent;
|
||||
set
|
||||
{
|
||||
_emptyContent = value;
|
||||
OnPropertyChanged(nameof(EmptyContent));
|
||||
}
|
||||
}
|
||||
public virtual IGridProperties? GridProperties { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual ICommandItem? EmptyContent { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IListItem[] GetItems() => [];
|
||||
|
||||
|
||||
@@ -6,17 +6,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class MarkdownContent : BaseObservable, IMarkdownContent
|
||||
{
|
||||
public virtual string Body
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Body));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
public virtual string Body { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public MarkdownContent()
|
||||
{
|
||||
|
||||
@@ -6,15 +6,5 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class MediumGridLayout : BaseObservable, IMediumGridLayout
|
||||
{
|
||||
public virtual bool ShowTitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
public virtual bool ShowTitle { get; set => SetProperty(ref field, value); } = true;
|
||||
}
|
||||
|
||||
@@ -6,37 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Page : Command, IPage
|
||||
{
|
||||
private bool _loading;
|
||||
private string _title = string.Empty;
|
||||
private OptionalColor _accentColor;
|
||||
public virtual bool IsLoading { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual bool IsLoading
|
||||
{
|
||||
get => _loading;
|
||||
set
|
||||
{
|
||||
_loading = value;
|
||||
OnPropertyChanged(nameof(IsLoading));
|
||||
}
|
||||
}
|
||||
public virtual string Title { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public virtual string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual OptionalColor AccentColor
|
||||
{
|
||||
get => _accentColor;
|
||||
set
|
||||
{
|
||||
_accentColor = value;
|
||||
OnPropertyChanged(nameof(AccentColor));
|
||||
}
|
||||
}
|
||||
public virtual OptionalColor AccentColor { get; set => SetProperty(ref field, value); }
|
||||
}
|
||||
|
||||
@@ -6,27 +6,7 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ProgressState : BaseObservable, IProgressState
|
||||
{
|
||||
private bool _isIndeterminate;
|
||||
public virtual bool IsIndeterminate { get; set => SetProperty(ref field, value); }
|
||||
|
||||
private uint _progressPercent;
|
||||
|
||||
public virtual bool IsIndeterminate
|
||||
{
|
||||
get => _isIndeterminate;
|
||||
set
|
||||
{
|
||||
_isIndeterminate = value;
|
||||
OnPropertyChanged(nameof(IsIndeterminate));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual uint ProgressPercent
|
||||
{
|
||||
get => _progressPercent;
|
||||
set
|
||||
{
|
||||
_progressPercent = value;
|
||||
OnPropertyChanged(nameof(ProgressPercent));
|
||||
}
|
||||
}
|
||||
public virtual uint ProgressPercent { get; set => SetProperty(ref field, value); }
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class PropChangedEventArgs : IPropChangedEventArgs
|
||||
|
||||
@@ -12,11 +12,6 @@ public sealed partial class Section : IEnumerable<IListItem>
|
||||
|
||||
public string SectionTitle { get; set; } = string.Empty;
|
||||
|
||||
private Separator CreateSectionListItem()
|
||||
{
|
||||
return new Separator(SectionTitle);
|
||||
}
|
||||
|
||||
public Section(string sectionName, IListItem[] items)
|
||||
{
|
||||
SectionTitle = sectionName;
|
||||
@@ -33,6 +28,11 @@ public sealed partial class Section : IEnumerable<IListItem>
|
||||
{
|
||||
}
|
||||
|
||||
private Separator CreateSectionListItem()
|
||||
{
|
||||
return new Separator(SectionTitle);
|
||||
}
|
||||
|
||||
public IEnumerator<IListItem> GetEnumerator() => Items.ToList().GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
@@ -4,15 +4,8 @@
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
public partial class Separator : BaseObservable, IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
{
|
||||
public Separator(string? title = "")
|
||||
: base()
|
||||
{
|
||||
Section = title ?? string.Empty;
|
||||
Command = null;
|
||||
}
|
||||
|
||||
public IDetails? Details => null;
|
||||
|
||||
public string? Section { get; private set; }
|
||||
@@ -21,7 +14,7 @@ public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFil
|
||||
|
||||
public string? TextToSuggest => null;
|
||||
|
||||
public ICommand? Command { get; private set; }
|
||||
public ICommand? Command => null;
|
||||
|
||||
public IIconInfo? Icon => null;
|
||||
|
||||
@@ -32,12 +25,19 @@ public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFil
|
||||
public string? Title
|
||||
{
|
||||
get => Section;
|
||||
set => Section = value;
|
||||
set
|
||||
{
|
||||
if (Section != value)
|
||||
{
|
||||
Section = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(Section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event Windows.Foundation.TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
|
||||
public Separator(string? title = "")
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
Section = title ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,37 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class StatusMessage : BaseObservable, IStatusMessage
|
||||
{
|
||||
public virtual string Message
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Message));
|
||||
}
|
||||
}
|
||||
public virtual string Message { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
= string.Empty;
|
||||
public virtual MessageState State { get; set => SetProperty(ref field, value); } = MessageState.Info;
|
||||
|
||||
public virtual MessageState State
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(State));
|
||||
}
|
||||
}
|
||||
|
||||
= MessageState.Info;
|
||||
|
||||
public virtual IProgressState? Progress
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Progress));
|
||||
}
|
||||
}
|
||||
public virtual IProgressState? Progress { get; set => SetProperty(ref field, value); }
|
||||
}
|
||||
|
||||
@@ -6,63 +6,15 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Tag : BaseObservable, ITag
|
||||
{
|
||||
private OptionalColor _foreground;
|
||||
private OptionalColor _background;
|
||||
private string _text = string.Empty;
|
||||
public virtual OptionalColor Foreground { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual OptionalColor Foreground
|
||||
{
|
||||
get => _foreground;
|
||||
set
|
||||
{
|
||||
_foreground = value;
|
||||
OnPropertyChanged(nameof(Foreground));
|
||||
}
|
||||
}
|
||||
public virtual OptionalColor Background { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual OptionalColor Background
|
||||
{
|
||||
get => _background;
|
||||
set
|
||||
{
|
||||
_background = value;
|
||||
OnPropertyChanged(nameof(Background));
|
||||
}
|
||||
}
|
||||
public virtual IIconInfo Icon { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
|
||||
public virtual IIconInfo Icon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
public virtual string Text { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Text
|
||||
{
|
||||
get => _text;
|
||||
set
|
||||
{
|
||||
_text = value;
|
||||
OnPropertyChanged(nameof(Text));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string ToolTip
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ToolTip));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
public virtual string ToolTip { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
|
||||
public Tag()
|
||||
{
|
||||
@@ -70,6 +22,6 @@ public partial class Tag : BaseObservable, ITag
|
||||
|
||||
public Tag(string text)
|
||||
{
|
||||
_text = text;
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,11 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class TreeContent : BaseObservable, ITreeContent
|
||||
{
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public IContent[] Children { get; set; } = [];
|
||||
|
||||
public virtual IContent? RootContent
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(RootContent));
|
||||
}
|
||||
}
|
||||
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
public virtual IContent? RootContent { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual IContent[] GetChildren() => Children;
|
||||
|
||||
|
||||
@@ -191,27 +191,22 @@ bool EditorParameters::Save(const WorkAreaConfiguration& configuration, OnThread
|
||||
|
||||
monitorJson.dpi = dpi;
|
||||
|
||||
MONITORINFOEX monitorInfo{};
|
||||
// Get DPI-unaware values for dimensions (virtual coordinates for WPF sizing)
|
||||
MONITORINFOEX monitorInfoUnaware{};
|
||||
dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
|
||||
monitorInfo.cbSize = sizeof(monitorInfo);
|
||||
if (!GetMonitorInfo(monitor, &monitorInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
monitorInfoUnaware.cbSize = sizeof(monitorInfoUnaware);
|
||||
GetMonitorInfo(monitor, &monitorInfoUnaware);
|
||||
} }).wait();
|
||||
|
||||
float width = static_cast<float>(monitorInfo.rcMonitor.right - monitorInfo.rcMonitor.left);
|
||||
float height = static_cast<float>(monitorInfo.rcMonitor.bottom - monitorInfo.rcMonitor.top);
|
||||
DPIAware::Convert(monitor, width, height);
|
||||
// Dimensions in virtual coordinates (from DPI-unaware thread)
|
||||
monitorJson.monitorWidth = monitorInfoUnaware.rcMonitor.right - monitorInfoUnaware.rcMonitor.left;
|
||||
monitorJson.monitorHeight = monitorInfoUnaware.rcMonitor.bottom - monitorInfoUnaware.rcMonitor.top;
|
||||
monitorJson.workAreaWidth = monitorInfoUnaware.rcWork.right - monitorInfoUnaware.rcWork.left;
|
||||
monitorJson.workAreaHeight = monitorInfoUnaware.rcWork.bottom - monitorInfoUnaware.rcWork.top;
|
||||
|
||||
monitorJson.monitorWidth = static_cast<int>(std::roundf(width));
|
||||
monitorJson.monitorHeight = static_cast<int>(std::roundf(height));
|
||||
|
||||
// use dpi-unaware values
|
||||
monitorJson.top = monitorInfo.rcWork.top;
|
||||
monitorJson.left = monitorInfo.rcWork.left;
|
||||
monitorJson.workAreaWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left;
|
||||
monitorJson.workAreaHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top;
|
||||
// Position in virtual coordinates (matched by DPI-unaware context in WPF editor)
|
||||
monitorJson.left = monitorInfoUnaware.rcWork.left;
|
||||
monitorJson.top = monitorInfoUnaware.rcWork.top;
|
||||
|
||||
argsJson.monitors.emplace_back(std::move(monitorJson));
|
||||
}
|
||||
|
||||
@@ -67,10 +67,18 @@ namespace FancyZonesEditor.Models
|
||||
Window.KeyUp += ((App)Application.Current).App_KeyUp;
|
||||
Window.KeyDown += ((App)Application.Current).App_KeyDown;
|
||||
|
||||
// Store for DPI-unaware positioning
|
||||
_virtualWorkArea = workArea;
|
||||
|
||||
// Set initial WPF properties
|
||||
Window.Left = workArea.X;
|
||||
Window.Top = workArea.Y;
|
||||
Window.Width = workArea.Width;
|
||||
Window.Height = workArea.Height;
|
||||
|
||||
// After HWND is created, reposition using DPI-unaware context
|
||||
// This matches the C++ backend which uses a DPI-unaware thread
|
||||
Window.SourceInitialized += OnWindowSourceInitialized;
|
||||
}
|
||||
|
||||
public Monitor(string monitorName, string monitorInstanceId, string monitorSerialNumber, string virtualDesktop, int dpi, Rect workArea, Size monitorSize)
|
||||
@@ -80,16 +88,33 @@ namespace FancyZonesEditor.Models
|
||||
}
|
||||
|
||||
private LayoutSettings _settings;
|
||||
private Rect _virtualWorkArea;
|
||||
|
||||
private void OnWindowSourceInitialized(object sender, EventArgs e)
|
||||
{
|
||||
// Reposition window using DPI-unaware context to match the virtual coordinates
|
||||
// from the FancyZones C++ backend (which uses a DPI-unaware thread)
|
||||
Utils.NativeMethods.SetWindowPositionDpiUnaware(
|
||||
Window,
|
||||
(int)_virtualWorkArea.X,
|
||||
(int)_virtualWorkArea.Y,
|
||||
(int)_virtualWorkArea.Width,
|
||||
(int)_virtualWorkArea.Height);
|
||||
}
|
||||
|
||||
public void Scale(double scaleFactor)
|
||||
{
|
||||
Device.Scale(scaleFactor);
|
||||
|
||||
var workArea = Device.WorkAreaRect;
|
||||
Window.Left = workArea.X;
|
||||
Window.Top = workArea.Y;
|
||||
Window.Width = workArea.Width;
|
||||
Window.Height = workArea.Height;
|
||||
_virtualWorkArea = Device.WorkAreaRect;
|
||||
|
||||
// Use DPI-unaware positioning
|
||||
Utils.NativeMethods.SetWindowPositionDpiUnaware(
|
||||
Window,
|
||||
(int)_virtualWorkArea.X,
|
||||
(int)_virtualWorkArea.Y,
|
||||
(int)_virtualWorkArea.Width,
|
||||
(int)_virtualWorkArea.Height);
|
||||
}
|
||||
|
||||
public void SetLayoutSettings(LayoutModel model)
|
||||
|
||||
@@ -69,7 +69,11 @@ namespace FancyZonesEditor.Utils
|
||||
}
|
||||
else
|
||||
{
|
||||
return ScreenBoundsWidth + " × " + ScreenBoundsHeight;
|
||||
// Convert virtual coordinates to physical resolution by applying DPI scale
|
||||
double scale = DPI / 96.0;
|
||||
int physicalWidth = (int)Math.Round(ScreenBoundsWidth * scale);
|
||||
int physicalHeight = (int)Math.Round(ScreenBoundsHeight * scale);
|
||||
return physicalWidth + " × " + physicalHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -17,14 +17,48 @@ namespace FancyZonesEditor.Utils
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||
|
||||
private const int GWL_EX_STYLE = -20;
|
||||
private const int WS_EX_APPWINDOW = 0x00040000;
|
||||
private const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
|
||||
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
|
||||
|
||||
public static void SetWindowStyleToolWindow(Window hwnd)
|
||||
{
|
||||
var helper = new WindowInteropHelper(hwnd).Handle;
|
||||
_ = SetWindowLong(helper, GWL_EX_STYLE, (GetWindowLong(helper, GWL_EX_STYLE) | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates
|
||||
/// from the FancyZones C++ backend (which uses a DPI-unaware thread).
|
||||
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
/// </summary>
|
||||
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
|
||||
{
|
||||
var helper = new WindowInteropHelper(window).Handle;
|
||||
if (helper != IntPtr.Zero)
|
||||
{
|
||||
// Temporarily switch to DPI-unaware context to position window.
|
||||
// This matches how the C++ backend gets coordinates via dpiUnawareThread.
|
||||
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
|
||||
try
|
||||
{
|
||||
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetThreadDpiAwarenessContext(oldContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PRIResource Include="..\Settings.UI\Strings\en-us\Resources.resw">
|
||||
<Link>Strings\en-us\Resources.resw</Link>
|
||||
<PRIResource Include="..\Settings.UI\Strings\**\Resources.resw">
|
||||
<Link>Strings\%(RecursiveDir)Resources.resw</Link>
|
||||
</PRIResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.QuickAccess.Helpers;
|
||||
using Microsoft.PowerToys.QuickAccess.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.Controls;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
@@ -26,6 +27,7 @@ public sealed class AllAppsViewModel : Observable
|
||||
private readonly SettingsUtils _settingsUtils;
|
||||
private readonly ResourceLoader _resourceLoader;
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
private readonly List<FlyoutMenuItem> _allFlyoutMenuItems = new();
|
||||
private GeneralSettings _generalSettings;
|
||||
|
||||
public ObservableCollection<FlyoutMenuItem> FlyoutMenuItems { get; }
|
||||
@@ -58,9 +60,28 @@ public sealed class AllAppsViewModel : Observable
|
||||
_resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader;
|
||||
FlyoutMenuItems = new ObservableCollection<FlyoutMenuItem>();
|
||||
|
||||
BuildFlyoutMenuItems();
|
||||
RefreshFlyoutMenuItems();
|
||||
}
|
||||
|
||||
private void BuildFlyoutMenuItems()
|
||||
{
|
||||
_allFlyoutMenuItems.Clear();
|
||||
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
|
||||
{
|
||||
if (moduleType == ModuleType.GeneralSettings)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_allFlyoutMenuItems.Add(new FlyoutMenuItem
|
||||
{
|
||||
Tag = moduleType,
|
||||
EnabledChangedCallback = EnabledChangedOnUI,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(GeneralSettings newSettings)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
@@ -82,63 +103,37 @@ public sealed class AllAppsViewModel : Observable
|
||||
|
||||
private void RefreshFlyoutMenuItems()
|
||||
{
|
||||
var desiredItems = new List<FlyoutMenuItem>();
|
||||
|
||||
foreach (ModuleType moduleType in Enum.GetValues<ModuleType>())
|
||||
foreach (var item in _allFlyoutMenuItems)
|
||||
{
|
||||
if (moduleType == ModuleType.GeneralSettings)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var moduleType = item.Tag;
|
||||
var gpo = Helpers.ModuleGpoHelper.GetModuleGpoConfiguration(moduleType);
|
||||
var isLocked = gpo is GpoRuleConfigured.Enabled or GpoRuleConfigured.Disabled;
|
||||
var isEnabled = gpo == GpoRuleConfigured.Enabled || (!isLocked && Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetIsModuleEnabled(_generalSettings, moduleType));
|
||||
|
||||
var existingItem = FlyoutMenuItems.FirstOrDefault(x => x.Tag == moduleType);
|
||||
item.Label = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType));
|
||||
item.IsLocked = isLocked;
|
||||
item.Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType);
|
||||
|
||||
if (existingItem != null)
|
||||
if (item.IsEnabled != isEnabled)
|
||||
{
|
||||
existingItem.Label = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType));
|
||||
existingItem.IsLocked = isLocked;
|
||||
existingItem.Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType);
|
||||
|
||||
if (existingItem.IsEnabled != isEnabled)
|
||||
{
|
||||
var callback = existingItem.EnabledChangedCallback;
|
||||
existingItem.EnabledChangedCallback = null;
|
||||
existingItem.IsEnabled = isEnabled;
|
||||
existingItem.EnabledChangedCallback = callback;
|
||||
}
|
||||
|
||||
desiredItems.Add(existingItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredItems.Add(new FlyoutMenuItem
|
||||
{
|
||||
Label = _resourceLoader.GetString(Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleLabelResourceName(moduleType)),
|
||||
IsEnabled = isEnabled,
|
||||
IsLocked = isLocked,
|
||||
Tag = moduleType,
|
||||
Icon = Microsoft.PowerToys.Settings.UI.Library.Helpers.ModuleHelper.GetModuleTypeFluentIconName(moduleType),
|
||||
EnabledChangedCallback = EnabledChangedOnUI,
|
||||
});
|
||||
item.UpdateStatus(isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
var sortedItems = DashboardSortOrder switch
|
||||
{
|
||||
DashboardSortOrder.ByStatus => desiredItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
|
||||
_ => desiredItems.OrderBy(x => x.Label).ToList(),
|
||||
DashboardSortOrder.ByStatus => _allFlyoutMenuItems.OrderByDescending(x => x.IsEnabled).ThenBy(x => x.Label).ToList(),
|
||||
_ => _allFlyoutMenuItems.OrderBy(x => x.Label).ToList(),
|
||||
};
|
||||
|
||||
for (int i = FlyoutMenuItems.Count - 1; i >= 0; i--)
|
||||
if (FlyoutMenuItems.Count == 0)
|
||||
{
|
||||
if (!sortedItems.Contains(FlyoutMenuItems[i]))
|
||||
foreach (var item in sortedItems)
|
||||
{
|
||||
FlyoutMenuItems.RemoveAt(i);
|
||||
FlyoutMenuItems.Add(item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sortedItems.Count; i++)
|
||||
@@ -146,20 +141,17 @@ public sealed class AllAppsViewModel : Observable
|
||||
var item = sortedItems[i];
|
||||
var oldIndex = FlyoutMenuItems.IndexOf(item);
|
||||
|
||||
if (oldIndex < 0)
|
||||
{
|
||||
FlyoutMenuItems.Insert(i, item);
|
||||
}
|
||||
else if (oldIndex != i)
|
||||
if (oldIndex != -1 && oldIndex != i)
|
||||
{
|
||||
FlyoutMenuItems.Move(oldIndex, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnabledChangedOnUI(FlyoutMenuItem item)
|
||||
private void EnabledChangedOnUI(ModuleListItem item)
|
||||
{
|
||||
if (_coordinator.UpdateModuleEnabled(item.Tag, item.IsEnabled))
|
||||
var flyoutItem = (FlyoutMenuItem)item;
|
||||
if (_coordinator.UpdateModuleEnabled(flyoutItem.Tag, flyoutItem.IsEnabled))
|
||||
{
|
||||
_coordinator.NotifyUserSettingsInteraction();
|
||||
}
|
||||
|
||||
@@ -22,21 +22,6 @@ public sealed class FlyoutMenuItem : ModuleListItem
|
||||
set => base.Tag = value;
|
||||
}
|
||||
|
||||
public override bool IsEnabled
|
||||
{
|
||||
get => base.IsEnabled;
|
||||
set
|
||||
{
|
||||
if (base.IsEnabled != value)
|
||||
{
|
||||
base.IsEnabled = value;
|
||||
EnabledChangedCallback?.Invoke(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Action<FlyoutMenuItem>? EnabledChangedCallback { get; set; }
|
||||
|
||||
public bool Visible
|
||||
{
|
||||
get => _visible;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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; // For Action
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
@@ -17,6 +18,22 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
private bool _isLocked;
|
||||
private object? _tag;
|
||||
private ICommand? _clickCommand;
|
||||
private bool _isUpdating;
|
||||
|
||||
public Action<ModuleListItem>? EnabledChangedCallback { get; set; }
|
||||
|
||||
public void UpdateStatus(bool isEnabled)
|
||||
{
|
||||
_isUpdating = true;
|
||||
try
|
||||
{
|
||||
IsEnabled = isEnabled;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Label
|
||||
{
|
||||
@@ -79,6 +96,11 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
{
|
||||
_isEnabled = value;
|
||||
OnPropertyChanged();
|
||||
|
||||
if (!_isUpdating)
|
||||
{
|
||||
EnabledChangedCallback?.Invoke(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,13 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
eventHandle.Set();
|
||||
}
|
||||
|
||||
return true;
|
||||
case ModuleType.LightSwitch:
|
||||
using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.LightSwitchToggleEvent()))
|
||||
{
|
||||
eventHandle.Set();
|
||||
}
|
||||
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -64,6 +64,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
AddFlyoutMenuItem(ModuleType.EnvironmentVariables);
|
||||
AddFlyoutMenuItem(ModuleType.FancyZones);
|
||||
AddFlyoutMenuItem(ModuleType.Hosts);
|
||||
AddFlyoutMenuItem(ModuleType.LightSwitch);
|
||||
AddFlyoutMenuItem(ModuleType.PowerLauncher);
|
||||
AddFlyoutMenuItem(ModuleType.PowerOCR);
|
||||
AddFlyoutMenuItem(ModuleType.RegistryPreview);
|
||||
@@ -120,6 +121,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
|
||||
{
|
||||
ModuleType.ColorPicker => SettingsRepository<ColorPickerSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
|
||||
ModuleType.FancyZones => SettingsRepository<FancyZonesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.FancyzonesEditorHotkey.Value.ToString(),
|
||||
ModuleType.LightSwitch => SettingsRepository<LightSwitchSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ToggleThemeHotkey.Value.ToString(),
|
||||
ModuleType.PowerLauncher => SettingsRepository<PowerLauncherSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.OpenPowerLauncher.ToString(),
|
||||
ModuleType.PowerOCR => SettingsRepository<PowerOcrSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.ActivationShortcut.ToString(),
|
||||
ModuleType.Workspaces => SettingsRepository<WorkspacesSettings>.GetInstance(SettingsUtils.Default).SettingsConfig.Properties.Hotkey.Value.ToString(),
|
||||
|
||||
@@ -11,7 +11,7 @@ using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
|
||||
|
||||
namespace Settings.UI.Library
|
||||
namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
{
|
||||
public class LightSwitchSettings : BasePTModuleSettings, ISettingsConfig, ICloneable, IHotkeyConfig
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonSerializable(typeof(HostsSettings))]
|
||||
[JsonSerializable(typeof(ImageResizerSettings))]
|
||||
[JsonSerializable(typeof(KeyboardManagerSettings))]
|
||||
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
|
||||
[JsonSerializable(typeof(LightSwitchSettings))]
|
||||
[JsonSerializable(typeof(MeasureToolSettings))]
|
||||
[JsonSerializable(typeof(MouseHighlighterSettings))]
|
||||
[JsonSerializable(typeof(MouseJumpSettings))]
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
[JsonSerializable(typeof(FileLocksmithSettings))]
|
||||
[JsonSerializable(typeof(FindMyMouseSettings))]
|
||||
[JsonSerializable(typeof(IList<PowerToysReleaseInfo>))]
|
||||
[JsonSerializable(typeof(SettingsUILibrary.LightSwitchSettings))]
|
||||
[JsonSerializable(typeof(LightSwitchSettings))]
|
||||
[JsonSerializable(typeof(MeasureToolSettings))]
|
||||
[JsonSerializable(typeof(MouseHighlighterSettings))]
|
||||
[JsonSerializable(typeof(MouseJumpSettings))]
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
var darkSettings = this.moduleSettingsRepository.SettingsConfig;
|
||||
|
||||
// Pass them into the ViewModel
|
||||
this.ViewModel = new LightSwitchViewModel(darkSettings, this.sendConfigMsg);
|
||||
this.ViewModel = new LightSwitchViewModel(this.generalSettingsRepository, darkSettings, ShellPage.SendDefaultIPCMessage);
|
||||
this.ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
this.LoadSettings(this.generalSettingsRepository, this.moduleSettingsRepository);
|
||||
@@ -185,7 +185,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
// need to save the values
|
||||
this.ViewModel.Latitude = latitude.ToString(CultureInfo.InvariantCulture);
|
||||
this.ViewModel.Longitude = longitude.ToString(CultureInfo.InvariantCulture);
|
||||
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}<EFBFBD>, {this.ViewModel.Longitude}<EFBFBD>";
|
||||
this.ViewModel.SyncButtonInformation = $"{this.ViewModel.Latitude}°, {this.ViewModel.Longitude}°";
|
||||
|
||||
var result = SunCalc.CalculateSunriseSunset(latitude, longitude, DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
|
||||
|
||||
@@ -293,18 +293,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private void UpdateEnabledState(bool recommendedState)
|
||||
{
|
||||
var enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredLightSwitchEnabledValue();
|
||||
|
||||
if (enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
|
||||
{
|
||||
// Get the enabled state from GPO.
|
||||
this.ViewModel.IsEnabledGpoConfigured = true;
|
||||
this.ViewModel.EnabledGPOConfiguration = enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ViewModel.IsEnabled = recommendedState;
|
||||
}
|
||||
ViewModel.RefreshEnabledState();
|
||||
}
|
||||
|
||||
private async void SyncLocationButton_Click(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -26,21 +26,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
set => base.Tag = value;
|
||||
}
|
||||
|
||||
public Action<DashboardListItem> EnabledChangedCallback { get; set; }
|
||||
|
||||
public override bool IsEnabled
|
||||
{
|
||||
get => base.IsEnabled;
|
||||
set
|
||||
{
|
||||
if (base.IsEnabled != value)
|
||||
{
|
||||
base.IsEnabled = value;
|
||||
EnabledChangedCallback?.Invoke(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Visible
|
||||
{
|
||||
get => _visible;
|
||||
|
||||
@@ -50,7 +50,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
// Flag to prevent circular updates when a UI toggle triggers settings changes.
|
||||
private bool _isUpdatingFromUI;
|
||||
private bool _isUpdatingFromSettings;
|
||||
|
||||
private AllHotkeyConflictsData _allHotkeyConflictsData = new AllHotkeyConflictsData();
|
||||
|
||||
@@ -258,7 +257,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Only update if there's an actual change to minimize UI notifications.
|
||||
if (item.IsEnabled != newEnabledState)
|
||||
{
|
||||
item.IsEnabled = newEnabledState;
|
||||
item.UpdateStatus(newEnabledState);
|
||||
}
|
||||
|
||||
if (item.IsLocked != newLockedState)
|
||||
@@ -275,19 +274,17 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
/// Sets the _isUpdatingFromUI flag to prevent circular updates, then updates
|
||||
/// settings, re-sorts if needed, and refreshes dependent collections.
|
||||
/// </summary>
|
||||
private void EnabledChangedOnUI(DashboardListItem dashboardListItem)
|
||||
private void EnabledChangedOnUI(ModuleListItem item)
|
||||
{
|
||||
if (_isUpdatingFromSettings)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var dashboardListItem = (DashboardListItem)item;
|
||||
var isEnabled = dashboardListItem.IsEnabled;
|
||||
|
||||
_isUpdatingFromUI = true;
|
||||
try
|
||||
{
|
||||
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, dashboardListItem.IsEnabled);
|
||||
Views.ShellPage.UpdateGeneralSettingsCallback(dashboardListItem.Tag, isEnabled);
|
||||
|
||||
if (dashboardListItem.Tag == ModuleType.NewPlus && dashboardListItem.IsEnabled == true)
|
||||
if (dashboardListItem.Tag == ModuleType.NewPlus && isEnabled == true)
|
||||
{
|
||||
var settingsUtils = SettingsUtils.Default;
|
||||
var settings = NewPlusViewModel.LoadSettings(settingsUtils);
|
||||
@@ -325,7 +322,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return;
|
||||
}
|
||||
|
||||
_isUpdatingFromSettings = true;
|
||||
try
|
||||
{
|
||||
RefreshModuleList();
|
||||
@@ -340,10 +336,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
{
|
||||
Logger.LogError($"Updating active/disabled modules list failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdatingFromSettings = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,8 +14,10 @@ using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
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 Newtonsoft.Json.Linq;
|
||||
using PowerToys.GPOWrapper;
|
||||
using Settings.UI.Library;
|
||||
using Settings.UI.Library.Helpers;
|
||||
|
||||
@@ -27,10 +29,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
private Func<string, int> SendConfigMSG { get; }
|
||||
|
||||
private GeneralSettings GeneralSettingsConfig { get; set; }
|
||||
|
||||
public ObservableCollection<SearchLocation> SearchLocations { get; } = new();
|
||||
|
||||
public LightSwitchViewModel(LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
|
||||
public LightSwitchViewModel(ISettingsRepository<GeneralSettings> settingsRepository, LightSwitchSettings initialSettings = null, Func<string, int> ipcMSGCallBackFunc = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||
InitializeEnabledValue();
|
||||
|
||||
_moduleSettings = initialSettings ?? new LightSwitchSettings();
|
||||
SendConfigMSG = ipcMSGCallBackFunc;
|
||||
|
||||
@@ -58,6 +66,21 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
return hotkeysDict;
|
||||
}
|
||||
|
||||
private void InitializeEnabledValue()
|
||||
{
|
||||
_enabledGpoRuleConfiguration = GPOWrapper.GetConfiguredLightSwitchEnabledValue();
|
||||
if (_enabledGpoRuleConfiguration == GpoRuleConfigured.Disabled || _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled)
|
||||
{
|
||||
// Get the enabled state from GPO.
|
||||
_enabledStateIsGPOConfigured = true;
|
||||
_isEnabled = _enabledGpoRuleConfiguration == GpoRuleConfigured.Enabled;
|
||||
}
|
||||
else
|
||||
{
|
||||
_isEnabled = GeneralSettingsConfig.Enabled.LightSwitch;
|
||||
}
|
||||
}
|
||||
|
||||
private void ForceLightNow()
|
||||
{
|
||||
Logger.LogInfo("Sending custom action: forceLight");
|
||||
@@ -93,33 +116,26 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured)
|
||||
{
|
||||
return _enabledGPOConfiguration;
|
||||
}
|
||||
else
|
||||
{
|
||||
return _isEnabled;
|
||||
}
|
||||
}
|
||||
get => _isEnabled;
|
||||
|
||||
set
|
||||
{
|
||||
if (_isEnabled != value)
|
||||
if (_enabledStateIsGPOConfigured)
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured)
|
||||
{
|
||||
// If it's GPO configured, shouldn't be able to change this state.
|
||||
return;
|
||||
}
|
||||
// If it's GPO configured, shouldn't be able to change this state.
|
||||
return;
|
||||
}
|
||||
|
||||
if (value != _isEnabled)
|
||||
{
|
||||
_isEnabled = value;
|
||||
|
||||
RefreshEnabledState();
|
||||
// Set the status in the general settings configuration
|
||||
GeneralSettingsConfig.Enabled.LightSwitch = value;
|
||||
OutGoingGeneralSettings snd = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
||||
|
||||
NotifyPropertyChanged();
|
||||
SendConfigMSG(snd.ToString());
|
||||
OnPropertyChanged(nameof(IsEnabled));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,24 +143,16 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
public bool IsEnabledGpoConfigured
|
||||
{
|
||||
get => _enabledStateIsGPOConfigured;
|
||||
set
|
||||
{
|
||||
if (_enabledStateIsGPOConfigured != value)
|
||||
{
|
||||
_enabledStateIsGPOConfigured = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnabledGPOConfiguration
|
||||
public GpoRuleConfigured EnabledGPOConfiguration
|
||||
{
|
||||
get => _enabledGPOConfiguration;
|
||||
get => _enabledGpoRuleConfiguration;
|
||||
set
|
||||
{
|
||||
if (_enabledGPOConfiguration != value)
|
||||
if (_enabledGpoRuleConfiguration != value)
|
||||
{
|
||||
_enabledGPOConfiguration = value;
|
||||
_enabledGpoRuleConfiguration = value;
|
||||
NotifyPropertyChanged();
|
||||
}
|
||||
}
|
||||
@@ -575,7 +583,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
|
||||
private bool _enabledStateIsGPOConfigured;
|
||||
private bool _enabledGPOConfiguration;
|
||||
private GpoRuleConfigured _enabledGpoRuleConfiguration;
|
||||
private LightSwitchSettings _moduleSettings;
|
||||
private bool _isEnabled;
|
||||
private HotkeySettings _toggleThemeHotkey;
|
||||
|
||||
Reference in New Issue
Block a user