mac: tie highlight to panel visibility
This commit is contained in:
@@ -14,7 +14,7 @@ struct ClawdisApp: App {
|
|||||||
@StateObject private var activityStore = WorkActivityStore.shared
|
@StateObject private var activityStore = WorkActivityStore.shared
|
||||||
@State private var statusItem: NSStatusItem?
|
@State private var statusItem: NSStatusItem?
|
||||||
@State private var isMenuPresented = false
|
@State private var isMenuPresented = false
|
||||||
private let menuDelegate = StatusMenuDelegate()
|
@State private var isPanelVisible = false
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
_state = StateObject(wrappedValue: AppStateStore.shared)
|
_state = StateObject(wrappedValue: AppStateStore.shared)
|
||||||
@@ -37,7 +37,6 @@ struct ClawdisApp: App {
|
|||||||
self.statusItem = item
|
self.statusItem = item
|
||||||
self.applyStatusItemAppearance(paused: self.state.isPaused)
|
self.applyStatusItemAppearance(paused: self.state.isPaused)
|
||||||
self.installStatusItemMouseHandler(for: item)
|
self.installStatusItemMouseHandler(for: item)
|
||||||
self.attachMenuDelegate(to: item)
|
|
||||||
}
|
}
|
||||||
.onChange(of: self.state.isPaused) { _, paused in
|
.onChange(of: self.state.isPaused) { _, paused in
|
||||||
self.applyStatusItemAppearance(paused: paused)
|
self.applyStatusItemAppearance(paused: paused)
|
||||||
@@ -62,15 +61,18 @@ struct ClawdisApp: App {
|
|||||||
if button.subviews.contains(where: { $0 is StatusItemMouseHandlerView }) { return }
|
if button.subviews.contains(where: { $0 is StatusItemMouseHandlerView }) { return }
|
||||||
|
|
||||||
WebChatManager.shared.onPanelVisibilityChanged = { [self] visible in
|
WebChatManager.shared.onPanelVisibilityChanged = { [self] visible in
|
||||||
|
self.isPanelVisible = visible
|
||||||
self.statusItem?.button?.highlight(visible)
|
self.statusItem?.button?.highlight(visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.menuDelegate.button = button
|
|
||||||
|
|
||||||
let handler = StatusItemMouseHandlerView()
|
let handler = StatusItemMouseHandlerView()
|
||||||
handler.translatesAutoresizingMaskIntoConstraints = false
|
handler.translatesAutoresizingMaskIntoConstraints = false
|
||||||
handler.onLeftClick = { [self] in self.toggleWebChatPanel() }
|
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)
|
button.addSubview(handler)
|
||||||
NSLayoutConstraint.activate([
|
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
|
@MainActor
|
||||||
private func toggleWebChatPanel() {
|
private func toggleWebChatPanel() {
|
||||||
|
guard AppStateStore.webChatEnabled else {
|
||||||
|
self.isMenuPresented = true
|
||||||
|
return
|
||||||
|
}
|
||||||
self.isMenuPresented = false
|
self.isMenuPresented = false
|
||||||
WebChatManager.shared.togglePanel(
|
WebChatManager.shared.togglePanel(
|
||||||
sessionKey: WebChatManager.shared.preferredSessionKey(),
|
sessionKey: WebChatManager.shared.preferredSessionKey(),
|
||||||
@@ -132,19 +131,7 @@ private final class StatusItemMouseHandlerView: NSView {
|
|||||||
|
|
||||||
override func rightMouseDown(with event: NSEvent) {
|
override func rightMouseDown(with event: NSEvent) {
|
||||||
self.onRightClick?()
|
self.onRightClick?()
|
||||||
super.rightMouseDown(with: event) // forward to MenuBarExtra so the menu still opens
|
// Do not call super; menu will be driven by isMenuPresented binding.
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
private var bootWatchTask: Task<Void, Never>?
|
private var bootWatchTask: Task<Void, Never>?
|
||||||
let presentation: WebChatPresentation
|
let presentation: WebChatPresentation
|
||||||
var onPanelClosed: (() -> Void)?
|
var onPanelClosed: (() -> Void)?
|
||||||
|
var onVisibilityChanged: ((Bool) -> Void)?
|
||||||
private var panelCloseNotified = false
|
private var panelCloseNotified = false
|
||||||
|
private var localDismissMonitor: Any?
|
||||||
|
private var observers: [NSObjectProtocol] = []
|
||||||
|
|
||||||
init(sessionKey: String, presentation: WebChatPresentation = .window) {
|
init(sessionKey: String, presentation: WebChatPresentation = .window) {
|
||||||
webChatLogger.debug("init WebChatWindowController sessionKey=\(sessionKey, privacy: .public)")
|
webChatLogger.debug("init WebChatWindowController sessionKey=\(sessionKey, privacy: .public)")
|
||||||
@@ -50,6 +53,10 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
|
|
||||||
self.loadPlaceholder()
|
self.loadPlaceholder()
|
||||||
Task { await self.bootstrap() }
|
Task { await self.bootstrap() }
|
||||||
|
|
||||||
|
if case .panel = presentation {
|
||||||
|
self.installPanelObservers()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
@@ -59,6 +66,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
self.reachabilityTask?.cancel()
|
self.reachabilityTask?.cancel()
|
||||||
self.bootWatchTask?.cancel()
|
self.bootWatchTask?.cancel()
|
||||||
self.stopTunnel(allowRestart: false)
|
self.stopTunnel(allowRestart: false)
|
||||||
|
self.removeDismissMonitor()
|
||||||
|
self.removePanelObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeWindow(for presentation: WebChatPresentation, contentView: NSView) -> NSWindow {
|
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 }
|
guard case .panel = self.presentation, let window else { return }
|
||||||
self.panelCloseNotified = false
|
self.panelCloseNotified = false
|
||||||
self.repositionPanel(using: anchorProvider)
|
self.repositionPanel(using: anchorProvider)
|
||||||
|
self.installDismissMonitor()
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
window.makeFirstResponder(self.webView)
|
window.makeFirstResponder(self.webView)
|
||||||
|
self.onVisibilityChanged?(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func closePanel() {
|
func closePanel() {
|
||||||
guard case .panel = self.presentation else { return }
|
guard case .panel = self.presentation else { return }
|
||||||
|
self.removeDismissMonitor()
|
||||||
self.window?.orderOut(nil)
|
self.window?.orderOut(nil)
|
||||||
|
self.onVisibilityChanged?(false)
|
||||||
self.notifyPanelClosedOnce()
|
self.notifyPanelClosedOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +337,8 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
func windowWillClose(_ notification: Notification) {
|
||||||
guard case .panel = self.presentation else { return }
|
guard case .panel = self.presentation else { return }
|
||||||
|
self.removeDismissMonitor()
|
||||||
|
self.onVisibilityChanged?(false)
|
||||||
self.notifyPanelClosedOnce()
|
self.notifyPanelClosedOnce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +347,59 @@ final class WebChatWindowController: NSWindowController, WKNavigationDelegate, N
|
|||||||
self.panelCloseNotified = true
|
self.panelCloseNotified = true
|
||||||
self.onPanelClosed?()
|
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 {
|
extension WebChatWindowController {
|
||||||
@@ -396,10 +464,8 @@ final class WebChatManager {
|
|||||||
if let controller = self.panelController {
|
if let controller = self.panelController {
|
||||||
if controller.window?.isVisible == true {
|
if controller.window?.isVisible == true {
|
||||||
controller.closePanel()
|
controller.closePanel()
|
||||||
self.onPanelVisibilityChanged?(false)
|
|
||||||
} else {
|
} else {
|
||||||
controller.presentAnchoredPanel(anchorProvider: anchorProvider)
|
controller.presentAnchoredPanel(anchorProvider: anchorProvider)
|
||||||
self.onPanelVisibilityChanged?(true)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -411,14 +477,15 @@ final class WebChatManager {
|
|||||||
controller.onPanelClosed = { [weak self] in
|
controller.onPanelClosed = { [weak self] in
|
||||||
self?.panelHidden()
|
self?.panelHidden()
|
||||||
}
|
}
|
||||||
|
controller.onVisibilityChanged = { [weak self] visible in
|
||||||
|
self?.onPanelVisibilityChanged?(visible)
|
||||||
|
}
|
||||||
controller.presentAnchoredPanel(anchorProvider: anchorProvider)
|
controller.presentAnchoredPanel(anchorProvider: anchorProvider)
|
||||||
self.onPanelVisibilityChanged?(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func closePanel() {
|
func closePanel() {
|
||||||
guard let controller = self.panelController else { return }
|
guard let controller = self.panelController else { return }
|
||||||
controller.closePanel()
|
controller.closePanel()
|
||||||
self.onPanelVisibilityChanged?(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func preferredSessionKey() -> String {
|
func preferredSessionKey() -> String {
|
||||||
@@ -451,6 +518,7 @@ final class WebChatManager {
|
|||||||
|
|
||||||
private func panelHidden() {
|
private func panelHidden() {
|
||||||
self.onPanelVisibilityChanged?(false)
|
self.onPanelVisibilityChanged?(false)
|
||||||
|
self.panelController = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user