Monitor/Sources/FloatingMonitor/App/FloatingWindowController.swift

224 lines
7.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import AppKit
import SwiftUI
/// + SwiftUI +
final class FloatingWindowController: NSWindowController {
private var hostingView: NSHostingView<ContentView>?
private var visualEffect: NSVisualEffectView!
private let debugLogPath = NSHomeDirectory() + "/Desktop/fm_debug.log"
convenience init() {
let window = FloatingWindow(
contentRect: NSRect(x: 0, y: 0, width: 250, height: 300),
styleMask: [.borderless, .resizable],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
self.init(window: window)
setupVisualEffect()
observeBackdropToggle()
clearDebugLog()
}
// MARK: -
private func clearDebugLog() {
try? "".write(toFile: debugLogPath, atomically: true, encoding: .utf8)
}
private func log(_ msg: String) {
let ts = ISO8601DateFormatter().string(from: Date())
let line = "[\(ts)] \(msg)\n"
if let data = line.data(using: .utf8) {
if let handle = FileHandle(forWritingAtPath: debugLogPath) {
handle.seekToEndOfFile()
handle.write(data)
handle.closeFile()
} else {
try? data.write(to: URL(fileURLWithPath: debugLogPath))
}
}
}
func dumpGeometry(_ tag: String) {
guard let window = window else { return }
log("===== \(tag) =====")
log("window.frame = \(window.frame)")
log("window.styleMask = \(window.styleMask.rawValue)")
log("window.isOpaque = \(window.isOpaque)")
log("window.hasShadow = \(window.hasShadow)")
log("window.level = \(window.level.rawValue)")
log("window.backgroundColor= \(String(describing: window.backgroundColor))")
log("window.isVisible = \(window.isVisible)")
log("window.titlebarAppearsTransparent = \(window.titlebarAppearsTransparent)")
guard let cv = window.contentView else { return }
log("--- contentView ---")
log("contentView.frame = \(cv.frame)")
log("contentView.bounds = \(cv.bounds)")
log("contentView.subviews.count = \(cv.subviews.count)")
for (i, sv) in cv.subviews.enumerated() {
log(" subview[\(i)]: \(type(of: sv))")
log(" frame = \(sv.frame)")
log(" bounds = \(sv.bounds)")
log(" isHidden = \(sv.isHidden)")
log(" alphaValue = \(sv.alphaValue)")
log(" wantsLayer = \(sv.wantsLayer)")
if let layer = sv.layer {
log(" layer.frame = \(layer.frame)")
log(" layer.bounds = \(layer.bounds)")
log(" layer.cornerRadius= \(layer.cornerRadius)")
log(" layer.masksToBounds=\(layer.masksToBounds)")
log(" layer.isHidden = \(layer.isHidden)")
log(" layer.opacity = \(layer.opacity)")
log(" layer.sublayers?.count = \(layer.sublayers?.count ?? 0)")
if let subs = layer.sublayers {
for (j, sl) in subs.enumerated() {
log(" sublayer[\(j)]: \(type(of: sl)) frame=\(sl.frame) bounds=\(sl.bounds) hidden=\(sl.isHidden)")
}
}
}
}
log("")
}
// MARK: -
private func setupVisualEffect() {
guard let window = window else { return }
let ve = NSVisualEffectView()
ve.wantsLayer = true
ve.material = .hudWindow
ve.blendingMode = .behindWindow
ve.state = .active
ve.alphaValue = 1.0
ve.translatesAutoresizingMaskIntoConstraints = false
ve.layer?.cornerRadius = 16
ve.layer?.masksToBounds = true
ve.layer?.borderWidth = 0
ve.layer?.borderColor = NSColor.clear.cgColor
ve.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.35).cgColor
self.visualEffect = ve
guard let contentView = window.contentView else { return }
contentView.addSubview(ve, positioned: .below, relativeTo: contentView.subviews.first)
NSLayoutConstraint.activate([
ve.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
ve.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
ve.topAnchor.constraint(equalTo: contentView.topAnchor),
ve.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
applyBackdropEnabled(Configuration.shared.backdropEnabled)
}
// MARK: -
private func observeBackdropToggle() {
NotificationCenter.default.addObserver(
forName: .backdropEnabledChanged, object: nil, queue: .main
) { [weak self] note in
guard let enabled = note.object as? Bool else { return }
self?.applyBackdropEnabled(enabled)
}
}
private func applyBackdropEnabled(_ enabled: Bool) {
guard let ve = visualEffect else { return }
if enabled {
ve.blendingMode = .behindWindow
ve.alphaValue = 1.0
ve.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.35).cgColor
} else {
ve.blendingMode = .withinWindow
ve.alphaValue = 0.0
ve.layer?.backgroundColor = NSColor.clear.cgColor
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.dumpGeometry("backdropEnabled=\(enabled)")
}
}
// MARK: - SwiftUI
func embed(rootView: ContentView) {
guard let window = window, let contentView = window.contentView else { return }
let hv = NSHostingView(rootView: rootView)
hv.wantsLayer = true
hv.layer?.cornerRadius = Configuration.shared.backdropEnabled ? 16 : 0
hv.layer?.masksToBounds = true
hv.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(hv)
NSLayoutConstraint.activate([
hv.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
hv.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
hv.topAnchor.constraint(equalTo: contentView.topAnchor),
hv.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
self.hostingView = hv
}
// MARK: -
func toggleVisible() {
guard let window = window else { return }
if window.isVisible {
window.orderOut(nil)
} else {
positionNearMenuBar()
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: false)
}
}
func show() {
guard let window = window else { return }
if !window.isVisible {
positionNearMenuBar()
}
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.dumpGeometry("show")
}
}
func hide() {
window?.orderOut(nil)
}
var isVisible: Bool {
window?.isVisible ?? false
}
// MARK: - &
func resize(width: CGFloat, height: CGFloat) {
guard let window = window else { return }
var frame = window.frame
frame.size.width = width
frame.size.height = height
window.setFrame(frame, display: true, animate: true)
}
private func positionNearMenuBar() {
guard let window = window, let screen = NSScreen.main else { return }
let screenFrame = screen.visibleFrame
window.setFrameOrigin(NSPoint(
x: screenFrame.maxX - window.frame.width - 24,
y: screenFrame.maxY - window.frame.height - 24
))
}
}