Skip to content

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
dwarvesf:developfrom
grimlor:feature/notch-overflow
Open

feat: Notch Overflow — access menu bar icons hidden behind the camera notch (rebased, review feedback addressed)#388
grimlor wants to merge 8 commits into
dwarvesf:developfrom
grimlor:feature/notch-overflow

Conversation

@grimlor

@grimlor grimlor commented Sep 4, 2026

Copy link
Copy Markdown

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 develop and 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:

  • Notch boundary: now derived from NSScreen.auxiliaryTopRightArea
    instead of screen.frame.width / 8, which produced the wrong boundary on
    screens other than the one it was tuned against.
  • Global hotkey: removed the hardcoded ⌘⇧B (conflicts with Chrome's
    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.
  • Right-click behavior: no longer shows a separate duplicate menu for
    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.
  • Deprecated API: replaced NSImage lockFocus()/unlockFocus() with
    the size:flipped:drawingHandler: initializer.
  • Localization: user-facing strings now go through .localized.
    Source strings are added to en.lproj; the other 8 locales will need
    community translation for these new keys (I'm not a speaker of any of
    them and didn't want to guess).
  • Enable/disable: @laveez asked "should we have a way to enable/disable
    this feature manually?" — feat: Notch Overflow — access menu bar icons hidden behind the camera notch #350 added a notchOverflowEnabled pref key
    but 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).
  • Hidden-item indicator: changed from an orange color tint to a
    ⚠️ glyph + bold weight at the normal label color. Color alone isn't an
    accessible 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 contents
    right before display — content-mutating an already-displayed-before
    NSMenu instance 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 pattern
    NotchOverflowController.showOverflowMenuFromSeparator uses safely
    elsewhere in this same file) — crashes when called from
    showContextMenu(from:): SIGSEGV/stack overflow in
    NSStatusBarButtonCell._sendActionFrom:, because that method is called
    from within the button's own click-action handler, so performClick
    recursively re-enters it. showOverflowMenuFromSeparator is safe only
    because 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" runs
it), with 15 tests covering the logic extracted specifically to make it
testable:

  • NotchGeometryTests (4): no-notch detection, notch detection, boundary
    sourced from auxiliaryTopRightArea, fallback when a notch is reported
    without an aux rect — this last one regression-guards the exact
    width-fraction bug flagged in review.
  • MenuBarExtraClassifierTests (4): hidden/visible bucketing, zero-size
    extras 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, persistence
    round-trip.
Test Suite 'All tests' passed
** TEST SUCCEEDED **

Not unit-tested, named explicitly rather than faked: live AXUIElement
enumeration 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):

  • Right-click the separator and the collapse/expand button — "Show
    Notch Items" renders immediately, no hover needed
  • Hidden items show the ⚠️ glyph in bold; visible items don't
  • Clicking a hidden item activates the real menu-bar extra underneath
  • Accessibility permission prompt appears on first use, as expected
  • Preferences checkbox lives in the Settings panel (not floating
    elsewhere), toggling it removes the "Show Notch Items" entry
  • Automated: xcodebuild test -scheme "Hidden Bar" — 15/15 passing
  • Build succeeds with code signing disabled (no Developer ID available
    in this environment)
  • Not verified: behavior on a non-notch Mac (no such hardware available
    in this environment; code-reviewed to be a no-op via the same
    hasNotch gate used everywhere else in the file)

grimlor and others added 8 commits September 4, 2026 07:15
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.
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