feat, open multi windows

Signed-off-by: dignow <linlong1265@gmail.com>
This commit is contained in:
dignow
2023-10-17 00:30:34 +08:00
Unverified
parent f5d8e99fc7
commit bf83d552f8
47 changed files with 363 additions and 80 deletions

View File

@@ -14,8 +14,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/formatter/id_formatter.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
@@ -53,6 +53,9 @@ int androidVersion = 0;
int windowsBuildNumber = 0;
DesktopType? desktopType;
bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm;
/// Check if the app is running with single view mode.
bool isSingleViewApp() {
return desktopType == DesktopType.cm;
@@ -1672,6 +1675,7 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
/// Note that windowId must be provided if it's subwindow
Future<bool> restoreWindowPosition(WindowType type,
{int? windowId, String? peerId}) async {
debugPrintStack(label: "restoreWindowPosition");
if (bind
.mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
.isNotEmpty) {
@@ -2605,3 +2609,109 @@ bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
pi.isSupportMultiDisplay &&
useTextureRender &&
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
Future<List<Rect>> getScreenListWayland() async {
final screenRectList = <Rect>[];
if (isMainDesktopWindow) {
for (var screen in await window_size.getScreenList()) {
final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
double l = screen.frame.left;
double t = screen.frame.top;
double r = screen.frame.right;
double b = screen.frame.bottom;
final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
screenRectList.add(rect);
}
} else {
final screenList = await rustDeskWinManager.call(
WindowType.Main, kWindowGetScreenList, '');
try {
for (var screen in jsonDecode(screenList.result) as List<dynamic>) {
final scale = kIgnoreDpi ? 1.0 : screen['scaleFactor'];
double l = screen['frame']['l'];
double t = screen['frame']['t'];
double r = screen['frame']['r'];
double b = screen['frame']['b'];
final rect = Rect.fromLTRB(l / scale, t / scale, r / scale, b / scale);
screenRectList.add(rect);
}
} catch (e) {
debugPrint('Failed to parse screenList: $e');
}
}
return screenRectList;
}
Future<List<Rect>> getScreenListNotWayland() async {
final screenRectList = <Rect>[];
final displays = bind.mainGetDisplays();
if (displays.isEmpty) {
return screenRectList;
}
try {
for (var display in jsonDecode(displays) as List<dynamic>) {
// to-do: scale factor ?
// final scale = kIgnoreDpi ? 1.0 : screen.scaleFactor;
double l = display['x'].toDouble();
double t = display['y'].toDouble();
double r = (display['x'] + display['w']).toDouble();
double b = (display['y'] + display['h']).toDouble();
screenRectList.add(Rect.fromLTRB(l, t, r, b));
}
} catch (e) {
debugPrint('Failed to parse displays: $e');
}
return screenRectList;
}
Future<List<Rect>> getScreenRectList() async {
return bind.mainCurrentIsWayland()
? await getScreenListWayland()
: await getScreenListNotWayland();
}
openMonitorInTheSameTab(int i, FFI ffi, PeerInfo pi) {
final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index)
: [i];
bind.sessionSwitchDisplay(
sessionId: ffi.sessionId, value: Int32List.fromList(displays));
ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, ffi.id);
}
// Open new tab or window to show this monitor.
// For now just open new window.
//
// screenRect is used to move the new window to the specified screen and set fullscreen.
openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
{Rect? screenRect}) {
final args = {
'window_id': stateGlobal.windowId,
'peer_id': peerId,
'display': i,
'display_count': pi.displays.length,
};
if (screenRect != null) {
args['screen_rect'] = {
'l': screenRect.left,
't': screenRect.top,
'r': screenRect.right,
'b': screenRect.bottom,
};
}
DesktopMultiWindow.invokeMethod(
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
}
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
if (screenRect == null) {
return;
}
final frame =
Rect.fromLTWH(screenRect.left + 30, screenRect.top + 30, 600, 400);
await WindowController.fromWindowId(stateGlobal.windowId).setFrame(frame);
// An duration is needed to avoid the window being restored after fullscreen.
Future.delayed(Duration(milliseconds: 300), () async {
stateGlobal.setFullscreen(true);
});
}

