Skip to content

Add Chrome Classic Menu Bar mod - #5024

Open
GokhanGerkz wants to merge 5 commits into
ramensoftware:mainfrom
GokhanGerkz:main
Open

Add Chrome Classic Menu Bar mod#5024
GokhanGerkz wants to merge 5 commits into
ramensoftware:mainfrom
GokhanGerkz:main

Conversation

@GokhanGerkz

Copy link
Copy Markdown

Changelog

If this pull request updates an existing mod, describe the changes below:

Changelog

Initial release.

Features:

  • Native File, Edit, View, History, Bookmarks, Tools and Help menus.
  • Optional dark menu mode.
  • High DPI support.
  • Automatic attachment to Chrome windows.

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@GokhanGerkz

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Nice idea, and the owner-drawn dark menu looks well put together. There are, however, several issues that affect the rest of the system, the process lifetime and mod reloads.

1. The global hotkeys hijack Ctrl+Shift+E / Ctrl+Alt+S / Ctrl+Alt+A for the whole desktop.
RegisterHotKey (lines 1067-1092) registers system-wide hotkeys: while Chrome runs, no other application on the desktop can receive those combinations any more (Ctrl+Shift+E is "search in files" in VS Code, for example). The handler even throws the hotkey away when Chrome isn't the foreground window (lines 968-971) — the keystroke is still swallowed. These also aren't real Chrome shortcuts; the mod invents them and then advertises them in the menu labels. A mod shouldn't take over input outside its target process. Please remove them — the menu items already invoke the same three commands — or at minimum make them opt-in and configurable through mod settings. Removing them also removes the need for the entire hotkey window / worker thread / window class (items 4 and 5 below).

2. Synthesized keystrokes can land in a different application.
OpenChromePageWorker (lines 420-454) runs on a background thread and performs a ~800 ms scripted sequence: SetForegroundWindowCtrl+TCtrl+A → type the URL → Enter, with fixed Sleeps in between. SetForegroundWindow can fail (foreground lock), and the user can click or Alt+Tab away at any point during that second — SendInput then delivers the whole sequence to whatever window has focus. Ctrl+A + typed text + Enter in someone's editor or a web form is destructive. RunCommand has the same exposure with Ctrl+W and Ctrl+Shift+Delete.

Concretely:

  • Bookmark Manager has a real Chrome shortcut — Ctrl+Shift+O. Use SendKey(L'O', true, true) instead of typing chrome://bookmarks/.
  • For the remaining chrome:// pages, re-check GetForegroundWindow() == hwnd immediately before every SendInput batch and abort the sequence if it no longer matches, instead of relying on Sleep timing. Worth testing whether launching them via the browser's own command line (chrome.exe chrome://settings/) works in your environment — it would remove the input synthesis for these entirely.

3. Deadlock on unload: cross-thread SendMessage while holding the state lock.
DetachAllMenus (lines 1134-1160) holds g_stateLock exclusively while calling SetMenu, DrawMenuBar, SetWindowPos(..., SWP_FRAMECHANGED) and WindhawkUtils::RemoveWindowSubclassFromAnyThread — the last two send messages synchronously to the window's owning thread. Wh_ModUninit runs on an arbitrary thread, so this is a genuine cross-thread send. Meanwhile ChromeSubclassProc's WM_NCDESTROY (lines 816-827) acquires the same lock on the Chrome UI thread. If a window is being destroyed at that moment: the UI thread waits for the lock, the uninit thread waits for the UI thread to dispatch the message, and Chrome hangs.

Copy the state out under the lock, release it, then do the window work:

std::vector<std::pair<HWND, WindowState>> windows;
{
    AcquireSRWLockExclusive(&g_stateLock);
    windows.assign(std::make_move_iterator(g_windows.begin()),
                   std::make_move_iterator(g_windows.end()));
    g_windows.clear();
    ReleaseSRWLockExclusive(&g_stateLock);
}
for (auto& [hwnd, state] : windows) { /* SetMenu / RemoveWindowSubclassFromAnyThread / delete */ }

RedrawAllMenus (lines 503-522) has the same shape — RedrawWindow(..., RDW_UPDATENOW) under a shared lock — and should be restructured the same way. See the note about this in the development tips.

4. Worker-thread teardown isn't reliable, and the DLL can be unloaded while mod code is running.
Several problems in Wh_ModUninit (lines 1203-1224):

  • WaitForSingleObject(..., 3000) gives up after 3 seconds and lets the mod unload anyway. If the thread is still inside mod code at that point, the process crashes when the DLL is unmapped. Signal reliably and wait without a timeout.
  • The wait will time out in a plausible case: if Wh_ModUninit runs before MenuWorkerThread has assigned g_hotkeyWindow, the PostMessageW is skipped (g_hotkeyWindow is still nullptr), the thread then enters GetMessageW and blocks forever — g_stopWorker is only re-checked after a message arrives (line 1097). Use PostThreadMessageW(threadId, WM_QUIT, 0, 0) with the thread ID captured from CreateThread, which doesn't depend on the window existing yet.
  • g_hotkeyWindow is written on the worker thread and read from Wh_ModUninit without synchronization.
  • The OpenChromePageWorker threads are fire-and-forget (CloseHandle right after creation, line 773) and sleep for ~800 ms inside mod code. Nothing waits for them on unload, so one of them can be executing SendKey/TypeUnicode after the DLL is gone. Track them (or replace the whole sequence per item 2) and wait for them in Wh_ModUninit.

