mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-03 18:00:25 +01:00
Compare commits
7 Commits
issue/4435
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
920d3a878c | ||
|
|
67d96b0a13 | ||
|
|
c5d4f992c1 | ||
|
|
11b406feee | ||
|
|
256af8f6e0 | ||
|
|
87c65f9eec | ||
|
|
971c7e9fba |
15
.github/actions/spell-check/expect.txt
vendored
15
.github/actions/spell-check/expect.txt
vendored
@@ -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
|
||||
@@ -91,6 +91,7 @@ extends:
|
||||
official: true
|
||||
codeSign: true
|
||||
runTests: false
|
||||
buildTests: false
|
||||
signingIdentity:
|
||||
serviceName: $(SigningServiceName)
|
||||
appId: $(SigningAppId)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
152
aat-context-menu.txt
Normal 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 5:PowerToys 在“外部进程”执行动作
|
||||
|
||||
这是关键设计点:
|
||||
|
||||
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 行为 一致
|
||||
@@ -18,13 +18,28 @@ Advanced Paste is a PowerToys module that provides enhanced clipboard pasting wi
|
||||
|
||||
TODO: Add implementation details
|
||||
|
||||
### Paste with AI Preview
|
||||
|
||||
The "Show preview" setting (`ShowCustomPreview`) controls whether AI-generated results are displayed in a preview window before pasting. **The preview feature does not consume additional AI credits**—the preview displays the same AI response that was already generated, cached locally from a single API call.
|
||||
|
||||
The implementation flow:
|
||||
1. User initiates "Paste with AI" action
|
||||
2. A single AI API call is made via `ExecutePasteFormatAsync`
|
||||
3. The result is cached in `GeneratedResponses`
|
||||
4. If preview is enabled, the cached result is displayed in the preview UI
|
||||
5. User can paste the cached result without any additional API calls
|
||||
|
||||
See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `OptionsViewModel.cs` for the implementation.
|
||||
|
||||
## Debugging
|
||||
|
||||
TODO: Add debugging information
|
||||
|
||||
## Settings
|
||||
|
||||
TODO: Add settings documentation
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `ShowCustomPreview` | When enabled, shows AI-generated results in a preview window before pasting. Does not affect AI credit consumption. |
|
||||
|
||||
## Future Improvements
|
||||
|
||||
|
||||
39
docs/alwaysontop-context-menu.md
Normal file
39
docs/alwaysontop-context-menu.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Always On Top – System Menu Injection & Handling
|
||||
|
||||
## Overview
|
||||
Adds “Always on top” to a window’s system menu and keeps the menu state in sync with the module’s 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 isn’t 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.
|
||||
@@ -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?>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4121,7 +4121,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<value>Advanced AI</value>
|
||||
</data>
|
||||
<data name="Oobe_AdvancedPaste.Description" xml:space="preserve">
|
||||
<value>Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, markdown, or json directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an AI powered option that is 100% opt-in and requires an Open AI key. Note: this will replace the formatted text in your clipboard with the selected format.</value>
|
||||
<value>Advanced Paste is a tool to put your clipboard content into any format you need, focused towards developer workflows. It can paste as plain text, Markdown, or JSON directly with the UX or with a direct keystroke invoke. These are fully locally executed. In addition, it has an opt-in AI feature that can use an online or local language model endpoint. Note: this will replace the formatted text in your clipboard with the selected format.</value>
|
||||
</data>
|
||||
<data name="Oobe_AdvancedPaste.Title" xml:space="preserve">
|
||||
<value>Advanced Paste</value>
|
||||
|
||||
Reference in New Issue
Block a user