Skip to content

New mod: Tray Utility Customizer#4841

Merged
m417z merged 4 commits into
ramensoftware:mainfrom
sb4ssman:add-tray-utility-customizer
Jul 23, 2026
Merged

New mod: Tray Utility Customizer#4841
m417z merged 4 commits into
ramensoftware:mainfrom
sb4ssman:add-tray-utility-customizer

Conversation

@sb4ssman

@sb4ssman sb4ssman commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Adds Tray Utility Customizer v1.1, providing granular per-icon control over the low-frequency Windows 11 tray utilities: Show hidden icons, Emoji and more, touch keyboard, pen menu, virtual touchpad, and the input/language indicator.

The v1.1 rewrite keeps every native control alive and Windows-owned, but gathers their hosts into one reversible group and positions each IconView individually. This handles utilities that share a native host while preserving normal clicks, flyouts, tooltips, hover surfaces, and native sizing.

Highlights:

  • One nestable layout expression: | places groups along the primary axis, , uses the cross axis, and parentheses create alternating nested groups. Missing or hidden utilities collapse automatically; visible unplaced icons are appended so nothing is lost.
  • Native-size icons by default, optional explicit width/height, per-icon X/Y nudges, configurable spacing, primary axis, and cross-axis alignment.
  • Placement in the hidden-icons or Emoji column, a dedicated marker-tracked tray column, around the clock and Show Desktop strip, or at experimental Left/Right-of-Start positions.
  • Reacts to utilities appearing or disappearing, Explorer/taskbar rebuilds, and settings changes. All modified XAML properties and native parentage are restored on unload.
  • Language-neutral Segoe Fluent glyph detection for Emoji and touch keyboard. Pen menu, virtual touchpad, and input indicator currently rely on English accessibility text; this limitation is documented in the readme.
  • Uses the taskbar.dll TrayUI::StartTaskbar lifecycle hook plus a bounded startup/rebuild retry. The earlier IconView constructor hook and SystemTray.dll/Taskbar.View.dll/ExplorerExtensions.dll dependency were removed in response to review.

Screenshots (the mod readme contains the complete gallery):

Native tray before the mod

Default native-size row

Nested row and stack layout

Dedicated tray column

Busy double-height taskbar

Right of Start on a double-height taskbar

Tested live on Windows 11 (primary taskbar): default row, nested row/stack layouts, a full column, borrowed and dedicated tray columns, a busy double-height tray, Right of Start, utility-icon appearance/churn, Explorer/taskbar rebuild, settings reload, and disable → native restoration → re-enable.

Local validation passed with the bundled Windhawk compiler, exit-time destructor audit, embedded/folder readme parity check, and the current upstream PR validator.

Changelog

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

  • v1.1 — Complete per-icon layout rewrite; replaces host-level grid modes with a nestable expression, expands placement and sizing controls, hardens lifecycle/restoration, adds ARM64 support, removes the redundant enabled setting and multi-module IconView hook, and documents detection limitations.

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.

🤖 Generated with Claude Code

@m417z

m417z commented Jul 20, 2026

Copy link
Copy Markdown
Member

Regarding item 1, you can look at how the Taskbar tray system icon tweaks mod handles this. If only English is supported for now, I think it's worth adding it to the readme.