5. The hotkey window class survives the mod, leaving a dangling lpfnWndProc.
RegisterClassExW (line 1048) is unchecked, and UnregisterClassW only runs on the normal exit path of MenuWorkerThread (line 1109) — the early return 0 when CreateWindowExW fails (lines 1061-1065) skips it, as does the 3-second-timeout case above. A registered class isn't removed when the mod DLL unloads, so its lpfnWndProc then points into unmapped memory. On the next load RegisterClassExW fails with ERROR_CLASS_ALREADY_EXISTS, but CreateWindowExW still succeeds against the stale class, and messages get dispatched into freed memory. Register/unregister deterministically, and use the mod's own module handle rather than GetModuleHandleW(nullptr) (which is chrome.exe) — see mods/saved-run-commands.wh.cpp#L1985-L1994 for obtaining it and #L2035-L2041 for unregistering it in Wh_ModUninit.

6. The mod loads into every Chrome subprocess.
@include chrome.exe matches the renderer, GPU, network and utility processes too. Wh_ModInit installs both hooks and starts the 250 ms EnumWindows watcher thread (line 1193) in all of them — on a busy browser that's a dozen-plus processes each enumerating every top-level window on the desktop four times a second. Bail out early for subprocesses, the same way the mod you adapted the frame code from does it (mods/titlebar-for-everyone.wh.cpp#L650-L655):

if (wcsstr(GetCommandLineW(), L"--type=")) {
    return FALSE;
}

7. The two pollers force a frame recalculation on every Chrome window 4-6 times per second, forever.
AttachMenu calls EnableNativeChromeFrame unconditionally on every visit (line 877), and that function always does SetWindowLongPtrW(GWL_STYLE) + SetWindowPos(..., SWP_FRAMECHANGED)SWP_FRAMECHANGED forces WM_NCCALCSIZE and a non-client repaint even when nothing changed. AttachMenu is reached from both the 250 ms watcher thread (line 1034) and the 500 ms WM_TIMER (line 989), for the lifetime of the browser. Make EnableNativeChromeFrame a no-op when the style already has the bits, drop one of the two redundant pollers, and consider relying on the CreateWindowExW hook (plus a SetWinEventHook-based trigger if a retry is genuinely needed) instead of unconditional polling.

8. WM_DRAWITEM / WM_MEASUREITEM are swallowed for every owner-draw item, and itemData is cast blindly.
Lines 793-800 return TRUE unconditionally, so any other owner-draw menu or control on that window — including one added by another mod — is never measured or drawn, and DrawOwnerItem/MeasureOwnerItem reinterpret its itemData as a MenuItemData* and dereference it. Verify the item is one of yours before handling it (for WM_DRAWITEM, (HMENU)dis->hwndItem should be in the window's allMenus; for WM_MEASUREITEM, keep the MenuItemData* pointers in a set and check membership), and fall through to DefSubclassProc otherwise.

9. The same window can be attached twice.
AttachMenu checks g_windows under a shared lock (lines 882-887), then builds the menu, subclasses and calls SetMenu, and only afterwards takes the exclusive lock to emplace (lines 939-944). Three threads call it concurrently: the window's own thread via the CreateWindowExW hook, the watcher thread, and the worker's WM_TIMER. Two of them can both pass the "not attached" check; emplace then silently drops the second entry, so its HMENU and all its MenuItemData allocations leak, and g_windows tracks a different menu than the one actually on the window — which DetachAllMenus and WM_NCDESTROY then clean up incorrectly. Do the lookup and the insert under a single exclusive lock (e.g. insert a placeholder entry first, and bail out if one already exists).

10. WM_APP + 100 is posted to a window the mod doesn't own, carrying a heap pointer.
WM_APP-range values aren't owned by the mod — anything else in the process that posts WM_APP+100 to a Chrome frame will make ChromeSubclassProc delete an arbitrary LPARAM as a ChromePageRequest* (lines 138, 456-472, 760-781). It also isn't needed: OpenChromeUrl is already called from WM_COMMAND on the window's own thread, so it can just call CreateThread directly. If you ever do need cross-thread signalling on a foreign window, allocate the message with RegisterWindowMessage.

11. Drawing is hard-coded to 96 DPI while measuring uses the real DPI.
DrawOwnerItem sets UINT dpi = 96; (line 644), which makes both ScaleForDpi calls below it no-ops, and DrawCheckMark uses raw unscaled pixel offsets — while MeasureOwnerItem scales everything by GetDpiForWindowSafe(hwnd). At 150%/200% the items are sized for the real DPI but the text is inset with 96-DPI padding. On top of that, g_menuFont is a single global rebuilt by whichever window measured last, so two Chrome windows on monitors with different scaling fight over it. Simplest fix: record the DPI used at measure time (e.g. in MenuItemData or in WindowState) and use it in the draw path, and cache the font per DPI instead of recreating it on every WM_MEASUREITEM. SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, ...) is also the DPI-correct way to get lfMenuFont. The README currently advertises "High DPI support".

12. There's no settings block; the only option lives inside the menu and isn't persisted.
Dark mode is toggled through a View menu item, is per-process, and is lost when Chrome restarts. That belongs in ==WindhawkModSettings== (darkMode: true, read with Wh_GetIntSetting(L"darkMode")) with a Wh_ModSettingsChanged that re-applies it, which is also where the hotkey configuration from item 1 would go. Defaulting to the system app theme (AppsUseLightTheme) rather than always-dark would be a better default.

