import AppKit import SwiftUI /// 悬浮窗口控制器:管理毛玻璃 + SwiftUI 视图嵌入 + 窗口显隐 final class FloatingWindowController: NSWindowController { private var hostingView: NSHostingView? 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 )) } }