224 lines
7.9 KiB
Swift
224 lines
7.9 KiB
Swift
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
|
||
))
|
||
}
|
||
}
|