13. Overlap with the existing native-frame mods.
The frame half of this mod (the CreateWindowExW WS_EX_NOREDIRECTIONBITMAP strip, the SetWindowThemeAttribute/WTA_NONCLIENT hook, and the WM_NCCALCSIZE/WM_NCHITTEST/WM_NCLBUTTONDOWNDefWindowProcW subclass) is the same technique as Titlebar For Everyone — which you credit — and Native titlebars in Chromium-based browsers. Users running one of those alongside this mod will get the frame handling applied twice. Please state in the README how this mod relates to them, and consider making the frame restoration an option that can be turned off so the mod can be used purely for the menu bar.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Use WindhawkUtils::SetFunctionHook() instead of Wh_SetFunctionHook with reinterpret_cast<void*> (lines 1177-1191); it's type-safe and much shorter.
  • #include <algorithm> is unused (the min/max comparisons are written out by hand in MeasureOwnerItem).
  • g_darkSeparatorBrush is created and destroyed but never used — the separator is drawn with CreatePen(..., kDarkSeparator).
  • MenuItemData is managed with raw new/delete across four cleanup sites (lines 914, 927, 820, 1153). A std::deque<MenuItemData> (stable addresses) or std::vector<std::unique_ptr<MenuItemData>> inside WindowState would remove all of them and the leak paths in item 9.
  • After the message loop exits (line 1102), the window has already been destroyed by WM_CLOSE/WM_DESTROY, so the KillTimer/UnregisterHotKey/DestroyWindow calls operate on an invalid handle. Harmless, but the cleanup would be clearer in the WM_DESTROY handler.
  • Wh_Log(L"Initializing Chrome Classic Menu Bar v1.0.0") — Windhawk already prefixes the mod name in the log, and WH_MOD_VERSION is available if you want the version without having to update the string on every release. Same for the Chrome Classic Menu Bar v1.0.0 header at the top of the README.
  • wcsncmp(className, L"Chrome_WidgetWin_", 17) repeats a magic length in two places; std::wstring_view(className).starts_with(L"Chrome_WidgetWin_") avoids it.
  • EnableNativeChromeFrame ORs in WS_CAPTION | WS_SYSMENU but nothing restores the original style on unload. DetachAllMenus does trigger a frame change so Chromium should reclaim the frame, but saving the original style per window and restoring it would make the mod fully self-reverting.
  • No @license. Since part of the code is adapted from another mod, it'd be good to declare one (and, since Titlebar For Everyone declares none, to check with Ingan121 that they're fine with the reuse).

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • The README has no screenshot. This is a very visual mod — a picture of the menu bar (light and dark) would help a lot. Images must be hosted on i.imgur.com or raw.githubusercontent.com.
  • Mnemonics won't work. For MF_OWNERDRAW items the system can't read the item text, so Alt+F etc. do nothing unless the window handles WM_MENUCHAR and resolves the mnemonic itself. On top of that, DT_HIDEPREFIX is applied unconditionally (line 662), so the underlines never appear even after the user presses Alt — the usual approach is to hide them only when ODS_NOACCEL is set.
  • The class-name + WS_CAPTION test also matches non-browser Chrome_WidgetWin_* top-level windows — undocked DevTools, the Chrome task manager, PWA/app windows, the profile picker. Those will get a File/Edit/View menu bar whose items mostly do the wrong thing there. Worth narrowing the detection.
  • The Edit menu items (Undo/Redo/Cut/Copy/Paste/Select All) only do anything when an editable element has focus in the page, since they're plain keystrokes. That's inherent to the approach, but it may look broken to users.
  • File → Exit posts WM_CLOSE to the current window only; Chrome's own Exit closes the whole browser.
  • In DrawOwnerItem, the accelerator text is drawn DT_RIGHT into the same rect as the label, so a long label and its shortcut will overlap rather than clip. Also, DrawFocusRect on menu items (line 682) isn't part of the standard Windows menu look.
  • The accelerators shown next to Extensions / Settings / About (Ctrl+Shift+E, Ctrl+Alt+S, Ctrl+Alt+A) aren't Chrome shortcuts — they only exist because of the global hotkeys in item 1. If those go away, the labels should too.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@GokhanGerkz

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Nice idea, and the frame integration is credited properly. The main problems are around process scope, continuous polling, and unload safety — several of them can hang or crash Chrome.

1. The mod runs in every chrome.exe process, including renderers/GPU/utility

