mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 16:24:32 +00:00
176 lines
7.7 KiB
Swift
176 lines
7.7 KiB
Swift
import AppKit
|
|
|
|
final class OverlayWindowController: NSWindowController {
|
|
private let statusView = StatusView()
|
|
// 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 = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
|
|
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
|
|
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)
|
|
|
|
// 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()
|
|
window?.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
|
|
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()
|
|
window?.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
|
|
dlog("Overlay window ordered front")
|
|
}
|
|
|
|
func showContextMenu() {
|
|
guard let window = window, window.isVisible else { return }
|
|
let menu = NSMenu(title: "Prole Tools")
|
|
menu.addItem(withTitle: "Prole Tools — System Status", action: nil, keyEquivalent: "")
|
|
menu.addItem(.separator())
|
|
menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self
|
|
menu.addItem(withTitle: "Restart Port Forwards", action: #selector(resetPortForwards), keyEquivalent: "").target = self
|
|
menu.addItem(.separator())
|
|
menu.addItem(withTitle: "Quit Prole Tools", 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)
|
|
}
|
|
|
|
@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() {
|
|
_ = PFScriptBridge.restart()
|
|
}
|
|
}
|
|
|
|
final class OverlayWindow: NSWindow {
|
|
override var canBecomeKey: Bool { return false }
|
|
override var canBecomeMain: Bool { return false }
|
|
|
|
// Allow interaction with the window even if it's not key
|
|
override var acceptsMouseMovedEvents: Bool {
|
|
get { return true }
|
|
set { }
|
|
}
|
|
|
|
override func mouseDown(with event: NSEvent) {
|
|
dlog("OverlayWindow: mouseDown at \(event.locationInWindow)")
|
|
super.mouseDown(with: event)
|
|
}
|
|
|
|
override func sendEvent(_ event: NSEvent) {
|
|
if event.type == .leftMouseDown {
|
|
dlog("OverlayWindow: sendEvent .leftMouseDown at \(event.locationInWindow)")
|
|
// Fallback: Manually check if the click is in the maximize button area
|
|
if let contentView = self.contentView,
|
|
let statusView = contentView.subviews.first(where: { $0 is StatusView }) as? StatusView {
|
|
let pointInStatusView = statusView.convert(event.locationInWindow, from: nil)
|
|
if let hitView = statusView.hitTest(pointInStatusView), hitView.toolTip == "Show Main Window" {
|
|
dlog("OverlayWindow: Manual hit detected for maximize button via sendEvent")
|
|
NotificationCenter.default.post(name: NSNotification.Name("ProleStatus.showMainWindow"), object: nil)
|
|
return // Intercepted
|
|
}
|
|
}
|
|
}
|
|
super.sendEvent(event)
|
|
}
|
|
|
|
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 NOT click-through so we can intercept clicks on the maximize button.
|
|
// TransparentContainerView.hitTest handles passing through clicks for non-button areas.
|
|
ignoresMouseEvents = false
|
|
acceptsMouseMovedEvents = true
|
|
isMovableByWindowBackground = false
|
|
// Ensure the window level is high and it doesn't ignore mouse events
|
|
level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()) + 1)
|
|
dlog("OverlayWindow initialized (borderless, transparent, level: \(level.rawValue))")
|
|
}
|
|
}
|
|
|
|
// 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? {
|
|
let view = super.hitTest(point)
|
|
dlog("TransparentContainerView: hitTest at \(point) -> \(view?.description ?? "nil")")
|
|
return view
|
|
}
|
|
}
|