feat: Notch Overflow — access menu bar icons hidden behind the camera notch (rebased, review feedback addressed) - #388
Open
grimlor wants to merge 8 commits into
Open
Conversation
First test target in the project. Wires an XCTest bundle against the Hidden Bar.app host (TEST_HOST/BUNDLE_LOADER) and registers it in the shared scheme's Test action so `xcodebuild test -scheme "Hidden Bar"` runs it. Ships with one placeholder test proving the @testable import and test-host wiring actually works end to end.
…otch On Macs with a camera notch, menu bar icons that overflow past the notch become inaccessible. This feature uses the Accessibility API to enumerate all menu bar extras and their positions, then presents hidden items in a dropdown menu. Access methods: - Global hotkey: Cmd+Shift+B (shows menu at cursor position) - Right-click the expand/collapse button → "Show Notch Items" - Right-click the separator → "Show Notch Items" Features: - Detects notch presence via NSScreen.safeAreaInsets (macOS 12+) - Enumerates all menu bar extras via AXUIElement API - Classifies items as hidden (behind notch) or visible by position - Hidden items displayed in orange for visual distinction - Clicking an item triggers AXPress on the original menu extra - Info label in Preferences window explains available shortcuts - Only activates on Macs with a notch; no-op on other models New files: - hidden/Features/NotchOverflow/NotchOverflowController.swift Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…iring Resolves every blocker laveez raised on the original notch-overflow PR: - Notch boundary now comes from NSScreen.auxiliaryTopRightArea instead of the screen.frame.width / 8 heuristic, which produced the wrong boundary on screens other than the one it was tuned against. - Drop the hardcoded Cmd+Shift+B global hotkey (conflicted with Chrome's bookmarks-bar shortcut) rather than build a second configurable-hotkey system; the existing right-click "Show Notch Items" entry already covers reachability. - Right-click no longer shows a second, duplicate menu; the notch item is folded into the same context menu right-click already opens (dwarvesf#356), so non-notch users see no behavior change. - Replace deprecated NSImage lockFocus()/unlockFocus() with the size:flipped:drawingHandler: initializer. - Localize the new user-facing strings; source strings added to en.lproj (the other 8 locales need community translation for these keys). Also closes a gap the reviewer flagged but the original PR never actually shipped: "Enable Notch Overflow" is now a real Preferences checkbox bound to notchOverflowEnabled, and toggling it live-rebuilds the context menu so the entry actually appears/disappears instead of only a no-op setup/ teardown pair. Deleted that dead code along with the hotkey removal. Original notch-overflow feature by Nadir A. (dwarvesf#350).
15 tests across 4 classes, exercising the pure logic extracted while fixing dwarvesf#350's review feedback: - NotchGeometryTests: no-notch detection, notch detection via safeAreaInsets, boundary sourced from auxiliaryTopRightArea, and the fallback when a notch is reported without an aux rect. - MenuBarExtraClassifierTests: hidden/visible bucketing against a boundary, zero-size extras excluded, closest-to-boundary-first ordering. - NotchOverflowMenuBuilderTests: empty state, section header counts, empty-title fallback to app name, hidden-item tinting. Runs against headless NSMenu/NSMenuItem construction, no window needed. - NotchOverflowPreferencesTests: fresh-install default, persistence round-trip, and the toggle notification StatusBarController listens for. Not unit-tested, named explicitly rather than faked: live AXUIElement enumeration against real running apps, real notch geometry on physical hardware, and the context-menu wiring's end-to-end behavior (verifying it needs a real StatusBarController, which creates live status items — too heavy/side-effecting for a unit test). These need a real notched Mac and Accessibility permission; component/manual-QA tier, not unit.
The separator/expand-button context menu was built once at init and reused, so "Show Notch Items" and the auto-collapse toggle title could go stale. getContextMenu() is now called fresh from showContextMenu(from:) on every open, computing the toggle title inline instead of patching it after the fact - the now-unneeded notchOverflowToggle notification (posted on every Preferences.notchOverflowEnabled set, listened for only by this menu) is removed along with it. Also fixes a real, consistently reproducible bug this same freshness work surfaced: the menu was anchored at `button.bounds.maxY + 5`, 5pt above the button's top edge - effectively the screen's top edge for a menu-bar item. Adding "Show Notch Items" made the menu one row taller, so that top row's requested position fell into the sliver above the visible screen. AppKit's response was exactly what was observed: a scroll-indicator caret in place of the row that didn't fit, with hovering scrolling the whole menu into view. Anchoring at the button's bottom edge instead keeps the menu entirely within on-screen space. Two other approaches were tried first and are documented inline rather than silently discarded: rebuilding via NSMenuDelegate.menuNeedsUpdate(_:) didn't fix the render issue and introduced a different artifact (a stray caret on first paint), and reusing NotchOverflowController's `button.menu = ...; performClick(nil)` pattern crashed - confirmed via crash report (SIGSEGV / stack overflow in NSStatusBarButtonCell._sendActionFrom:) - because this method is called from within the button's own click-action handler, unlike showOverflowMenuFromSeparator which is invoked from a different control's action.
Hidden items were tinted systemOrange to set them apart from visible ones. Color alone isn't an accessible way to convey that distinction (colorblind users get nothing from it), and it read poorly against a dark selection highlight in manual testing. Hidden items now get a leading warning glyph and bold weight at the normal label color instead. Section headers (the "Notch Overflow" title, and the "Hidden Behind Notch (N)"/"Visible (N)" counts) had the same legibility problem from a different cause - the title used secondaryLabelColor and the count headers had no explicit color at all, falling back to AppKit's dim default disabled-item style. Both now render bold at full-contrast labelColor via a shared helper.
The "Enable Notch Overflow" checkbox was added as a standalone NSButton positioned above the tutorial text, outside the Settings panel where every other preference checkbox lives - an easy one to miss. It's now a real storyboard row in the same vertical stack as the other Settings checkboxes, wired through an IBOutlet/IBAction like its neighbors instead of built programmatically. Visibility on non-notch Macs is still handled in code (checkBoxNotchOverflow.isHidden), and the containing stack view's detachesHiddenViews reflows the panel on its own when it's hidden.
Reflects the two accessibility fixes: hidden items are now asserted by glyph, bold weight, and label color instead of the removed orange tint, and a new test covers section headers rendering at full-contrast label color. Also drops the notchOverflowToggle notification test now that the context menu reads Preferences.notchOverflowEnabled fresh on every open instead of needing a push notification to stay in sync.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on #350 by @mehmetnadir — same feature (an overflow menu to reach
menu-bar icons macOS silently drops behind the camera notch), rebased onto
current
developand with every item from @laveez's review addressed.#350 went quiet for ~5 months with no author response to the review, so
this continues it as a new PR rather than waiting further; #350 should
probably be closed in favor of this one. Original authorship is preserved
via cherry-pick (see first commit).
This is one of the "notch overlap" candidate implementations named in the
managed-overflow epic (#366) — a smaller, immediately mergeable fix, not a
replacement for that larger redesign.
Manually verified end-to-end on real notched hardware.
What changed since #350
Addressing @laveez's review comments directly:
NSScreen.auxiliaryTopRightAreainstead of
screen.frame.width / 8, which produced the wrong boundary onscreens other than the one it was tuned against.
bookmarks-bar shortcut) rather than rebuilding it behind a second
configurable-hotkey system — the right-click "Show Notch Items" entry
already covers reachability, so a second hotkey-recorder UI wasn't
justified for this PR.
all users. The notch item is folded into the same context menu
right-click already opens ([Feature request] Context menu for collapse/expand icon (on right click) #356), so non-notch users see zero behavior
change. The menu is built fresh on every open (
getContextMenu()called from
showContextMenu(from:), not a menu built once and reused)so "Show Notch Items" and the auto-collapse toggle title always reflect
current state, and it's anchored at the button's bottom edge so it
renders entirely within on-screen space regardless of item count.
NSImagelockFocus()/unlockFocus()withthe
size:flipped:drawingHandler:initializer..localized.Source strings are added to
en.lproj; the other 8 locales will needcommunity translation for these new keys (I'm not a speaker of any of
them and didn't want to guess).
this feature manually?" — feat: Notch Overflow — access menu bar icons hidden behind the camera notch #350 added a
notchOverflowEnabledpref keybut never wired it into any UI or actual gating. This PR adds a real
Preferences checkbox, placed in the Settings panel using the same
storyboard-outlet mechanism as the other checkboxes (not floating
elsewhere in the window).
⚠️glyph + bold weight at the normal label color. Color alone isn't anaccessible way to convey "hidden" (colorblind users get nothing from it),
and the orange read poorly against a dark selection highlight in manual
testing. Section headers ("Notch Overflow", "Hidden Behind Notch (N)",
"Visible (N)") were also bumped from a dim secondary/default-disabled
style to full-contrast bold for the same legibility reason.
Approaches that don't work here (kept for future reference)
Two other ways of keeping the context menu's content fresh were tried
during development and discarded — noting them so nobody spends time
re-trying either:
NSMenuDelegate.menuNeedsUpdate(_:), rebuilding the menu's contentsright before display — content-mutating an already-displayed-before
NSMenuinstance while its own tracking loop is active produced a stray^caret glyph on first paint instead of the real top item.button.menu = menu; button.performClick(nil)(the patternNotchOverflowController.showOverflowMenuFromSeparatoruses safelyelsewhere in this same file) — crashes when called from
showContextMenu(from:):SIGSEGV/stack overflow inNSStatusBarButtonCell._sendActionFrom:, because that method is calledfrom within the button's own click-action handler, so
performClickrecursively re-enters it.
showOverflowMenuFromSeparatoris safe onlybecause it's invoked from a different control's action (a menu item).
Both are documented inline at
StatusBarController.showContextMenu(from:).Test evidence
This project had no test target at all before this PR. Added
HiddenTests(wired into the shared scheme;
xcodebuild test -scheme "Hidden Bar"runsit), with 15 tests covering the logic extracted specifically to make it
testable:
NotchGeometryTests(4): no-notch detection, notch detection, boundarysourced from
auxiliaryTopRightArea, fallback when a notch is reportedwithout an aux rect — this last one regression-guards the exact
width-fraction bug flagged in review.
MenuBarExtraClassifierTests(4): hidden/visible bucketing, zero-sizeextras excluded, closest-to-boundary-first ordering.
NotchOverflowMenuBuilderTests(5): empty state, section header counts,section headers use full-contrast label color, empty-title fallback to
app name, hidden-item distinguished by glyph and weight (not color
alone).
NotchOverflowPreferencesTests(2): fresh-install default, persistenceround-trip.
Not unit-tested, named explicitly rather than faked: live
AXUIElementenumeration against real running apps, and real notch/menu geometry on
physical hardware (menu positioning specifically needed a real,
interactive menu-tracking session on real hardware — not reproducible in a
headless test).
Test plan
Manually verified on real notched hardware (MacBook Pro, physical camera
notch):
Notch Items" renders immediately, no hover needed
⚠️glyph in bold; visible items don'telsewhere), toggling it removes the "Show Notch Items" entry
xcodebuild test -scheme "Hidden Bar"— 15/15 passingin this environment)
in this environment; code-reviewed to be a no-op via the same
hasNotchgate used everywhere else in the file)