mirror of
https://github.com/dredx/prole.git
synced 2026-09-27 07:14:29 +00:00
107 lines
4.3 KiB
Swift
107 lines
4.3 KiB
Swift
import AppKit
|
||
|
||
final class OverlayWindowController: NSWindowController {
|
||
private let statusView = StatusView()
|
||
private let visualEffect = NSVisualEffectView()
|
||
|
||
init(serviceChecker: ServiceChecker) {
|
||
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
|
||
|
||
// Visual effect that matches the menu bar
|
||
visualEffect.material = .menu
|
||
visualEffect.blendingMode = .behindWindow
|
||
visualEffect.state = .active
|
||
visualEffect.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||
|
||
let container = NSView()
|
||
container.wantsLayer = true
|
||
container.translatesAutoresizingMaskIntoConstraints = false
|
||
container.addSubview(visualEffect)
|
||
visualEffect.addSubview(statusView)
|
||
|
||
window.contentView = container
|
||
|
||
NSLayoutConstraint.activate([
|
||
visualEffect.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||
visualEffect.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||
visualEffect.topAnchor.constraint(equalTo: container.topAnchor),
|
||
visualEffect.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||
|
||
statusView.centerXAnchor.constraint(equalTo: visualEffect.centerXAnchor),
|
||
statusView.centerYAnchor.constraint(equalTo: visualEffect.centerYAnchor)
|
||
])
|
||
|
||
statusView.bindTo(serviceChecker: serviceChecker)
|
||
|
||
// Initial placement
|
||
recomputeFrameAndVisibility()
|
||
}
|
||
|
||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||
|
||
func showIfMenuBarVisible() { recomputeFrameAndVisibility() }
|
||
|
||
func recomputeFrameAndVisibility() {
|
||
guard let screen = NSScreen.main else { 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
|
||
window?.orderOut(nil)
|
||
return
|
||
}
|
||
|
||
let barHeight = NSStatusBar.system.thickness // expected to be same as menu bar height
|
||
let y = visible.maxY - barHeight
|
||
let rect = NSRect(x: frame.minX, y: y, width: frame.width, height: barHeight)
|
||
|
||
window?.setFrame(rect, display: true)
|
||
window?.orderFrontRegardless()
|
||
}
|
||
|
||
func showContextMenu() {
|
||
guard let window = window, window.isVisible else { return }
|
||
let menu = NSMenu(title: "ProleStatus")
|
||
menu.addItem(withTitle: statusView.aggregateStatusSummary(), action: nil, keyEquivalent: "")
|
||
menu.addItem(.separator())
|
||
menu.addItem(withTitle: "Refresh Now", action: #selector(refreshNow), keyEquivalent: "r").target = self
|
||
menu.addItem(.separator())
|
||
menu.addItem(withTitle: "Quit ProleStatus", 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() {
|
||
NSApp.terminate(nil)
|
||
}
|
||
}
|
||
|
||
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
|
||
ignoresMouseEvents = false // allow hover tracking, but we’ll pass clicks through via hitTest
|
||
acceptsMouseMovedEvents = true
|
||
}
|
||
}
|