mac: tie highlight to panel visibility

This commit is contained in:
Peter Steinberger
2025-12-09 23:20:16 +01:00
parent 1dd5c97ae0
commit 6d91dad8e4
2 changed files with 84 additions and 29 deletions

View File

@@ -14,7 +14,7 @@ struct ClawdisApp: App {
@StateObject private var activityStore = WorkActivityStore.shared
@State private var statusItem: NSStatusItem?
@State private var isMenuPresented = false
private let menuDelegate = StatusMenuDelegate()
@State private var isPanelVisible = false
init() {
_state = StateObject(wrappedValue: AppStateStore.shared)
@@ -37,7 +37,6 @@ struct ClawdisApp: App {
self.statusItem = item
self.applyStatusItemAppearance(paused: self.state.isPaused)
self.installStatusItemMouseHandler(for: item)
self.attachMenuDelegate(to: item)
}
.onChange(of: self.state.isPaused) { _, paused in
self.applyStatusItemAppearance(paused: paused)
@@ -62,15 +61,18 @@ struct ClawdisApp: App {
if button.subviews.contains(where: { $0 is StatusItemMouseHandlerView }) { return }
WebChatManager.shared.onPanelVisibilityChanged = { [self] visible in
self.isPanelVisible = visible
self.statusItem?.button?.highlight(visible)
}
self.menuDelegate.button = button
let handler = StatusItemMouseHandlerView()
handler.translatesAutoresizingMaskIntoConstraints = false
handler.onLeftClick = { [self] in self.toggleWebChatPanel() }
handler.onRightClick = { WebChatManager.shared.closePanel() }
handler.onRightClick = { [self] in
WebChatManager.shared.closePanel()
self.statusItem?.button?.highlight(false)
self.isMenuPresented = true
}
button.addSubview(handler)
NSLayoutConstraint.activate([
@@ -81,15 +83,12 @@ struct ClawdisApp: App {
])
}
@MainActor
private func attachMenuDelegate(to item: NSStatusItem) {
guard let menu = item.menu else { return }
self.menuDelegate.button = item.button
menu.delegate = self.menuDelegate
}
@MainActor
private func toggleWebChatPanel() {
guard AppStateStore.webChatEnabled else {
self.isMenuPresented = true
return
}
self.isMenuPresented = false
WebChatManager.shared.togglePanel(
sessionKey: WebChatManager.shared.preferredSessionKey(),
@@ -132,19 +131,7 @@ private final class StatusItemMouseHandlerView: NSView {
override func rightMouseDown(with event: NSEvent) {
self.onRightClick?()
super.rightMouseDown(with: event) // forward to MenuBarExtra so the menu still opens
}
}
private final class StatusMenuDelegate: NSObject, NSMenuDelegate {
weak var button: NSStatusBarButton?
func menuWillOpen(_ menu: NSMenu) {
self.button?.highlight(true)
}
func menuDidClose(_ menu: NSMenu) {
self.button?.highlight(false)
// Do not call super; menu will be driven by isMenuPresented binding.
}
}

View File

@@ -28,7 +28,10 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
private var bootWatchTask: Task<Void, Never>?
let presentation: WebChatPresentation
var onPanelClosed: (() -> Void)?
var onVisibilityChanged: ((Bool) -> Void)?
private var panelCloseNotified = false
private var localDismissMonitor: Any?
private var observers: [NSObjectProtocol] = []
init(sessionKey: String, presentation: WebChatPresentation = .window) {
webChatLogger.debug("init WebChatWindowController sessionKey=\(sessionKey, privacy: .public)")
@@ -50,6 +53,10 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
self.loadPlaceholder()
Task { await self.bootstrap() }
if case .panel = presentation {
self.installPanelObservers()
}
}
@available(*, unavailable)
@@ -59,6 +66,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
self.reachabilityTask?.cancel()
self.bootWatchTask?.cancel()
self.stopTunnel(allowRestart: false)
self.removeDismissMonitor()
self.removePanelObservers()
}
private static func makeWindow(for presentation: WebChatPresentation, contentView: NSView) -> NSWindow {
@@ -266,14 +275,18 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
guard case .panel = self.presentation, let window else { return }
self.panelCloseNotified = false
self.repositionPanel(using: anchorProvider)
self.installDismissMonitor()
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
window.makeFirstResponder(self.webView)
self.onVisibilityChanged?(true)
}
func closePanel() {
guard case .panel = self.presentation else { return }
self.removeDismissMonitor()
self.window?.orderOut(nil)
self.onVisibilityChanged?(false)
self.notifyPanelClosedOnce()
}
@@ -324,6 +337,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
func windowWillClose(_ notification: Notification) {
guard case .panel = self.presentation else { return }
self.removeDismissMonitor()
self.onVisibilityChanged?(false)
self.notifyPanelClosedOnce()
}
@@ -332,6 +347,59 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
self.panelCloseNotified = true
self.onPanelClosed?()
}
private func installDismissMonitor() {
guard self.localDismissMonitor == nil, let panel = self.window else { return }
self.localDismissMonitor = NSEvent.addLocalMonitorForEvents(
matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown]
) { [weak self] event in
guard let self else { return event }
if event.window !== panel {
self.closePanel()
}
return event
}
}
private func removeDismissMonitor() {
if let monitor = self.localDismissMonitor {
NSEvent.removeMonitor(monitor)
self.localDismissMonitor = nil
}
}
private func installPanelObservers() {
guard let window = self.window else { return }
let nc = NotificationCenter.default
let o1 = nc.addObserver(
forName: NSApplication.didResignActiveNotification,
object: nil,
queue: .main
) { [weak self] _ in
Task { @MainActor in
self?.closePanel()
}
}
let o2 = nc.addObserver(
forName: NSWindow.didChangeOcclusionStateNotification,
object: window,
queue: .main
) { [weak self] _ in
Task { @MainActor in
guard let self, case .panel = self.presentation else { return }
if !(window.occlusionState.contains(.visible)) {
self.closePanel()
}
}
}
self.observers.append(contentsOf: [o1, o2])
}
private func removePanelObservers() {
let nc = NotificationCenter.default
for o in self.observers { nc.removeObserver(o) }
self.observers.removeAll()
}
}
extension WebChatWindowController {
@@ -396,10 +464,8 @@ final class WebChatManager {
if let controller = self.panelController {
if controller.window?.isVisible == true {
controller.closePanel()
self.onPanelVisibilityChanged?(false)
} else {
controller.presentAnchoredPanel(anchorProvider: anchorProvider)
self.onPanelVisibilityChanged?(true)
}
return
}
@@ -411,14 +477,15 @@ final class WebChatManager {
controller.onPanelClosed = { [weak self] in
self?.panelHidden()
}
controller.onVisibilityChanged = { [weak self] visible in
self?.onPanelVisibilityChanged?(visible)
}
controller.presentAnchoredPanel(anchorProvider: anchorProvider)
self.onPanelVisibilityChanged?(true)
}
func closePanel() {
guard let controller = self.panelController else { return }
controller.closePanel()
self.onPanelVisibilityChanged?(false)
}
func preferredSessionKey() -> String {
@@ -451,6 +518,7 @@ final class WebChatManager {
private func panelHidden() {
self.onPanelVisibilityChanged?(false)
self.panelController = nil
}
}