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 _inactiveWindows = {}; final Set _activeWindows = {}; final List _windowActiveCallbacks = List.empty(growable: true); final List _remoteDesktopWindows = List.empty(growable: true); final List _fileTransferWindows = List.empty(growable: true); final List _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 newSessionWindow( WindowType type, String remoteId, String msg, List 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()); } registerActiveWindow(windowId); windows.add(windowId); return windowId; } Future _newSession( bool openInTabs, WindowType type, String methodName, String remoteId, List 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 newSession( WindowType type, String methodName, String remoteId, List 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 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 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 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 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 _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 Function(MethodCall call, int fromWindowId)? handler) { DesktopMultiWindow.setMethodHandler(handler); } Future closeAllSubWindows() async { await Future.wait(WindowType.values.map((e) => closeWindows(e))); } Future closeWindows(WindowType type) async { if (type == WindowType.Main) { // skip main window, use window manager instead return; } List windows = []; try { windows = await DesktopMultiWindow.getAllSubWindowIds(); } catch (e) { debugPrint('Failed to getAllSubWindowIds of $type, $e'); return; } if (windows.isEmpty) { return; } for (final wId in windows) { debugPrint("closing multi window: ${type.toString()}"); await saveWindowPosition(type, windowId: wId); try { // final ids = await DesktopMultiWindow.getAllSubWindowIds(); // if (!ids.contains(wId)) { // // no such window already // return; // } await WindowController.fromWindowId(wId).setPreventClose(false); await WindowController.fromWindowId(wId).close(); _activeWindows.remove(wId); } catch (e) { debugPrint("$e"); return; } } await _notifyActiveWindow(); clearWindowType(type); } Future> getAllSubWindowIds() async { try { final windows = await DesktopMultiWindow.getAllSubWindowIds(); return windows; } catch (err) { if (err is AssertionError) { return []; } else { rethrow; } } } Set getActiveWindows() { return _activeWindows; } Future _notifyActiveWindow() async { for (final callback in _windowActiveCallbacks) { await callback.call(); } } Future registerActiveWindow(int windowId) async { _activeWindows.add(windowId); _inactiveWindows.remove(windowId); await _notifyActiveWindow(); } Future destroyWindow(int windowId) async { await WindowController.fromWindowId(windowId).setPreventClose(false); await WindowController.fromWindowId(windowId).close(); _remoteDesktopWindows.remove(windowId); _fileTransferWindows.remove(windowId); _portForwardWindows.remove(windowId); } /// 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 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> getOtherRemoteWindowCoords(int wId) async { List 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> getOtherRemoteWindowCoordsFromMain() async { List coords = []; // Call the main window to get the coords of other remote windows. String res = await DesktopMultiWindow.invokeMethod( kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString()); List list = jsonDecode(res); for (var item in list) { coords.add(RemoteWindowCoords.fromJson(jsonDecode(item))); } return coords; } } final rustDeskWinManager = RustDeskMultiWindowManager.instance;