import Cocoa import FlutterMacOS import Foundation typealias WindowId = String extension WindowId { static func generate() -> WindowId { return UUID().uuidString } } class CustomWindow: NSWindow { init(configuration: WindowConfiguration) { super.init( contentRect: NSRect(x: 10, y: 10, width: 800, height: 600), styleMask: [.miniaturizable, .closable, .titled, .resizable], backing: .buffered, defer: false) self.isReleasedWhenClosed = false } deinit { debugPrint("Child window deinit") } } class FlutterWindow: NSObject { let windowId: WindowId let windowArgument: String private(set) var window: NSWindow private var channel: FlutterMethodChannel? private var willBecomeActiveObserver: NSObjectProtocol? private var didResignActiveObserver: NSObjectProtocol? private var closeObserver: NSObjectProtocol? init(windowId: WindowId, windowArgument: String, window: NSWindow) { self.windowId = windowId self.windowArgument = windowArgument self.window = window super.init() willBecomeActiveObserver = NotificationCenter.default.addObserver( forName: NSApplication.willBecomeActiveNotification, object: nil, queue: .main ) { [weak self] notification in self?.didChangeOcclusionState(notification) } didResignActiveObserver = NotificationCenter.default.addObserver( forName: NSApplication.didResignActiveNotification, object: nil, queue: .main ) { [weak self] notification in self?.didChangeOcclusionState(notification) } closeObserver = NotificationCenter.default.addObserver( forName: NSWindow.willCloseNotification, object: window, queue: .main ) { [windowId] _ in MultiWindowManager.shared.removeWindow(windowId: windowId) } } deinit { if let willBecomeActiveObserver = willBecomeActiveObserver { NotificationCenter.default.removeObserver(willBecomeActiveObserver) } if let didResignActiveObserver = didResignActiveObserver { NotificationCenter.default.removeObserver(didResignActiveObserver) } if let closeObserver = closeObserver { NotificationCenter.default.removeObserver(closeObserver) } } @objc func didChangeOcclusionState(_ notification: Notification) { if let controller = window.contentViewController as? FlutterViewController { controller.engine.handleDidChangeOcclusionState(notification) } } func setChannel(_ channel: FlutterMethodChannel) { self.channel = channel } func notifyWindowEvent(_ event: String, data: [String: Any]) { if let channel = channel { channel.invokeMethod(event, arguments: data) } else { debugPrint("Channel not set for window \(windowId), cannot notify event \(event)") } } func handleWindowMethod(method: String, arguments: Any?, result: @escaping FlutterResult) { let args = arguments as? [String: Any?] switch method { case "window_show": window.makeKeyAndOrderFront(nil) window.setIsVisible(true) NSApp.activate(ignoringOtherApps: true) result(nil) case "window_hide": window.orderOut(nil) result(nil) case "window_close": window.close() result(nil) case "window_setFrame": if let x = args?["x"] as? Double, let y = args?["y"] as? Double, let w = args?["width"] as? Double, let h = args?["height"] as? Double { window.setFrame(NSRect(x: x, y: y, width: w, height: h), display: true) } result(nil) case "window_coverScreen": // Make this window a borderless surface that fills an entire screen — // used to show the audience slide fullscreen on the beamer while the // main (presenter) window stays on the laptop. We deliberately do not // make it the key window, so the keyboard stays with the presenter. let external = (args?["external"] as? Bool) ?? true let screens = NSScreen.screens var target: NSScreen? = NSScreen.main if external, let ext = screens.first(where: { $0 != NSScreen.main }) { target = ext } if let screen = target ?? screens.first { window.styleMask = [.borderless] // Raise above the menu bar (.mainMenu == 24) so the macOS menu // bar and notch area on the beamer are covered by the slide; a // plain .normal window would sit *under* the menu bar and leave // the Apple/Wi-Fi strip visible during the presentation. We stay // below .popUpMenu (101) so context menus still show on top. window.level = .statusBar // Keep the cover in place across Spaces/displays without ever // stealing keyboard focus from the presenter window. window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary] window.isOpaque = true window.setFrame(screen.frame, display: true) window.orderFrontRegardless() window.setIsVisible(true) } result(nil) default: result(FlutterError(code: "-1", message: "unknown method \(method)", details: nil)) } } }