Wh_ModInit unconditionally calls StartAttachWatcherIfNeeded(), so a polling thread is spun up in every Chromium child process — none of which will ever own a browser frame. Bail out early instead, the way Titlebar For Everyone does (titlebar-for-everyone.wh.cpp#L650-L655):

if (wcsstr(GetCommandLineW(), L"--type=") != nullptr) {
    Wh_Log(L"Auxiliary process detected, skipping");
    return FALSE;
}

Returning FALSE is fine here — Windhawk just re-runs Wh_ModInit after each settings change.

2. Two polling loops, plus a SWP_FRAMECHANGED storm on every Chrome window

AttachWatcherThread calls EnumWindows every 250 ms, and the hotkey window's ATTACH_TIMER_ID timer calls it again every 500 ms — both enumerating every top-level window on the desktop (with a cross-process GetClassNameW on each). Worse, every pass reaches AttachMenu, which calls EnableNativeChromeFrame(hwnd) before the alreadyAttached early-out, so each already-attached window gets SetWindowLongPtrW + SetWindowPos(..., SWP_FRAMECHANGED) roughly 6 times per second, forever. That's a continuous non-client recalculation/repaint cycle for the whole life of the browser.

At minimum, make EnableNativeChromeFrame idempotent (skip the SetWindowPos when the style bits are already set) and move it after the alreadyAttached check. Better: drop the polling entirely — you already hook CreateWindowExW; back it up with a bounded retry (e.g. a few WM_TIMER ticks that stop once a window is attached) or a SetWinEventHook(EVENT_OBJECT_SHOW) scoped to the current process, rather than an unbounded 250 ms sweep.

3. Deadlock on unload: cross-thread sends while holding g_stateLock

DetachAllMenus holds the SRW lock exclusively while calling SetMenu, DrawMenuBar, SetWindowPos and WindhawkUtils::RemoveWindowSubclassFromAnyThread — the last of which is implemented as a SendMessage to the window's owning thread (windhawk_utils.h), and SetWindowPos sends WM_NCCALCSIZE/WM_WINDOWPOSCHANGING synchronously too. Meanwhile ChromeSubclassProc's WM_NCDESTROY case acquires the same lock exclusively. If a Chrome window is being destroyed while Wh_ModUninit runs, thread A holds the lock and blocks in SendMessage, the UI thread blocks on the lock, and the process hangs.

Copy the state out, release the lock, then do the window work:

std::unordered_map<HWND, WindowState> windows;
{
    AcquireSRWLockExclusive(&g_stateLock);
    windows.swap(g_windows);
    ReleaseSRWLockExclusive(&g_stateLock);
}
for (auto& [hwnd, state] : windows) {
    if (IsWindow(hwnd)) {
        SetMenu(hwnd, nullptr);
        ...
        WindhawkUtils::RemoveWindowSubclassFromAnyThread(hwnd, ChromeSubclassProc);
    }
    ...
}

RedrawAllMenus has the same shape — it holds the shared lock across DrawMenuBar and RedrawWindow(..., RDW_UPDATENOW), which synchronously dispatch paint messages. Copy the HWND/HMENU list out first there too.

4. Worker threads can outlive the mod DLL

Three separate paths let a thread keep running code in an unmapped DLL after unload:

  • OpenChromePageWorker threads are fire-and-forget (CloseHandle immediately, never tracked). Each one sleeps for ~800 ms total. Disabling the mod during that window means the thread returns into freed memory → crash. Track the handle (or a count) and wait for it in Wh_ModUninit.
  • WaitForSingleObject(g_workerThread, 3000) / WaitForSingleObject(g_attachWatcherThread, 3000) — on timeout the code closes the handle and unloads anyway. A join with a timeout is not a join; use INFINITE.
  • g_hotkeyWindow is written by MenuWorkerThread but read unsynchronized in Wh_ModUninit. If uninit runs before the window exists, the WM_CLOSE is never posted, the worker blocks in GetMessageW forever, the 3 s wait expires, and the mod unloads with the thread live. Signal the worker with PostThreadMessage on a stored thread id (or set an event the thread also waits on) instead of relying on a window handle that may not exist yet.

See https://github.com/ramensoftware/windhawk/wiki/Global-objects-and-process-shutdown for the teardown rules around worker threads and globals (#1-worker-thread-stdthread).

5. The hotkey window class is registered with the wrong HINSTANCE and isn't always unregistered

wc.hInstance = GetModuleHandleW(nullptr);   // this is chrome.exe, not the mod
RegisterClassExW(&wc);                      // result ignored
...
if (!g_hotkeyWindow) { return 0; }           // class never unregistered on this path

A window class isn't removed when the mod DLL unloads, and lpfnWndProc points into the mod image. So after the leak path above (or any unload where the worker didn't reach its cleanup), the next load's RegisterClassExW fails silently and CreateWindowExW reuses the stale class → messages dispatched to a dangling WndProc. Use the mod's own module handle, check the registration result, and unregister on every exit path. See audio-scroll-switcher.wh.cpp#L125-L134 for GetCurrentModuleHandle(), and autoscroll-win32.wh.cpp#L1007-L1013 for the "don't fall back on ERROR_CLASS_ALREADY_EXISTS" note.

6. RegisterHotKey takes Ctrl+Shift+E / Ctrl+Alt+S / Ctrl+Alt+A away from the whole desktop

RegisterHotKey is system-wide, not per-process. Once Chrome is running, those three combos are swallowed in every application — HotkeyWindowProc returns 0 when Chrome isn't foreground, so the keystroke is consumed and never reaches the app the user was actually typing in. Ctrl+Shift+E in particular is a real shortcut in a lot of editors and Office apps.

None of these are Chrome shortcuts to begin with, and the menu items work without them. I'd drop the hotkey window entirely; if you want to keep them, make them opt-in settings, off by default.

7. Race in AttachMenu between the two pollers and the CreateWindowExW hook

The alreadyAttached check runs under a shared lock, the lock is released, the menu is built and SetMenu is called, and only then is the exclusive lock taken for g_windows.emplace(...). Two threads (attach watcher, hotkey timer, or the CreateWindowExW hook) can both see "not attached" for the same HWND: two menu bars get built, both SetMenu calls run, and emplace silently drops the second insert (its return value is ignored). Result: one HMENU plus its whole MenuItemData array leaks, and the map can end up tracking a menu the window is no longer using — so DetachAllMenus destroys the wrong one and the live one leaks too.

