Flutter's macOS engine only sends mouse-moved events to the key window by default. The borderless audience (beamer) window deliberately never becomes key, so chart tooltips never appeared on the second screen, and hover styling stuck around whenever a window lost key status before the exit event arrived. Track the mouse whenever the app is active instead, for both the main window and every secondary window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
177 lines
6.1 KiB
Swift
177 lines
6.1 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)
|
|
// By default Flutter only delivers hover (mouse-moved) events to the
|
|
// key window. The audience/beamer window is borderless and never
|
|
// becomes key (the keyboard must stay with the presenter), so without
|
|
// this it would never see hover at all — and state set by a click
|
|
// (e.g. a chart highlight) would never be cleared again.
|
|
flutterViewController.mouseTrackingMode = .inActiveApp
|
|
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
|
|
}
|
|
|
|
}
|