Skip to content

Commit 03f6bfb

Browse files
committed
Fixed bug where Bool binding could unexpectedly invert its polarity (#14)
1 parent b529c55 commit 03f6bfb

File tree

3 files changed

+100
-12
lines changed

3 files changed

+100
-12
lines changed

Sources/MenuBarExtraAccess/MenuBarExtraAccess.swift

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ struct MenuBarExtraAccess<Content: Scene>: Scene {
9696
print("Status item button state observer: called with change: \(change.newValue?.description ?? "nil")")
9797
#endif
9898

99+
// only continue if the MenuBarExtra is menu-based.
100+
// window-based MenuBarExtras are handled with app-bound window observers instead.
101+
guard MenuBarExtraUtils.statusItem(for: .index(index))?
102+
.isMenuBarExtraMenuBased == true
103+
else { return }
104+
99105
guard let newVal = change.newValue else { return }
100106
let newBool = newVal != .off
101107
if isMenuPresented != newBool {
@@ -108,24 +114,46 @@ struct MenuBarExtraAccess<Content: Scene>: Scene {
108114
}
109115
}
110116

111-
observerContainer.setupGlobalMouseDownMonitor {
112-
// note that this won't fire when mouse events within the app cause the window to dismiss
113-
MenuBarExtraUtils.newGlobalMouseDownEventsMonitor { event in
117+
// TODO: this mouse event observer is now redundant and can be deleted in the future
118+
119+
// observerContainer.setupGlobalMouseDownMonitor {
120+
// // note that this won't fire when mouse events within the app cause the window to dismiss
121+
// MenuBarExtraUtils.newGlobalMouseDownEventsMonitor { event in
122+
// #if DEBUG
123+
// print("Global mouse-down events monitor: called with event: \(event.type)")
124+
// #endif
125+
//
126+
// // close window when user clicks outside of it
127+
//
128+
// MenuBarExtraUtils.setPresented(for: .index(index), state: false)
129+
//
130+
// #if DEBUG
131+
// print("Global mouse-down events monitor: Setting isMenuPresented to false")
132+
// #endif
133+
//
134+
// isMenuPresented = false
135+
// }
136+
// }
137+
138+
observerContainer.setupWindowObservers(
139+
index: index,
140+
didBecomeKey: { window in
114141
#if DEBUG
115-
print("Global mouse-down events monitor: called with event: \(event.type)")
142+
print("MenuBarExtra index \(index) drop-down window did become key.")
116143
#endif
117144

118-
// close window when user clicks outside of it
119-
120-
MenuBarExtraUtils.setPresented(for: .index(index), state: false)
121-
145+
MenuBarExtraUtils.setKnownPresented(for: .index(index), state: true)
146+
isMenuPresented = true
147+
},
148+
didResignKey: { window in
122149
#if DEBUG
123-
print("Global mouse-down events monitor: Setting isMenuPresented to false")
150+
print("MenuBarExtra index \(index) drop-down window did resign as key.")
124151
#endif
125152

153+
MenuBarExtraUtils.setKnownPresented(for: .index(index), state: false)
126154
isMenuPresented = false
127155
}
128-
}
156+
)
129157

130158
return 0
131159
}
@@ -135,9 +163,11 @@ struct MenuBarExtraAccess<Content: Scene>: Scene {
135163
private var observerContainer = ObserverContainer()
136164

137165
private class ObserverContainer {
166+
private var statusItemIntrospectionSetup: Bool = false
138167
private var observer: NSStatusItem.ButtonStateObserver?
139168
private var eventsMonitor: Any?
140-
private var statusItemIntrospectionSetup: Bool = false
169+
private var windowDidBecomeKeyObserver: AnyCancellable?
170+
private var windowDidResignKeyObserver: AnyCancellable?
141171

142172
init() { }
143173

@@ -173,6 +203,26 @@ struct MenuBarExtraAccess<Content: Scene>: Scene {
173203
eventsMonitor = block()
174204
}
175205
}
206+
207+
func setupWindowObservers(
208+
index: Int,
209+
didBecomeKey didBecomeKeyBlock: @escaping (_ window: NSWindow) -> Void,
210+
didResignKey didResignKeyBlock: @escaping (_ window: NSWindow) -> Void
211+
) {
212+
// run async so that it can execute after SwiftUI sets up the NSStatusItem
213+
DispatchQueue.main.async { [self] in
214+
windowDidBecomeKeyObserver = MenuBarExtraUtils.newWindowObserver(
215+
index: index,
216+
for: NSWindow.didBecomeKeyNotification
217+
) { window in didBecomeKeyBlock(window) }
218+
219+
windowDidResignKeyObserver = MenuBarExtraUtils.newWindowObserver(
220+
index: index,
221+
for: NSWindow.didResignKeyNotification
222+
) { window in didResignKeyBlock(window) }
223+
224+
}
225+
}
176226
}
177227
}
178228

Sources/MenuBarExtraAccess/MenuBarExtraUtils/MenuBarExtraUtils.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ enum MenuBarExtraUtils {
3232
guard let item = statusItem(for: ident) else { return }
3333
item.setPresented(state: state)
3434
}
35+
36+
/// Set MenuBarExtra menu/window presentation state only when its state is reliably known.
37+
static func setKnownPresented(for ident: StatusItemIdentity? = nil, state: Bool) {
38+
#if DEBUG
39+
print("MenuBarExtraUtils.\(#function) called for status item \(ident?.description ?? "nil") with state \(state)")
40+
#endif
41+
42+
guard let item = statusItem(for: ident) else { return }
43+
item.setKnownPresented(state: state)
44+
}
3545
}
3646

