mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-18 18:17:43 +01:00
Compare commits
2 Commits
stable
...
user/yeela
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
336f7d8c47 | ||
|
|
045c33930e |
7
.github/actions/spell-check/expect.txt
vendored
7
.github/actions/spell-check/expect.txt
vendored
@@ -209,14 +209,12 @@ changecursor
|
||||
CHILDACTIVATE
|
||||
CHILDWINDOW
|
||||
CHOOSEFONT
|
||||
CIBUILD
|
||||
cidl
|
||||
CIELCh
|
||||
cim
|
||||
CImage
|
||||
cla
|
||||
CLASSDC
|
||||
classmethod
|
||||
CLASSNOTAVAILABLE
|
||||
CLEARTYPE
|
||||
clickable
|
||||
@@ -1604,7 +1602,7 @@ sharpfuzz
|
||||
SHCNE
|
||||
SHCNF
|
||||
SHCONTF
|
||||
shcore
|
||||
Shcore
|
||||
shellapi
|
||||
SHELLDETAILS
|
||||
SHELLDLL
|
||||
@@ -1703,7 +1701,6 @@ srw
|
||||
srwlock
|
||||
sse
|
||||
ssf
|
||||
sszzz
|
||||
STACKFRAME
|
||||
stackoverflow
|
||||
STARTF
|
||||
@@ -1714,7 +1711,6 @@ STARTUPINFOW
|
||||
startupscreen
|
||||
STATFLAG
|
||||
STATICEDGE
|
||||
staticmethod
|
||||
STATSTG
|
||||
stdafx
|
||||
STDAPI
|
||||
@@ -1881,7 +1877,6 @@ 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,45 +6,13 @@ description: 'Generate an 80-character git commit title for the local diff'
|
||||
|
||||
# Generate Commit Title
|
||||
|
||||
## Purpose
|
||||
Provide a single-line, ready-to-paste git commit title (<= 80 characters) that reflects the most important local changes since `HEAD`.
|
||||
**Goal:** Provide a ready-to-paste git commit title (<= 80 characters) that captures the most important local changes since `HEAD`.
|
||||
|
||||
## 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`
|
||||
**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`.
|
||||
|
||||
1
.github/prompts/create-pr-summary.prompt.md
vendored
1
.github/prompts/create-pr-summary.prompt.md
vendored
@@ -22,4 +22,3 @@ 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`.
|
||||
- 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.
|
||||
- Resolve findings solely by editing `.github/actions/spell-check/expect.txt`; reuse existing entries.
|
||||
- Leave all other files and topics untouched.
|
||||
|
||||
**Prerequisites:**
|
||||
- Install GitHub CLI if it is not present: `winget install GitHub.cli`.
|
||||
@@ -20,6 +20,5 @@ 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, 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.
|
||||
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.
|
||||
@@ -125,10 +125,6 @@
|
||||
"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",
|
||||
|
||||
@@ -10,7 +10,7 @@ parameters:
|
||||
default: {}
|
||||
|
||||
steps:
|
||||
- task: EsrpCodeSigning@6
|
||||
- task: EsrpCodeSigning@5
|
||||
displayName: 🔏 ${{ parameters.displayName }}
|
||||
inputs:
|
||||
ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }}
|
||||
|
||||
17
.vscode/settings.json
vendored
17
.vscode/settings.json
vendored
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -13,8 +13,6 @@
|
||||
<PackageVersion Include="Microsoft.Bot.AdaptiveExpressions.Core" Version="4.23.0" />
|
||||
<PackageVersion Include="Appium.WebDriver" Version="4.4.5" />
|
||||
<PackageVersion Include="CoenM.ImageSharp.ImageHash" Version="1.3.6" />
|
||||
<!-- Pin the SixLabors.ImageSharp version (a transitive dependency of CoenM.ImageSharp.ImageHash) to restore functionality and apply patches. -->
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="2.1.12" />
|
||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.250402" />
|
||||
@@ -26,7 +24,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260107-build.2454" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||
|
||||
@@ -48,7 +48,7 @@ But to get started quickly, choose one of the installation methods below:
|
||||
<details open>
|
||||
<summary><strong>Download .exe from GitHub</strong></summary>
|
||||
<br/>
|
||||
Go to the <a href="https://aka.ms/installPowerToys">PowerToys GitHub releases</a>, click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
Go to the [PowerToys GitHub releases][github-release-link], click Assets to reveal the downloads, and choose the installer that matches your architecture and install scope. For most devices, that's the x64 per-user installer.
|
||||
|
||||
<!-- items that need to be updated release to release -->
|
||||
[github-next-release-work]: https://github.com/microsoft/PowerToys/issues?q=is%3Aissue+milestone%3A%22PowerToys+0.97%22
|
||||
@@ -83,7 +83,7 @@ You can easily install PowerToys from the Microsoft Store:
|
||||
<details>
|
||||
<summary><strong>WinGet</strong></summary>
|
||||
<br/>
|
||||
Download PowerToys from <a href="https://github.com/microsoft/winget-cli#installing-the-client">WinGet</a>. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
Download PowerToys from [WinGet][winget-link]. Updating PowerToys via winget will respect the current PowerToys installation scope. To install PowerToys, run the following command from the command line / PowerShell:
|
||||
|
||||
*User scope installer [default]*
|
||||
```powershell
|
||||
@@ -99,7 +99,7 @@ winget install --scope machine Microsoft.PowerToys -s winget
|
||||
<details>
|
||||
<summary><strong>Other methods</strong></summary>
|
||||
<br/>
|
||||
There are <a href="https://learn.microsoft.com/windows/powertoys/install#community-driven-install-tools">community driven install methods</a> such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
There are [community driven install methods](./doc/unofficialInstallMethods.md) such as Chocolatey and Scoop. If these are your preferred install solutions, you can find the install instructions there.
|
||||
</details>
|
||||
|
||||
## ✨ What's new
|
||||
|
||||
@@ -1549,7 +1549,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
}
|
||||
processes.resize(bytes / sizeof(processes[0]));
|
||||
|
||||
std::array<std::wstring_view, 44> processesToTerminate = {
|
||||
std::array<std::wstring_view, 42> processesToTerminate = {
|
||||
L"PowerToys.PowerLauncher.exe",
|
||||
L"PowerToys.Settings.exe",
|
||||
L"PowerToys.AdvancedPaste.exe",
|
||||
@@ -1584,14 +1584,12 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
|
||||
L"PowerToys.MouseWithoutBordersService.exe",
|
||||
L"PowerToys.CropAndLock.exe",
|
||||
L"PowerToys.EnvironmentVariables.exe",
|
||||
L"PowerToys.QuickAccess.exe",
|
||||
L"PowerToys.WorkspacesSnapshotTool.exe",
|
||||
L"PowerToys.WorkspacesLauncher.exe",
|
||||
L"PowerToys.WorkspacesLauncherUI.exe",
|
||||
L"PowerToys.WorkspacesEditor.exe",
|
||||
L"PowerToys.WorkspacesWindowArranger.exe",
|
||||
L"Microsoft.CmdPal.UI.exe",
|
||||
L"Microsoft.CmdPal.Ext.PowerToys.exe",
|
||||
L"PowerToys.ZoomIt.exe",
|
||||
L"PowerToys.exe",
|
||||
};
|
||||
|
||||
@@ -61,16 +61,6 @@
|
||||
</RegistryKey>
|
||||
<File Source="$(var.RepoDir)\Notice.md" Id="Notice.md" />
|
||||
</Component>
|
||||
<Directory Id="SvgsFolder" Name="svgs">
|
||||
<Component Id="svgs_icons" Guid="A9B7C5D3-E1F2-4A6B-8C9D-0E1F2A3B4C5D" Bitness="always64">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="svgs_icons" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="icon.ico" Source="$(var.BinDir)svgs\icon.ico" />
|
||||
<File Id="PowerToysWhite.ico" Source="$(var.BinDir)svgs\PowerToysWhite.ico" />
|
||||
<File Id="PowerToysDark.ico" Source="$(var.BinDir)svgs\PowerToysDark.ico" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
@@ -122,7 +112,6 @@
|
||||
<RemoveFolder Id="RemoveBaseApplicationsAssetsFolder" Directory="BaseApplicationsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsInstallFolder" Directory="WinUI3AppsInstallFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveWinUI3AppsAssetsFolder" Directory="WinUI3AppsAssetsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveSvgsFolder" Directory="SvgsFolder" On="uninstall" />
|
||||
<RemoveFolder Id="RemoveINSTALLFOLDER" Directory="INSTALLFOLDER" On="uninstall" />
|
||||
</Component>
|
||||
<ComponentRef Id="powertoys_exe" />
|
||||
@@ -131,7 +120,6 @@
|
||||
<ComponentRef Id="powertoys_toast_clsid" />
|
||||
<ComponentRef Id="License_rtf" />
|
||||
<ComponentRef Id="Notice_md" />
|
||||
<ComponentRef Id="svgs_icons" />
|
||||
<ComponentRef Id="DesktopShortcut" />
|
||||
<?if $(var.PerUser) = "true" ?>
|
||||
<ComponentRef Id="powertoys_env_path_user" />
|
||||
|
||||
@@ -66,10 +66,5 @@ namespace PowerToys.GPOWrapperProjection
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredWorkspacesEnabledValue();
|
||||
}
|
||||
|
||||
public static GpoRuleConfigured GetConfiguredLightSwitchEnabledValue()
|
||||
{
|
||||
return (GpoRuleConfigured)PowerToys.GPOWrapper.GPOWrapper.GetConfiguredLightSwitchEnabledValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ManagedCommon
|
||||
{
|
||||
public static bool IsWindows10()
|
||||
{
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build < 22000;
|
||||
return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Minor < 22000;
|
||||
}
|
||||
|
||||
public static bool IsWindows11()
|
||||
|
||||
@@ -466,27 +466,39 @@
|
||||
TextChanged="EditVariableDialogValueTxtBox_TextChanged"
|
||||
TextWrapping="Wrap" />
|
||||
<MenuFlyoutSeparator Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<ItemsControl
|
||||
<ListView
|
||||
x:Name="EditVariableValuesList"
|
||||
Margin="0,-8,0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
AllowDrop="True"
|
||||
CanDragItems="True"
|
||||
CanReorderItems="True"
|
||||
DragItemsCompleted="EditVariableValuesList_DragItemsCompleted"
|
||||
ItemsSource="{Binding ValuesList, Mode=TwoWay}"
|
||||
Visibility="{Binding ShowAsList, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="40" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<FontIcon
|
||||
Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
FontSize="16"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
LostFocus="EditVariableValuesListTextBox_LostFocus"
|
||||
Text="{Binding Text}" />
|
||||
<Button
|
||||
x:Uid="More_Options_Button"
|
||||
Grid.Column="1"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Content=""
|
||||
FontFamily="{ThemeResource SymbolThemeFontFamily}"
|
||||
@@ -523,8 +535,8 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -16,6 +16,8 @@ namespace EnvironmentVariablesUILib
|
||||
{
|
||||
public sealed partial class EnvironmentVariablesMainPage : Page
|
||||
{
|
||||
private const string ValueListSeparator = ";";
|
||||
|
||||
private sealed class RelayCommandParameter
|
||||
{
|
||||
public RelayCommandParameter(Variable variable, VariablesSet set)
|
||||
@@ -440,7 +442,7 @@ namespace EnvironmentVariablesUILib
|
||||
variable.ValuesList.Move(index, index - 1);
|
||||
}
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -461,7 +463,7 @@ namespace EnvironmentVariablesUILib
|
||||
variable.ValuesList.Move(index, index + 1);
|
||||
}
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -476,7 +478,7 @@ namespace EnvironmentVariablesUILib
|
||||
var variable = EditVariableDialog.DataContext as Variable;
|
||||
variable.ValuesList.Remove(listItem);
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
}
|
||||
|
||||
@@ -492,7 +494,7 @@ namespace EnvironmentVariablesUILib
|
||||
var index = variable.ValuesList.IndexOf(listItem);
|
||||
variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty });
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -510,7 +512,7 @@ namespace EnvironmentVariablesUILib
|
||||
var index = variable.ValuesList.IndexOf(listItem);
|
||||
variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty });
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -532,7 +534,7 @@ namespace EnvironmentVariablesUILib
|
||||
listItem.Text = (sender as TextBox)?.Text;
|
||||
var variable = EditVariableDialog.DataContext as Variable;
|
||||
|
||||
var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList?.Select(x => x.Text).ToArray());
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
@@ -548,5 +550,16 @@ namespace EnvironmentVariablesUILib
|
||||
CancelAddVariable();
|
||||
ConfirmAddVariableBtn.IsEnabled = false;
|
||||
}
|
||||
|
||||
private void EditVariableValuesList_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||
{
|
||||
if (EditVariableDialog.DataContext is Variable variable && variable.ValuesList != null)
|
||||
{
|
||||
var newValues = string.Join(ValueListSeparator, variable.ValuesList.Select(x => x.Text));
|
||||
EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged;
|
||||
EditVariableDialogValueTxtBox.Text = newValues;
|
||||
EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,5 @@
|
||||
#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
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# 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!
|
||||
```
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
#!/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
|
||||
}
|
||||
@@ -1,430 +0,0 @@
|
||||
"""
|
||||
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()
|
||||
@@ -1,892 +0,0 @@
|
||||
"""
|
||||
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,58 +506,8 @@ 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;
|
||||
@@ -596,22 +546,12 @@ 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;
|
||||
|
||||
@@ -664,15 +604,6 @@ 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;
|
||||
|
||||
@@ -722,22 +653,13 @@ public:
|
||||
|
||||
// *** FIXED HORIZONTAL WRAPPING LOGIC ***
|
||||
// Move to opposite end of horizontal stack when hitting left/right edge
|
||||
// Only wrap if there's NO adjacent monitor in the coordinate space (let Windows handle natural transitions)
|
||||
// Only handle horizontal wrapping if we haven't already wrapped vertically
|
||||
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;
|
||||
|
||||
@@ -790,15 +712,6 @@ 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;
|
||||
|
||||
@@ -1068,104 +981,45 @@ void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
int totalWidth = totalBounds.right - totalBounds.left;
|
||||
int totalHeight = totalBounds.bottom - totalBounds.top;
|
||||
int gridWidth = max(1, totalWidth / 3);
|
||||
int gridHeight = max(1, totalHeight / 3);
|
||||
|
||||
// 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
|
||||
// Place monitors in the 3x3 grid based on their center points
|
||||
for (const auto& monitor : monitors)
|
||||
{
|
||||
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
||||
int row = monitorToRow[&monitor];
|
||||
int col = monitorToCol[&monitor];
|
||||
|
||||
// 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));
|
||||
|
||||
grid[row][col] = hMonitor;
|
||||
monitorToPosition[hMonitor] = {row, col, true};
|
||||
positionToMonitor[{row, col}] = hMonitor;
|
||||
|
||||
#ifdef _DEBUG
|
||||
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);
|
||||
Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}], center=({}, {})",
|
||||
monitor.monitorId, row, col, centerX, centerY);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,10 @@ public struct ApplicationWrapper
|
||||
{
|
||||
public struct WindowPositionWrapper
|
||||
{
|
||||
[JsonPropertyName("X")]
|
||||
[JsonPropertyName("x")]
|
||||
public int X { get; set; }
|
||||
|
||||
[JsonPropertyName("Y")]
|
||||
[JsonPropertyName("y")]
|
||||
public int Y { get; set; }
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed partial class BuiltInsCommandProvider : CommandProvider
|
||||
public override ICommandItem[] TopLevelCommands() =>
|
||||
[
|
||||
new CommandItem(openSettings) { },
|
||||
new CommandItem(_newExtension) { Title = _newExtension.Title },
|
||||
new CommandItem(_newExtension) { Title = _newExtension.Title, Subtitle = Properties.Resources.builtin_new_extension_subtitle },
|
||||
];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() =>
|
||||
|
||||
@@ -547,15 +547,6 @@ public partial class MainListPage : DynamicListPage,
|
||||
// above "git" from "whatever"
|
||||
max = max + extensionTitleMatch;
|
||||
|
||||
// Apply a penalty to fallback items so they rank below direct matches.
|
||||
// Fallbacks that dynamically match queries (like RDP connections) should
|
||||
// appear after apps and direct command matches.
|
||||
if (isFallback && max > 1)
|
||||
{
|
||||
// Reduce fallback scores by 50% to prioritize direct matches
|
||||
max = max * 0.5;
|
||||
}
|
||||
|
||||
var matchSomething = max
|
||||
+ (isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0));
|
||||
|
||||
|
||||
@@ -134,9 +134,6 @@
|
||||
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,6 +99,8 @@ 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>
|
||||
@@ -348,7 +350,7 @@ public sealed partial class WrapPanel : Panel
|
||||
return;
|
||||
}
|
||||
|
||||
var isFullLine = GetIsFullLine(child);
|
||||
var isFullLine = IsSectionItem(child);
|
||||
var desiredMeasure = new UvMeasure(Orientation, child.DesiredSize);
|
||||
|
||||
if (isFullLine)
|
||||
|
||||
@@ -18,19 +18,8 @@ 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,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using 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,10 +27,6 @@
|
||||
<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" />
|
||||
@@ -98,7 +94,6 @@
|
||||
<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">
|
||||
@@ -160,70 +155,6 @@
|
||||
</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}"
|
||||
@@ -235,19 +166,6 @@
|
||||
ToolTipService.ToolTip="{x:Bind ToolTip, Mode=OneWay}" />
|
||||
</DataTemplate>
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
x:Key="ListItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
ListItem="{StaticResource ListItemViewModelTemplate}"
|
||||
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"
|
||||
@@ -255,16 +173,21 @@
|
||||
GridProperties="{x:Bind ViewModel.GridProperties, Mode=OneWay}"
|
||||
Medium="{StaticResource MediumGridItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource GridSeparatorViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}"
|
||||
Small="{StaticResource SmallGridItemViewModelTemplate}" />
|
||||
|
||||
<cmdpalUI:ListItemTemplateSelector
|
||||
x:Key="ListItemTemplateSelector"
|
||||
x:DataType="coreViewModels:ListItemViewModel"
|
||||
ListItem="{StaticResource ListItemViewModelTemplate}"
|
||||
Section="{StaticResource ListSectionViewModelTemplate}"
|
||||
Separator="{StaticResource ListSeparatorViewModelTemplate}" />
|
||||
|
||||
<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 -->
|
||||
@@ -332,21 +255,21 @@
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="ListSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid>
|
||||
<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,8,0,0"
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
cpcontrols:WrapPanel.IsFullLine="True"
|
||||
ColumnSpacing="8"
|
||||
@@ -358,9 +281,13 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
Grid.Column="0"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Foreground="{ThemeResource TextFillColorDisabled}"
|
||||
Style="{ThemeResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Section}" />
|
||||
<Rectangle
|
||||
Grid.Column="1"
|
||||
Height="1"
|
||||
Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
@@ -487,7 +414,7 @@
|
||||
VerticalAlignment="Center"
|
||||
CharacterSpacing="11"
|
||||
FontSize="11"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Foreground="{ThemeResource TextFillColorTertiary}"
|
||||
Text="{x:Bind Subtitle, Mode=OneWay}"
|
||||
TextAlignment="Center"
|
||||
TextTrimming="WordEllipsis"
|
||||
@@ -496,10 +423,6 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="GridSeparatorViewModelTemplate" x:DataType="coreViewModels:ListItemViewModel">
|
||||
<Rectangle Height="1" Fill="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</DataTemplate>
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
@@ -525,7 +448,6 @@
|
||||
IsDoubleTapEnabled="True"
|
||||
IsItemClickEnabled="True"
|
||||
ItemClick="Items_ItemClick"
|
||||
ItemContainerStyleSelector="{StaticResource ListItemContainerStyleSelector}"
|
||||
ItemTemplateSelector="{StaticResource ListItemTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
|
||||
RightTapped="Items_RightTapped"
|
||||
@@ -538,7 +460,7 @@
|
||||
<controls:Case Value="True">
|
||||
<GridView
|
||||
x:Name="ItemsGrid"
|
||||
Padding="16,16"
|
||||
Padding="16,0"
|
||||
CanDragItems="True"
|
||||
ContextCanceled="Items_OnContextCanceled"
|
||||
ContextRequested="Items_OnContextRequested"
|
||||
@@ -555,10 +477,7 @@
|
||||
SelectionChanged="Items_SelectionChanged">
|
||||
<GridView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<cpcontrols:WrapPanel
|
||||
HorizontalSpacing="8"
|
||||
Orientation="Horizontal"
|
||||
VerticalSpacing="8" />
|
||||
<cpcontrols:WrapPanel HorizontalSpacing="8" Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</GridView.ItemsPanel>
|
||||
<GridView.ItemContainerTransitions>
|
||||
|
||||
@@ -18,9 +18,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -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.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
@@ -17,41 +18,19 @@ internal static class BuildInfo
|
||||
// Runtime AOT detection
|
||||
public static bool IsNativeAot => !RuntimeFeature.IsDynamicCodeSupported;
|
||||
|
||||
// build-time values
|
||||
public static bool PublishTrimmed
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_PUBLISH_TRIMMED
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishTrimmed => GetBoolMetadata("PublishTrimmed", false);
|
||||
|
||||
// build-time values
|
||||
public static bool PublishAot
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_PUBLISH_AOT
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
// From assembly metadata (build-time values)
|
||||
public static bool PublishAot => GetBoolMetadata("PublishAot", false);
|
||||
|
||||
public static bool IsCiBuild
|
||||
{
|
||||
get
|
||||
{
|
||||
#if BUILD_INFO_CIBUILD
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
public static bool IsCiBuild => GetBoolMetadata("CIBuild", false);
|
||||
|
||||
private static string? GetMetadata(string key) =>
|
||||
Assembly.GetExecutingAssembly()
|
||||
.GetCustomAttributes<AssemblyMetadataAttribute>()
|
||||
.FirstOrDefault(a => a.Key == key)?.Value;
|
||||
|
||||
private static bool GetBoolMetadata(string key, bool defaultValue) =>
|
||||
bool.TryParse(GetMetadata(key), out var result) ? result : defaultValue;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- This disables the auto-generated main, so we can be single-instanced -->
|
||||
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- BODGY: XES Versioning and WinAppSDK get into a fight about the app manifest, which breaks WinAppSDK. -->
|
||||
@@ -291,15 +291,24 @@
|
||||
</ItemGroup>
|
||||
<!-- </AdaptiveCardsWorkaround> -->
|
||||
|
||||
<!-- Build information -->
|
||||
<PropertyGroup Condition=" '$(PublishAot)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_AOT</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(PublishTrimmed)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_PUBLISH_TRIMMED</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(CIBuild)' == 'true' ">
|
||||
<DefineConstants>$(DefineConstants);BUILD_INFO_CIBUILD</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<!-- Metadata for build information -->
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishTrimmed</_Parameter1>
|
||||
<_Parameter2>$(PublishTrimmed)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>PublishAot</_Parameter1>
|
||||
<_Parameter2>$(PublishAot)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CIBuild</_Parameter1>
|
||||
<_Parameter2>$(CIBuild)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
||||
<_Parameter1>CommandPaletteBranding</_Parameter1>
|
||||
<_Parameter2>$(CommandPaletteBranding)</_Parameter2>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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,1"
|
||||
Visibility="{x:Bind help:BindTransformers.EmptyOrWhitespaceToVisible(Key), Mode=OneWay, FallbackValue=Collapsed}" />
|
||||
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>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="DetailsTagsTemplate" x:DataType="coreViewModels:DetailsTagsViewModel">
|
||||
|
||||
@@ -2,10 +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;
|
||||
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;
|
||||
@@ -34,56 +30,8 @@ public sealed partial class GeneralPage : Page
|
||||
{
|
||||
get
|
||||
{
|
||||
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;
|
||||
var version = Package.Current.Id.Version;
|
||||
return $"Version {version.Major}.{version.Minor}.{version.Build}.{version.Revision}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<value>Windows Command Palette</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_About_SettingsExpander.Description" xml:space="preserve">
|
||||
<value>© 2026. All rights reserved.</value>
|
||||
<value>© 2025. All rights reserved.</value>
|
||||
</data>
|
||||
<data name="Settings_GeneralPage_About_GithubLink_Hyperlink.Content" xml:space="preserve">
|
||||
<value>View GitHub repository</value>
|
||||
@@ -724,7 +724,4 @@ 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,5 +90,20 @@ 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class BasicTests : CommandPaletteTestBase
|
||||
|
||||
SetTimeAndDaterExtensionSearchBox("year");
|
||||
|
||||
Assert.IsNotNull(this.Find<NavigationViewItem>("2026"));
|
||||
Assert.IsNotNull(this.Find<NavigationViewItem>("2025"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<XesUseOneStoreVersioning>true</XesUseOneStoreVersioning>
|
||||
<XesBaseYearForStoreVersion>2025</XesBaseYearForStoreVersion>
|
||||
<VersionMajor>0</VersionMajor>
|
||||
<VersionMinor>8</VersionMinor>
|
||||
<VersionMinor>7</VersionMinor>
|
||||
<VersionInfoProductName>Microsoft Command Palette</VersionInfoProductName>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -15,6 +15,7 @@ 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,6 +19,7 @@ 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),
|
||||
|
||||
@@ -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;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -42,19 +41,15 @@ internal sealed partial class FancyZonesMonitorListItem : ListItem
|
||||
public static Details BuildMonitorDetails(FancyZonesMonitorDescriptor monitor)
|
||||
{
|
||||
var currentVirtualDesktop = FancyZonesVirtualDesktop.GetCurrentVirtualDesktopIdString();
|
||||
|
||||
// Calculate physical resolution from logical pixels and DPI
|
||||
var scaleFactor = monitor.Data.Dpi > 0 ? monitor.Data.Dpi / 96.0 : 1.0;
|
||||
var physicalWidth = (int)Math.Round(monitor.Data.MonitorWidth * scaleFactor);
|
||||
var physicalHeight = (int)Math.Round(monitor.Data.MonitorHeight * scaleFactor);
|
||||
var resolution = $"{physicalWidth}\u00D7{physicalHeight}";
|
||||
|
||||
var tags = new List<IDetailsElement>
|
||||
{
|
||||
DetailTag(Resources.FancyZones_Monitor, monitor.Data.Monitor),
|
||||
DetailTag(Resources.FancyZones_Instance, monitor.Data.MonitorInstanceId),
|
||||
DetailTag(Resources.FancyZones_Serial, monitor.Data.MonitorSerialNumber),
|
||||
DetailTag(Resources.FancyZones_Number, monitor.Data.MonitorNumber.ToString(CultureInfo.InvariantCulture)),
|
||||
DetailTag(Resources.FancyZones_VirtualDesktop, currentVirtualDesktop),
|
||||
DetailTag(Resources.FancyZones_Resolution, resolution),
|
||||
DetailTag(Resources.FancyZones_WorkArea, $"{monitor.Data.LeftCoordinate},{monitor.Data.TopCoordinate} {monitor.Data.WorkAreaWidth}\u00D7{monitor.Data.WorkAreaHeight}"),
|
||||
DetailTag(Resources.FancyZones_Resolution, $"{monitor.Data.MonitorWidth}\u00D7{monitor.Data.MonitorHeight}"),
|
||||
DetailTag(Resources.FancyZones_DPI, monitor.Data.Dpi.ToString(CultureInfo.InvariantCulture)),
|
||||
};
|
||||
|
||||
|
||||
@@ -41,13 +41,16 @@ internal static class FancyZonesDataService
|
||||
|
||||
try
|
||||
{
|
||||
// Request FancyZones to save current monitor configuration.
|
||||
// The editor-parameters.json file is only written when:
|
||||
// 1. Opening the FancyZones Editor
|
||||
// 2. Receiving the WM_PRIV_SAVE_EDITOR_PARAMETERS message
|
||||
// Without this, monitor changes (plug/unplug) won't be reflected in the file.
|
||||
var editorParams = ReadEditorParametersWithRefresh();
|
||||
Logger.LogInfo($"TryGetMonitors: ReadEditorParametersWithRefreshWithRefresh returned. Monitors={editorParams.Monitors?.Count ?? -1}");
|
||||
if (!File.Exists(FZPaths.EditorParameters))
|
||||
{
|
||||
error = Resources.FancyZones_MonitorDataNotFound;
|
||||
Logger.LogWarning($"TryGetMonitors: File not found. Path={FZPaths.EditorParameters}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogInfo($"TryGetMonitors: File exists, reading...");
|
||||
var editorParams = FancyZonesDataIO.ReadEditorParameters();
|
||||
Logger.LogInfo($"TryGetMonitors: ReadEditorParameters returned. Monitors={editorParams.Monitors?.Count ?? -1}");
|
||||
|
||||
var editorMonitors = editorParams.Monitors;
|
||||
if (editorMonitors is null || editorMonitors.Count == 0)
|
||||
@@ -71,23 +74,6 @@ internal static class FancyZonesDataService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests FancyZones to save the current monitor configuration and reads the file.
|
||||
/// This is a best-effort approach for performance: we send the save request and immediately
|
||||
/// read the file without waiting. If the file hasn't been updated yet, the next call will
|
||||
/// see the updated data since FancyZones processes the message asynchronously.
|
||||
/// </summary>
|
||||
private static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh()
|
||||
{
|
||||
// Request FancyZones to save the current monitor configuration.
|
||||
// This is fire-and-forget for performance - we don't wait for the save to complete.
|
||||
// If this is the first call after a monitor change, we may read stale data, but the
|
||||
// next call will see the updated file since FancyZones will have processed the message.
|
||||
FancyZonesNotifier.NotifySaveEditorParameters();
|
||||
|
||||
return FancyZonesDataIO.ReadEditorParameters();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<FancyZonesLayoutDescriptor> GetLayouts()
|
||||
{
|
||||
Logger.LogInfo($"GetLayouts: Starting. LayoutTemplatesPath={FZPaths.LayoutTemplates} CustomLayoutsPath={FZPaths.CustomLayouts}");
|
||||
|
||||
@@ -19,12 +19,8 @@ internal readonly record struct FancyZonesMonitorDescriptor(
|
||||
{
|
||||
get
|
||||
{
|
||||
// MonitorWidth/Height are logical (DPI-scaled) pixels, calculate physical resolution
|
||||
var scaleFactor = Data.Dpi > 0 ? Data.Dpi / 96.0 : 1.0;
|
||||
var physicalWidth = (int)Math.Round(Data.MonitorWidth * scaleFactor);
|
||||
var physicalHeight = (int)Math.Round(Data.MonitorHeight * scaleFactor);
|
||||
var size = $"{physicalWidth}×{physicalHeight}";
|
||||
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(scaleFactor * 100)) : "n/a";
|
||||
var size = $"{Data.MonitorWidth}×{Data.MonitorHeight}";
|
||||
var scaling = Data.Dpi > 0 ? string.Format(CultureInfo.InvariantCulture, "{0}%", (int)Math.Round(Data.Dpi * 100 / 96.0)) : "n/a";
|
||||
return $"{size} \u2022 {scaling}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,25 +10,13 @@ namespace PowerToysExtension.Helpers;
|
||||
internal static class FancyZonesNotifier
|
||||
{
|
||||
private const string AppliedLayoutsFileUpdateMessage = "{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}";
|
||||
private const string SaveEditorParametersMessage = "{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}";
|
||||
|
||||
private static readonly uint WmPrivAppliedLayoutsFileUpdate = RegisterWindowMessageW(AppliedLayoutsFileUpdateMessage);
|
||||
private static readonly uint WmPrivSaveEditorParameters = RegisterWindowMessageW(SaveEditorParametersMessage);
|
||||
|
||||
public static void NotifyAppliedLayoutsChanged()
|
||||
{
|
||||
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivAppliedLayoutsFileUpdate, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notifies FancyZones to save the current monitor configuration to editor-parameters.json.
|
||||
/// This is needed because FancyZones only writes this file when opening the editor or when explicitly requested.
|
||||
/// </summary>
|
||||
public static void NotifySaveEditorParameters()
|
||||
{
|
||||
_ = PostMessageW(new IntPtr(0xFFFF), WmPrivSaveEditorParameters, UIntPtr.Zero, IntPtr.Zero);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern uint RegisterWindowMessageW(string lpString);
|
||||
|
||||
|
||||
@@ -485,21 +485,12 @@ internal static class FancyZonesThumbnailRenderer
|
||||
|
||||
private static List<NormalizedRect> GetFocusRects(int zoneCount)
|
||||
{
|
||||
// Focus layout parameters from FancyZonesEditor CanvasLayoutModel:
|
||||
// - DefaultOffset = 100px from top-left (normalized: ~0.05 for typical screen)
|
||||
// - OffsetShift = 50px per zone (normalized: ~0.025)
|
||||
// - ZoneSizeMultiplier = 0.4 (zones are 40% of screen)
|
||||
zoneCount = Math.Clamp(zoneCount, 1, 8);
|
||||
var rects = new List<NormalizedRect>(zoneCount);
|
||||
|
||||
const float defaultOffset = 0.05f; // ~100px on 1920px screen
|
||||
const float offsetShift = 0.025f; // ~50px on 1920px screen
|
||||
const float zoneSize = 0.4f; // 40% of screen
|
||||
|
||||
for (var i = 0; i < zoneCount; i++)
|
||||
{
|
||||
var offset = i * offsetShift;
|
||||
rects.Add(new NormalizedRect(defaultOffset + offset, defaultOffset + offset, zoneSize, zoneSize));
|
||||
var offset = i * 0.06f;
|
||||
rects.Add(new NormalizedRect(0.1f + offset, 0.1f + offset, 0.8f, 0.8f));
|
||||
}
|
||||
|
||||
return rects;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>PowerToysExtension</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>..\..\..\..\runner\svgs\icon.ico</ApplicationIcon>
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<EnableMsixTooling>false</EnableMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
|
||||
@@ -31,6 +31,7 @@ public partial class RemoteDesktopCommandProvider : CommandProvider
|
||||
|
||||
listPageCommand = new CommandItem(listPage)
|
||||
{
|
||||
Subtitle = Resources.remotedesktop_subtitle,
|
||||
Icon = Icons.RDPIcon,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(settingsManager.Settings.SettingsPage),
|
||||
|
||||
@@ -39,6 +39,7 @@ public partial class ShellCommandsProvider : CommandProvider
|
||||
{
|
||||
Icon = Icons.RunV2Icon,
|
||||
Title = Resources.shell_command_name,
|
||||
Subtitle = Resources.cmd_plugin_description,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(Settings.SettingsPage),
|
||||
],
|
||||
|
||||
@@ -28,6 +28,7 @@ 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,6 +26,7 @@ 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,6 +30,7 @@ public sealed partial class WindowsSettingsCommandsProvider : CommandProvider
|
||||
_searchSettingsListItem = new CommandItem(new WindowsSettingsListPage(_windowsSettings))
|
||||
{
|
||||
Title = Resources.settings_title,
|
||||
Subtitle = Resources.settings_subtitle,
|
||||
};
|
||||
_fallback = new(_windowsSettings);
|
||||
|
||||
|
||||
@@ -35,10 +35,6 @@ 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,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.Runtime.CompilerServices;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -15,7 +14,7 @@ public partial class BaseObservable : INotifyPropChanged
|
||||
{
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs>? PropChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
protected void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -23,37 +22,10 @@ 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,11 +6,31 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Command : BaseObservable, ICommand
|
||||
{
|
||||
public virtual string Name { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Name
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public virtual string Id { get; set; } = string.Empty;
|
||||
|
||||
public virtual IconInfo Icon { get; set => SetProperty(ref field, value); } = new();
|
||||
public virtual IconInfo Icon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
= 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 => SetProperty(ref field, value); }
|
||||
public virtual bool IsCritical { get; set; }
|
||||
|
||||
public virtual KeyChord RequestedShortcut { get; set => SetProperty(ref field, value); }
|
||||
public virtual KeyChord RequestedShortcut { get; set; }
|
||||
|
||||
public CommandContextItem(ICommand command)
|
||||
: base(command)
|
||||
|
||||
@@ -19,36 +19,44 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
private DataPackage? _dataPackage;
|
||||
private DataPackageView? _dataPackageView;
|
||||
|
||||
public virtual IIconInfo? Icon { get; set => SetProperty(ref field, value); }
|
||||
public virtual IIconInfo? Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Title
|
||||
{
|
||||
get => !string.IsNullOrEmpty(_title) ? _title : _command?.Name ?? string.Empty;
|
||||
|
||||
set
|
||||
{
|
||||
var oldTitle = Title;
|
||||
_title = value;
|
||||
if (Title != oldTitle)
|
||||
{
|
||||
OnPropertyChanged();
|
||||
}
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Subtitle { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Subtitle
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Subtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= 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();
|
||||
@@ -63,8 +71,8 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
value.PropChanged += _commandListener.OnEvent;
|
||||
}
|
||||
|
||||
OnPropertyChanged();
|
||||
if (string.IsNullOrEmpty(_title) && oldTitle != Title)
|
||||
OnPropertyChanged(nameof(Command));
|
||||
if (string.IsNullOrEmpty(_title))
|
||||
{
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
@@ -80,7 +88,17 @@ public partial class CommandItem : BaseObservable, ICommandItem, IExtendedAttrib
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IContextItem[] MoreCommands { get; set => SetProperty(ref field, value); } = [];
|
||||
public virtual IContextItem[] MoreCommands
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(MoreCommands));
|
||||
}
|
||||
}
|
||||
|
||||
= [];
|
||||
|
||||
public DataPackage? DataPackage
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class CommandResult : ICommandResult
|
||||
{
|
||||
public ICommandResultArgs? Args { get; private init; }
|
||||
public ICommandResultArgs? Args { get; private set; }
|
||||
|
||||
public CommandResultKind Kind { get; private init; } = CommandResultKind.Dismiss;
|
||||
public CommandResultKind Kind { get; private set; } = CommandResultKind.Dismiss;
|
||||
|
||||
public static CommandResult Dismiss()
|
||||
{
|
||||
|
||||
@@ -10,9 +10,17 @@ public abstract partial class ContentPage : Page, IContentPage
|
||||
{
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
|
||||
public virtual IDetails? Details
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Details));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IContextItem[] Commands { get; set => SetProperty(ref field, value); } = [];
|
||||
public virtual IContextItem[] Commands { get; set; } = [];
|
||||
|
||||
public abstract IContent[] GetContent();
|
||||
|
||||
|
||||
@@ -7,15 +7,65 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Details : BaseObservable, IDetails, IExtendedAttributesProvider
|
||||
{
|
||||
public virtual IIconInfo HeroImage { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
public virtual IIconInfo HeroImage
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(HeroImage));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Title { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Body { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Title
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IDetailsElement[] Metadata { get; set => SetProperty(ref field, value); } = [];
|
||||
= string.Empty;
|
||||
|
||||
public virtual ContentSize Size { get; set => SetProperty(ref field, value); } = ContentSize.Small;
|
||||
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 IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||
{
|
||||
|
||||
@@ -6,9 +6,39 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Filter : BaseObservable, IFilter
|
||||
{
|
||||
public virtual IIconInfo Icon { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
public virtual IIconInfo Icon
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Id { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
= new IconInfo();
|
||||
|
||||
public virtual string Name { 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;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,17 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class Filters : BaseObservable, IFilters
|
||||
{
|
||||
public string CurrentFilterId { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public string CurrentFilterId
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(CurrentFilterId));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
// This method should be overridden in derived classes to provide the actual filters.
|
||||
public abstract IFilterItem[] GetFilters();
|
||||
|
||||
@@ -6,11 +6,41 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class FormContent : BaseObservable, IFormContent
|
||||
{
|
||||
public virtual string DataJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string DataJson
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(DataJson));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string StateJson { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
= string.Empty;
|
||||
|
||||
public virtual string TemplateJson { 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 ICommandResult SubmitForm(string inputs, string data) => SubmitForm(inputs);
|
||||
|
||||
|
||||
@@ -6,7 +6,27 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class GalleryGridLayout : BaseObservable, IGalleryGridLayout
|
||||
{
|
||||
public virtual bool ShowTitle { get; set => SetProperty(ref field, value); } = true;
|
||||
public virtual bool ShowTitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool ShowSubtitle { get; set => SetProperty(ref field, value); } = true;
|
||||
= true;
|
||||
|
||||
public virtual bool ShowSubtitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowSubtitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
}
|
||||
|
||||
@@ -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.Diagnostics.CodeAnalysis;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -6,13 +6,51 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ListItem : CommandItem, IListItem
|
||||
{
|
||||
public virtual ITag[] Tags { get; set => SetProperty(ref field, value); } = [];
|
||||
private ITag[] _tags = [];
|
||||
private IDetails? _details;
|
||||
|
||||
public virtual IDetails? Details { get; set => SetProperty(ref field, value); }
|
||||
private string _section = string.Empty;
|
||||
private string _textToSuggest = string.Empty;
|
||||
|
||||
public virtual string Section { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual ITag[] Tags
|
||||
{
|
||||
get => _tags;
|
||||
set
|
||||
{
|
||||
_tags = value;
|
||||
OnPropertyChanged(nameof(Tags));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string TextToSuggest { 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 ListItem(ICommand command)
|
||||
: base(command)
|
||||
|
||||
@@ -8,23 +8,85 @@ 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;
|
||||
|
||||
private string _searchText = string.Empty;
|
||||
public virtual string PlaceholderText
|
||||
{
|
||||
get => _placeholderText;
|
||||
set
|
||||
{
|
||||
_placeholderText = value;
|
||||
OnPropertyChanged(nameof(PlaceholderText));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string PlaceholderText { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set
|
||||
{
|
||||
_searchText = value;
|
||||
OnPropertyChanged(nameof(SearchText));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string SearchText { get => _searchText; set => SetProperty(ref _searchText, value); }
|
||||
public virtual bool ShowDetails
|
||||
{
|
||||
get => _showDetails;
|
||||
set
|
||||
{
|
||||
_showDetails = value;
|
||||
OnPropertyChanged(nameof(ShowDetails));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool ShowDetails { get; set => SetProperty(ref field, value); }
|
||||
public virtual bool HasMoreItems
|
||||
{
|
||||
get => _hasMore;
|
||||
set
|
||||
{
|
||||
_hasMore = value;
|
||||
OnPropertyChanged(nameof(HasMoreItems));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual bool HasMoreItems { get; set => SetProperty(ref field, value); }
|
||||
public virtual IFilters? Filters
|
||||
{
|
||||
get => _filters;
|
||||
set
|
||||
{
|
||||
_filters = value;
|
||||
OnPropertyChanged(nameof(Filters));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IFilters? Filters { get; set => SetProperty(ref field, value); }
|
||||
public virtual IGridProperties? GridProperties
|
||||
{
|
||||
get => _gridProperties;
|
||||
set
|
||||
{
|
||||
_gridProperties = value;
|
||||
OnPropertyChanged(nameof(GridProperties));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IGridProperties? GridProperties { get; set => SetProperty(ref field, value); }
|
||||
|
||||
public virtual ICommandItem? EmptyContent { get; set => SetProperty(ref field, value); }
|
||||
public virtual ICommandItem? EmptyContent
|
||||
{
|
||||
get => _emptyContent;
|
||||
set
|
||||
{
|
||||
_emptyContent = value;
|
||||
OnPropertyChanged(nameof(EmptyContent));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IListItem[] GetItems() => [];
|
||||
|
||||
|
||||
@@ -6,7 +6,17 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class MarkdownContent : BaseObservable, IMarkdownContent
|
||||
{
|
||||
public virtual string Body { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Body
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Body));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
public MarkdownContent()
|
||||
{
|
||||
|
||||
@@ -6,5 +6,15 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class MediumGridLayout : BaseObservable, IMediumGridLayout
|
||||
{
|
||||
public virtual bool ShowTitle { get; set => SetProperty(ref field, value); } = true;
|
||||
public virtual bool ShowTitle
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(ShowTitle));
|
||||
}
|
||||
}
|
||||
|
||||
= true;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,37 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Page : Command, IPage
|
||||
{
|
||||
public virtual bool IsLoading { get; set => SetProperty(ref field, value); }
|
||||
private bool _loading;
|
||||
private string _title = string.Empty;
|
||||
private OptionalColor _accentColor;
|
||||
|
||||
public virtual string Title { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual bool IsLoading
|
||||
{
|
||||
get => _loading;
|
||||
set
|
||||
{
|
||||
_loading = value;
|
||||
OnPropertyChanged(nameof(IsLoading));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual OptionalColor AccentColor { get; set => SetProperty(ref field, value); }
|
||||
public virtual string Title
|
||||
{
|
||||
get => _title;
|
||||
set
|
||||
{
|
||||
_title = value;
|
||||
OnPropertyChanged(nameof(Title));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual OptionalColor AccentColor
|
||||
{
|
||||
get => _accentColor;
|
||||
set
|
||||
{
|
||||
_accentColor = value;
|
||||
OnPropertyChanged(nameof(AccentColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,27 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ProgressState : BaseObservable, IProgressState
|
||||
{
|
||||
public virtual bool IsIndeterminate { get; set => SetProperty(ref field, value); }
|
||||
private bool _isIndeterminate;
|
||||
|
||||
public virtual uint ProgressPercent { 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class PropChangedEventArgs : IPropChangedEventArgs
|
||||
|
||||
@@ -12,6 +12,11 @@ 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;
|
||||
@@ -28,11 +33,6 @@ 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,8 +4,15 @@
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Separator : BaseObservable, IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
public partial class Separator : IListItem, ISeparatorContextItem, ISeparatorFilterItem
|
||||
{
|
||||
public Separator(string? title = "")
|
||||
: base()
|
||||
{
|
||||
Section = title ?? string.Empty;
|
||||
Command = null;
|
||||
}
|
||||
|
||||
public IDetails? Details => null;
|
||||
|
||||
public string? Section { get; private set; }
|
||||
@@ -14,7 +21,7 @@ public partial class Separator : BaseObservable, IListItem, ISeparatorContextIte
|
||||
|
||||
public string? TextToSuggest => null;
|
||||
|
||||
public ICommand? Command => null;
|
||||
public ICommand? Command { get; private set; }
|
||||
|
||||
public IIconInfo? Icon => null;
|
||||
|
||||
@@ -25,19 +32,12 @@ public partial class Separator : BaseObservable, IListItem, ISeparatorContextIte
|
||||
public string? Title
|
||||
{
|
||||
get => Section;
|
||||
set
|
||||
{
|
||||
if (Section != value)
|
||||
{
|
||||
Section = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(Section);
|
||||
}
|
||||
}
|
||||
set => Section = value;
|
||||
}
|
||||
|
||||
public Separator(string? title = "")
|
||||
public event Windows.Foundation.TypedEventHandler<object, IPropChangedEventArgs>? PropChanged
|
||||
{
|
||||
Section = title ?? string.Empty;
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,37 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class StatusMessage : BaseObservable, IStatusMessage
|
||||
{
|
||||
public virtual string Message { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual string Message
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Message));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual MessageState State { get; set => SetProperty(ref field, value); } = MessageState.Info;
|
||||
= string.Empty;
|
||||
|
||||
public virtual IProgressState? Progress { get; set => SetProperty(ref field, value); }
|
||||
public virtual MessageState State
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(State));
|
||||
}
|
||||
}
|
||||
|
||||
= MessageState.Info;
|
||||
|
||||
public virtual IProgressState? Progress
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Progress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,63 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class Tag : BaseObservable, ITag
|
||||
{
|
||||
public virtual OptionalColor Foreground { get; set => SetProperty(ref field, value); }
|
||||
private OptionalColor _foreground;
|
||||
private OptionalColor _background;
|
||||
private string _text = string.Empty;
|
||||
|
||||
public virtual OptionalColor Background { get; set => SetProperty(ref field, value); }
|
||||
public virtual OptionalColor Foreground
|
||||
{
|
||||
get => _foreground;
|
||||
set
|
||||
{
|
||||
_foreground = value;
|
||||
OnPropertyChanged(nameof(Foreground));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual IIconInfo Icon { get; set => SetProperty(ref field, value); } = new IconInfo();
|
||||
public virtual OptionalColor Background
|
||||
{
|
||||
get => _background;
|
||||
set
|
||||
{
|
||||
_background = value;
|
||||
OnPropertyChanged(nameof(Background));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string Text { get; set => SetProperty(ref field, value); } = string.Empty;
|
||||
public virtual IIconInfo Icon
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string ToolTip { 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 Tag()
|
||||
{
|
||||
@@ -22,6 +70,6 @@ public partial class Tag : BaseObservable, ITag
|
||||
|
||||
public Tag(string text)
|
||||
{
|
||||
Text = text;
|
||||
_text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,19 @@ 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 => SetProperty(ref field, value); }
|
||||
public virtual IContent? RootContent
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(RootContent));
|
||||
}
|
||||
}
|
||||
|
||||
public event TypedEventHandler<object, IItemsChangedEventArgs>? ItemsChanged;
|
||||
|
||||
public virtual IContent[] GetChildren() => Children;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ using System.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
using FancyZonesCLI.Utils;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
@@ -36,19 +35,13 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
{
|
||||
// FancyZones running guard is handled by FancyZonesBaseCommand.
|
||||
int key = context.ParseResult.GetValueForArgument(_key);
|
||||
string layoutInput = context.ParseResult.GetValueForArgument(_layout);
|
||||
string layout = context.ParseResult.GetValueForArgument(_layout);
|
||||
|
||||
if (key < 0 || key > 9)
|
||||
{
|
||||
throw new InvalidOperationException(Properties.Resources.set_hotkey_error_invalid_key);
|
||||
}
|
||||
|
||||
// Normalize GUID to Windows format with braces (supports input with or without braces)
|
||||
if (!GuidHelper.TryNormalizeGuid(layoutInput, out string layout))
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layoutInput));
|
||||
}
|
||||
|
||||
// Editor only allows assigning hotkeys to existing custom layouts.
|
||||
var customLayouts = FancyZonesDataIO.ReadCustomLayouts();
|
||||
|
||||
@@ -67,7 +60,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
|
||||
if (!matchedLayout.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layoutInput));
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layout));
|
||||
}
|
||||
|
||||
string layoutName = matchedLayout.Value.Name;
|
||||
|
||||
@@ -140,12 +140,9 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalize GUID to Windows format with braces (supports input with or without braces)
|
||||
string normalizedLayout = GuidHelper.NormalizeGuid(layout) ?? layout;
|
||||
|
||||
foreach (var customLayout in customLayouts.CustomLayouts)
|
||||
{
|
||||
if (customLayout.Uuid.Equals(normalizedLayout, StringComparison.OrdinalIgnoreCase))
|
||||
if (customLayout.Uuid.Equals(layout, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return customLayout;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine;
|
||||
@@ -14,15 +13,15 @@ internal static class FancyZonesCliUsage
|
||||
public static void PrintUsage()
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
Console.WriteLine(Properties.Resources.usage_title);
|
||||
Console.WriteLine("FancyZones CLI - Command line interface for FancyZones");
|
||||
Console.WriteLine();
|
||||
|
||||
var cmd = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
|
||||
Console.WriteLine(Properties.Resources.usage_syntax);
|
||||
Console.WriteLine("Usage: FancyZonesCLI [command] [options]");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine(Properties.Resources.usage_options);
|
||||
Console.WriteLine("Options:");
|
||||
foreach (var option in cmd.Options)
|
||||
{
|
||||
var aliases = string.Join(", ", option.Aliases);
|
||||
@@ -31,7 +30,7 @@ internal static class FancyZonesCliUsage
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.usage_commands);
|
||||
Console.WriteLine("Commands:");
|
||||
foreach (var command in cmd.Subcommands)
|
||||
{
|
||||
if (command.IsHidden)
|
||||
@@ -52,7 +51,7 @@ internal static class FancyZonesCliUsage
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.usage_examples);
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(" FancyZonesCLI --help");
|
||||
Console.WriteLine(" FancyZonesCLI --version");
|
||||
Console.WriteLine(" FancyZonesCLI get-monitors");
|
||||
@@ -60,135 +59,4 @@ internal static class FancyZonesCliUsage
|
||||
Console.WriteLine(" FancyZonesCLI set-layout <uuid> --monitor 1");
|
||||
Console.WriteLine(" FancyZonesCLI get-hotkeys");
|
||||
}
|
||||
|
||||
public static void PrintCommandUsage(string commandName)
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
var rootCmd = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
|
||||
// Find matching subcommand by name or alias
|
||||
var subcommand = rootCmd.Subcommands.FirstOrDefault(c =>
|
||||
c.Aliases.Any(a => string.Equals(a, commandName, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (subcommand == null)
|
||||
{
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.usage_unknown_command, commandName));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.usage_run_help);
|
||||
return;
|
||||
}
|
||||
|
||||
// Command name and description
|
||||
Console.WriteLine($"{Properties.Resources.usage_command} {subcommand.Name}");
|
||||
if (!string.IsNullOrEmpty(subcommand.Description))
|
||||
{
|
||||
Console.WriteLine($" {subcommand.Description}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
|
||||
// Usage line
|
||||
string argsLabel = string.Join(" ", subcommand.Arguments.Select(a => $"<{a.Name}>"));
|
||||
string optionsLabel = subcommand.Options.Any() ? " [options]" : string.Empty;
|
||||
Console.WriteLine($"Usage: FancyZonesCLI {subcommand.Name} {argsLabel}{optionsLabel}".TrimEnd());
|
||||
Console.WriteLine();
|
||||
|
||||
// Aliases
|
||||
var aliases = subcommand.Aliases.Where(a => !string.Equals(a, subcommand.Name, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (aliases.Count > 0)
|
||||
{
|
||||
Console.WriteLine($"{Properties.Resources.usage_aliases} {string.Join(", ", aliases)}");
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
// Arguments
|
||||
if (subcommand.Arguments.Any())
|
||||
{
|
||||
Console.WriteLine(Properties.Resources.usage_arguments);
|
||||
foreach (var arg in subcommand.Arguments)
|
||||
{
|
||||
var argDescription = arg.Description ?? string.Empty;
|
||||
Console.WriteLine($" <{arg.Name}>{(arg.Arity.MinimumNumberOfValues == 0 ? $" {Properties.Resources.usage_optional}" : string.Empty),-20} {argDescription}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
// Options
|
||||
if (subcommand.Options.Any())
|
||||
{
|
||||
Console.WriteLine(Properties.Resources.usage_options);
|
||||
foreach (var option in subcommand.Options)
|
||||
{
|
||||
var optAliases = string.Join(", ", option.Aliases);
|
||||
var optDescription = option.Description ?? string.Empty;
|
||||
Console.WriteLine($" {optAliases,-25} {optDescription}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
}
|
||||
|
||||
// Command-specific examples
|
||||
PrintCommandExamples(subcommand.Name);
|
||||
}
|
||||
|
||||
private static void PrintCommandExamples(string commandName)
|
||||
{
|
||||
Console.WriteLine(Properties.Resources.usage_examples);
|
||||
|
||||
switch (commandName.ToLowerInvariant())
|
||||
{
|
||||
case "get-monitors":
|
||||
Console.WriteLine(" FancyZonesCLI get-monitors");
|
||||
Console.WriteLine(" FancyZonesCLI m");
|
||||
break;
|
||||
|
||||
case "get-layouts":
|
||||
Console.WriteLine(" FancyZonesCLI get-layouts");
|
||||
Console.WriteLine(" FancyZonesCLI ls");
|
||||
break;
|
||||
|
||||
case "get-active-layout":
|
||||
Console.WriteLine(" FancyZonesCLI get-active-layout");
|
||||
Console.WriteLine(" FancyZonesCLI active");
|
||||
break;
|
||||
|
||||
case "set-layout":
|
||||
Console.WriteLine(" FancyZonesCLI set-layout focus");
|
||||
Console.WriteLine(" FancyZonesCLI set-layout columns --monitor 1");
|
||||
Console.WriteLine(" FancyZonesCLI set-layout {uuid} --all");
|
||||
Console.WriteLine(" FancyZonesCLI s rows -m 2");
|
||||
break;
|
||||
|
||||
case "open-editor":
|
||||
Console.WriteLine(" FancyZonesCLI open-editor");
|
||||
Console.WriteLine(" FancyZonesCLI e");
|
||||
break;
|
||||
|
||||
case "open-settings":
|
||||
Console.WriteLine(" FancyZonesCLI open-settings");
|
||||
Console.WriteLine(" FancyZonesCLI settings");
|
||||
break;
|
||||
|
||||
case "get-hotkeys":
|
||||
Console.WriteLine(" FancyZonesCLI get-hotkeys");
|
||||
Console.WriteLine(" FancyZonesCLI hk");
|
||||
break;
|
||||
|
||||
case "set-hotkey":
|
||||
Console.WriteLine(" FancyZonesCLI set-hotkey 1 {layout-uuid}");
|
||||
Console.WriteLine(" FancyZonesCLI shk 2 0CEBCBA9-9C32-4395-B93E-DC77485AD6D0");
|
||||
break;
|
||||
|
||||
case "remove-hotkey":
|
||||
Console.WriteLine(" FancyZonesCLI remove-hotkey 1");
|
||||
Console.WriteLine(" FancyZonesCLI rhk 2");
|
||||
break;
|
||||
|
||||
default:
|
||||
Console.WriteLine($" FancyZonesCLI {commandName}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ namespace FancyZonesCLI;
|
||||
|
||||
internal sealed class Program
|
||||
{
|
||||
private static readonly string[] HelpFlags = ["--help", "-h", "-?"];
|
||||
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
Logger.InitializeLogger();
|
||||
@@ -23,17 +21,14 @@ internal sealed class Program
|
||||
NativeMethods.InitializeWindowMessages();
|
||||
|
||||
// Intercept help requests early and print custom usage.
|
||||
if (TryHandleHelpRequest(args))
|
||||
if (args.Any(a => string.Equals(a, "--help", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(a, "-h", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(a, "-?", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
FancyZonesCliUsage.PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Detect PowerShell script block expansion (when {} is interpreted as script block)
|
||||
if (DetectPowerShellScriptBlockArgs(args))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
RootCommand rootCommand = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
int exitCode = await rootCommand.InvokeAsync(args);
|
||||
|
||||
@@ -48,69 +43,4 @@ internal sealed class Program
|
||||
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles help requests for root command and subcommands.
|
||||
/// </summary>
|
||||
/// <returns>True if help was printed, false otherwise.</returns>
|
||||
private static bool TryHandleHelpRequest(string[] args)
|
||||
{
|
||||
bool hasHelpFlag = args.Any(a => HelpFlags.Any(h => string.Equals(a, h, StringComparison.OrdinalIgnoreCase)));
|
||||
if (!hasHelpFlag)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get non-help arguments to identify subcommand
|
||||
var nonHelpArgs = args.Where(a => !HelpFlags.Any(h => string.Equals(a, h, StringComparison.OrdinalIgnoreCase))).ToArray();
|
||||
|
||||
if (nonHelpArgs.Length == 0)
|
||||
{
|
||||
// Root help: fancyzones cli --help
|
||||
FancyZonesCliUsage.PrintUsage();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Subcommand help: fancyzones cli <command> --help
|
||||
string subcommandName = nonHelpArgs[0];
|
||||
FancyZonesCliUsage.PrintCommandUsage(subcommandName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects when PowerShell interprets {GUID} as a script block and converts it to encoded command args.
|
||||
/// This happens when users forget to quote GUIDs with braces in PowerShell.
|
||||
/// </summary>
|
||||
/// <returns>True if PowerShell script block args were detected, false otherwise.</returns>
|
||||
private static bool DetectPowerShellScriptBlockArgs(string[] args)
|
||||
{
|
||||
// PowerShell converts {scriptblock} to: -encodedCommand <base64> -inputFormat xml -outputFormat text
|
||||
bool hasEncodedCommand = args.Any(a => string.Equals(a, "-encodedCommand", StringComparison.OrdinalIgnoreCase));
|
||||
bool hasInputFormat = args.Any(a => string.Equals(a, "-inputFormat", StringComparison.OrdinalIgnoreCase));
|
||||
bool hasOutputFormat = args.Any(a => string.Equals(a, "-outputFormat", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (hasEncodedCommand || (hasInputFormat && hasOutputFormat))
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_title);
|
||||
Console.ResetColor();
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_explanation);
|
||||
Console.WriteLine(Properties.Resources.error_powershell_scriptblock_hint);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option1}");
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option1_example}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option2}");
|
||||
Console.WriteLine($" {Properties.Resources.error_powershell_scriptblock_option2_example}");
|
||||
Console.WriteLine();
|
||||
|
||||
Logger.LogWarning("PowerShell script block expansion detected - user needs to quote GUID or omit braces");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,113 +349,5 @@ namespace FancyZonesCLI.Properties {
|
||||
return ResourceManager.GetString("editor_params_timeout", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_title {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_explanation {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_explanation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_hint {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_hint", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option1 {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option1", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option1_example {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option1_example", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option2 {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option2", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string error_powershell_scriptblock_option2_example {
|
||||
get {
|
||||
return ResourceManager.GetString("error_powershell_scriptblock_option2_example", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_title {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_syntax {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_syntax", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_options {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_options", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_commands {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_commands", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_examples {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_examples", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_arguments {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_arguments", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_aliases {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_aliases", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_command {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_command", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_optional {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_optional", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_unknown_command {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_unknown_command", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string usage_run_help {
|
||||
get {
|
||||
return ResourceManager.GetString("usage_run_help", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,62 +230,4 @@ Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid',
|
||||
<data name="editor_params_timeout" xml:space="preserve">
|
||||
<value>Could not get current monitor information (timed out after {0}ms waiting for '{1}').</value>
|
||||
</data>
|
||||
|
||||
<!-- PowerShell Script Block Detection -->
|
||||
<data name="error_powershell_scriptblock_title" xml:space="preserve">
|
||||
<value>Error: Invalid GUID format detected.</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_explanation" xml:space="preserve">
|
||||
<value>PowerShell interprets curly braces {} as script blocks.</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_hint" xml:space="preserve">
|
||||
<value>Please quote your GUID or use it without braces:</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option1" xml:space="preserve">
|
||||
<value>Option 1 - Quote the GUID:</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option1_example" xml:space="preserve">
|
||||
<value>FancyZonesCLI shk 1 '{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}'</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option2" xml:space="preserve">
|
||||
<value>Option 2 - Omit the braces (recommended):</value>
|
||||
</data>
|
||||
<data name="error_powershell_scriptblock_option2_example" xml:space="preserve">
|
||||
<value>FancyZonesCLI shk 1 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</value>
|
||||
</data>
|
||||
|
||||
<!-- CLI Usage -->
|
||||
<data name="usage_title" xml:space="preserve">
|
||||
<value>FancyZones CLI - Command line interface for FancyZones</value>
|
||||
</data>
|
||||
<data name="usage_syntax" xml:space="preserve">
|
||||
<value>Usage: FancyZonesCLI [command] [options]</value>
|
||||
</data>
|
||||
<data name="usage_options" xml:space="preserve">
|
||||
<value>Options:</value>
|
||||
</data>
|
||||
<data name="usage_commands" xml:space="preserve">
|
||||
<value>Commands:</value>
|
||||
</data>
|
||||
<data name="usage_examples" xml:space="preserve">
|
||||
<value>Examples:</value>
|
||||
</data>
|
||||
<data name="usage_arguments" xml:space="preserve">
|
||||
<value>Arguments:</value>
|
||||
</data>
|
||||
<data name="usage_aliases" xml:space="preserve">
|
||||
<value>Aliases:</value>
|
||||
</data>
|
||||
<data name="usage_command" xml:space="preserve">
|
||||
<value>Command:</value>
|
||||
</data>
|
||||
<data name="usage_optional" xml:space="preserve">
|
||||
<value>(optional)</value>
|
||||
</data>
|
||||
<data name="usage_unknown_command" xml:space="preserve">
|
||||
<value>Unknown command: {0}</value>
|
||||
</data>
|
||||
<data name="usage_run_help" xml:space="preserve">
|
||||
<value>Run 'FancyZonesCLI --help' to see available commands.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FancyZonesCLI.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for normalizing GUID strings to Windows format with braces.
|
||||
/// Supports input with or without braces: both "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
/// and "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" are accepted.
|
||||
/// </summary>
|
||||
internal static class GuidHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes a GUID string to Windows format with braces.
|
||||
/// Returns null if the input is not a valid GUID.
|
||||
/// </summary>
|
||||
/// <param name="input">GUID string with or without braces.</param>
|
||||
/// <returns>GUID in "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" format, or null if invalid.</returns>
|
||||
public static string? NormalizeGuid(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Guid.TryParse(input, out Guid guid))
|
||||
{
|
||||
// "B" format includes braces: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
|
||||
return guid.ToString("B").ToUpperInvariant();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to normalize a GUID string to Windows format with braces.
|
||||
/// </summary>
|
||||
/// <param name="input">GUID string with or without braces.</param>
|
||||
/// <param name="normalizedGuid">The normalized GUID string, or the original input if normalization fails.</param>
|
||||
/// <returns>True if the input was successfully normalized; otherwise, false.</returns>
|
||||
public static bool TryNormalizeGuid(string? input, [NotNullWhen(true)] out string? normalizedGuid)
|
||||
{
|
||||
normalizedGuid = NormalizeGuid(input);
|
||||
return normalizedGuid != null;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using static FancyZonesEditorCommon.Data.CustomLayouts;
|
||||
|
||||
@@ -24,10 +23,8 @@ namespace FancyZonesEditorCommon.Data
|
||||
{
|
||||
public struct CanvasZoneWrapper
|
||||
{
|
||||
[JsonPropertyName("X")]
|
||||
public int X { get; set; }
|
||||
|
||||
[JsonPropertyName("Y")]
|
||||
public int Y { get; set; }
|
||||
|
||||
public int Width { get; set; }
|
||||
|
||||
@@ -191,22 +191,27 @@ bool EditorParameters::Save(const WorkAreaConfiguration& configuration, OnThread
|
||||
|
||||
monitorJson.dpi = dpi;
|
||||
|
||||
// Get DPI-unaware values for dimensions (virtual coordinates for WPF sizing)
|
||||
MONITORINFOEX monitorInfoUnaware{};
|
||||
MONITORINFOEX monitorInfo{};
|
||||
dpiUnawareThread.submit(OnThreadExecutor::task_t{ [&] {
|
||||
monitorInfoUnaware.cbSize = sizeof(monitorInfoUnaware);
|
||||
GetMonitorInfo(monitor, &monitorInfoUnaware);
|
||||
monitorInfo.cbSize = sizeof(monitorInfo);
|
||||
if (!GetMonitorInfo(monitor, &monitorInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
} }).wait();
|
||||
|
||||
// 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;
|
||||
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);
|
||||
|
||||
// Position in virtual coordinates (matched by DPI-unaware context in WPF editor)
|
||||
monitorJson.left = monitorInfoUnaware.rcWork.left;
|
||||
monitorJson.top = 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;
|
||||
|
||||
argsJson.monitors.emplace_back(std::move(monitorJson));
|
||||
}
|
||||
|
||||
@@ -67,18 +67,10 @@ 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)
|
||||
@@ -88,33 +80,16 @@ 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);
|
||||
|
||||
_virtualWorkArea = Device.WorkAreaRect;
|
||||
|
||||
// Use DPI-unaware positioning
|
||||
Utils.NativeMethods.SetWindowPositionDpiUnaware(
|
||||
Window,
|
||||
(int)_virtualWorkArea.X,
|
||||
(int)_virtualWorkArea.Y,
|
||||
(int)_virtualWorkArea.Width,
|
||||
(int)_virtualWorkArea.Height);
|
||||
var workArea = Device.WorkAreaRect;
|
||||
Window.Left = workArea.X;
|
||||
Window.Top = workArea.Y;
|
||||
Window.Width = workArea.Width;
|
||||
Window.Height = workArea.Height;
|
||||
}
|
||||
|
||||
public void SetLayoutSettings(LayoutModel model)
|
||||
|
||||
@@ -69,11 +69,7 @@ namespace FancyZonesEditor.Utils
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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;
|
||||
return ScreenBoundsWidth + " × " + ScreenBoundsHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,48 +17,14 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ internal static class Program
|
||||
Console.InputEncoding = Encoding.Unicode;
|
||||
|
||||
// Initialize logger to file (same as other modules)
|
||||
CliLogger.Initialize("\\Image Resizer\\CLI");
|
||||
CliLogger.Initialize("\\ImageResizer\\Logs");
|
||||
CliLogger.Info($"ImageResizerCLI started with {args.Length} argument(s)");
|
||||
|
||||
try
|
||||
|
||||
@@ -126,10 +126,13 @@ namespace ImageResizer.Properties
|
||||
h => ncc.CollectionChanged -= h,
|
||||
() => settings.CustomSize = new CustomSize());
|
||||
|
||||
// Reset is used instead of Replace to avoid ArgumentOutOfRangeException
|
||||
// when notifying changes for virtual items (CustomSize/AiSize) that exist
|
||||
// outside the bounds of the underlying _sizes collection.
|
||||
Assert.AreEqual(NotifyCollectionChangedAction.Reset, result.Arguments.Action);
|
||||
Assert.AreEqual(NotifyCollectionChangedAction.Replace, result.Arguments.Action);
|
||||
Assert.AreEqual(1, result.Arguments.NewItems.Count);
|
||||
Assert.AreEqual(settings.CustomSize, result.Arguments.NewItems[0]);
|
||||
Assert.AreEqual(0, result.Arguments.NewStartingIndex);
|
||||
Assert.AreEqual(1, result.Arguments.OldItems.Count);
|
||||
Assert.AreEqual(originalCustomSize, result.Arguments.OldItems[0]);
|
||||
Assert.AreEqual(0, result.Arguments.OldStartingIndex);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace ImageResizer
|
||||
{
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private const string LogSubFolder = "\\Image Resizer\\Logs";
|
||||
private const string LogSubFolder = "\\ImageResizer\\Logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached AI availability state, checked at app startup.
|
||||
@@ -67,27 +67,12 @@ namespace ImageResizer
|
||||
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
|
||||
NativeMethods.SetProcessDPIAware();
|
||||
|
||||
// TODO: Re-enable AI Super Resolution in next release by removing this #if block
|
||||
// Temporarily disable AI Super Resolution feature (hide from UI but keep code)
|
||||
#if true // Set to false to re-enable AI Super Resolution
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
|
||||
// Skip AI detection mode as well
|
||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||
{
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
// Check for AI detection mode (called by Runner in background)
|
||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||
{
|
||||
RunAiDetectionMode();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
|
||||
@@ -216,15 +216,27 @@ namespace ImageResizer.Properties
|
||||
{
|
||||
if (e.PropertyName == nameof(Models.CustomSize))
|
||||
{
|
||||
var oldCustomSize = _customSize;
|
||||
_customSize = settings.CustomSize;
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace,
|
||||
_customSize,
|
||||
oldCustomSize,
|
||||
_sizes.Count));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Models.AiSize))
|
||||
{
|
||||
var oldAiSize = _aiSize;
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
OnCollectionChanged(
|
||||
new NotifyCollectionChangedEventArgs(
|
||||
NotifyCollectionChangedAction.Replace,
|
||||
_aiSize,
|
||||
oldAiSize,
|
||||
_sizes.Count + 1));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Sizes))
|
||||
{
|
||||
|
||||
@@ -388,13 +388,6 @@ namespace Peek.UI
|
||||
IsErrorVisible = true;
|
||||
}
|
||||
|
||||
public void ShowError(string message)
|
||||
{
|
||||
IsErrorVisible = false;
|
||||
ErrorMessage = message;
|
||||
IsErrorVisible = true;
|
||||
}
|
||||
|
||||
private void NavigationThrottleTimer_Tick(object? sender, object e)
|
||||
{
|
||||
if (sender == null)
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
Item="{x:Bind ViewModel.CurrentItem, Mode=OneWay}"
|
||||
NumberOfFiles="{x:Bind ViewModel.DisplayItemCount, Mode=OneWay}"
|
||||
PreviewSizeChanged="FilePreviewer_PreviewSizeChanged"
|
||||
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}"
|
||||
Visibility="{x:Bind ContentVisibility(ViewModel.IsErrorVisible), Mode=OneWay}" />
|
||||
ScalingFactor="{x:Bind ViewModel.ScalingFactor, Mode=OneWay}" />
|
||||
|
||||
<InfoBar
|
||||
x:Name="ErrorInfoBar"
|
||||
@@ -60,7 +59,6 @@
|
||||
Grid.RowSpan="2"
|
||||
Margin="4,0,4,6"
|
||||
VerticalAlignment="Bottom"
|
||||
Closed="ErrorInfoBar_Closed"
|
||||
IsOpen="{x:Bind ViewModel.IsErrorVisible, Mode=TwoWay}"
|
||||
Message="{x:Bind ViewModel.ErrorMessage, Mode=OneWay}"
|
||||
Severity="Error" />
|
||||
|
||||
@@ -14,7 +14,6 @@ using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Peek.Common.Constants;
|
||||
using Peek.Common.Extensions;
|
||||
using Peek.Common.Helpers;
|
||||
using Peek.FilePreviewer.Models;
|
||||
using Peek.FilePreviewer.Previewers;
|
||||
using Peek.UI.Extensions;
|
||||
@@ -196,20 +195,6 @@ namespace Peek.UI
|
||||
bootTime.Start();
|
||||
|
||||
ViewModel.Initialize(selectedItem);
|
||||
|
||||
// If no files were found (e.g., in virtual folders like Home/Recent), show an error
|
||||
if (ViewModel.CurrentItem == null)
|
||||
{
|
||||
Logger.LogInfo("Peek: No files found to preview, showing error.");
|
||||
var errorMessage = ResourceLoaderInstance.ResourceLoader.GetString("NoFilesSelected");
|
||||
ViewModel.ShowError(errorMessage);
|
||||
|
||||
// Still show the window so user can see the warning
|
||||
this.Show();
|
||||
WindowHelpers.BringToForeground(this.GetWindowHandle());
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.ScalingFactor = this.GetMonitorScale();
|
||||
this.Content.KeyUp += Content_KeyUp;
|
||||
|
||||
@@ -317,24 +302,5 @@ namespace Peek.UI
|
||||
{
|
||||
themeListener?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns Visibility.Collapsed when error is showing, Visibility.Visible when not.
|
||||
/// </summary>
|
||||
public Visibility ContentVisibility(bool isErrorVisible)
|
||||
{
|
||||
return isErrorVisible ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle InfoBar closed - if there's no current item, close the window.
|
||||
/// </summary>
|
||||
private void ErrorInfoBar_Closed(InfoBar sender, InfoBarClosedEventArgs args)
|
||||
{
|
||||
if (ViewModel.CurrentItem == null)
|
||||
{
|
||||
Uninitialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,10 +341,6 @@
|
||||
<value>No more files to preview.</value>
|
||||
<comment>The message to show when there are no files remaining to preview.</comment>
|
||||
</data>
|
||||
<data name="NoFilesSelected" xml:space="preserve">
|
||||
<value>No files selected or this folder is not supported for preview.</value>
|
||||
<comment>Displayed when Peek is activated in a virtual folder (like Home or Recent) where file selection cannot be retrieved.</comment>
|
||||
</data>
|
||||
<data name="DeleteFileError_NotFound" xml:space="preserve">
|
||||
<value>The file cannot be found. Please check if the file has been moved, renamed, or deleted.</value>
|
||||
<comment>Displayed if the file or path was not found</comment>
|
||||
|
||||
@@ -193,102 +193,6 @@ GeneralSettings get_general_settings()
|
||||
return settings;
|
||||
}
|
||||
|
||||
void apply_module_status_update(const json::JsonObject& module_config, bool save)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: {}", std::wstring{ module_config.ToString() });
|
||||
|
||||
// Expected format: {"ModuleName": true/false} - only one module per update
|
||||
auto iter = module_config.First();
|
||||
if (!iter.HasCurrent())
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Empty module config");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& element = iter.Current();
|
||||
const auto value = element.Value();
|
||||
if (value.ValueType() != json::JsonValueType::Boolean)
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Invalid value type for module status");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::wstring name{ element.Key().c_str() };
|
||||
if (modules().find(name) == modules().end())
|
||||
{
|
||||
Logger::warn(L"apply_module_status_update: Module {} not found", name);
|
||||
return;
|
||||
}
|
||||
|
||||
PowertoyModule& powertoy = modules().at(name);
|
||||
const bool module_inst_enabled = powertoy->is_enabled();
|
||||
bool target_enabled = value.GetBoolean();
|
||||
|
||||
auto gpo_rule = powertoy->gpo_policy_enabled_configuration();
|
||||
if (gpo_rule == powertoys_gpo::gpo_rule_configured_enabled || gpo_rule == powertoys_gpo::gpo_rule_configured_disabled)
|
||||
{
|
||||
// Apply the GPO Rule.
|
||||
target_enabled = gpo_rule == powertoys_gpo::gpo_rule_configured_enabled;
|
||||
}
|
||||
|
||||
if (module_inst_enabled == target_enabled)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Module {} already in target state {}", name, target_enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target_enabled)
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Enabling powertoy {}", name);
|
||||
powertoy->enable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.EnableHotkeyByModule(name);
|
||||
|
||||
// Trigger AI capability detection when ImageResizer is enabled
|
||||
if (name == L"Image Resizer")
|
||||
{
|
||||
Logger::info(L"ImageResizer enabled, triggering AI capability detection");
|
||||
DetectAiCapabilitiesAsync(true); // Skip settings check since we know it's being enabled
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"apply_module_status_update: Disabling powertoy {}", name);
|
||||
powertoy->disable();
|
||||
auto& hkmng = HotkeyConflictDetector::HotkeyConflictManager::GetInstance();
|
||||
hkmng.DisableHotkeyByModule(name);
|
||||
}
|
||||
// Sync the hotkey state with the module state, so it can be removed for disabled modules.
|
||||
powertoy.UpdateHotkeyEx();
|
||||
|
||||
if (save)
|
||||
{
|
||||
// Load existing settings and only update the specific module's enabled state
|
||||
json::JsonObject current_settings = PTSettingsHelper::load_general_settings();
|
||||
|
||||
json::JsonObject enabled;
|
||||
if (current_settings.HasKey(L"enabled"))
|
||||
{
|
||||
enabled = current_settings.GetNamedObject(L"enabled");
|
||||
}
|
||||
|
||||
// Check if the saved state is different from the requested state
|
||||
bool current_saved = enabled.HasKey(name) ? enabled.GetNamedBoolean(name, true) : true;
|
||||
|
||||
if (current_saved != target_enabled)
|
||||
{
|
||||
// Update only this module's enabled state
|
||||
enabled.SetNamedValue(name, json::value(target_enabled));
|
||||
current_settings.SetNamedValue(L"enabled", enabled);
|
||||
|
||||
PTSettingsHelper::save_general_settings(current_settings);
|
||||
|
||||
GeneralSettings settings_for_trace = get_general_settings();
|
||||
Trace::SettingsChanged(settings_for_trace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
||||
{
|
||||
std::wstring old_settings_json_string;
|
||||
@@ -463,21 +367,11 @@ void apply_general_settings(const json::JsonObject& general_configs, bool save)
|
||||
if (json::has(general_configs, L"show_theme_adaptive_tray_icon", json::JsonValueType::Boolean))
|
||||
{
|
||||
bool new_theme_adaptive = general_configs.GetNamedBoolean(L"show_theme_adaptive_tray_icon");
|
||||
Logger::info(L"apply_general_settings: show_theme_adaptive_tray_icon current={}, new={}",
|
||||
show_theme_adaptive_tray_icon, new_theme_adaptive);
|
||||
if (show_theme_adaptive_tray_icon != new_theme_adaptive)
|
||||
{
|
||||
show_theme_adaptive_tray_icon = new_theme_adaptive;
|
||||
set_tray_icon_theme_adaptive(show_theme_adaptive_tray_icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::info(L"apply_general_settings: show_theme_adaptive_tray_icon unchanged, skipping update");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::warn(L"apply_general_settings: show_theme_adaptive_tray_icon not found in config");
|
||||
}
|
||||
|
||||
if (json::has(general_configs, L"ignored_conflict_properties", json::JsonValueType::Object))
|
||||
|
||||
@@ -38,5 +38,4 @@ struct GeneralSettings
|
||||
json::JsonObject load_general_settings();
|
||||
GeneralSettings get_general_settings();
|
||||
void apply_general_settings(const json::JsonObject& general_configs, bool save = true);
|
||||
void apply_module_status_update(const json::JsonObject& module_config, bool save = true);
|
||||
void start_enabled_powertoys();
|
||||
@@ -215,12 +215,6 @@ void dispatch_received_json(const std::wstring& json_to_parse)
|
||||
// current_settings_ipc->send(settings_string);
|
||||
// }
|
||||
}
|
||||
else if (name == L"module_status")
|
||||
{
|
||||
// Handle single module enable/disable update
|
||||
// Expected format: {"module_status": {"ModuleName": true/false}}
|
||||
apply_module_status_update(value.GetObjectW());
|
||||
}
|
||||
else if (name == L"powertoys")
|
||||
{
|
||||
dispatch_json_config_to_modules(value.GetObjectW());
|
||||
|
||||
@@ -273,19 +273,12 @@ static HICON get_icon(Theme theme)
|
||||
{
|
||||
std::wstring icon_path = get_module_folderpath();
|
||||
icon_path += theme == Theme::Dark ? L"\\svgs\\PowerToysWhite.ico" : L"\\svgs\\PowerToysDark.ico";
|
||||
Logger::trace(L"get_icon: Loading icon from path: {}", icon_path);
|
||||
|
||||
HICON icon = static_cast<HICON>(LoadImage(NULL,
|
||||
return static_cast<HICON>(LoadImage(NULL,
|
||||
icon_path.c_str(),
|
||||
IMAGE_ICON,
|
||||
0,
|
||||
0,
|
||||
LR_LOADFROMFILE | LR_DEFAULTSIZE | LR_SHARED));
|
||||
if (!icon)
|
||||
{
|
||||
Logger::warn(L"get_icon: Failed to load icon from {}, error: {}", icon_path, GetLastError());
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
|
||||
@@ -381,45 +374,13 @@ void set_tray_icon_visible(bool shouldIconBeVisible)
|
||||
|
||||
void set_tray_icon_theme_adaptive(bool theme_adaptive)
|
||||
{
|
||||
Logger::info(L"set_tray_icon_theme_adaptive: Called with theme_adaptive={}, current theme_adaptive_enabled={}",
|
||||
theme_adaptive, theme_adaptive_enabled);
|
||||
|
||||
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
HICON icon = nullptr;
|
||||
|
||||
if (theme_adaptive)
|
||||
{
|
||||
icon = get_icon(theme_listener.AppTheme);
|
||||
if (!icon)
|
||||
{
|
||||
Logger::warn(L"set_tray_icon_theme_adaptive: Failed to load theme adaptive icon, falling back to default");
|
||||
}
|
||||
}
|
||||
|
||||
// If not requesting adaptive icon, or if adaptive icon failed to load, use default icon
|
||||
if (!icon)
|
||||
{
|
||||
icon = LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
if (theme_adaptive && icon)
|
||||
{
|
||||
// We requested adaptive but had to fall back, so update the flag
|
||||
theme_adaptive = false;
|
||||
Logger::info(L"set_tray_icon_theme_adaptive: Using default icon as fallback");
|
||||
}
|
||||
}
|
||||
|
||||
theme_adaptive_enabled = theme_adaptive;
|
||||
|
||||
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||
HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||
if (icon)
|
||||
{
|
||||
tray_icon_data.hIcon = icon;
|
||||
BOOL result = Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
||||
Logger::info(L"set_tray_icon_theme_adaptive: Icon updated, theme_adaptive_enabled={}, Shell_NotifyIcon result={}",
|
||||
theme_adaptive_enabled, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger::error(L"set_tray_icon_theme_adaptive: Failed to load any icon");
|
||||
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user