Merge pull request #5256 from dignow/refact/separate_remote_window

Refact/separate remote window
This commit is contained in:
RustDesk
2023-08-06 09:31:05 +08:00
committed by GitHub
58 changed files with 727 additions and 320 deletions

View File

@@ -19,7 +19,6 @@ import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:uuid/uuid.dart';
@@ -47,11 +46,6 @@ var isMobile = isAndroid || isIOS;
var version = "";
int androidVersion = 0;
/// Incriment count for textureId.
int _textureId = 0;
int get newTextureId => _textureId++;
final textureRenderer = TextureRgbaRenderer();
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
@@ -549,7 +543,7 @@ closeConnection({String? id}) {
}
}
void window_on_top(int? id) async {
void windowOnTop(int? id) async {
if (!isDesktop) {
return;
}
@@ -1225,7 +1219,7 @@ FFI get gFFI => _globalFFI;
Future<void> initGlobalFFI() async {
debugPrint("_globalFFI init");
_globalFFI = FFI();
_globalFFI = FFI(null);
debugPrint("_globalFFI init end");
// after `put`, can also be globally found by Get.find<FFI>();
Get.put(_globalFFI, permanent: true);
@@ -1417,8 +1411,24 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
sz.width, sz.height, position.dx, position.dy, isMaximized);
debugPrint(
"Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}");
await bind.setLocalFlutterConfig(
k: kWindowPrefix + type.name, v: pos.toString());
if (type == WindowType.RemoteDesktop && windowId != null) {
await _saveSessionWindowPosition(windowId, pos);
}
}
Future _saveSessionWindowPosition(int windowId, LastWindowPosition pos) async {
final remoteList = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventGetRemoteList, null);
if (remoteList != null) {
for (final peerId in remoteList.split(',')) {
bind.sessionSetFlutterConfigByPeerId(
id: peerId, k: kWindowPrefix, v: pos.toString());
}
}
}
Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
@@ -1508,7 +1518,8 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
/// Restore window position and size on start
/// Note that windowId must be provided if it's subwindow
Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
Future<bool> restoreWindowPosition(WindowType type,
{int? windowId, String? peerId}) async {
if (bind
.mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
.isNotEmpty) {
@@ -1517,13 +1528,31 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
if (type != WindowType.Main && windowId == null) {
debugPrint(
"Error: windowId cannot be null when saving positions for sub window");
return false;
}
final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name);
bool isRemotePeerPos = false;
String? pos;
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
pos = await bind.sessionGetFlutterConfigByPeerId(
id: peerId, k: kWindowPrefix);
isRemotePeerPos = pos != null;
}
pos ??= bind.getLocalFlutterConfig(k: kWindowPrefix + type.name);
var lpos = LastWindowPosition.loadFromString(pos);
if (lpos == null) {
debugPrint("no window position saved, ignoring position restoration");
return false;
}
if (type == WindowType.RemoteDesktop && !isRemotePeerPos && windowId != null) {
if (lpos.offsetWidth != null) {
lpos.offsetWidth = lpos.offsetWidth! + windowId * 20;
}
if (lpos.offsetHeight != null) {
lpos.offsetHeight = lpos.offsetHeight! + windowId * 20;
}
}
switch (type) {
case WindowType.Main:
@@ -1701,7 +1730,7 @@ bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
Future.delayed(Duration.zero, () {
rustDeskWinManager.newRemoteDesktop(id!,
password: password,
switch_uuid: switchUuid,
switchUuid: switchUuid,
forceRelay: forceRelay);
});
break;
@@ -1766,17 +1795,20 @@ List<String>? urlLinkToCmdArgs(Uri uri) {
return null;
}
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
required bool isRDP,
bool? forceRelay}) async {
connectMainDesktop(
String id, {
required bool isFileTransfer,
required bool isTcpTunneling,
required bool isRDP,
bool? forceRelay,
bool forceSeparateWindow = false,
}) async {
if (isFileTransfer) {
await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay);
} else {
await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay);
await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay, forceSeparateWindow: forceSeparateWindow);
}
}
@@ -1784,10 +1816,14 @@ connectMainDesktop(String id,
/// If [isFileTransfer], starts a session only for file transfer.
/// If [isTcpTunneling], starts a session only for tcp tunneling.
/// If [isRDP], starts a session only for rdp.
connect(BuildContext context, String id,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false}) async {
connect(
BuildContext context,
String id, {
bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false,
bool forceSeparateWindow = false,
}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
final oldId = id;
@@ -1798,18 +1834,22 @@ connect(BuildContext context, String id,
if (isDesktop) {
if (desktopType == DesktopType.main) {
await connectMainDesktop(id,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
forceRelay: forceRelay);
await connectMainDesktop(
id,
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
forceRelay: forceRelay,
forceSeparateWindow: forceSeparateWindow,
);
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
"forceRelay": forceRelay,
'forceRelay': forceRelay,
'forceSeparateWindow': forceSeparateWindow,
});
}
} else {

View File

@@ -399,10 +399,14 @@ abstract class BasePeerCard extends StatelessWidget {
Future<List<MenuEntryBase<String>>> _buildMenuItems(BuildContext context);
MenuEntryBase<String> _connectCommonAction(
BuildContext context, String id, String title,
{bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false}) {
BuildContext context,
String id,
String title, {
bool isFileTransfer = false,
bool isTcpTunneling = false,
bool isRDP = false,
bool forceSeparateWindow = false,
}) {
return MenuEntryButton<String>(
childBuilder: (TextStyle? style) => Text(
title,
@@ -415,6 +419,7 @@ abstract class BasePeerCard extends StatelessWidget {
isFileTransfer: isFileTransfer,
isTcpTunneling: isTcpTunneling,
isRDP: isRDP,
forceSeparateWindow: forceSeparateWindow,
);
},
padding: menuPadding,
@@ -423,13 +428,26 @@ abstract class BasePeerCard extends StatelessWidget {
}
@protected
MenuEntryBase<String> _connectAction(BuildContext context, Peer peer) {
List<MenuEntryBase<String>> _connectActions(BuildContext context, Peer peer) {
final actions = [_connectAction(context, peer, false)];
if (!mainGetLocalBoolOptionSync(kOptionSeparateRemoteWindow)) {
actions.add(_connectAction(context, peer, true));
}
return actions;
}
@protected
MenuEntryBase<String> _connectAction(
BuildContext context, Peer peer, bool forceSeparateWindow) {
return _connectCommonAction(
context,
peer.id,
peer.alias.isEmpty
? translate('Connect')
: "${translate('Connect')} ${peer.id}");
context,
peer.id,
(peer.alias.isEmpty
? translate('Connect')
: '${translate('Connect')} ${peer.id}') +
(forceSeparateWindow ? ' (${translate('separate window')})' : ''),
forceSeparateWindow: forceSeparateWindow,
);
}
@protected
@@ -796,7 +814,7 @@ class RecentPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
..._connectActions(context, peer),
_transferFileAction(context, peer.id),
];
@@ -852,7 +870,7 @@ class FavoritePeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
..._connectActions(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
@@ -902,7 +920,7 @@ class DiscoveredPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
..._connectActions(context, peer),
_transferFileAction(context, peer.id),
];
@@ -954,7 +972,7 @@ class AddressBookPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
..._connectActions(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
@@ -1016,7 +1034,7 @@ class MyGroupPeerCard extends BasePeerCard {
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
..._connectActions(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {

View File

@@ -22,6 +22,8 @@ const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
const bool kCloseMultiWindowByHide = true;
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info";
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
@@ -30,6 +32,17 @@ const String kWindowEventHide = "hide";
const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
const String kWindowEventNewFileTransfer = "new_file_transfer";
const String kWindowEventNewPortForward = "new_port_forward";
const String kWindowEventActiveSession = "active_session";
const String kWindowEventGetRemoteList = "get_remote_list";
const String kWindowEventGetSessionIdList = "get_session_id_list";
const String kWindowEventCloseForSeparateWindow = "close_for_separate_window";
const String kOptionSeparateRemoteWindow = "enable-separate-remote-window";
const String kUniLinksPrefix = "rustdesk://";
const String kUrlActionClose = "close";

View File

@@ -527,7 +527,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
if (call.method == kWindowMainWindowOnTop) {
window_on_top(null);
windowOnTop(null);
} else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
@@ -554,7 +554,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} else if (call.method == kWindowEventShow) {
await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
} else if (call.method == kWindowEventHide) {
await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
final wId = call.arguments['id'];
final isSeparateWindowEnabled =
mainGetLocalBoolOptionSync(kOptionSeparateRemoteWindow);
if (isSeparateWindowEnabled && !kCloseMultiWindowByHide) {
await rustDeskWinManager.destroyWindow(wId);
}
await rustDeskWinManager.unregisterActiveWindow(wId);
} else if (call.method == kWindowConnect) {
await connectMainDesktop(
call.arguments['id'],
@@ -562,6 +568,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
forceRelay: call.arguments['forceRelay'],
forceSeparateWindow: call.arguments['forceSeparateWindow'],
);
}
});

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
@@ -248,7 +249,7 @@ class _General extends StatefulWidget {
class _GeneralState extends State<_General> {
final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service');
RxBool serviceBtnEabled = true.obs;
RxBool serviceBtnEnabled = true.obs;
@override
Widget build(BuildContext context) {
@@ -300,14 +301,14 @@ class _GeneralState extends State<_General> {
return _Card(title: 'Service', children: [
Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
() async {
serviceBtnEabled.value = false;
serviceBtnEnabled.value = false;
await start_service(serviceStop.value);
// enable the button after 1 second
Future.delayed(const Duration(seconds: 1), () {
serviceBtnEabled.value = true;
serviceBtnEnabled.value = true;
});
}();
}, enabled: serviceBtnEabled.value))
}, enabled: serviceBtnEnabled.value))
]);
}
@@ -316,7 +317,20 @@ class _GeneralState extends State<_General> {
_OptionCheckBox(context, 'Confirm before closing multiple tabs',
'enable-confirm-closing-tabs',
isServer: false),
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr')
_OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
_OptionCheckBox(
context,
'Separate remote window',
kOptionSeparateRemoteWindow,
isServer: false,
update: () {
final useSeparateWindow =
mainGetLocalBoolOptionSync(kOptionSeparateRemoteWindow);
if (useSeparateWindow) {
rustDeskWinManager.separateWindows();
}
},
),
];
// though this is related to GUI, but opengl problem affects all users, so put in config rather than local
children.add(Tooltip(
@@ -1671,12 +1685,13 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
var ref = value.obs;
onChanged(option) async {
if (option != null) {
ref.value = option;
if (reverse) option = !option;
isServer
? await mainSetBoolOption(key, option)
: await mainSetLocalBoolOption(key, option);
;
ref.value = isServer
? mainGetBoolOptionSync(key)
: mainGetLocalBoolOptionSync(key);
update?.call();
}
}

View File

@@ -80,7 +80,7 @@ class _FileManagerPageState extends State<FileManagerPage>
@override
void initState() {
super.initState();
_ffi = FFI();
_ffi = FFI(null);
_ffi.start(widget.id,
isFileTransfer: true,
password: widget.password,

View File

@@ -60,10 +60,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
print(
"[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
// for simplify, just replace connectionId
if (call.method == "new_file_transfer") {
if (call.method == kWindowEventNewFileTransfer) {
final args = jsonDecode(call.arguments);
final id = args['id'];
window_on_top(windowId());
windowOnTop(windowId());
tabController.add(TabInfo(
key: id,
label: id,

View File

@@ -54,7 +54,7 @@ class _PortForwardPageState extends State<PortForwardPage>
@override
void initState() {
super.initState();
_ffi = FFI();
_ffi = FFI(null);
_ffi.start(widget.id,
isPortForward: true,
password: widget.password,

View File

@@ -60,11 +60,11 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
debugPrint(
"[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
// for simplify, just replace connectionId
if (call.method == "new_port_forward") {
if (call.method == kWindowEventNewPortForward) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final isRDP = args['isRDP'];
window_on_top(windowId());
windowOnTop(windowId());
if (tabController.state.value.tabs.indexWhere((e) => e.key == id) >=
0) {
debugPrint("port forward $id exists");

View File

@@ -18,6 +18,7 @@ import '../../common/widgets/remote_input.dart';
import '../../common.dart';
import '../../common/widgets/dialog.dart';
import '../../models/model.dart';
import '../../models/desktop_render_texture.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
import '../../utils/image.dart';
@@ -27,10 +28,13 @@ import '../widgets/tabbar_widget.dart';
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
final Map<String, bool> closeSessionOnDispose = {};
class RemotePage extends StatefulWidget {
RemotePage({
Key? key,
required this.id,
required this.sessionId,
required this.password,
required this.toolbarState,
required this.tabController,
@@ -39,6 +43,7 @@ class RemotePage extends StatefulWidget {
}) : super(key: key);
final String id;
final SessionID? sessionId;
final String? password;
final ToolbarState toolbarState;
final String? switchUuid;
@@ -66,9 +71,7 @@ class _RemotePageState extends State<RemotePage>
late RxBool _zoomCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
late RxInt _textureId;
late int _textureKey;
final useTextureRender = bind.mainUseTextureRender();
late RenderTexture _renderTexture;
final _blockableOverlayState = BlockableOverlayState();
@@ -86,15 +89,13 @@ class _RemotePageState extends State<RemotePage>
_showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id);
_remoteCursorMoved = RemoteCursorMovedState.find(id);
_textureKey = newTextureId;
_textureId = RxInt(-1);
}
@override
void initState() {
super.initState();
_initStates(widget.id);
_ffi = FFI();
_ffi = FFI(widget.sessionId);
Get.put(_ffi, tag: widget.id);
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
showKBLayoutTypeChooserIfNeeded(
@@ -115,17 +116,13 @@ class _RemotePageState extends State<RemotePage>
Wakelock.enable();
}
// Register texture.
_textureId.value = -1;
if (useTextureRender) {
textureRenderer.createTexture(_textureKey).then((id) async {
debugPrint("id: $id, texture_key: $_textureKey");
if (id != -1) {
final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerTexture(sessionId, ptr);
_textureId.value = id;
}
});
if (mainGetLocalBoolOptionSync(kOptionSeparateRemoteWindow)) {
_renderTexture = renderTexture;
} else {
_renderTexture = RenderTexture();
}
_renderTexture.create(sessionId);
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
@@ -206,26 +203,25 @@ class _RemotePageState extends State<RemotePage>
@override
Future<void> dispose() async {
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
// https://github.com/flutter/flutter/issues/64935
super.dispose();
debugPrint("REMOTE PAGE dispose ${widget.id}");
if (useTextureRender) {
platformFFI.registerTexture(sessionId, 0);
// sleep for a while to avoid the texture is used after it's unregistered.
await Future.delayed(Duration(milliseconds: 100));
await textureRenderer.closeTexture(_textureKey);
}
debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
await _renderTexture.destroy();
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(sessionId: sessionId, enter: false);
DesktopMultiWindow.removeListener(this);
_ffi.dialogManager.hideMobileActionsOverlay();
_ffi.recordingModel.onClose();
_rawKeyFocusNode.dispose();
await _ffi.close();
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
if (closeSession) {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: SystemUiOverlay.values);
}
if (!Platform.isLinux) {
await Wakelock.disable();
}
@@ -392,8 +388,8 @@ class _RemotePageState extends State<RemotePage>
cursorOverImage: _cursorOverImage,
keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved,
textureId: _textureId,
useTextureRender: useTextureRender,
textureId: _renderTexture.textureId,
useTextureRender: _renderTexture.useTextureRender,
listenerBuilder: (child) =>
_buildRawTouchAndPointerRegion(child, enterView, leaveView),
);

View File

@@ -52,6 +52,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
_toolbarState = ToolbarState();
RemoteCountState.init();
final peerId = params['id'];
final sessionId = params['session_id'];
if (peerId != null) {
ConnectionTypeState.init(peerId);
tabController.onSelected = (id) {
@@ -73,6 +74,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
page: RemotePage(
key: ValueKey(peerId),
id: peerId,
sessionId: sessionId == null ? null : SessionID(sessionId),
password: params['password'],
toolbarState: _toolbarState,
tabController: tabController,
@@ -95,11 +97,12 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
// for simplify, just replace connectionId
if (call.method == "new_remote_desktop") {
if (call.method == kWindowEventNewRemoteDesktop) {
final args = jsonDecode(call.arguments);
final id = args['id'];
final switchUuid = args['switch_uuid'];
window_on_top(windowId());
final sessionId = args['session_id'];
windowOnTop(windowId());
ConnectionTypeState.init(id);
_toolbarState.setShow(
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
@@ -112,6 +115,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
page: RemotePage(
key: ValueKey(id),
id: id,
sessionId: sessionId == null ? null : SessionID(sessionId),
password: args['password'],
toolbarState: _toolbarState,
tabController: tabController,
@@ -125,11 +129,37 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventActiveSession) {
final jumpOk = tabController.jumpToByKey(call.arguments);
if (jumpOk) {
windowOnTop(windowId());
}
return jumpOk;
} else if (call.method == kWindowEventGetRemoteList) {
return tabController.state.value.tabs
.map((e) => e.key)
.toList()
.join(',');
} else if (call.method == kWindowEventGetSessionIdList) {
return tabController.state.value.tabs
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
.toList()
.join(';');
} else if (call.method == kWindowEventCloseForSeparateWindow) {
final peerId = call.arguments;
closeSessionOnDispose[peerId] = false;
tabController.closeBy(peerId);
}
_update_remote_count();
});
Future.delayed(Duration.zero, () {
restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId());
restoreWindowPosition(
WindowType.RemoteDesktop,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
);
});
}

View File

@@ -13,6 +13,7 @@ import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
@@ -146,8 +147,10 @@ class DesktopTabController {
/// For addTab, tabPage has not been initialized, set [callOnSelected] to false,
/// and call [onSelected] at the end of initState
void jumpTo(int index, {bool callOnSelected = true}) {
if (!isDesktop || index < 0) return;
bool jumpTo(int index, {bool callOnSelected = true}) {
if (!isDesktop || index < 0) {
return false;
}
state.update((val) {
val!.selected = index;
Future.delayed(Duration(milliseconds: 100), (() {
@@ -168,8 +171,13 @@ class DesktopTabController {
onSelected?.call(key);
}
}
return true;
}
bool jumpToByKey(String key, {bool callOnSelected = true}) =>
jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
callOnSelected: callOnSelected);
void closeBy(String? key) {
if (!isDesktop) return;
assert(onRemoved != null);
@@ -574,6 +582,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
}
await windowManager.hide();
} else {
renderTexture.destroy();
// it's safe to hide the subwindow
final controller = WindowController.fromWindowId(kWindowId!);
if (Platform.isMacOS && await controller.isFullScreen()) {

View File

@@ -197,7 +197,7 @@ void runMultiWindow(
switch (appType) {
case kAppTypeDesktopRemote:
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: kWindowId!);
windowId: kWindowId!, peerId: argument['id'] as String?);
break;
case kAppTypeDesktopFileTransfer:
await restoreWindowPosition(WindowType.FileTransfer,
@@ -250,7 +250,7 @@ showCmWindow({bool isStartup = false}) async {
await windowManager.minimize(); //needed
await windowManager.setSizeAlignment(
kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
window_on_top(null);
windowOnTop(null);
}
}
}

View File

@@ -367,7 +367,7 @@ class ChatModel with ChangeNotifier {
// not minisized: add count
if (await WindowController.fromWindowId(stateGlobal.windowId)
.isMinimized()) {
window_on_top(stateGlobal.windowId);
windowOnTop(stateGlobal.windowId);
if (notSelected) {
tabController.jumpTo(index);
}
@@ -386,7 +386,7 @@ class ChatModel with ChangeNotifier {
return;
}
if (isDesktop) {
window_on_top(null);
windowOnTop(null);
// disable auto jumpTo other tab when hasFocus, and mark unread message
final currentSelectedTab =
session.serverModel.tabController.state.value.selectedTabInfo;

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import '../../common.dart';
import './platform_model.dart';
class RenderTexture {
final RxInt textureId = RxInt(-1);
int _textureKey = -1;
SessionID? _sessionId;
final useTextureRender = bind.mainUseTextureRender();
final textureRenderer = TextureRgbaRenderer();
RenderTexture();
create(SessionID sessionId) {
if (useTextureRender) {
_textureKey = bind.getNextTextureKey();
_sessionId = sessionId;
textureRenderer.createTexture(_textureKey).then((id) async {
debugPrint("id: $id, texture_key: $_textureKey");
if (id != -1) {
final ptr = await textureRenderer.getTexturePtr(_textureKey);
platformFFI.registerTexture(sessionId, ptr);
textureId.value = id;
}
});
}
}
destroy() async {
if (useTextureRender && _textureKey != -1 && _sessionId != null) {
platformFFI.registerTexture(_sessionId!, 0);
await textureRenderer.closeTexture(_textureKey);
_textureKey = -1;
}
}
static final RenderTexture instance = RenderTexture();
}
// Global instance for separate texture
final renderTexture = RenderTexture.instance;

View File

@@ -260,7 +260,7 @@ class FfiModel with ChangeNotifier {
});
break;
default:
window_on_top(null);
windowOnTop(null);
break;
}
}
@@ -1583,6 +1583,7 @@ class FFI {
/// dialogManager use late to ensure init after main page binding [globalKey]
late final dialogManager = OverlayDialogManager();
late final bool isSessionAdded;
late final SessionID sessionId;
late final ImageModel imageModel; // session
late final FfiModel ffiModel; // session
@@ -1600,8 +1601,9 @@ class FFI {
late final InputModel inputModel; // session
late final ElevationModel elevationModel; // session
FFI() {
sessionId = isDesktop ? Uuid().v4obj() : _constSessionId;
FFI(SessionID? sId) {
isSessionAdded = sId != null;
sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
imageModel = ImageModel(WeakReference(this));
ffiModel = FfiModel(WeakReference(this));
cursorModel = CursorModel(WeakReference(this));
@@ -1641,23 +1643,31 @@ class FFI {
imageModel.id = id;
cursorModel.id = id;
}
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
sessionId: sessionId,
id: id,
isFileTransfer: isFileTransfer,
isPortForward: isPortForward,
isRdp: isRdp,
switchUuid: switchUuid ?? "",
forceRelay: forceRelay ?? false,
password: password ?? "",
);
if (!isSessionAdded) {
// ignore: unused_local_variable
final addRes = bind.sessionAddSync(
sessionId: sessionId,
id: id,
isFileTransfer: isFileTransfer,
isPortForward: isPortForward,
isRdp: isRdp,
switchUuid: switchUuid ?? '',
forceRelay: forceRelay ?? false,
password: password ?? '',
);
}
final stream = bind.sessionStart(sessionId: sessionId, id: id);
final cb = ffiModel.startEventListener(sessionId, id);
final useTextureRender = bind.mainUseTextureRender();
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
// Preserved for the rgba data.
stream.listen((message) {
if (closed) return;
if (isSessionAdded && !isToNewWindowNotified.value) {
bind.sessionReadyToNewWindow(sessionId: sessionId);
isToNewWindowNotified.value = true;
}
() async {
if (message is EventToUI_Event) {
if (message.field0 == "close") {
@@ -1717,7 +1727,7 @@ class FFI {
}
/// Close the remote session.
Future<void> close() async {
Future<void> close({bool closeSession = true}) async {
closed = true;
chatModel.close();
if (imageModel.image != null && !isWebDesktop) {
@@ -1735,7 +1745,9 @@ class FFI {
ffiModel.clear();
canvasModel.clear();
inputModel.resetModifiers();
await bind.sessionClose(sessionId: sessionId);
if (closeSession) {
await bind.sessionClose(sessionId: sessionId);
}
debugPrint('model $id closed');
id = '';
}

View File

@@ -473,7 +473,7 @@ class ServerModel with ChangeNotifier {
onTap: () {},
page: desktop.buildConnectionCard(client)));
Future.delayed(Duration.zero, () async {
if (!hideCm) window_on_top(null);
if (!hideCm) windowOnTop(null);
});
// Only do the hidden task when on Desktop.
if (client.authorized && isDesktop) {
@@ -612,7 +612,7 @@ class ServerModel with ChangeNotifier {
if (client.incomingVoiceCall) {
// Has incoming phone call, let's set the window on top.
Future.delayed(Duration.zero, () {
window_on_top(null);
windowOnTop(null);
});
}
notifyListeners();

View File

@@ -5,6 +5,7 @@ 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';
/// must keep the order
@@ -35,146 +36,204 @@ class RustDeskMultiWindowManager {
static final instance = RustDeskMultiWindowManager._();
final List<int> _activeWindows = List.empty(growable: true);
final Set<int> _inactiveWindows = {};
final Set<int> _activeWindows = {};
final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
int? _remoteDesktopWindowId;
int? _fileTransferWindowId;
int? _portForwardWindowId;
final List<int> _remoteDesktopWindows = List.empty(growable: true);
final List<int> _fileTransferWindows = List.empty(growable: true);
final List<int> _portForwardWindows = List.empty(growable: true);
Future<dynamic> newRemoteDesktop(
String remoteId, {
separateWindows() async {
for (final windowId in _remoteDesktopWindows.toList()) {
final String sessionIdList = await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventGetSessionIdList, null);
final idList = sessionIdList.split(';');
if (idList.length <= 1) {
continue;
}
for (final idPair in idList.sublist(1)) {
final peerSession = idPair.split(',');
var params = {
'type': WindowType.RemoteDesktop.index,
'id': peerSession[0],
'session_id': peerSession[1],
};
await _newSession(
true,
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
peerSession[0],
_remoteDesktopWindows,
jsonEncode(params),
);
await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventCloseForSeparateWindow, peerSession[0]);
}
}
}
newSessionWindow(
WindowType type, String remoteId, String msg, List<int> windows) async {
final windowController = await DesktopMultiWindow.createWindow(msg);
windowController
..setFrame(const Offset(0, 0) &
Size(1280 + windowController.windowId * 20,
720 + windowController.windowId * 20))
..center()
..setTitle(getWindowNameWithId(
remoteId,
overrideType: type,
));
if (Platform.isMacOS) {
Future.microtask(() => windowController.show());
}
registerActiveWindow(windowController.windowId);
windows.add(windowController.windowId);
}
_newSession(
bool separateWindow,
WindowType type,
String methodName,
String remoteId,
List<int> windows,
String msg,
) async {
if (separateWindow) {
if (kCloseMultiWindowByHide && _inactiveWindows.isNotEmpty) {
final windowId = _inactiveWindows.first;
final invokeRes =
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
final windowController = WindowController.fromWindowId(windowId);
windowController.show();
registerActiveWindow(windowController.windowId);
windows.add(windowController.windowId);
return invokeRes;
} else {
await newSessionWindow(type, remoteId, msg, windows);
}
} else {
if (windows.isEmpty) {
await newSessionWindow(type, remoteId, msg, windows);
} else {
return call(type, methodName, msg);
}
}
}
Future<dynamic> newSession(
WindowType type,
String methodName,
String remoteId,
List<int> windows, {
String? password,
String? switch_uuid,
bool? forceRelay,
String? switchUuid,
bool? isRDP,
bool forceSeparateWindow = false,
}) async {
var params = {
"type": WindowType.RemoteDesktop.index,
"type": type.index,
"id": remoteId,
"password": password,
"forceRelay": forceRelay
};
if (switch_uuid != null) {
params['switch_uuid'] = switch_uuid;
if (switchUuid != null) {
params['switch_uuid'] = switchUuid;
}
if (isRDP != null) {
params['isRDP'] = isRDP;
}
final msg = jsonEncode(params);
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
if (!ids.contains(_remoteDesktopWindowId)) {
_remoteDesktopWindowId = null;
// separate window for file transfer is not supported
bool separateWindow = forceSeparateWindow ||
(type != WindowType.FileTransfer &&
mainGetLocalBoolOptionSync(kOptionSeparateRemoteWindow));
if (windows.length > 1 || separateWindow) {
for (final windowId in windows) {
if (await DesktopMultiWindow.invokeMethod(
windowId, kWindowEventActiveSession, remoteId)) {
return;
}
}
} on Error {
_remoteDesktopWindowId = null;
}
if (_remoteDesktopWindowId == null) {
final remoteDesktopController =
await DesktopMultiWindow.createWindow(msg);
remoteDesktopController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle(getWindowNameWithId(remoteId,
overrideType: WindowType.RemoteDesktop));
if (Platform.isMacOS) {
Future.microtask(() => remoteDesktopController.show());
}
registerActiveWindow(remoteDesktopController.windowId);
_remoteDesktopWindowId = remoteDesktopController.windowId;
} else {
return call(WindowType.RemoteDesktop, "new_remote_desktop", msg);
}
await _newSession(separateWindow, type, methodName, remoteId, windows, msg);
}
Future<dynamic> newRemoteDesktop(
String remoteId, {
String? password,
String? switchUuid,
bool? forceRelay,
bool forceSeparateWindow = false,
}) async {
return await newSession(
WindowType.RemoteDesktop,
kWindowEventNewRemoteDesktop,
remoteId,
_remoteDesktopWindows,
password: password,
forceRelay: forceRelay,
switchUuid: switchUuid,
forceSeparateWindow: forceSeparateWindow,
);
}
Future<dynamic> newFileTransfer(String remoteId,
{String? password, bool? forceRelay}) async {
var msg = jsonEncode({
"type": WindowType.FileTransfer.index,
"id": remoteId,
"password": password,
"forceRelay": forceRelay,
});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
if (!ids.contains(_fileTransferWindowId)) {
_fileTransferWindowId = null;
}
} on Error {
_fileTransferWindowId = null;
}
if (_fileTransferWindowId == null) {
final fileTransferController = await DesktopMultiWindow.createWindow(msg);
fileTransferController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle(getWindowNameWithId(remoteId,
overrideType: WindowType.FileTransfer));
if (Platform.isMacOS) {
Future.microtask(() => fileTransferController.show());
}
registerActiveWindow(fileTransferController.windowId);
_fileTransferWindowId = fileTransferController.windowId;
} else {
return call(WindowType.FileTransfer, "new_file_transfer", msg);
}
return await newSession(
WindowType.FileTransfer,
kWindowEventNewFileTransfer,
remoteId,
_fileTransferWindows,
password: password,
forceRelay: forceRelay,
);
}
Future<dynamic> newPortForward(String remoteId, bool isRDP,
{String? password, bool? forceRelay}) async {
final msg = jsonEncode({
"type": WindowType.PortForward.index,
"id": remoteId,
"isRDP": isRDP,
"password": password,
"forceRelay": forceRelay,
});
try {
final ids = await DesktopMultiWindow.getAllSubWindowIds();
if (!ids.contains(_portForwardWindowId)) {
_portForwardWindowId = null;
}
} on Error {
_portForwardWindowId = null;
}
if (_portForwardWindowId == null) {
final portForwardController = await DesktopMultiWindow.createWindow(msg);
portForwardController
..setFrame(const Offset(0, 0) & const Size(1280, 720))
..center()
..setTitle(getWindowNameWithId(remoteId,
overrideType: WindowType.PortForward));
if (Platform.isMacOS) {
Future.microtask(() => portForwardController.show());
}
registerActiveWindow(portForwardController.windowId);
_portForwardWindowId = portForwardController.windowId;
} else {
return call(WindowType.PortForward, "new_port_forward", msg);
}
return await newSession(
WindowType.PortForward,
kWindowEventNewPortForward,
remoteId,
_portForwardWindows,
password: password,
forceRelay: forceRelay,
isRDP: isRDP,
);
}
Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
int? windowId = findWindowByType(type);
if (windowId == null) {
final wnds = _findWindowsByType(type);
if (wnds.isEmpty) {
return;
}
return await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
for (final windowId in wnds) {
if (_activeWindows.contains(windowId)) {
return await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
}
}
return await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
}
int? findWindowByType(WindowType type) {
List<int> _findWindowsByType(WindowType type) {
switch (type) {
case WindowType.Main:
return 0;
return [0];
case WindowType.RemoteDesktop:
return _remoteDesktopWindowId;
return _remoteDesktopWindows;
case WindowType.FileTransfer:
return _fileTransferWindowId;
return _fileTransferWindows;
case WindowType.PortForward:
return _portForwardWindowId;
return _portForwardWindows;
case WindowType.Unknown:
break;
}
return null;
return [];
}
void clearWindowType(WindowType type) {
@@ -182,13 +241,13 @@ class RustDeskMultiWindowManager {
case WindowType.Main:
return;
case WindowType.RemoteDesktop:
_remoteDesktopWindowId = null;
_remoteDesktopWindows.clear();
break;
case WindowType.FileTransfer:
_fileTransferWindowId = null;
_fileTransferWindows.clear();
break;
case WindowType.PortForward:
_portForwardWindowId = null;
_portForwardWindows.clear();
break;
case WindowType.Unknown:
break;
@@ -209,27 +268,37 @@ class RustDeskMultiWindowManager {
// skip main window, use window manager instead
return;
}
int? wId = findWindowByType(type);
if (wId != null) {
List<int> 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;
}
// 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();
// unregister the sub window in the main window.
unregisterActiveWindow(wId);
_activeWindows.remove(wId);
} catch (e) {
debugPrint("$e");
return;
} finally {
clearWindowType(type);
}
}
await _notifyActiveWindow();
clearWindowType(type);
}
Future<List<int>> getAllSubWindowIds() async {
@@ -245,7 +314,7 @@ class RustDeskMultiWindowManager {
}
}
List<int> getActiveWindows() {
Set<int> getActiveWindows() {
return _activeWindows;
}
@@ -256,14 +325,19 @@ class RustDeskMultiWindowManager {
}
Future<void> registerActiveWindow(int windowId) async {
if (_activeWindows.contains(windowId)) {
// ignore
} else {
_activeWindows.add(windowId);
}
_activeWindows.add(windowId);
_inactiveWindows.remove(windowId);
await _notifyActiveWindow();
}
Future<void> 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]
@@ -271,10 +345,9 @@ class RustDeskMultiWindowManager {
/// 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 {
if (!_activeWindows.contains(windowId)) {
// ignore
} else {
_activeWindows.remove(windowId);
_activeWindows.remove(windowId);
if (windowId != kMainWindowId) {
_inactiveWindows.add(windowId);
}
await _notifyActiveWindow();
}