View File

@@ -29,6 +29,7 @@ const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info";
const String kWindowGetScreenList = "get_screen_list";
const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide";
@@ -65,6 +66,7 @@ const String kPointerEventKindTouch = "touch";
const String kPointerEventKindMouse = "mouse";
const String kKeyShowDisplaysAsIndividualWindows = 'displays_as_individual_windows';
const String kKeyUseAllMyMonitorsWhenConnecting = 'use_all_my_monitors_when_connecting';
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
// the executable name of the portable version

View File

@@ -329,8 +329,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
"Click to download", () async {
final Uri url = Uri.parse('https://rustdesk.com/download');
await launchUrl(url);
},
closeButton: true);
}, closeButton: true);
}
if (systemError.isNotEmpty) {
return buildInstallCard("", systemError, "", () {});
@@ -397,7 +396,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Widget buildInstallCard(String title, String content, String btnText,
GestureTapCallback onPressed,
{String? help, String? link, bool? closeButton}) {
void closeCard() {
setState(() {
isCardClosed = true;
@@ -555,6 +553,22 @@ class _DesktopHomePageState extends State<DesktopHomePage>
Get.put<RxBool>(svcStopped, tag: 'stop-service');
rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged);
screenToMap(window_size.Screen screen) => {
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
};
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
debugPrint(
"[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
@@ -563,24 +577,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
} else if (call.method == kWindowGetWindowInfo) {
final screen = (await window_size.getWindowInfo()).screen;
if (screen == null) {
return "";
return '';
} else {
return jsonEncode({
'frame': {
'l': screen.frame.left,
't': screen.frame.top,
'r': screen.frame.right,
'b': screen.frame.bottom,
},
'visibleFrame': {
'l': screen.visibleFrame.left,
't': screen.visibleFrame.top,
'r': screen.visibleFrame.right,
'b': screen.visibleFrame.bottom,
},
'scaleFactor': screen.scaleFactor,
});
return jsonEncode(screenToMap(screen));
}
} else if (call.method == kWindowGetScreenList) {
return jsonEncode(
(await window_size.getScreenList()).map(screenToMap).toList());
} else if (call.method == kWindowActionRebuild) {
reloadCurrentWindow();
} else if (call.method == kWindowEventShow) {
@@ -613,8 +616,9 @@ class _DesktopHomePageState extends State<DesktopHomePage>
final peerId = args['peer_id'] as String;
final display = args['display'] as int;
final displayCount = args['display_count'] as int;
final screenRect = args['screen_rect'];
await rustDeskWinManager.openMonitorSession(
windowId, peerId, display, displayCount);
windowId, peerId, display, displayCount, screenRect);
}
});
_uniLinksSubscription = listenUniLinks();

View File

@@ -1324,6 +1324,8 @@ class _DisplayState extends State<_Display> {
if (useTextureRender) {
children.add(otherRow('Show displays as individual windows',
kKeyShowDisplaysAsIndividualWindows));
children.add(otherRow('Use all my displays when connecting',
kKeyUseAllMyMonitorsWhenConnecting));
}
return _Card(title: 'Other Default Options', children: children);
}

View File

@@ -48,6 +48,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
late ToolbarState _toolbarState;
String? peerId;
bool isScreenRectSet = false;
var connectionMap = RxList<Widget>.empty(growable: true);
@@ -59,6 +60,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabWindowId = params['tab_window_id'];
final display = params['display'];
final displays = params['displays'];
final screenRect = parseScreenRect(params);
isScreenRectSet = screenRect != null;
tryMoveToScreenAndSetFullscreen(screenRect);
if (peerId != null) {
ConnectionTypeState.init(peerId!);
tabController.onSelected = (id) {
@@ -95,6 +99,18 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
}
}
parseScreenRect(Map<String, dynamic> params) {
Rect? screenRect;
if (params['screen_rect'] != null) {
double l = params['screen_rect']['l'];
double t = params['screen_rect']['t'];
double r = params['screen_rect']['r'];
double b = params['screen_rect']['b'];
screenRect = Rect.fromLTRB(l, t, r, b);
}
return screenRect;
}
@override
void initState() {
super.initState();
@@ -115,7 +131,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
final tabWindowId = args['tab_window_id'];
final display = args['display'];
final displays = args['displays'];
final screenRect = parseScreenRect(args);
windowOnTop(windowId());
tryMoveToScreenAndSetFullscreen(screenRect);
if (tabController.length == 0) {
if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
stateGlobal.setFullscreen(true);
@@ -196,15 +214,17 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
_update_remote_count();
return returnValue;
});
Future.delayed(Duration.zero, () {
restoreWindowPosition(
WindowType.RemoteDesktop,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
);
});
if (!isScreenRectSet) {
Future.delayed(Duration.zero, () {
restoreWindowPosition(
WindowType.RemoteDesktop,
windowId: windowId(),
peerId: tabController.state.value.tabs.isEmpty
? null
: tabController.state.value.tabs[0].key,
);
});
}
}
@override
@@ -451,6 +471,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
c++;
}
}
loopCloseWindow();
}
ConnectionTypeState.delete(id);