Hold the exclusive lock across the check and the insert, or insert a placeholder entry under the exclusive lock first so the second thread sees it and bails.

8. WM_APP + 100 is posted to a window the mod doesn't own, with a raw heap pointer

WM_APP-range values belong to the application (Chromium) that owns the window, so the value can collide with something Chromium — or another mod — already uses on Chrome_WidgetWin_1. And the pointer leaks whenever the message isn't dispatched: window destroyed before it's pumped, or the mod unloaded so the subclass is gone and Chrome's own WndProc gets it.

Since OpenChromeUrl already runs on a thread that can call CreateThread directly, the whole post/handler round trip looks unnecessary — just create the thread inline. If you do need a message, use RegisterWindowMessage(L"..."), which returns a process-wide-unique value that can't collide.

9. The synthesized keystrokes go wherever focus happens to be

OpenChromePageWorker does Sleep(150)SetForegroundWindowSleep(100)Ctrl+TSleep(300)Ctrl+A → type the URL → Sleep(150)Enter. SetForegroundWindow frequently fails outright (foreground lock), and even when it succeeds anything can take focus during the ~800 ms sequence — at which point chrome://settings/ and a Return are typed into an unrelated window. That's a genuine safety problem, not just a reliability one.

Prefer handing the URL to the browser instead of typing it:

wchar_t exePath[MAX_PATH];
GetModuleFileNameW(nullptr, exePath, ARRAYSIZE(exePath));
std::wstring cmd = L"\"" + std::wstring(exePath) + L"\" \"" + url + L"\"";
STARTUPINFOW si{sizeof(si)};
PROCESS_INFORMATION pi{};
if (CreateProcessW(nullptr, cmd.data(), nullptr, nullptr, FALSE, 0, nullptr,
                   nullptr, &si, &pi)) { ... }

Chrome forwards the URL to the running instance and opens it in a tab, with no focus dependency and no sleeps. If you keep SendInput for the shortcut-based commands, at least re-check that the target is still foreground immediately before each batch, and release any modifier the user is physically holding (GetAsyncKeyState(VK_MENU) etc.) — otherwise invoking an item right after Alt+F turns Ctrl+T into Ctrl+Alt+T.

10. No settings block at all

The mod has no ==WindhawkModSettings==, so nothing is configurable from the Windhawk UI, and the dark-mode state lives only in a process-global std::atomic toggled from a menu item — it resets every time Chrome restarts. Expose it (and, if you keep them, the hotkeys and the frame handling) as real settings and read them in Wh_ModInit/Wh_ModSettingsChanged:

// ==WindhawkModSettings==
/*
- darkMenu: true
  $name: Dark menu mode
  $description: Use a dark theme for the menu bar and its popups
*/
// ==/WindhawkModSettings==

11. Overlaps and conflicts with Titlebar For Everyone

