159 lines
5.1 KiB
Dart
159 lines
5.1 KiB
Dart
|
|
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<void> 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<WindowController> create(
|
||
|
|
WindowConfiguration configuration) async {
|
||
|
|
final windowId = await _channel.invokeMethod<String>(
|
||
|
|
'createWindow',
|
||
|
|
configuration.toJson(),
|
||
|
|
);
|
||
|
|
assert(windowId != null, 'windowId is null');
|
||
|
|
assert(windowId!.isNotEmpty, 'windowId is empty');
|
||
|
|
return WindowController._(windowId!, configuration.arguments);
|
||
|
|
}
|
||
|
|
|
||
|
|
static Future<WindowController> fromCurrentEngine() async {
|
||
|
|
final definition = await _channel
|
||
|
|
.invokeMethod<Map<dynamic, dynamic>>('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<List<WindowController>> getAll() async {
|
||
|
|
final result = await _channel.invokeMethod<List<dynamic>>('getAllWindows');
|
||
|
|
if (result == null) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
return result.cast<Map<dynamic, dynamic>>().map((e) {
|
||
|
|
final windowId = e['windowId'] as String;
|
||
|
|
final windowArgument = e['windowArgument'] as String;
|
||
|
|
return WindowController._(windowId, windowArgument);
|
||
|
|
}).toList();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _callWindowMethod(String method,
|
||
|
|
[Map<String, dynamic>? arguments]) {
|
||
|
|
assert(windowId.isNotEmpty, 'windowId is empty');
|
||
|
|
assert(method.startsWith('window_'), 'method must start with "window_"');
|
||
|
|
return _channel.invokeMethod(
|
||
|
|
method,
|
||
|
|
{
|
||
|
|
'windowId': windowId,
|
||
|
|
...?arguments,
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> show() => _callWindowMethod('window_show', {});
|
||
|
|
|
||
|
|
Future<void> hide() => _callWindowMethod('window_hide', {});
|
||
|
|
|
||
|
|
/// Close (destroy) this window. (macOS)
|
||
|
|
Future<void> close() => _callWindowMethod('window_close', {});
|
||
|
|
|
||
|
|
/// Position/size this window in screen coordinates. (macOS)
|
||
|
|
Future<void> 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<void> coverScreen({bool external = true}) =>
|
||
|
|
_callWindowMethod('window_coverScreen', {'external': external});
|
||
|
|
|
||
|
|
@optionalTypeArgs
|
||
|
|
Future<T?> invokeMethod<T>(String method, [dynamic arguments]) =>
|
||
|
|
_windowChannel.invokeMethod<T>(method, arguments);
|
||
|
|
|
||
|
|
Future<void> setWindowMethodHandler(
|
||
|
|
Future<dynamic> 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<MethodCall> _windowEventAsStream() {
|
||
|
|
late StreamController<MethodCall> controller;
|
||
|
|
controller = StreamController<MethodCall>.broadcast(
|
||
|
|
onListen: () {
|
||
|
|
_channel.setMethodCallHandler((call) async {
|
||
|
|
controller.add(call);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
onCancel: () {
|
||
|
|
_channel.setMethodCallHandler(null);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
return controller.stream;
|
||
|
|
}
|