Submission review

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

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


  1. Utility detection keys off English accessibility text, so most utilities won't be found on non-English Windows. FindUtilityElement / MetadataContainsAny search the lowercased concatenation of Name + AutomationId + AutomationProperties::GetName + GetHelpText for English substrings — L"touch keyboard", L"pen menu", L"virtual touchpad", L"input indicator" / L"language bar". Those accessibility names are localized by Windows (e.g. German "Bildschirmtastatur", "Stiftmenü", "Virtuelles Touchpad"), so on a non-English UI these tokens silently won't match and the utility is reported "not found." The two defaults happen to survive — overflow keys off the stable element name NotifyIconStack and emoji off the loanword "emoji" — but everything else quietly won't be gathered. Note that the overflow case already shows the robust approach: keying off a language-neutral element Name. Where a stable Name/AutomationId exists for the other hosts, prefer matching that; otherwise, please at least document the English-only detection limitation in the README so non-English users know why a selected utility isn't picked up.

  2. The enabled option. Does it just disable the mod? Why is it needed?

  3. The IconView::IconView hook: the only reason you use is to apply the layout with retries. Hooking TrayUI::StartTaskbar instead will remove the dependency on SystemTray.dll, Taskbar.View.dll, ExplorerExtensions.dll, simplify the code, and reduce the chances of the mod breaking with updates.

    Also, why is g_retryThread needed as well?

  4. GetTaskbarXamlRoot - add ARM64 support. Use one of the mods, e.g. taskbar-start-button-position, as a reference.

Optional improvements

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

  • GetStringSetting's NULL check is unnecessary. Wh_GetStringSetting never returns NULL (it returns L"" when unset/on error), so value ? value : L"" and the if (value) guard around Wh_FreeStringSetting are redundant. This is a fine helper as-is, but WindhawkUtils::StringSetting (RAII) would remove the manual free entirely.

Functionality notes

