mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-04 10:19:40 +01:00
Compare commits
1 Commits
feature/ru
...
dev/vanzue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
920d3a878c |
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 行为 一致
|
||||
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.
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user