The README credits it, and the frame code really is the same approach: strip WS_EX_NOREDIRECTIONBITMAP in CreateWindowExW, swallow WTA_NONCLIENT in SetWindowThemeAttribute, force WS_CAPTION | WS_SYSMENU, and route WM_NCCALCSIZE/WM_NCHITTEST/WM_NCLBUTTONDOWN/WM_NCPAINT/WM_NCACTIVATE to DefWindowProcW (titlebar-for-everyone.wh.cpp#L203-L240). Titlebar For Everyone also includes chrome.exe, so both mods will be subclassing the same window and fighting over the same messages for a lot of users.

I understand the menu bar needs the native frame to be visible at all, so the duplication isn't gratuitous — but please make the frame handling a setting that can be turned off, so users who already run Titlebar For Everyone can let that mod own the frame, and say in the README how the two interact.

12. Add a screenshot to the README

The whole point of the mod is a visible menu bar, and there's no image. Please add a screenshot (or GIF) showing both the light and dark menu — allowed hosts are i.imgur.com and raw.githubusercontent.com.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Use WindhawkUtils::SetFunctionHook() instead of Wh_SetFunctionHook with reinterpret_cast<void*> — it's type-safe and catches signature mismatches at compile time.
  • WM_MEASUREITEM and WM_DRAWITEM unconditionally return TRUE, even when the handler bailed out (CtlType != ODT_MENU, null itemData). Return TRUE only when the item was actually handled and fall through to DefSubclassProc otherwise, so owner-draw items that aren't yours still get processed. While you're there, it'd be worth verifying that itemData actually points at one of this mod's items (e.g. dis->hwndItem is one of the tracked HMENUs) before reinterpret_cast-ing it — right now a foreign owner-draw item would be dereferenced as a MenuItemData*.
  • g_darkSeparatorBrush is created, null-checked (and fails init if creation fails) and deleted, but never used anywhere — separators are drawn with a CreatePen from kDarkSeparator. Dead resource, drop it.
  • EnsureMenuFont deletes and recreates the font on every WM_MEASUREITEM, i.e. once per menu item per menu open. Cache it keyed by DPI and only rebuild when the DPI changes.
  • IsChromeBrowserFrame does a cross-process GetClassNameW on every top-level window on the desktop before anything else; IsMainChromeWindow only checks the PID afterwards. Filtering by GetCurrentProcessId() first would make each sweep much cheaper.
  • Wh_ModInit and Wh_ModAfterInit both call EnumWindows(EnumWindowsProc, 0), and the watcher thread starts sweeping immediately as well — three passes for the same job at startup.
  • MenuItemData is managed with raw new/delete across three cleanup paths. Storing the items in a std::deque<MenuItemData> inside WindowState (stable addresses, freed with the state) would remove all of them.
  • #include <algorithm> doesn't appear to be used. -luser32 is also likely redundant (most mods don't list it).
  • CMD_TOOLS_DOWNLOADS is defined inside the CMD_HISTORY_* block (so it's 10305), which reads oddly next to CMD_TOOLS_EXTENSIONS = 10501.
  • The mod name is used in log messages (L"Initializing Chrome Classic Menu Bar v1.0.0") — Windhawk already prefixes the mod name, so a plain Wh_Log(L"Init") is enough. Same for the hardcoded version string, which will silently go stale; WH_MOD_VERSION is available if you want it.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • DPI scaling is only half applied. MeasureOwnerItem uses the real per-window DPI, but DrawOwnerItem hardcodes UINT dpi = 96; before calling ScaleForDpi, making those two calls no-ops. So at 150%/200% the items are measured wide but the text is drawn with 96-DPI padding — the label and the shortcut column won't line up. Pass the DPI in (or derive it from dis->hwndItem's owner window).
  • The menu font may be double-scaled. SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ...) already returns metrics for the process's DPI context, and then lfMenuFont.lfHeight is multiplied by dpi / 96 again. SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0, dpi) gives you the right values directly — see classic-min-max-animations.wh.cpp#L740.
  • The Ctrl+Shift+E, Ctrl+Alt+S and Ctrl+Alt+A accelerators shown next to Extensions/Settings/About aren't Chrome shortcuts — they only exist because this mod registers them. If the hotkeys go away (item 6), those labels should go too.
  • CMD_FILE_EXIT posts WM_CLOSE to the current window, which closes that window rather than exiting the browser. "Exit" in Chrome's own menu closes all windows.
  • Disabling the mod removes the menu but leaves WS_CAPTION | WS_SYSMENU on the window, so Chrome keeps a half-native frame until it's restarted. Titlebar For Everyone has the same limitation, so this may be acceptable — worth a line in the README either way.
  • Driving every command through synthesized keystrokes means anything that changes Chrome's key handling (a page grabbing the key, a user-remapped shortcut, an extension) silently changes what the menu item does. There isn't a clean alternative for most of these since Chromium doesn't expose its commands to the Win32 layer — just noting it as a known limitation of the approach.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@GokhanGerkz

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Nice idea, and the lifecycle handling (subclass removal, per-window state, no locks held across SendMessage) is mostly right. The main problems are window targeting, the frame-ownership setting not actually being honored, and unsafe owner-draw message handling.

1. The menu is attached to any captioned Chrome_WidgetWin_* window, not just browser windows.

IsChromeBrowserFrame / CreateWindowExW_Hook match the class prefix plus WS_CAPTION. Chrome uses Chrome_WidgetWin_1 for a lot more than browser frames: the Task Manager, the profile picker, undocked DevTools, --app= / installed-PWA windows, and various captioned dialogs. All of those will get a File/Edit/View/History/Bookmarks/Tools/Help menu bar whose commands mostly don't apply to them (and the prefix match also accepts Chrome_WidgetWin_0).

At minimum compare the class name exactly and exclude owned windows, as chrome-pip-alt-tab-window-hider does:

if (wcscmp(className, L"Chrome_WidgetWin_1") != 0) return false;
if (GetWindow(hwnd, GW_OWNER) != nullptr) return false;

That still won't separate the Task Manager / profile picker from a real browser window, so please check those cases explicitly and add whatever further filter works (e.g. requiring a Chrome_RenderWidgetHostHWND descendant). See also chrome-wheel-scroll-tabs.wh.cpp#L369.

2. "Manage Chrome native frame" doesn't actually stop the mod from owning the frame.

The README tells users to disable this setting when Titlebar For Everyone is enabled, but the subclass routes the frame messages to DefWindowProcW unconditionally:

case WM_NCCALCSIZE:
case WM_NCHITTEST:
case WM_NCLBUTTONDOWN:
    return DefWindowProcW(hwnd, msg, wParam, lParam);

That is exactly what "owning the frame" means here, so with the setting off the two mods still fight over the non-client area. Gate these on g_manageNativeFrame.load() (and fall through to DefSubclassProc otherwise).

Two related points in the same area:

  • Returning DefWindowProcW also skips the rest of the subclass chain, so any other mod subclassing the same window never sees these messages. Unavoidable to a degree given what the mod does, but worth being deliberate about.
  • EnableNativeChromeFrame can never add WS_CAPTION: it early-returns unless IsChromeBrowserFrame(hwnd) is true, and that function already requires WS_CAPTION. In practice it can only ever add WS_SYSMENU. Either drop it or fix the condition.

3. WM_DRAWITEM / WM_MEASUREITEM are swallowed for items that aren't yours, and itemData is cast blindly.

case WM_MEASUREITEM:
    MeasureOwnerItem(hwnd, ...);
    return TRUE;
case WM_DRAWITEM:
    DrawOwnerItem(...);
    return TRUE;