Non-critical observations about the feature behavior itself.

  • Every IconView Loaded triggers a full RestoreLayout + rediscover + reapply synchronously, bypassing the 150 ms ScheduleReapply coalescing you already built for visibility changes. During tray churn (icons appearing/disappearing) this repeats the whole discovery/snapshot/apply pass more often than needed. Routing the IconView-Loaded path through ScheduleReapply() too would coalesce bursts into a single reapply. Not a correctness issue (each pass is idempotent, and it mirrors vd-switcher's approach), just wasted work.
  • The experimental Left/Right-of-Start positions re-run PositionStartGroup on every RootGrid LayoutUpdated and write back the repeater margin / Start RenderTransform. The >0.5 px thresholds make it converge, but it does contend with the taskbar's own re-centering (and with other Start-position mods) each layout pass. Already flagged as experimental in the README — just an FYI.

sb4ssman and others added 3 commits July 22, 2026 12:19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sb4ssman
sb4ssman force-pushed the add-tray-utility-customizer branch from 52ba261 to 43d1ac7 Compare July 22, 2026 19:23
@sb4ssman

Copy link
Copy Markdown
Contributor Author

Addressed the review in the v1.1 rewrite: documented the remaining English-only detection cases; removed the redundant enabled setting; replaced the IconView/multi-module hooks with TrayUI::StartTaskbar plus a bounded startup/rebuild retry; and added ARM64 GetTaskbarXamlRoot support. The rewrite was tested live through icon churn, Explorer/taskbar rebuild, and disable/restore/re-enable, and the local compiler, lifecycle audit, README parity check, and upstream validator all pass.

@m417z

m417z commented Jul 22, 2026

Copy link
Copy Markdown
Member

Submission review

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

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

Address 1. I haven't verified 2, see if it's worth addressing.


  1. [[clang::no_destroy]] audit. g_settings and g_columnLease only own std::wstrings — a heap-free-only destructor is already safe at process shutdown, so the attribute is unnecessary noise there. Conversely, for the containers that do hold XAML references (g_hostSnapshots, g_iconSnapshots, g_hostWatchers) the established form is [[clang::no_destroy]] std::optional<std::vector<...>> released with reset() on the controlled-unload path — .clear() releases the elements but keeps the vector's heap buffer alive forever after each unload. The bare attribute is fine as-is on the nullable WinRT handles (g_layoutGrid, g_group, the timers).

  2. Phantom "straggler" targets inflate the group when the MainStack fallback is active. The straggler pass skips only overflowHost (if (host == overflowHost) continue;), but when emoji resolves via the MainStack fallback (mergeMode = Force MainStack, or auto mode with exactly one visible icon — the documented path for non-English systems), MainStack is also a host-leaf item in managedHosts. Its inner IconViews are not in targets (only the whole MainStack element is), so every one of them is appended as a straggler, advancing extraCursor and widening total. The apply loop then hits the leafItem branch and continues, so those straggler targets are never actually placed — the result is a group Width/Height inflated by the sum of the phantom icons plus spacing, i.e. a visible empty gap in the tray in exactly the fallback scenarios the feature exists for. Generalize the skip from "is the overflow host" to "is any host-leaf item's host":

    for (auto const& host : managedHosts) {
        bool leafHost = false;
        for (auto const& item : items) {
            if (item.hostLeaf && item.host == host) {
                leafHost = true;
                break;
            }
        }
        if (leafHost) {
            continue;  // host-leaf; carries no separate icons
        }
        ...

    (The overflow host is itself a host-leaf item, so this replaces the existing special case rather than adding to it.)

Optional improvements

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

  • Retry-thread handle races. StartRetryThread/StopRetryThread manipulate g_retryThread/g_retryStopEvent from several threads with no synchronization: the TrayUI::StartTaskbar hook runs on the taskbar thread, while Wh_ModAfterInit/Wh_ModSettingsChanged/Wh_ModUninit run on Windhawk threads. A settings change that coincides with a taskbar rebuild can have two threads in StopRetryThread at once (double CloseHandle, or one thread waiting on a handle the other just closed) or two in StartRetryThread (leaked event/thread handle, two concurrent retry loops). A small mutex around both functions is enough — the waits inside already pump sent messages, so holding it briefly is safe. Relatedly, g_layoutApplied is a plain bool written on the UI thread and read by the retry thread; make it std::atomic<bool> like g_unloading.
  • Wh_ModSettingsChanged is a single-shot apply. If the XAML root happens to be unavailable at that instant, the reapply is silently skipped until the next rebuild. Calling StartRetryThread() there instead of ApplyLayoutOnWindowThread() would make it as robust as init, and removes one of the two call sites racing on the handles above.
  • The stale-drop path zeroes event tokens instead of revoking them. In the rebuild cleanup at the top of ApplyLayout you still hold strong references (g_layoutGrid, g_startLease.rootGrid), so revoking g_trayLayoutToken and the start lease's LayoutUpdated token there is safe and free; zeroing them leaves the handlers registered on the old tree and relies on the rebuild destroying it promptly. Cheap insurance inside a try/catch.
  • GetStringSetting's NULL check is unnecessary (carried over from the previous review): Wh_GetStringSetting never returns NULL, so value ? value : L"" and the if (value) guard are redundant; WindhawkUtils::StringSetting (RAII) would remove the helper entirely.
  • Unused libraries: nothing in the mod uses version.dll (GetFileVersionInfo/VerQueryValue) or oleaut32 (BSTR/VARIANT), so -lversion and -loleaut32 in @compilerOptions appear droppable.
  • @description omits the input/language indicator, which the readme, settings, and code all support — worth adding for catalog searchability.

Functionality notes

Non-critical observations about the feature behavior itself.

  • The parser accepts unbalanced parentheses"(overflow | emoji" parses successfully, although the Compute comment says unbalanced parentheses are a parse error. Harmless leniency, but the comment and the log-on-parse-error path don't match the actual behavior.

…dening

Required items:
- no_destroy audit: dropped the attribute from g_settings and g_columnLease
  (heap-only), moved g_hostSnapshots / g_iconSnapshots / g_hostWatchers to
  [[clang::no_destroy]] std::optional<std::vector<>> released with reset() on
  the controlled unload path, and left the nullable WinRT handles as-is.
- Straggler discovery now skips any host that backs a host-leaf item rather
  than only the overflow host, so the MainStack fallback no longer appends
  phantom targets that inflate the group and leave an empty tray gap.

Optional items also addressed:
- Retry thread/event handoff is serialized under a lock; g_layoutApplied is
  std::atomic<bool>.
- Wh_ModSettingsChanged goes through the retry path instead of a single-shot
  apply.
- The rebuild cleanup revokes LayoutUpdated tokens instead of zeroing them.
- Removed the redundant NULL check in the string-setting helper.
- Dropped the unused -lversion; -loleaut32 stays because the mod calls
  SysFreeString.
- @description now mentions the input/language indicator.
- The shared layout parser rejects unbalanced parentheses and empty units,
  matching its documented contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sb4ssman

Copy link
Copy Markdown
Contributor Author

Both required items are addressed, and I took all of the optional ones too.

1. [[clang::no_destroy]] audit. Done exactly as described. g_settings
and g_columnLease lost the attribute — they only own std::wstrings, so a
heap-free-only destructor is already safe at shutdown; both carry an inline
// exit-time-safe: heap-only marker so the annotation does not creep back.
g_hostSnapshots, g_iconSnapshots, and g_hostWatchers are now
[[clang::no_destroy]] std::optional<std::vector<...>> constructed with
std::in_place and released with reset() on the controlled-unload path, so
the backing buffers no longer survive an unload. The nullable WinRT handles
(g_layoutGrid, g_group, the timers) keep the bare attribute.

2. Phantom stragglers under the MainStack fallback. Confirmed, and your
diagnosis was exactly right — thank you for catching it. This was a real
visible gap in precisely the non-English fallback path the feature exists for.
Implemented as you suggested: the straggler pass now skips any host that backs
a host-leaf item rather than special-casing the overflow host, which subsumes
the old check.

Optional items — all adopted:

  • Retry-thread handle races. StartRetryThread / StopRetryThread now
    serialize on a lock; the stop path detaches the handles under the lock and
    waits and closes outside it, so a worker dispatching synchronously to the
    taskbar thread cannot deadlock against a stop. g_layoutApplied is
    std::atomic<bool>.

  • Wh_ModSettingsChanged goes through StartRetryThread(true) instead of a
    single-shot apply, so a settings change made while the XAML root is briefly
    unavailable still lands. That also removes one of the two racing call sites.

  • Stale-drop path revokes tokens instead of zeroing them. The rebuild
    cleanup revokes g_trayLayoutToken and the start lease's LayoutUpdated
    token while the strong references are still held, inside try/catch.

  • GetStringSetting no longer has the redundant NULL check. I kept the
    small helper rather than switching to WindhawkUtils::StringSetting, since
    callers want an owning std::wstring that outlives the setting handle.

  • Unused libraries: -lversion is dropped — you were right, nothing uses
    it. -loleaut32 has to stay, though, and I want to flag why in case it is
    useful for other reviews: the mod never touches a BSTR itself, but
    winrt::hresult_error does. Dropping it fails the link with

    ld.lld: error: undefined symbol: SysStringLen
    >>> referenced by winrt::hresult_error::hresult_error(winrt::hresult, ...)
    >>> referenced by winrt::hresult_error::message() const
    ld.lld: error: undefined symbol: SysFreeString
    

    So any mod that catches winrt::hresult_error needs it transitively. A
    syntax-only check misses this; I now link a temporary DLL with the declared
    @compilerOptions as part of my local gate.

  • @description now names the input/language indicator.

  • Parser leniency: the shared layout parser now rejects unbalanced
    parentheses and empty or trailing units, so the behavior matches the Compute
    comment and the log-on-parse-error path.

This build was live-tested on Windows 11 before pushing and works as described.
Locally it also passes a Windhawk compile-and-link check with the declared
@compilerOptions, an exit-time destructor audit, README parity between the
folder README and the embedded ==WindhawkModReadme==, and the upstream PR
validator.

@m417z
m417z merged commit c0d2cdf into ramensoftware:main Jul 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants