Skip to content

Commit 4b926b1

Browse files
authored
Merge pull request #369 from kdroidFilter/fix/macos-context-menu-dark-mode
Fix macOS context menu always appearing in dark mode
2 parents 0704b92 + 9df769d commit 4b926b1

1 file changed

Lines changed: 68 additions & 3 deletions

File tree

maclib/tray.swift

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ private class MenuBarAppearanceObserver {
7272
private var workItem: DispatchWorkItem?
7373
private var settleItem: DispatchWorkItem?
7474
private var lastAppearance: NSAppearance.Name?
75+
private var lastSystemTheme: Bool? // Track system theme separately
7576
private let trayPtr: UnsafeMutableRawPointer?
7677

7778
/// Debounce delay before first evaluation (keep tiny but non‑zero).
@@ -84,12 +85,41 @@ private class MenuBarAppearanceObserver {
8485
}
8586

8687
func startObserving(_ statusItem: NSStatusItem) {
88+
// Observe menu bar appearance changes
8789
observation = statusItem.button?.observe(
8890
\.effectiveAppearance,
8991
options: [.initial, .new]
9092
) { [weak self] button, _ in
9193
self?.scheduleCheck(for: button.effectiveAppearance)
9294
}
95+
96+
// Observe system-wide theme changes via DistributedNotificationCenter
97+
DistributedNotificationCenter.default().addObserver(
98+
forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
99+
object: nil,
100+
queue: .main
101+
) { [weak self] _ in
102+
self?.handleSystemThemeChange()
103+
}
104+
105+
// Initial update of menu appearance
106+
handleSystemThemeChange()
107+
}
108+
109+
private func handleSystemThemeChange() {
110+
let isDark = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
111+
112+
// Only update if theme actually changed
113+
if lastSystemTheme != isDark {
114+
lastSystemTheme = isDark
115+
116+
// Update menu appearance for all contexts
117+
if let ptr = trayPtr, let ctx = contexts[ptr] {
118+
if let menu = ctx.contextMenu {
119+
updateMenuAppearance(menu, to: systemAppearance())
120+
}
121+
}
122+
}
93123
}
94124

95125
private func scheduleCheck(for appearance: NSAppearance) {
@@ -113,6 +143,11 @@ private class MenuBarAppearanceObserver {
113143
if let img = isDark ? ctx.darkImage : ctx.lightImage {
114144
ctx.statusItem.button?.image = img
115145
}
146+
147+
// Update menu appearance to match the system theme (not menu bar)
148+
if let menu = ctx.contextMenu {
149+
updateMenuAppearance(menu, to: systemAppearance())
150+
}
116151
}
117152

118153
// Cancel any pending settle callback before scheduling a new one.
@@ -131,18 +166,48 @@ private class MenuBarAppearanceObserver {
131166
observation = nil
132167
workItem?.cancel()
133168
settleItem?.cancel()
169+
170+
// Remove system theme observer
171+
DistributedNotificationCenter.default().removeObserver(
172+
self,
173+
name: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
174+
object: nil
175+
)
134176
}
135177
}
136178

137179
// MARK: - Globals that need to live for app lifetime
138180
private var menuDelegate: MenuDelegate?
139181

140182
// MARK: - Helpers
141-
private func nativeMenu(from menuPtr: UnsafeMutableRawPointer) -> NSMenu {
183+
184+
/// Returns the system-wide appearance (not the menu bar appearance)
185+
private func systemAppearance() -> NSAppearance {
186+
// Check if system is in dark mode via UserDefaults
187+
let isDarkMode = UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark"
188+
let appearanceName: NSAppearance.Name = isDarkMode ? .darkAqua : .aqua
189+
return NSAppearance(named: appearanceName) ?? NSApp.effectiveAppearance
190+
}
191+
192+
/// Updates the appearance of a menu and all its submenus recursively
193+
private func updateMenuAppearance(_ menu: NSMenu, to appearance: NSAppearance) {
194+
menu.appearance = appearance
195+
196+
// Recursively update all submenus
197+
for item in menu.items {
198+
if let submenu = item.submenu {
199+
updateMenuAppearance(submenu, to: appearance)
200+
}
201+
}
202+
}
203+
private func nativeMenu(from menuPtr: UnsafeMutableRawPointer, statusItem: NSStatusItem? = nil) -> NSMenu {
142204
let menu = NSMenu()
143205
menu.autoenablesItems = false
144206
menu.delegate = menuDelegate
145207

208+
// Set menu appearance to match the system theme (not menu bar)
209+
menu.appearance = systemAppearance()
210+
146211
var currentPtr = menuPtr
147212
while true {
148213
guard let textPtr = currentPtr.load(as: UnsafePointer<CChar>?.self) else { break }
@@ -179,7 +244,7 @@ private func nativeMenu(from menuPtr: UnsafeMutableRawPointer) -> NSMenu {
179244
menu.addItem(item)
180245

181246
if let submenuPtr = submenu {
182-
menu.setSubmenu(nativeMenu(from: submenuPtr), for: item)
247+
menu.setSubmenu(nativeMenu(from: submenuPtr, statusItem: statusItem), for: item)
183248
}
184249
}
185250

@@ -269,7 +334,7 @@ public func tray_update(_ tray: UnsafeMutableRawPointer) {
269334

270335
if let menuPtr = menuPtr {
271336
// Create and store the menu without assigning it to statusItem
272-
ctx.contextMenu = nativeMenu(from: menuPtr)
337+
ctx.contextMenu = nativeMenu(from: menuPtr, statusItem: statusItem)
273338
} else {
274339
ctx.contextMenu = nil
275340
}

0 commit comments

Comments
 (0)