Both handlers reinterpret_cast<MenuItemData*>(dis->itemData) for any owner-draw item that reaches this window, then read data->text as a std::wstring. If another mod (or a future Chrome build) puts an owner-draw item or owner-draw control on this window, that is a read of foreign memory interpreted as a std::wstring → crash. And even in the early-return cases (CtlType != ODT_MENU, null itemData) the message is still consumed with return TRUE, so a legitimate owner-draw control on that window is never measured or drawn.

You already keep every allocation in WindowState::itemData — check membership before casting, and break (fall through to DefSubclassProc) when the item isn't yours.

4. DPI handling is inconsistent between measure and draw.

  • DrawOwnerItem hardcodes UINT dpi = 96; (line 613), so ScaleForDpi(leftPadding, dpi) and ScaleForDpi(rightPadding, dpi) are no-ops, while MeasureOwnerItem sizes items with the real GetDpiForWindowSafe(hwnd). At 125%+ the padding no longer matches the measured width/height, so text and the shortcut column drift out of alignment. DRAWITEMSTRUCT::hwndItem is the HMENU for menu items, so cache the DPI you used in the measure pass (per window, or in MenuItemData) and use it when drawing.

  • EnsureMenuFont takes ncm.lfMenuFont from SystemParametersInfoW(SPI_GETNONCLIENTMETRICS, ...) and then multiplies lfHeight by dpi/96. Chrome is per-monitor-v2 aware, so those metrics already come back scaled for the thread's DPI context and this double-scales the font. Use SystemParametersInfoForDpi(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0, dpi) instead — see win95-nt4-menubars.wh.cpp#L112.

  • DT_HIDEPREFIX is applied unconditionally, so mnemonic underlines never appear even when the user presses Alt. The standard pattern is to apply it only when the system asks for it — dark-menus.wh.cpp#L250:

    if (dis->itemState & ODS_NOACCEL) flags |= DT_HIDEPREFIX;

5. Overlap with existing mods — consider dropping the owner-draw theming and the frame management.

Both non-menu-bar halves of this mod duplicate mods that are already in the catalog:

  • Dark menus: dark-menus is @include * and dark-themes every Win32 menu in the process via uxtheme's SetPreferredAppMode/FlushMenuThemes — including a SetMenu menu bar.
  • Native frame: titlebar-for-everyone already does this for Chrome, and the README says this mod's frame code is adapted from it.

If the menu were built with plain MF_STRING items instead of MF_OWNERDRAW, roughly 250 lines of drawing/measuring code would go away and the system would give you correct metrics, DPI scaling, shortcut-column alignment and working mnemonics for free — items 3, 4 and the mnemonic note below all disappear with it. The maintainer's general preference is to not duplicate functionality that an existing mod already provides, so please explain in the README/PR why the built-in theming and frame handling are needed rather than deferring to those mods, or drop them.

6. Frame changes aren't reverted when the mod is disabled.

