Compare commits

...

5 Commits

Author SHA1 Message Date
Kai Tao
920d3a878c aat window context menu 2026-02-03 16:37:06 +08:00
Kai Tao
67d96b0a13 PowerToys extension: Bundle localization files into installer (#45194)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #45171
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
<img width="925" height="612" alt="image"
src="https://github.com/user-attachments/assets/214ead95-504a-4e48-bc25-138323d973f9"
/>
2026-02-02 11:31:21 +08:00
Kai Tao
c5d4f992c1 Workspace: Fix an overlay issue for workspace snapshot draw (#45183)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request
Root cause: Workspaces uses DPI-unaware coordinates (via
GetDpiUnawareScreens()
which runs in a temporary DPI-unaware thread) to store/match window
positions
across different DPI settings. However, WorkspacesEditor itself uses
PerMonitorV2
DPI awareness for UI clarity. When assigning these DPI-unaware
coordinates directly
to WPF window properties, WPF automatically scaled them again based on
current DPI,
causing incorrect overlay positioning.

Fix: Use SetWindowPositionDpiUnaware() to bypass WPF's automatic DPI
scaling
by temporarily switching to DPI-unaware context when calling Win32
SetWindowPos.

Fix #45174

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #45174
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Verified in local build vs production build, and the problem fixed in
local build.
2026-02-02 09:34:50 +08:00
Kai Tao
11b406feee Build: Fix release pipeline and local build failure (#45211)
## Summary of the Pull Request
Release pipeline is keeping failed, and local build failed at ut.

This pull request introduces changes to improve how test projects are
handled during release builds, ensuring that test code is not compiled
or analyzed when not needed - in doing release build, to - succeed the
execution and reduce built time.

And, to upgrade from VS17 to VS18 in cmdpal sdk build, this is to keep
consistency with all other build step


<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Local build & release pipeline build should all pass:

Local build:
<img width="1815" height="281" alt="image"
src="https://github.com/user-attachments/assets/f350cf3f-b856-432d-97f3-e392d38ef7fa"
/>

Release pipeline is working too:
<img width="1195" height="163" alt="image"
src="https://github.com/user-attachments/assets/ce58de38-f0fb-45ad-9d70-2b8eb1c4db60"
/>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-02 09:15:53 +08:00
Jiří Polášek
256af8f6e0 Spellcheck: Add missing words and sort expect.txt (#45251)
## PR Checklist

This PR adds missing words to the spell checker dictionary.

- [ ] Closes: #xxx
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [ ] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [ ] **Tests:** Added/updated and all pass
- [ ] **Localization:** All end-user-facing strings can be localized
- [ ] **Dev docs:** Added/updated
- [ ] **New binaries:** Added on the required places
- [ ] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [ ] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder
- [ ] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects
- [ ] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: #xxx

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
2026-02-01 20:14:37 +08:00
19 changed files with 698 additions and 28 deletions

View File

@@ -635,6 +635,7 @@ GMEM
GNumber
googleai
googlegemini
Gotchas
gpedit
gpo
GPOCA
@@ -647,8 +648,6 @@ GSM
gtm
guiddata
GUITHREADINFO
Gotcha
Gotchas
GValue
gwl
GWLP
@@ -894,9 +893,9 @@ Lclean
Ldone
Ldr
LEFTALIGN
leftclick
LEFTSCROLLBAR
LEFTTEXT
leftclick
LError
LEVELID
LExit
@@ -1022,9 +1021,12 @@ MENUITEMINFO
MENUITEMINFOW
MERGECOPY
MERGEPAINT
Metacharacter
metadatamatters
Metadatas
Metacharacter
metafile
Metacharacter
mfc
Mgmt
Microwaved
@@ -1071,7 +1073,7 @@ mouseutils
MOVESIZEEND
MOVESIZESTART
MRM
MRT
Mrt
mru
MSAL
msc
@@ -1489,7 +1491,9 @@ regfile
REGISTERCLASSFAILED
REGISTRYHEADER
REGISTRYPREVIEWEXT
registryroot
regkey
regroot
regsvr
REINSTALLMODE
releaseblog
@@ -1534,7 +1538,6 @@ riid
RKey
RNumber
rollups
ROOTOWNER
rop
ROUNDSMALL
ROWSETEXT
@@ -2171,4 +2174,4 @@ Zoneszonabletester
Zoomin
zoomit
ZOOMITX
Zorder
Zorder

View File

@@ -91,6 +91,7 @@ extends:
official: true
codeSign: true
runTests: false
buildTests: false
signingIdentity:
serviceName: $(SigningServiceName)
appId: $(SigningAppId)

View File

@@ -258,6 +258,7 @@ jobs:
-restore -graph
/p:RestorePackagesConfig=true
/p:CIBuild=true
/p:BuildTests=${{ parameters.buildTests }}
/bl:$(LogOutputDirectory)\build-0-main.binlog
${{ parameters.additionalBuildOptions }}
$(MSBuildCacheParameters)

View File

@@ -59,6 +59,7 @@ stages:
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
runTests: ${{ parameters.runTests }}
buildTests: true
useVSPreview: ${{ parameters.useVSPreview }}
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
@@ -78,7 +79,9 @@ stages:
${{ else }}:
name: SHINE-OSS-L
${{ if eq(parameters.useVSPreview, true) }}:
demands: ImageOverride -equals SHINE-VS17-Preview
demands: ImageOverride -equals SHINE-VS18-Preview
${{ else }}:
demands: ImageOverride -equals SHINE-VS18-Latest
buildConfigurations: [Release]
official: false
codeSign: false

View File

@@ -90,9 +90,15 @@ if ($noticeMatch.Success) {
$currentNoticePackageList = ""
}
# Test-only packages that are allowed to be in NOTICE.md but not in the build
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
$allowedExtraPackages = @(
"- Moq"
)
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
{
Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
# Show detailed differences
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
@@ -105,7 +111,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in proj file list but not in NOTICE.md
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
if ($missingFromNotice.Count -gt 0) {
Write-Host -ForegroundColor Red "MissingFromNotice:"
Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
foreach ($pkg in $missingFromNotice) {
Write-Host -ForegroundColor Red " $pkg"
}
@@ -114,10 +120,23 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
# Find packages in NOTICE.md but not in proj file list
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
if ($extraInNotice.Count -gt 0) {
Write-Host -ForegroundColor Yellow "ExtraInNotice:"
foreach ($pkg in $extraInNotice) {
Write-Host -ForegroundColor Yellow " $pkg"
# Filter out allowed extra packages (test-only dependencies)
$unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
$allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
if ($allowedExtra.Count -gt 0) {
Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
foreach ($pkg in $allowedExtra) {
Write-Host -ForegroundColor Green " $pkg"
}
Write-Host ""
}
if ($unexpectedExtra.Count -gt 0) {
Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
foreach ($pkg in $unexpectedExtra) {
Write-Host -ForegroundColor Red " $pkg"
}
Write-Host ""
}
@@ -127,10 +146,17 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
Write-Host " Proj file list has $($generatedPackages.Count) packages"
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
Write-Host ""
exit 1
# Fail if there are missing packages OR unexpected extra packages
if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
exit 1
} else {
Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
}
}
exit 0

View File

@@ -2,6 +2,12 @@
<Project ToolsVersion="4.0"
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Skip building C++ test projects when BuildTests=false -->
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
<RunCodeAnalysis>false</RunCodeAnalysis>
</PropertyGroup>
<!-- Project configurations -->
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">

View File

@@ -19,6 +19,39 @@
<PlatformTarget>$(Platform)</PlatformTarget>
</PropertyGroup>
<!--
Completely skip building test projects when BuildTests=false (e.g., Release pipeline).
This avoids InternalsVisibleTo/signing issues by not compiling test code at all.
Match: projects ending in Test, Tests, UnitTests, UITests, FuzzTests, or in a folder named Tests.
Also matches projects starting with UnitTests- (e.g., UnitTests-CommonLib).
Also removes all PackageReference/ProjectReference to prevent NuGet restore and dependency builds.
Note: Checking both 'false' and 'False' to handle YAML boolean serialization.
-->
<PropertyGroup Condition="'$(BuildTests)' == 'false' or '$(BuildTests)' == 'False'">
<_ProjectName>$(MSBuildProjectName)</_ProjectName>
<!-- Match any project ending with "Test" or "Tests" (covers UnitTests, UITests, FuzzTests, etc.) -->
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true</_IsSkippedTestProject>
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true</_IsSkippedTestProject>
<!-- Match projects starting with UnitTests- or UITest- prefix -->
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true</_IsSkippedTestProject>
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true</_IsSkippedTestProject>
<!-- Match projects in a Tests folder -->
<_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true</_IsSkippedTestProject>
</PropertyGroup>
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<EnableDefaultItems>false</EnableDefaultItems>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateGlobalUsings>false</GenerateGlobalUsings>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Disable all code analysis for skipped test projects -->
<EnableNETAnalyzers>false</EnableNETAnalyzers>
<RunAnalyzers>false</RunAnalyzers>
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<Version>$(Version).0</Version>
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
@@ -30,7 +63,9 @@
<_PropertySheetDisplayName>PowerToys.Root.Props</_PropertySheetDisplayName>
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
</PropertyGroup>
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -28,4 +28,41 @@
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
</PropertyGroup>
</Project>
<!-- Skipped test projects when BuildTests=false: no-op build and remove references.
This must be in targets (not props) so it runs AFTER the project file adds its items. -->
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
<BuildDependsOn />
<CoreBuildDependsOn />
<RebuildDependsOn />
</PropertyGroup>
<!-- For C# projects: remove all items -->
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
<PackageReference Remove="@(PackageReference)" />
<ProjectReference Remove="@(ProjectReference)" />
<Reference Remove="@(Reference)" />
<Compile Remove="@(Compile)" />
<Content Remove="@(Content)" />
<EmbeddedResource Remove="@(EmbeddedResource)" />
<None Remove="@(None)" />
<Using Remove="@(Using)" />
<GlobalUsing Remove="@(GlobalUsing)" />
</ItemGroup>
<!-- For C++ projects (vcxproj): remove all compile/link items to prevent build -->
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
<ClCompile Remove="@(ClCompile)" />
<ClInclude Remove="@(ClInclude)" />
<Link Remove="@(Link)" />
<Lib Remove="@(Lib)" />
<ProjectReference Remove="@(ProjectReference)" />
<None Remove="@(None)" />
<ResourceCompile Remove="@(ResourceCompile)" />
<Midl Remove="@(Midl)" />
</ItemGroup>
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
on the Target element still override the default targets even when condition is false. -->
</Project>

152
aat-context-menu.txt Normal file
View File

@@ -0,0 +1,152 @@
整体架构概览
┌────────────────────┐
│ PowerToys (AOT) │
│ 自身进程 │
│ │
│ 1. 监听前台窗口 │
│ 2. 修改 SystemMenu│
│ 3. 处理命令 │
│ │
└─────────┬──────────┘
│ USER32 / HWND
┌────────────────────┐
│ 目标应用窗口 │
│ (任意 Win32 app) │
│ │
│ System Menu │
│ ├ Restore │
│ ├ Move │
│ ├ Minimize │
│ ├ Maximize │
│ ├ ────────── │
│ ├ Always on top │ ← 我们加的
│ └ Close │
└────────────────────┘
技术分解(按执行顺序)
Step 1识别目标窗口Foreground Window
PowerToys 侧需要始终知道“当前操作对象”:
GetForegroundWindow()
可选SetWinEventHook(EVENT_SYSTEM_FOREGROUND, …)
过滤条件(非常重要):
排除:
Desktop / Shell / Taskbar
PowerToys 自己
无 WS_SYSMENU 的窗口
可选:
只对顶层窗口生效
只对 visible 窗口
Step 2获取并修改 System Menu官方 API
HMENU hMenu = GetSystemMenu(hwnd, FALSE);
插入位置
推荐插在 SC_CLOSE 前
或统一放在 separator 后
插入示例(伪代码)
AppendMenu(hMenu, MF_SEPARATOR, 0, nullptr);
AppendMenu(
hMenu,
MF_STRING | (isTopMost ? MF_CHECKED : MF_UNCHECKED),
IDM_ALWAYS_ON_TOP, // 自定义 ID>= 0x1000
L"Always on top"
);
⚠️ 关键规则:
不能使用 SC_* 范围的 ID
推荐 0x1000 ~ 0xEFFF
Step 3避免重复插入必做
因为 System Menu 是持久的:
同一个窗口只插一次
切换窗口时同步状态checked / unchecked
常见做法:
在 PowerToys 内部维护:
HWND → 已注入菜单标记
或在插入前:
GetMenuItemInfo 检查是否已有该 ID
Step 4用户点击菜单项系统行为
当用户点击:
Always on top
消息会送到目标窗口:
WM_SYSCOMMAND
wParam = IDM_ALWAYS_ON_TOP
⚠️ 注意:
目标窗口 不会处理这个命令
默认行为是忽略
Step 5PowerToys 在“外部进程”执行动作
这是关键设计点:
PowerToys 不需要接管目标窗口的 WindowProc
而是:
PowerToys 自己知道:
当前 foreground HWND
上一次点击的菜单项
直接对该 HWND 执行动作
例如:
SetWindowPos(
hwnd,
isTopMost ? HWND_NOTOPMOST : HWND_TOPMOST,
0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE
);
👉 所有动作都是合法的跨进程窗口管理 API
权限 / UIPI 行为(现实边界)
场景 行为
普通 → 普通窗口 ✅ 可用
普通 → 管理员窗口 ❌ 菜单不可改
管理员 → 普通窗口 ✅
管理员 → 管理员窗口 ✅
结论:
和 PowerToys 现有 AOT 行为 一致

View File

@@ -0,0 +1,39 @@
# Always On Top System Menu Injection & Handling
## Overview
Adds “Always on top” to a windows system menu and keeps the menu state in sync with the modules pin/unpin logic and hotkeys.
## Algorithm
1. **Event triggers**
- On startup.
- On `EVENT_SYSTEM_FOREGROUND` and `EVENT_SYSTEM_MENUPOPUPSTART`.
2. **EnsureSystemMenuForWindow(hwnd)**
- Filter: top-level, visible, has `WS_SYSMENU`, not `WS_CHILD`, not system/PowerToys/excluded.
- If item missing: insert separator (only if previous item isnt one) and add menu item ID `0x1000` before `SC_CLOSE`; call `DrawMenuBar`.
- Always update the checkmark via `CheckMenuItem` based on `IsTopmost`.
3. **Click handling** (`EVENT_OBJECT_INVOKED` / `EVENT_OBJECT_COMMAND`, child ID `0x1000`)
- Resolve actionable window: `GW_OWNER``GA_ROOTOWNER``GA_ROOT` → foreground fallback.
- If the resolved window lacks our item, attempt foreground-root fallback.
- Toggle with `ProcessCommandWithSource(hwnd, "systemmenu")`, then refresh the menu item state.
4. **Hotkeys / LLKH events**
- Use the same toggle path via `ProcessCommandWithSource(hwnd, "hotkey"/"llkh")`, ensuring menu checkmark stays aligned with hotkey toggles.
## Key IDs & resources
- Menu command ID: `0x1000` (outside `SC_*` range).
- Label: `System_Menu_Always_On_Top` in `Resources.resx` (generated to `resource.h`).
## Logging (for diagnostics)
- Source-tagged toggles: `[AOT] ProcessCommand source=<hotkey|llkh|systemmenu> hwnd=...`
- Target resolution chain: `GW_OWNER / GA_ROOTOWNER / GA_ROOT` plus foreground fallback.
- Injection: insertions, “already present”, and `GetMenuItemCount` failures (with `GetLastError`).
- Clicks: `System menu click captured (event=..., src=..., target=...)`.
## Edge cases handled
- Menu popup HWND without `WS_SYSMENU`: we climb to owner/root and optionally foreground to find the real system menu.
- Duplicate separators avoided by checking the previous item.
- Foreground elevated windows still blocked by existing UIPI limits; we log skips accordingly.
## How to test quickly
1. Start Always On Top, open a normal Win32 app, open its system menu, click “Always on top”; check that the window pins and the menu item shows a checkmark.
2. Use the hotkey to toggle the same window; ensure the menu checkmark follows.
3. Check `AppData\Local\Microsoft\PowerToys\AlwaysOnTop\Logs\...` for the trace lines above if something is off.

View File

@@ -367,6 +367,12 @@
</RegistryKey>
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
</Component>
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
</RegistryKey>
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
</Component>
<?undef IdSafeLanguage?>
<?undef CompGUIDPrefix?>
<?endforeach?>

View File

@@ -2,8 +2,11 @@
// 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.Windows;
using WorkspacesEditor.Utils;
namespace WorkspacesEditor
{
/// <summary>
@@ -11,9 +14,40 @@ namespace WorkspacesEditor
/// </summary>
public partial class OverlayWindow : Window
{
private int _targetX;
private int _targetY;
private int _targetWidth;
private int _targetHeight;
public OverlayWindow()
{
InitializeComponent();
SourceInitialized += OnWindowSourceInitialized;
}
/// <summary>
/// Sets the target bounds for the overlay window.
/// The window will be positioned using DPI-unaware context after initialization.
/// </summary>
public void SetTargetBounds(int x, int y, int width, int height)
{
_targetX = x;
_targetY = y;
_targetWidth = width;
_targetHeight = height;
// Set initial WPF properties (will be corrected after HWND creation)
Left = x;
Top = y;
Width = width;
Height = height;
}
private void OnWindowSourceInitialized(object sender, EventArgs e)
{
// Reposition window using DPI-unaware context to match the virtual coordinates.
// This fixes overlay positioning on mixed-DPI multi-monitor setups.
NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight);
}
}
}

View File

@@ -4,6 +4,8 @@
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace WorkspacesEditor.Utils
{
@@ -17,6 +19,39 @@ namespace WorkspacesEditor.Utils
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[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 uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOACTIVATE = 0x0010;
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
/// <summary>
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates.
/// 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.
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
try
{
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
}
finally
{
SetThreadDpiAwarenessContext(oldContext);
}
}
}
[DllImport("USER32.DLL")]
public static extern bool SetForegroundWindow(IntPtr hWnd);

View File

@@ -495,10 +495,10 @@ namespace WorkspacesEditor.ViewModels
{
var bounds = screen.Bounds;
OverlayWindow overlayWindow = new OverlayWindow();
overlayWindow.Top = bounds.Top;
overlayWindow.Left = bounds.Left;
overlayWindow.Width = bounds.Width;
overlayWindow.Height = bounds.Height;
// Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups
overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
overlayWindow.ShowActivated = true;
overlayWindow.Topmost = true;
overlayWindow.Show();

View File

@@ -1,6 +1,10 @@
#include "pch.h"
#include "AlwaysOnTop.h"
#include <filesystem>
#include <string>
#include <cstring>
#include <common/display/dpi_aware.h>
#include <common/utils/game_mode.h>
#include <common/utils/excluded_apps.h>
@@ -16,11 +20,21 @@
#include <trace.h>
#include <WinHookEventIDs.h>
#ifndef EVENT_OBJECT_COMMAND
#define EVENT_OBJECT_COMMAND 0x8010
#endif
// Raised when a window's system menu is about to be displayed.
#ifndef EVENT_SYSTEM_MENUPOPUPSTART
#define EVENT_SYSTEM_MENUPOPUPSTART 0x0006
#endif
namespace NonLocalizable
{
const static wchar_t* TOOL_WINDOW_CLASS_NAME = L"AlwaysOnTopWindow";
const static wchar_t* WINDOW_IS_PINNED_PROP = L"AlwaysOnTop_Pinned";
const static UINT ALWAYS_ON_TOP_MENU_ITEM_ID = 0x1000;
}
bool isExcluded(HWND window)
@@ -53,6 +67,11 @@ AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
SubscribeToEvents();
StartTrackingTopmostWindows();
if (HWND foreground = GetForegroundWindow())
{
EnsureSystemMenuForWindow(foreground);
}
}
else
{
@@ -158,7 +177,7 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
{
if (hotkeyId == static_cast<int>(HotkeyId::Pin))
{
ProcessCommand(fw);
ProcessCommandWithSource(fw, L"hotkey");
}
else if (hotkeyId == static_cast<int>(HotkeyId::IncreaseOpacity))
{
@@ -193,6 +212,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
Sound::Type soundType = Sound::Type::Off;
bool topmost = IsTopmost(window);
Logger::trace(L"[AOT] ProcessCommand toggle start hwnd={:#x} topmost={}", reinterpret_cast<uintptr_t>(window), topmost);
if (topmost)
{
if (UnpinTopmostWindow(window))
@@ -208,6 +228,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
m_windowOriginalLayeredState.erase(window);
Trace::AlwaysOnTop::UnpinWindow();
Logger::trace(L"[AOT] Unpinned hwnd={:#x}", reinterpret_cast<uintptr_t>(window));
}
}
else
@@ -218,6 +239,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
AssignBorder(window);
Trace::AlwaysOnTop::PinWindow();
Logger::trace(L"[AOT] Pinned hwnd={:#x}", reinterpret_cast<uintptr_t>(window));
}
}
@@ -225,6 +247,8 @@ void AlwaysOnTop::ProcessCommand(HWND window)
{
m_sound.Play(soundType);
}
EnsureSystemMenuForWindow(window);
}
void AlwaysOnTop::StartTrackingTopmostWindows()
@@ -357,7 +381,7 @@ void AlwaysOnTop::RegisterLLKH()
case WAIT_OBJECT_0: // Pin event
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw);
ProcessCommandWithSource(fw, L"llkh");
}
break;
case WAIT_OBJECT_0 + 1: // Terminate event
@@ -392,7 +416,7 @@ void AlwaysOnTop::RegisterLLKH()
void AlwaysOnTop::SubscribeToEvents()
{
// subscribe to windows events
std::array<DWORD, 7> events_to_subscribe = {
std::array<DWORD, 10> events_to_subscribe = {
EVENT_OBJECT_LOCATIONCHANGE,
EVENT_SYSTEM_MINIMIZESTART,
EVENT_SYSTEM_MINIMIZEEND,
@@ -400,6 +424,9 @@ void AlwaysOnTop::SubscribeToEvents()
EVENT_SYSTEM_FOREGROUND,
EVENT_OBJECT_DESTROY,
EVENT_OBJECT_FOCUS,
EVENT_OBJECT_INVOKED,
EVENT_OBJECT_COMMAND,
EVENT_SYSTEM_MENUPOPUPSTART,
};
for (const auto event : events_to_subscribe)
@@ -492,7 +519,60 @@ bool AlwaysOnTop::IsTracked(HWND window) const noexcept
void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
{
if (!AlwaysOnTopSettings::settings().enableFrame || !data->hwnd)
if (!data || !data->hwnd)
{
return;
}
if (data->event == EVENT_SYSTEM_FOREGROUND || data->event == EVENT_SYSTEM_MENUPOPUPSTART)
{
HWND target = ResolveMenuTargetWindow(data->hwnd);
Logger::trace(L"[AOT:SystemMenu] Ensure on event {} (src={:#x}, target={:#x})", data->event, reinterpret_cast<uintptr_t>(data->hwnd), reinterpret_cast<uintptr_t>(target));
EnsureSystemMenuForWindow(target);
}
if ((data->event == EVENT_OBJECT_INVOKED || data->event == EVENT_OBJECT_COMMAND) &&
data->idChild == static_cast<LONG>(NonLocalizable::ALWAYS_ON_TOP_MENU_ITEM_ID))
{
HWND target = ResolveMenuTargetWindow(data->hwnd);
Logger::trace(L"System menu click captured (event={}, src={:#x}, target={:#x})", data->event, reinterpret_cast<uintptr_t>(data->hwnd), reinterpret_cast<uintptr_t>(target));
auto hasItem = [](HWND w) {
if (!w)
{
return false;
}
HMENU m = GetSystemMenu(w, FALSE);
return m && GetMenuState(m, NonLocalizable::ALWAYS_ON_TOP_MENU_ITEM_ID, MF_BYCOMMAND) != static_cast<UINT>(-1);
};
if (!hasItem(target))
{
HWND fg = GetForegroundWindow();
HWND fgRoot = fg ? GetAncestor(fg, GA_ROOT) : nullptr;
Logger::trace(L"[AOT:SystemMenu] Fallback to foreground (src={:#x}, fg={:#x}, fgRoot={:#x})",
reinterpret_cast<uintptr_t>(data->hwnd),
reinterpret_cast<uintptr_t>(fg),
reinterpret_cast<uintptr_t>(fgRoot));
if (hasItem(fgRoot))
{
target = fgRoot;
}
}
HMENU systemMenu = GetSystemMenu(target, FALSE);
if (systemMenu && GetMenuState(systemMenu, NonLocalizable::ALWAYS_ON_TOP_MENU_ITEM_ID, MF_BYCOMMAND) != static_cast<UINT>(-1))
{
ProcessCommandWithSource(target, L"systemmenu");
EnsureSystemMenuForWindow(target);
}
else
{
Logger::trace(L"Menu click ignored; menu item not present (target={:#x})", reinterpret_cast<uintptr_t>(target));
}
return;
}
if (!AlwaysOnTopSettings::settings().enableFrame)
{
return;
}
@@ -616,6 +696,200 @@ void AlwaysOnTop::RefreshBorders()
}
}
void AlwaysOnTop::ProcessCommandWithSource(HWND window, const wchar_t* sourceTag)
{
Logger::trace(L"[AOT] ProcessCommand source={} hwnd={:#x}", sourceTag ? sourceTag : L"unknown", reinterpret_cast<uintptr_t>(window));
ProcessCommand(window);
}
bool AlwaysOnTop::ShouldInjectSystemMenu(HWND window) const noexcept
{
if (!window || !IsWindow(window))
{
Logger::trace(L"[AOT:SystemMenu] Skip: invalid window handle");
return false;
}
// Only consider top-level, visible windows that expose a system menu.
LONG style = GetWindowLong(window, GWL_STYLE);
if ((style & WS_SYSMENU) == 0 || (style & WS_CHILD) == WS_CHILD)
{
Logger::trace(L"[AOT:SystemMenu] Skip: missing WS_SYSMENU or is child (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
return false;
}
if (!IsWindowVisible(window))
{
Logger::trace(L"[AOT:SystemMenu] Skip: not visible (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
return false;
}
if (GetAncestor(window, GA_ROOT) != window)
{
Logger::trace(L"[AOT:SystemMenu] Skip: not root window (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
return false;
}
char className[256]{};
if (GetClassNameA(window, className, ARRAYSIZE(className)) && is_system_window(window, className))
{
const std::wstring classNameW{ std::wstring(className, className + std::strlen(className)) };
Logger::trace(L"[AOT:SystemMenu] Skip: system window class {}", classNameW);
return false;
}
if (isExcluded(window))
{
Logger::trace(L"[AOT:SystemMenu] Skip: user excluded (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
return false;
}
DWORD processId = 0;
GetWindowThreadProcessId(window, &processId);
if (processId == GetCurrentProcessId())
{
Logger::trace(L"[AOT:SystemMenu] Skip: PowerToys process (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
return false;
}
auto processPath = get_process_path(window);
if (!processPath.empty())
{
const std::filesystem::path path{ processPath };
const auto fileName = path.filename().wstring();
if (_wcsnicmp(fileName.c_str(), L"PowerToys", 9) == 0 ||
_wcsicmp(fileName.c_str(), L"PowerLauncher.exe") == 0)
{
Logger::trace(L"[AOT:SystemMenu] Skip: PowerToys executable {}", fileName.c_str());
return false;
}
}
return true;
}
void AlwaysOnTop::UpdateSystemMenuItemState(HWND window, HMENU systemMenu) const noexcept
{
if (!systemMenu)
{
Logger::trace(L"[AOT:SystemMenu] Update state skipped: null menu");
return;
}
const UINT state = IsTopmost(window) ? MF_CHECKED : MF_UNCHECKED;
CheckMenuItem(systemMenu, NonLocalizable::ALWAYS_ON_TOP_MENU_ITEM_ID, MF_BYCOMMAND | state);
}
HWND AlwaysOnTop::ResolveMenuTargetWindow(HWND window) const noexcept
{
if (!window)
{
return nullptr;
}
HWND candidate = window;
auto log_choice = [&](const wchar_t* stage, HWND hwnd) {
Logger::trace(L"[AOT:SystemMenu] Resolve target: {} -> {:#x}", stage, reinterpret_cast<uintptr_t>(hwnd));
};
LONG style = GetWindowLong(candidate, GWL_STYLE);
if ((style & WS_SYSMENU) == 0 || (style & WS_CHILD) == WS_CHILD)
{
HWND owner = GetWindow(candidate, GW_OWNER);
if (owner)
{
candidate = owner;
log_choice(L"GW_OWNER", candidate);
}
else
{
candidate = GetAncestor(window, GA_ROOTOWNER);
log_choice(L"GA_ROOTOWNER", candidate);
}
if (!candidate)
{
candidate = GetAncestor(window, GA_ROOT);
log_choice(L"GA_ROOT", candidate);
}
if (!candidate)
{
candidate = GetForegroundWindow();
log_choice(L"Foreground fallback", candidate);
}
}
return candidate;
}
void AlwaysOnTop::EnsureSystemMenuForWindow(HWND window)
{
Logger::trace(L"[AOT:SystemMenu] Ensure request (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
if (!ShouldInjectSystemMenu(window))
{
return;
}
HMENU systemMenu = GetSystemMenu(window, FALSE);
if (!systemMenu)
{
Logger::trace(L"[AOT:SystemMenu] GetSystemMenu failed (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
return;
}
// Insert menu item once per window.
if (GetMenuState(systemMenu, NonLocalizable::ALWAYS_ON_TOP_MENU_ITEM_ID, MF_BYCOMMAND) == static_cast<UINT>(-1))
{
Logger::trace(L"[AOT:SystemMenu] Inserting menu item (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
int itemCount = GetMenuItemCount(systemMenu);
if (itemCount == -1)
{
Logger::trace(L"[AOT:SystemMenu] GetMenuItemCount failed (hwnd={:#x}, lastError={})", reinterpret_cast<uintptr_t>(window), GetLastError());
return;
}
int insertPos = itemCount;
for (int i = 0; i < itemCount; ++i)
{
if (GetMenuItemID(systemMenu, i) == SC_CLOSE)
{
insertPos = i;
break;
}
}
// Add a separator only if the previous item is not already a separator
if (insertPos > 0)
{
MENUITEMINFOW prevInfo{};
prevInfo.cbSize = sizeof(MENUITEMINFOW);
prevInfo.fMask = MIIM_FTYPE;
if (GetMenuItemInfoW(systemMenu, insertPos - 1, TRUE, &prevInfo) && !(prevInfo.fType & MFT_SEPARATOR))
{
InsertMenuW(systemMenu, insertPos, MF_BYPOSITION | MF_SEPARATOR, 0, nullptr);
++insertPos;
}
}
const std::wstring menuLabel = GET_RESOURCE_STRING_FALLBACK(IDS_SYSTEM_MENU_ALWAYS_ON_TOP, L"Always on top");
InsertMenuW(systemMenu,
insertPos + 1,
MF_BYPOSITION | MF_STRING,
NonLocalizable::ALWAYS_ON_TOP_MENU_ITEM_ID,
menuLabel.c_str());
DrawMenuBar(window);
}
else
{
Logger::trace(L"[AOT:SystemMenu] Already present, updating state only (hwnd={:#x})", reinterpret_cast<uintptr_t>(window));
}
UpdateSystemMenuItemState(window, systemMenu);
}
HWND AlwaysOnTop::ResolveTransparencyTargetWindow(HWND window)
{
if (!window || !IsWindow(window))
@@ -776,4 +1050,4 @@ void AlwaysOnTop::RestoreWindowAlpha(HWND window)
SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
}
}
}
}

View File

@@ -80,6 +80,7 @@ private:
void SubscribeToEvents();
void ProcessCommand(HWND window);
void ProcessCommandWithSource(HWND window, const wchar_t* sourceTag);
void StartTrackingTopmostWindows();
void UnpinAll();
void CleanUp();
@@ -92,6 +93,10 @@ private:
bool UnpinTopmostWindow(HWND window) const noexcept;
bool AssignBorder(HWND window);
void RefreshBorders();
void EnsureSystemMenuForWindow(HWND window);
bool ShouldInjectSystemMenu(HWND window) const noexcept;
void UpdateSystemMenuItemState(HWND window, HMENU systemMenu) const noexcept;
HWND ResolveMenuTargetWindow(HWND window) const noexcept;
// Transparency methods
HWND ResolveTransparencyTargetWindow(HWND window);

View File

@@ -131,4 +131,7 @@
<data name="System_Foreground_Elevated_Dialog_Dont_Show_Again" xml:space="preserve">
<value>Don't show again</value>
</data>
</root>
<data name="System_Menu_Always_On_Top" xml:space="preserve">
<value>Always on top</value>
</data>
</root>

View File

@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\CoreCommonProps.props" />
<PropertyGroup>
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Common" />

View File

@@ -84,7 +84,13 @@
<WarningsNotAsErrors>IL2081;$(WarningsNotAsErrors)</WarningsNotAsErrors>
</PropertyGroup>
<ItemGroup>
<!-- InternalsVisibleTo with public key for CI builds (signed assemblies) -->
<ItemGroup Condition="'$(CIBuild)'=='true'">
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests, PublicKey=002400000c80000014010000060200000024000052534131000800000100010085aad0bef0688d1b994a0d78e1fd29fc24ac34ed3d3ac3fb9b3d0c48386ba834aa880035060a8848b2d8adf58e670ed20914be3681a891c9c8c01eef2ab22872547c39be00af0e6c72485d7cfd1a51df8947d36ceba9989106b58abe79e6a3e71a01ed6bdc867012883e0b1a4d35b1b5eeed6df21e401bb0c22f2246ccb69979dc9e61eef262832ed0f2064853725a75485fa8a3efb7e027319c86dec03dc3b1bca2b5081bab52a627b9917450dfad534799e1c7af58683bdfa135f1518ff1ea60e90d7b993a6c87fd3dd93408e35d1296f9a7f9a97c5db56c0f3cc25ad11e9777f94d138b3cea53b9a8331c2e6dcb8d2ea94e18bf1163ff112a22dbd92d429a" />
</ItemGroup>
<!-- InternalsVisibleTo without public key for local builds (unsigned assemblies) -->
<ItemGroup Condition="'$(CIBuild)'!='true'">
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests" />
</ItemGroup>
</Project>