rustdesk/flutter/lib/utils/multi_window_manager.dart
fufesou 092e4089c7
fix: try workaround, macos, subwindow, frozen (#8729)
Signed-off-by: fufesou <linlong1266@gmail.com>
2024-07-17 09:55:46 +08:00

470 lines
14 KiB
Dart

import 'dart:convert';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/input_model.dart';
/// must keep the order
// ignore: constant_identifier_names
enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown }
extension Index on int {
WindowType get windowType {
switch (this) {
case 0:
return WindowType.Main;
case 1:
return WindowType.RemoteDesktop;
case 2:
return WindowType.FileTransfer;
case 3:
return WindowType.PortForward;
default:
return WindowType.Unknown;
}
}
}
class MultiWindowCallResult {
int windowId;
dynamic result;
MultiWindowCallResult(this.windowId, this.result);
}
/// Window Manager
/// mainly use it in `Main Window`
/// use it in sub window is not recommended
class RustDeskMultiWindowManager {
RustDeskMultiWindowManager._();
static final instance = RustDeskMultiWindowManager._();
final Set<int> _inactiveWindows = {};
final Set<int> _activeWindows = {};
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
final List<int> _remoteDesktopWindows = List.empty(growable: true);
final List<int> _fileTransferWindows = List.empty(growable: true);
final List<int> _portForwardWindows = List.empty(growable: true);
moveTabToNewWindow(int windowId, String peerId, String sessionId) async {
var params = {
'type': WindowType.RemoteDesktop.index,
'id': peerId,
'tab_window_id': windowId,
'session_id': sessionId,
};
await _newSession(
false,
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
peerId,
_remoteDesktopWindows,
jsonEncode(params),
);
}
// This function must be called in the main window thread.
// Because the _remoteDesktopWindows is managed in that thread.
openMonitorSession(int windowId, String peerId, int display, int displayCount,
Rect? screenRect) async {
if (_remoteDesktopWindows.length > 1) {
for (final windowId in _remoteDesktopWindows) {
if (await DesktopMultiWindow.invokeMethod(
windowId,
kWindowEventActiveDisplaySession,
jsonEncode({
'id': peerId,
'display': display,
}))) {
return;
}
}
}
final displays = display == kAllDisplayValue
? List.generate(displayCount, (index) => index)
: [display];
var params = {
'type': WindowType.RemoteDesktop.index,
'id': peerId,
'tab_window_id': windowId,
'display': display,
'displays': displays,
};
if (screenRect != null) {
params['screen_rect'] = {
'l': screenRect.left,
't': screenRect.top,
'r': screenRect.right,
'b': screenRect.bottom,
};
}
await _newSession(
false,
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
peerId,
_remoteDesktopWindows,
jsonEncode(params),
screenRect: screenRect,
);
}
Future<int> newSessionWindow(
WindowType type,
String remoteId,
String msg,
List<int> windows,
bool withScreenRect,
) async {
final windowController = await DesktopMultiWindow.createWindow(msg);
final windowId = windowController.windowId;
if (!withScreenRect) {
windowController
..setFrame(const Offset(0, 0) &
Size(1280 + windowId * 20, 720 + windowId * 20))
..center()
..setTitle(getWindowNameWithId(
remoteId,
overrideType: type,
));
} else {
windowController.setTitle(getWindowNameWithId(
remoteId,
overrideType: type,
));
}
if (isMacOS) {
Future.microtask(() {
windowController.show();
// Manually simulate the hide/show event to fix the issue
// https://github.com/rustdesk/rustdesk/issues/8548
// https://github.com/flutter/flutter/issues/133533
// https://github.com/MixinNetwork/flutter-plugins/issues/289#issuecomment-1817665239
// https://github.com/rustdesk/rustdesk/pull/8712#issuecomment-2229912473
Future.delayed(const Duration(milliseconds: 300), () {
DesktopMultiWindow.hideShow(-1);
});
});
}
registerActiveWindow(windowId);
windows.add(windowId);
return windowId;
}
Future<MultiWindowCallResult> _newSession(
bool openInTabs,
WindowType type,
String methodName,
String remoteId,
List<int> windows,
String msg, {
Rect? screenRect,
}) async {
if (openInTabs) {
if (windows.isEmpty) {
final windowId = await newSessionWindow(
type, remoteId, msg, windows, screenRect != null);
return MultiWindowCallResult(windowId, null);
} else {
return call(type, methodName, msg);
}
} else {
if (_inactiveWindows.isNotEmpty) {
for (final windowId in windows) {
if (_inactiveWindows.contains(windowId)) {
if (screenRect == null) {
await restoreWindowPosition(type,
windowId: windowId, peerId: remoteId);
}
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
if (methodName != kWindowEventNewRemoteDesktop) {
WindowController.fromWindowId(windowId).show();
}
registerActiveWindow(windowId);
return MultiWindowCallResult(windowId, null);
}
}
}
final windowId = await newSessionWindow(
type, remoteId, msg, windows, screenRect != null);
return MultiWindowCallResult(windowId, null);
}
}
Future<MultiWindowCallResult> newSession(
WindowType type,
String methodName,
String remoteId,
List<int> windows, {
String? password,
bool? forceRelay,
String? switchUuid,
bool? isRDP,
bool? isSharedPassword,
}) async {
var params = {
"type": type.index,
"id": remoteId,
"password": password,
"forceRelay": forceRelay
};
if (switchUuid != null) {
params['switch_uuid'] = switchUuid;
}
if (isRDP != null) {
params['isRDP'] = isRDP;
}
if (isSharedPassword != null) {
params['isSharedPassword'] = isSharedPassword;
}
final msg = jsonEncode(params);
// separate window for file transfer is not supported
bool openInTabs = type != WindowType.RemoteDesktop ||
mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs);
if (windows.length > 1 || !openInTabs) {
for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) {
return MultiWindowCallResult(windowId, null);
}
}
}
return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
}
Future<MultiWindowCallResult> newRemoteDesktop(
String remoteId, {
String? password,
bool? isSharedPassword,
String? switchUuid,
bool? forceRelay,
}) async {
return await newSession(
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
remoteId,
_remoteDesktopWindows,
password: password,
forceRelay: forceRelay,
switchUuid: switchUuid,
isSharedPassword: isSharedPassword,
);
}
Future<MultiWindowCallResult> newFileTransfer(String remoteId,
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
return await newSession(
WindowType.FileTransfer,
kWindowEventNewFileTransfer,
remoteId,
_fileTransferWindows,
password: password,
forceRelay: forceRelay,
isSharedPassword: isSharedPassword,
);
}
Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
{String? password, bool? isSharedPassword, bool? forceRelay}) async {
return await newSession(
WindowType.PortForward,
kWindowEventNewPortForward,
remoteId,
_portForwardWindows,
password: password,
forceRelay: forceRelay,
isRDP: isRDP,
isSharedPassword: isSharedPassword,
);
}
Future<MultiWindowCallResult> call(
WindowType type, String methodName, dynamic args) async {
final wnds = _findWindowsByType(type);
if (wnds.isEmpty) {
return MultiWindowCallResult(kInvalidWindowId, null);
}
for (final windowId in wnds) {
if (_activeWindows.contains(windowId)) {
final res =
await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
return MultiWindowCallResult(windowId, res);
}
}
final res =
await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
return MultiWindowCallResult(wnds[0], res);
}
List<int> _findWindowsByType(WindowType type) {
switch (type) {
case WindowType.Main:
return [kMainWindowId];
case WindowType.RemoteDesktop:
return _remoteDesktopWindows;
case WindowType.FileTransfer:
return _fileTransferWindows;
case WindowType.PortForward:
return _portForwardWindows;
case WindowType.Unknown:
break;
}
return [];
}
void clearWindowType(WindowType type) {
switch (type) {
case WindowType.Main:
return;
case WindowType.RemoteDesktop:
_remoteDesktopWindows.clear();
break;
case WindowType.FileTransfer:
_fileTransferWindows.clear();
break;
case WindowType.PortForward:
_portForwardWindows.clear();
break;
case WindowType.Unknown:
break;
}
}
void setMethodHandler(
Future<dynamic> Function(MethodCall call, int fromWindowId)? handler) {
DesktopMultiWindow.setMethodHandler(handler);
}
Future<void> closeAllSubWindows() async {
await Future.wait(WindowType.values.map((e) => _closeWindows(e)));
}
Future<void> _closeWindows(WindowType type) async {
if (type == WindowType.Main) {
// skip main window, use window manager instead
return;
}
List<int> windows = [];
try {
windows = _findWindowsByType(type);
} catch (e) {
debugPrint('Failed to getAllSubWindowIds of $type, $e');
return;
}
if (windows.isEmpty) {
return;
}
for (final wId in windows) {
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
await saveWindowPosition(type, windowId: wId);
try {
await WindowController.fromWindowId(wId).setPreventClose(false);
await WindowController.fromWindowId(wId).close();
_activeWindows.remove(wId);
} catch (e) {
debugPrint("$e");
return;
}
}
clearWindowType(type);
}
Future<List<int>> getAllSubWindowIds() async {
try {
final windows = await DesktopMultiWindow.getAllSubWindowIds();
return windows;
} catch (err) {
if (err is AssertionError) {
return [];
} else {
rethrow;
}
}
}
Set<int> getActiveWindows() {
return _activeWindows;
}
Future<void> _notifyActiveWindow() async {
for (final callback in _windowActiveCallbacks) {
await callback.call();
}
}
Future<void> registerActiveWindow(int windowId) async {
_activeWindows.add(windowId);
_inactiveWindows.remove(windowId);
await _notifyActiveWindow();
}
/// Remove active window which has [`windowId`]
///
/// [Availability]
/// This function should only be called from main window.
/// For other windows, please post a unregister(hide) event to main window handler:
/// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
Future<void> unregisterActiveWindow(int windowId) async {
_activeWindows.remove(windowId);
if (windowId != kMainWindowId) {
_inactiveWindows.add(windowId);
}
await _notifyActiveWindow();
}
void registerActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.add(callback);
}
void unregisterActiveWindowListener(AsyncCallback callback) {
_windowActiveCallbacks.remove(callback);
}
// This function is called from the main window.
// It will query the active remote windows to get their coords.
Future<List<String>> getOtherRemoteWindowCoords(int wId) async {
List<String> coords = [];
for (final windowId in _remoteDesktopWindows) {
if (windowId != wId) {
if (_activeWindows.contains(windowId)) {
final res = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventRemoteWindowCoords, '');
if (res != null) {
coords.add(res);
}
}
}
}
return coords;
}
// This function is called from one remote window.
// Only the main window knows `_remoteDesktopWindows` and `_activeWindows`.
// So we need to call the main window to get the other remote windows' coords.
Future<List<RemoteWindowCoords>> getOtherRemoteWindowCoordsFromMain() async {
List<RemoteWindowCoords> coords = [];
// Call the main window to get the coords of other remote windows.
String res = await DesktopMultiWindow.invokeMethod(
kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString());
List<dynamic> list = jsonDecode(res);
for (var item in list) {
coords.add(RemoteWindowCoords.fromJson(jsonDecode(item)));
}
return coords;
}
}
final rustDeskWinManager = RustDeskMultiWindowManager.instance;