DetachAllMenus removes the menu and the subclass, but nothing forces a frame recalculation after the subclass is gone (the SetMenu/DrawMenuBar recalc still runs through the mod's WM_NCCALCSIZE override). Chrome windows are left with the mod-imposed non-client layout until they're resized or the browser is restarted. Titlebar For Everyone handles this by re-applying the frame after unsubclassing — titlebar-for-everyone.wh.cpp#L399-L410:

WindhawkUtils::RemoveWindowSubclassFromAnyThread(hWnd, SubclassProc);
SetWindowPos(hWnd, NULL, 0, 0, 0, 0,
             SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
                 SWP_NOOWNERZORDER | SWP_NOACTIVATE);

7. OpenChromeUrl launches a fresh chrome.exe without the current instance's profile switches.

Bookmark Manager / Extensions / Settings / About all go through CreateProcessW("<chrome.exe>" "chrome://...") with no arguments. If the running browser was started with --user-data-dir= or --profile-directory= (custom/portable profiles, or simply a second profile window), the new process won't attach to it — it starts a separate browser on the default profile, or does nothing visible. Forward the relevant switches from GetCommandLineW(), or pick a mechanism that stays inside the current instance.

8. Add a screenshot to the README.

The mod's whole point is a visible UI change; a screenshot (light and dark) makes it much easier to evaluate and to find on windhawk.net. Only i.imgur.com and raw.githubusercontent.com are allowed image hosts.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • Use WindhawkUtils::SetFunctionHook(CreateWindowExW, CreateWindowExW_Hook, &CreateWindowExW_Original) instead of Wh_SetFunctionHook with reinterpret_cast<void*> casts — it's type-safe and much shorter.
  • g_darkSeparatorBrush is created in Wh_ModInit and deleted in Wh_ModUninit but never used (DrawOwnerItem creates its own pen from kDarkSeparator). Dead code.
  • #include <algorithm> is unused — and the two LONG h1 ... (h1 > h2) ? h1 : h2 blocks in MeasureOwnerItem would read better as std::max, in which case the include earns its place.
  • EnsureMenuFont deletes and re-creates the global HFONT on every WM_MEASUREITEM, i.e. once per item every time a menu opens. Cache it and only rebuild when the DPI actually changes. It's also written from the measure path and read from the draw path with no synchronization; that's fine today because Chrome's browser windows share one UI thread, but caching removes the question.
  • The EnumWindows(EnumWindowsProc, 0) in Wh_ModInit is redundant: Wh_ModInit runs before the process starts executing (so there are no windows), and Wh_ModAfterInit already covers the "enabled while Chrome is running" case.
  • In WM_NCDESTROY, DestroyMenu(state.menu) runs while the menu is still attached to the window being destroyed, and DestroyWindow destroys the window's menu itself afterwards. Call SetMenu(hwnd, nullptr) first, or just free itemData and let the window destroy the menu.
  • Wh_Log(L"Initializing Chrome Classic Menu Bar v1.0.0") — Windhawk already prefixes the mod name, and WH_MOD_VERSION avoids the hardcoded version drifting from the metadata.
  • MenuItemData is managed with raw new/delete and four near-identical cleanup blocks in AttachMenu. A std::vector<std::unique_ptr<MenuItemData>> (or a std::deque<MenuItemData> that hands out stable pointers) would remove all of them.
  • IsMainChromeWindow only adds a process-ID check on top of IsChromeBrowserFrame; the name suggests it identifies the main window.
  • SendKey doesn't set KEYEVENTF_EXTENDEDKEY for the extended keys it sends (VK_LEFT, VK_RIGHT, VK_HOME, VK_DELETE), so the synthesized scan codes are the numpad ones. Chrome keys its accelerators off the virtual-key code so it works in practice, but the flag is cheap correctness.
  • DrawCheckMark's +7 / +4 / +11 / -5 offsets aren't DPI-scaled, so the check mark shrinks relative to the item at high DPI.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Keyboard access doesn't work. Owner-draw menu items carry no text as far as USER is concerned, so the & mnemonics in the strings do nothing: Alt+F won't open the File menu, and typing a letter inside an open menu won't select an item. The classic fix is handling WM_MENUCHAR and mapping the character to the item yourself — or, much simpler, using MF_STRING items (see item 5 above).
  • No WM_INITMENUPOPUP. Undo/Redo/Back/Forward/Paste are always enabled even when the action isn't available, and "Show Bookmarks Bar" never reflects its actual state. Handling WM_INITMENUPOPUP and setting MF_CHECKED/MF_GRAYED would make the menu behave like a real one — though for Chrome most of that state isn't reachable from the Win32 side, so it may not be worth it.
  • Some displayed accelerators aren't real Chrome shortcuts: &Extensions\tCtrl+Shift+E, &Settings\tCtrl+Alt+S and &About Google Chrome\tCtrl+Alt+A. Those three commands are implemented via OpenChromeUrl precisely because there's no shortcut, so the text is misleading. (Zoom &In\tCtrl++ also reads oddly.)
  • SendInput for the command dispatch. It injects into the global input queue, so it depends on SetForegroundWindow having succeeded and on the user not physically holding a modifier at that moment. Chrome handles WM_APPCOMMAND, which is in-process and focus-independent, and covers a good part of this menu — APPCOMMAND_BROWSER_BACKWARD/FORWARD/HOME/REFRESH, APPCOMMAND_FIND, APPCOMMAND_COPY/CUT/PASTE/UNDO/REDO, APPCOMMAND_NEW/OPEN/SAVE/PRINT/CLOSE. Worth testing as the primary path with SendInput as the fallback.
  • The "Dark Menu Mode" menu item doesn't persist. CMD_VIEW_DARK_MENU flips g_darkMode at runtime, but the next Wh_ModSettingsChanged (or mod reload) resets it from the darkMenu setting, so the two controls silently disagree. Either persist it with Wh_SetIntValue and seed LoadSettings from that, or drop the menu item and leave it to the Windhawk setting.
  • PaintMenuBarBottomLine grabs a GetWindowDC from inside the WM_NCPAINT handler and ignores the clipping region passed in wParam; it's also called from AttachMenu, which can run on a thread other than the window's. It works, but it's drawing outside the paint contract.
  • No WM_DPICHANGED / WM_SETTINGCHANGE / WM_THEMECHANGED handling — moving a window between monitors of different scaling, or changing the system menu font/colors, won't refresh the menu until it's re-measured.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@Anixx

Anixx commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
Буфер обмена (3) Woks with Edge, possibly will work with Chromium, Supremum. Consider adding them.

@GokhanGerkz

Copy link
Copy Markdown
Author

Thanks for the suggestion! I haven't tested other Chromium-based browsers yet, but I'll definitely look into it.

@Anixx

Anixx commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

On Edge, "About Google Chrome" opens a homepage.
Settings also does not work.

@Anixx

Anixx commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Show bookmarks bar item has no checkbox on it.

@GokhanGerkz

Copy link
Copy Markdown
Author

Thanks for testing and greatly appreciated although I haven't officially tested or added Edge support yet, so those menu items are currently Chrome-specific. I'll keep this in mind when I work on official Edge compatibility.

@Anixx

Anixx commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

On Edge, does not work in newly-opened windows, only in those which were already open when the mod started.

Updated the Chrome Classic Menu Bar mod to support Microsoft Edge and improved descriptions. Enhanced functionality for menu attachment and hotkey handling.
@GokhanGerkz

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 5, 2026
@windhawk-reviewer

Copy link
Copy Markdown

This pull request has already had 3 AI reviews in the last 24 hours, which is the limit, so no review was posted this time.

Comment /ai-review again after 2026-08-06 13:12 UTC (in 21 hours) to get another one.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 5, 2026
@Anixx

Anixx commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Now works in new windows and the settings page opens.

A problem: new windows are always opened in dark theme, and the setting for it was removed. The menu checkbox only affects the current window.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-author The author's turn: request an AI review, or respond to one that was posted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants