import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'window_channel.dart'; import 'window_configuration.dart'; final _windowEvent = _windowEventAsStream(); /// A listenable that notifies when the windows list changes. /// Listen to this to be notified when windows are created or destroyed. Stream get onWindowsChanged => _windowEvent.map((call) { if (call.method == 'onWindowsChanged') { return call.method; } return null; }).where((event) => event != null); /// The [WindowController] instance that is used to control this window. class WindowController { WindowController._(this.windowId, this.arguments) : _windowChannel = WindowMethodChannel( 'mixin.one/window_controller/$windowId', mode: ChannelMode.unidirectional, ); final String windowId; final String arguments; final WindowMethodChannel _windowChannel; factory WindowController.fromWindowId(String id) => WindowController._(id, ''); static Future create( WindowConfiguration configuration) async { final windowId = await _channel.invokeMethod( 'createWindow', configuration.toJson(), ); assert(windowId != null, 'windowId is null'); assert(windowId!.isNotEmpty, 'windowId is empty'); return WindowController._(windowId!, configuration.arguments); } static Future fromCurrentEngine() async { final definition = await _channel .invokeMethod>('getWindowDefinition'); if (definition == null) { throw Exception('Failed to get window definition'); } final windowId = definition['windowId'] as String; final windowArgument = definition['windowArgument'] as String; return WindowController._(windowId, windowArgument); } static Future> getAll() async { final result = await _channel.invokeMethod>('getAllWindows'); if (result == null) { return []; } return result.cast>().map((e) { final windowId = e['windowId'] as String; final windowArgument = e['windowArgument'] as String; return WindowController._(windowId, windowArgument); }).toList(); } Future _callWindowMethod(String method, [Map? arguments]) { assert(windowId.isNotEmpty, 'windowId is empty'); assert(method.startsWith('window_'), 'method must start with "window_"'); return _channel.invokeMethod( method, { 'windowId': windowId, ...?arguments, }, ); } Future show() => _callWindowMethod('window_show', {}); Future hide() => _callWindowMethod('window_hide', {}); /// Close (destroy) this window. (macOS) Future close() => _callWindowMethod('window_close', {}); /// Position/size this window in screen coordinates. (macOS) Future setFrame(Rect frame) => _callWindowMethod('window_setFrame', { 'x': frame.left, 'y': frame.top, 'width': frame.width, 'height': frame.height, }); /// Make this window a borderless surface filling an entire screen. When /// [external] is true the first non-main screen (e.g. a beamer) is used, /// otherwise the main screen. The window does not become key, so keyboard /// focus stays with the window that had it. (macOS) Future coverScreen({bool external = true}) => _callWindowMethod('window_coverScreen', {'external': external}); @optionalTypeArgs Future invokeMethod(String method, [dynamic arguments]) => _windowChannel.invokeMethod(method, arguments); Future setWindowMethodHandler( Future Function(MethodCall call)? handler) { assert(() { scheduleMicrotask(() async { final c = await WindowController.fromCurrentEngine(); if (c.windowId != windowId) { throw FlutterError( 'setWindowMethodHandler can only be called on the current window controller. ' 'Current windowId: ${c.windowId}, this windowId: $windowId'); } }); return true; }()); return _windowChannel.setMethodCallHandler(handler); } @override bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; final WindowController otherController = other as WindowController; return windowId == otherController.windowId && arguments == otherController.arguments; } @override int get hashCode => windowId.hashCode ^ arguments.hashCode; @override String toString() { return 'WindowController(windowId: $windowId, arguments: $arguments)'; } } final _channel = MethodChannel('mixin.one/desktop_multi_window'); Stream _windowEventAsStream() { late StreamController controller; controller = StreamController.broadcast( onListen: () { _channel.setMethodCallHandler((call) async { controller.add(call); }); }, onCancel: () { _channel.setMethodCallHandler(null); }, ); return controller.stream; }