When a second display is connected (macOS), presenting now opens a borderless audience window on the beamer showing the slide, while the main window shows the presenter view (current/next slide, speaker notes, clock, controls) on the laptop. The two windows stay in sync over method channels: navigation, blank screen, audio-complete and beamer clicks are forwarded between them, and media plays only on the beamer to avoid double audio. Falls back to the existing single-window presenter when there is one display or the second window can't be created. - Vendors a fork of desktop_multi_window in third_party/ that re-adds the native macOS window geometry/fullscreen calls (coverScreen, setFrame, close) the published 0.3.0 dropped; wired via a path dependency. - Registers the app's plugins for sub-windows in MainFlutterWindow so video/image rendering works on the beamer. - Routes the multi_window dart entrypoint to a minimal AudienceWindowApp. Compiles (flutter analyze + macOS debug build) and all tests pass; runtime two-screen behaviour still needs verification on real hardware. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
171 lines
5.7 KiB
Swift
171 lines
5.7 KiB
Swift
import Cocoa
|
|
import FlutterMacOS
|
|
|
|
public class FlutterMultiWindowPlugin: NSObject, FlutterPlugin {
|
|
|
|
private let windowId: WindowId
|
|
private let windowArgument: String
|
|
|
|
|
|
init(window: FlutterWindow) {
|
|
self.windowId = window.windowId
|
|
self.windowArgument = window.windowArgument
|
|
super.init()
|
|
}
|
|
|
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
guard let app = NSApplication.shared.delegate as? FlutterAppDelegate else {
|
|
debugPrint(
|
|
"failed to find flutter main window, application delegate is not FlutterAppDelegate"
|
|
)
|
|
return
|
|
}
|
|
guard let window = app.mainFlutterWindow else {
|
|
debugPrint("failed to find flutter main window")
|
|
return
|
|
}
|
|
MultiWindowManager.shared.AttachWindow(window: window, registrar: registrar)
|
|
}
|
|
|
|
public typealias OnWindowCreatedCallback = (FlutterViewController) -> Void
|
|
static var onWindowCreatedCallback: OnWindowCreatedCallback?
|
|
|
|
public static func setOnWindowCreatedCallback(_ callback: @escaping OnWindowCreatedCallback) {
|
|
onWindowCreatedCallback = callback
|
|
}
|
|
|
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
let isWindowEvent = call.method.hasPrefix("window_")
|
|
if isWindowEvent {
|
|
let arguments = call.arguments as! [String: Any?]
|
|
let windowId = arguments["windowId"] as! WindowId
|
|
guard let window = MultiWindowManager.shared.windows[windowId] else {
|
|
result(
|
|
FlutterError(
|
|
code: "-1", message: "failed to find target window. \(windowId)",
|
|
details: nil))
|
|
return
|
|
}
|
|
|
|
window.handleWindowMethod(method: call.method, arguments: arguments, result: result)
|
|
return
|
|
}
|
|
|
|
switch call.method {
|
|
case "createWindow":
|
|
let arguments = call.arguments as! [String: Any?]
|
|
let windowId = MultiWindowManager.shared.CreateWindow(arguments: arguments)
|
|
result(windowId)
|
|
case "getWindowDefinition":
|
|
let definition: [String: Any] = [
|
|
"windowId": windowId,
|
|
"windowArgument": windowArgument,
|
|
]
|
|
result(definition)
|
|
case "getAllWindows":
|
|
let windows = MultiWindowManager.shared.getAllWindows()
|
|
result(windows)
|
|
default:
|
|
result(FlutterMethodNotImplemented)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
class MultiWindowManager: NSObject {
|
|
|
|
static let shared: MultiWindowManager = MultiWindowManager()
|
|
|
|
private override init() {}
|
|
|
|
var windows: [WindowId: FlutterWindow] = [:]
|
|
|
|
func AttachWindow(window: NSWindow, registrar: FlutterPluginRegistrar) {
|
|
// check window exists
|
|
for (_, flutterWindow) in windows {
|
|
if flutterWindow.window == window {
|
|
return
|
|
}
|
|
}
|
|
let windowId = WindowId.generate()
|
|
let flutterWindow = FlutterWindow(windowId: windowId, windowArgument: "", window: window)
|
|
windows[windowId] = flutterWindow
|
|
|
|
let channel = registerMultiWindowChannel(window: flutterWindow, with: registrar)
|
|
flutterWindow.setChannel(channel)
|
|
|
|
notifyWindowsChanged()
|
|
}
|
|
|
|
func CreateWindow(arguments: [String: Any?]) -> WindowId {
|
|
let windowId = WindowId.generate()
|
|
|
|
let config = WindowConfiguration.fromJson(arguments)
|
|
|
|
let window = CustomWindow(configuration: config)
|
|
|
|
let project = FlutterDartProject()
|
|
project.dartEntrypointArguments = ["multi_window", windowId, config.arguments]
|
|
let flutterViewController = FlutterViewController(project: project)
|
|
window.contentViewController = flutterViewController
|
|
window.setFrame(NSRect(x: 0, y: 0, width: 800, height: 600), display: true)
|
|
|
|
window.orderFront(nil)
|
|
window.setIsVisible(!config.hiddenAtLaunch)
|
|
|
|
FlutterMultiWindowPlugin.onWindowCreatedCallback?(flutterViewController)
|
|
|
|
let registrar = flutterViewController.registrar(forPlugin: "DesktopMultiWindowPlugin")
|
|
|
|
let flutterWindow = FlutterWindow(
|
|
windowId: windowId, windowArgument: config.arguments, window: window)
|
|
windows[windowId] = flutterWindow
|
|
|
|
let channel = registerMultiWindowChannel(window: flutterWindow, with: registrar)
|
|
flutterWindow.setChannel(channel)
|
|
|
|
notifyWindowsChanged()
|
|
|
|
return windowId
|
|
}
|
|
|
|
func removeWindow(windowId: WindowId) {
|
|
if windows.removeValue(forKey: windowId) != nil {
|
|
notifyWindowsChanged()
|
|
}
|
|
}
|
|
|
|
func getAllWindowIds() -> [WindowId] {
|
|
return Array(windows.keys)
|
|
}
|
|
|
|
func getAllWindows() -> [[String: String]] {
|
|
return windows.values.map { window in
|
|
[
|
|
"windowId": window.windowId,
|
|
"windowArgument": window.windowArgument,
|
|
]
|
|
}
|
|
}
|
|
|
|
private func notifyWindowsChanged() {
|
|
for (_, window) in windows {
|
|
window.notifyWindowEvent("onWindowsChanged", data: [:])
|
|
}
|
|
}
|
|
|
|
// register multi window method channel for all engine. include main or created by this plugin
|
|
private func registerMultiWindowChannel(
|
|
window: FlutterWindow, with registrar: FlutterPluginRegistrar
|
|
) -> FlutterMethodChannel {
|
|
let channel = FlutterMethodChannel(
|
|
name: "mixin.one/desktop_multi_window", binaryMessenger: registrar.messenger)
|
|
registrar.addMethodCallDelegate(FlutterMultiWindowPlugin(window: window), channel: channel)
|
|
|
|
// register window method channel plugin
|
|
WindowChannel.register(with: registrar)
|
|
|
|
return channel
|
|
}
|
|
|
|
}
|