View File

@@ -1,12 +1,10 @@
import 'dart:convert';
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/models/desktop_render_texture.dart';
@@ -744,42 +742,14 @@ class _MonitorMenu extends StatelessWidget {
);
}
// Open new tab or window to show this monitor.
// For now just open new window.
openMonitorInNewTabOrWindow(int i, PeerInfo pi) {
if (kWindowId == null) {
// unreachable
debugPrint('openMonitorInNewTabOrWindow, unreachable! kWindowId is null');
return;
}
DesktopMultiWindow.invokeMethod(
kMainWindowId,
kWindowEventOpenMonitorSession,
jsonEncode({
'window_id': kWindowId!,
'peer_id': ffi.id,
'display': i,
'display_count': pi.displays.length,
}));
}
openMonitorInTheSameTab(int i, PeerInfo pi) {
final displays = i == kAllDisplayValue
? List.generate(pi.displays.length, (index) => index)
: [i];
bind.sessionSwitchDisplay(
sessionId: ffi.sessionId, value: Int32List.fromList(displays));
ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, id);
}
onPressed(int i, PeerInfo pi) {
_menuDismissCallback(ffi);
RxInt display = CurrentDisplayState.find(id);
if (display.value != i) {
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
openMonitorInNewTabOrWindow(i, pi);
openMonitorInNewTabOrWindow(i, ffi.id, pi);
} else {
openMonitorInTheSameTab(i, pi);
openMonitorInTheSameTab(i, ffi, pi);
}
}
}

View File