3747
// MARK: - Objects and Metadata
@@ -213,6 +223,23 @@ extension MenuBarExtraUtils {
213223
block(value)
214224
})
215225
}
226+
227+
static func newWindowObserver(
228+
index: Int,
229+
for notification: Notification.Name,
230+
block: @escaping (_ window: NSWindow) -> Void
231+
) -> AnyCancellable? {
232+
NotificationCenter.default.publisher(for: notification)
233+
.filter { output in
234+
guard let window = output.object as? NSWindow else { return false }
235+
guard let windowWithIndex = MenuBarExtraUtils.window(for: .index(index)) else { return false }
236+
return window == windowWithIndex
237+
}
238+
.sink { output in
239+
guard let window = output.object as? NSWindow else { return }
240+
block(window)
241+
}
242+
}
216243
}
217244

218245
// MARK: - NSStatusItem Introspection
@@ -262,7 +289,7 @@ extension NSStatusItem {
262289
return mirror.menuBarExtraID()
263290
}
264291

265-
fileprivate var isMenuBarExtraMenuBased: Bool {
292+
var isMenuBarExtraMenuBased: Bool {
266293
// if window-based, target will be the internal type SwiftUI.WindowMenuBarExtraBehavior
267294
// if menu-based, target will be nil
268295
guard let behavior = button?.target

Sources/MenuBarExtraAccess/NSStatusItem Extensions.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ extension NSStatusItem {
4444
togglePresented()
4545
}
4646

47+
@_disfavoredOverload
4748
internal func updateHighlight() {
4849
#if DEBUG
4950
print("NSStatusItem.\(#function) called")
@@ -57,6 +58,16 @@ extension NSStatusItem {
5758

5859
button?.isHighlighted = s
5960
}
61+
62+
/// Only call this when the state of the drop-down window is known.
63+
internal func setKnownPresented(state: Bool) {
64+
switch state {
65+
case true:
66+
button?.state = .on
67+
case false:
68+
button?.state = .off
69+
}
70+
}
6071
}
6172

6273
// MARK: - KVO Observer

0 commit comments

Comments
 (0)