import AppKit final class OverlayWindowController: NSWindowController { private let statusView = StatusView() private var pfManager: PortForwardManager? // Fully transparent background; no visual effect overlay init(serviceChecker: ServiceChecker, pfManager: PortForwardManager? = nil) { let style: NSWindow.StyleMask = [.borderless] let window = OverlayWindow(contentRect: .zero, styleMask: style, backing: .buffered, defer: false) super.init(window: window) window.isReleasedWhenClosed = false window.level = .floating // below status bar level, above normal windows window.collectionBehavior = [.canJoinAllSpaces, .stationary] window.hasShadow = false window.backgroundColor = .clear window.isOpaque = false statusView.translatesAutoresizingMaskIntoConstraints = false let container = TransparentContainerView() container.wantsLayer = true container.translatesAutoresizingMaskIntoConstraints = false container.postsFrameChangedNotifications = true container.layer?.backgroundColor = NSColor.clear.cgColor container.addSubview(statusView) window.contentView = container NSLayoutConstraint.activate([ statusView.centerXAnchor.constraint(equalTo: container.centerXAnchor), statusView.centerYAnchor.constraint(equalTo: container.centerYAnchor) ]) statusView.bindTo(serviceChecker: serviceChecker) self.pfManager = pfManager statusView.setPortForwardManager(pfManager) // Initial placement recomputeFrameAndVisibility() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func showIfMenuBarVisible() { recomputeFrameAndVisibility() } // Show the overlay explicitly, regardless of whether the menu bar is auto-hidden. // Positions the overlay at the top of the visible frame. func showOverlayNow() { guard let screen = NSScreen.main else { return } let frame = screen.frame let visible = screen.visibleFrame let statusThickness = NSStatusBar.system.thickness let overlayHeight = statusThickness + 6.0 let y = visible.maxY - overlayHeight let rect = NSRect(x: frame.minX, y: y, width: frame.width, height: overlayHeight) window?.setFrame(rect, display: true) window?.orderFrontRegardless() // Allow mouse for our inline controls (refresh/maximize). The view will pass-through elsewhere. window?.ignoresMouseEvents = false dlog("Overlay window forced visible at top of visible frame") } func hideOverlay() { window?.orderOut(nil) } func recomputeFrameAndVisibility() { guard let screen = NSScreen.main else { dlog("No main screen detected; ordering window out") window?.orderOut(nil); return } let frame = screen.frame let visible = screen.visibleFrame let menuBarHeight = frame.maxY - visible.maxY // 0 if auto-hidden or full-screen space if menuBarHeight <= 0.5 { // treat as hidden/no menu bar dlog("Menu bar not visible (height≈0). Hiding overlay window.") window?.orderOut(nil) return } // Make the overlay a bit taller for better readability than the status bar thickness let statusThickness = NSStatusBar.system.thickness let overlayHeight = statusThickness + 6.0 let y = visible.maxY - overlayHeight let rect = NSRect(x: frame.minX, y: y, width: frame.width, height: overlayHeight) dlog("Computed overlay frame: x=\(rect.origin.x), y=\(rect.origin.y), w=\(rect.size.width), h=\(rect.size.height) | menuBarHeight=\(menuBarHeight), statusBarThickness=\(statusThickness), overlayHeight=\(overlayHeight)") window?.setFrame(rect, display: true) window?.orderFrontRegardless() // Enable mouse so the inline buttons work; StatusView forwards other clicks through. window?.ignoresMouseEvents = false dlog("Overlay window ordered front") } func showContextMenu() { guard let window = window, window.isVisible else { return } // Temporarily enable mouse interactions so the context menu can be used let wasIgnoring = window.ignoresMouseEvents if wasIgnoring { dlog("Enabling mouse events for context menu interaction") window.ignoresMouseEvents = false } let menu = NSMenu(title: "Prole") menu.addItem(withTitle: statusView.aggregateStatusSummary(), action: nil, keyEquivalent: "") menu.addItem(.separator()) menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self if pfManager != nil { menu.addItem(withTitle: "Reset Port Forwards", action: #selector(resetPortForwards), keyEquivalent: "").target = self } menu.addItem(.separator()) menu.addItem(withTitle: "Quit Prole", action: #selector(quit), keyEquivalent: "q").target = self let point = NSPoint(x: window.frame.midX, y: window.frame.minY) menu.popUp(positioning: nil, at: point, in: nil) // Restore click-through after the menu is dismissed if wasIgnoring { dlog("Restoring click-through after context menu") window.ignoresMouseEvents = true } } @objc private func refreshNow() { NotificationCenter.default.post(name: ServiceChecker.forceRefreshNotification, object: nil) } @objc private func quit() { dlog("Quit requested via context menu; terminating app") NSApp.terminate(nil) } @objc private func resetPortForwards() { pfManager?.reset() } } final class OverlayWindow: NSWindow { override var canBecomeKey: Bool { false } override var canBecomeMain: Bool { false } override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) isOpaque = false backgroundColor = .clear // Default to fully click-through. We temporarily disable this only while showing the context menu. ignoresMouseEvents = true acceptsMouseMovedEvents = true dlog("OverlayWindow initialized (borderless, transparent, accepts mouse moved events)") } } // Transparent container view that passes through mouse clicks everywhere // while still allowing mouse-moved events for subviews that add tracking areas. final class TransparentContainerView: NSView { override func hitTest(_ point: NSPoint) -> NSView? { // Allow interaction only with interactive subviews (e.g., buttons inside StatusView). // Otherwise, pass clicks through. if let hit = super.hitTest(point) { if hit is NSButton { return hit } // If a subview wants events but isn't a button, also allow it if hit.acceptsFirstResponder { return hit } } return nil } }