@@ -198,8 +198,11 @@ void runMultiWindow(
}
switch (appType) {
case kAppTypeDesktopRemote:
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: kWindowId!, peerId: argument['id'] as String?);
// If screen rect is set, the window will be moved to the target screen and then set fullscreen.
if (argument['screen_rect'] == null) {
await restoreWindowPosition(WindowType.RemoteDesktop,
windowId: kWindowId!, peerId: argument['id'] as String?);
}
break;
case kAppTypeDesktopFileTransfer:
await restoreWindowPosition(WindowType.FileTransfer,

View File

@@ -227,7 +227,7 @@ class FfiModel with ChangeNotifier {
}, sessionId, peerId);
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
setConnectionType(peerId, data.secure, data.direct);
await handlePeerInfo(data.peerInfo, peerId);
await handlePeerInfo(data.peerInfo, peerId, true);
for (final element in data.cursorDataList) {
updateLastCursorId(element);
await handleCursorData(element);
@@ -245,7 +245,7 @@ class FfiModel with ChangeNotifier {
if (name == 'msgbox') {
handleMsgBox(evt, sessionId, peerId);
} else if (name == 'peer_info') {
handlePeerInfo(evt, peerId);
handlePeerInfo(evt, peerId, false);
} else if (name == 'sync_peer_info') {
handleSyncPeerInfo(evt, sessionId, peerId);
} else if (name == 'connection_ready') {
@@ -623,7 +623,7 @@ class FfiModel with ChangeNotifier {
}
/// Handle the peer info event based on [evt].
handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
cachedPeerData.peerInfo = evt;
// recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
@@ -703,6 +703,51 @@ class FfiModel with ChangeNotifier {
stateGlobal.resetLastResolutionGroupValues(peerId);
notifyListeners();
if (!isCache) {
tryUseAllMyDisplaysWhenConnecting(peerId);
}
}
tryUseAllMyDisplaysWhenConnecting(String peerId) async {
if (bind.mainGetUserDefaultOption(
key: kKeyUseAllMyMonitorsWhenConnecting) !=
'Y') {
return;
}
if (!_pi.isSupportMultiDisplay || _pi.displays.length <= 1) {
return;
}
final screenRectList = await getScreenRectList();
if (screenRectList.length <= 1) {
return;
}
// to-do: peer currentDisplay is the primary display, but the primary display may not be the first display.
// local primary display also may not be the first display.
//
// 0 is assumed to be the primary display here, for now.
// move to the first display and set fullscreen
bind.sessionSwitchDisplay(
sessionId: sessionId, value: Int32List.fromList([0]));
_pi.currentDisplay = 0;
try {
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
} catch (e) {
//
}
await tryMoveToScreenAndSetFullscreen(screenRectList[0]);
final length = _pi.displays.length < screenRectList.length
? _pi.displays.length
: screenRectList.length;
for (var i = 1; i < length; i++) {
openMonitorInNewTabOrWindow(i, peerId, _pi,
screenRect: screenRectList[i]);
}
}
tryShowAndroidActionsOverlay({int delayMSecs = 10}) {

View File

@@ -64,13 +64,14 @@ class RustDeskMultiWindowManager {
peerId,
_remoteDesktopWindows,
jsonEncode(params),
false,
);
}
// 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) async {
openMonitorSession(int windowId, String peerId, int display, int displayCount,
dynamic screenRect) async {
if (_remoteDesktopWindows.length > 1) {
for (final windowId in _remoteDesktopWindows) {
if (await DesktopMultiWindow.invokeMethod(
@@ -94,6 +95,7 @@ class RustDeskMultiWindowManager {
'tab_window_id': windowId,
'display': display,
'displays': displays,
'screen_rect': screenRect,
};
await _newSession(
false,
@@ -102,21 +104,34 @@ class RustDeskMultiWindowManager {
peerId,
_remoteDesktopWindows,
jsonEncode(params),
screenRect != null,
);
}
Future<int> newSessionWindow(
WindowType type, String remoteId, String msg, List<int> windows) async {
WindowType type,
String remoteId,
String msg,
List<int> windows,
bool withScreenRect,
) async {
final windowController = await DesktopMultiWindow.createWindow(msg);
final windowId = windowController.windowId;
windowController
..setFrame(
const Offset(0, 0) & Size(1280 + windowId * 20, 720 + windowId * 20))
..center()
..setTitle(getWindowNameWithId(
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 (Platform.isMacOS) {
Future.microtask(() => windowController.show());
}
@@ -132,10 +147,12 @@ class RustDeskMultiWindowManager {
String remoteId,
List<int> windows,
String msg,
bool withScreenRect,
) async {
if (openInTabs) {
if (windows.isEmpty) {
final windowId = await newSessionWindow(type, remoteId, msg, windows);
final windowId = await newSessionWindow(
type, remoteId, msg, windows, withScreenRect);
return MultiWindowCallResult(windowId, null);
} else {
return call(type, methodName, msg);
@@ -153,7 +170,8 @@ class RustDeskMultiWindowManager {
}
}
}
final windowId = await newSessionWindow(type, remoteId, msg, windows);
final windowId =
await newSessionWindow(type, remoteId, msg, windows, withScreenRect);
return MultiWindowCallResult(windowId, null);
}
}
@@ -195,7 +213,8 @@ class RustDeskMultiWindowManager {
}
}
return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
return _newSession(
openInTabs, type, methodName, remoteId, windows, msg, false);
}
Future<MultiWindowCallResult> newRemoteDesktop(