mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
* Repurposed the MacOS-specific platform channel mechanism for all platforms: - Renamed the channel from "org.rustdesk.rustdesk/macos" to "org.rustdesk.rustdesk/host". - Renamed _osxMethodChannel in platform_channel.dart to _hostMethodChannel. - Updated linux/my_application.cc to use the fl_* API to set up a Method Channel and to dispose it during my_application_dispose. - Updated windows/runner/flutter_window.cpp to use the C++ API to set up a Method Channel. - Updated the channel name in macos/Runner/MainFlutterWindow.swift. Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Added a method "bumpMouse" to the Platform Channel. Added a thunk to call the method through the channel to platform_channel.dart. Added implementation bump_mouse() in linux/my_application.cc using Gdk API calls. Updated host_channel_call_handler to process "bumpMouse" method call messages by calling bump_mouse. Added implementation Win32Desktop::BumpMouse in windows/runner/win32_desktop.cpp/.h. Updated the inline method call handler in flutter_window.cpp to handle "bumpMouse" method calls by calling Win32Desktop::BumpMouse. Updated the method call handler in macos/Runner/MainFlutterWindow.swift to handle "bumpMouse" method call messages. Updated MainFlutterWindow to use a subclass of FlutterViewController exposing access to mouseLocationOutsideOfEventStream. Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Added message type kWindowBumpMouse to the multiwindow window event model: - Added constant kWindowBumpMouse to consts.dart. - Updated the method handler attached to rustDeskWinManager by DesktopHomePageState to recognize kWindowBumpMouse and translate it to a call to RdPlatformChannel.bumpMouse. Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Centralized serialization of ScrollStyle values, moving JSON and string conversions into methods toString/fromString and toJson/fromJson within the type. Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Added new scroll style for edge scrolling: - Added ScrollStyle enum member "scrolledge". Added corresponding constant kRemoteScrollStyleEdge to consts.dart for the string serialized form. - Updated sites checking specifically for ScrollStyle.scrollbar to instead check for NOT ScrollStyle.scrollauto. - Added radio buttons for the new "ScrollEdge" style to desktop_setting_page.dart and remote_toolbar.dart. Added new string "ScrollEdge" to lang/template.rs. Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Implemented edge scrolling: - Added methods edgeScrollMouse and pushScrollPositionToUI to class CanvasModel in model.dart. - Added boolean parameter edgeScroll to handleMouse, handlePointerDevicePos and processEventToPeer in input_model.dart. - Updated handlePointerDevicePos in input_model.dart to call edgeScrollMouse on move events when the edgeScroll parameter is true. - Added convenience accessor useEdgeScroll to the InputModel class. Updated call sites to handleMouse to use it to supply the value for the edgeScroll parameter. Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Updated CanvasModel.edgeScrollMouse to be resilient to receiving events when _horizontal/_vertical aren't wired up to any UI. * Updated CanvasModel to take notifications of resizes via method notifyResize and to suppress edge scrolling briefly after a resize. Updated the onWindowResized handler in tabbar_widget.dart to call notifyResize on the canvasModel of any RemotePage tabs. * Half a go at fixing MainFlutterWindow.swift. * Copilot feedback. * Applied fix suggested by Copilot in its explanation of the build error. * Fixed a couple of silly errors in windows/runner/flutter_window.cpp. * Fixed MainFlutterWindow.swift build errors. Co-Authored-By: fufesou <linlong1266@gmail.com> Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Moved new translation to the end of template.rs. Reran res/lang.py. Signed-off-by: Jonathan Gilbert <logic@deltaq.org> * Switched MainFlutterWindow.swift to use NSEvent.mouseLocation. * Updated MainFlutterWindow.swift code based on build error. * Fixed silly typo. * Reintroduced the coordinate system translation in MainFlutterWindow.swift. * Updated edgeScrollMouse in model.dart to add a "safe zone" around the window frame that doesn't trigger edge scrolling. * Updated the bumpMouse handler in MainFlutterWindow.swift to call CGAssociateMouseAndMouseCursorPosition to cancel event suppression. * Added debug annotation to the onWindowResized event in tabbar_widget.dart. * Fix parameter type for CGAssociateMouseAndMouseCursorPosition in MainFlutterWindow.swift. * tabbar_widget.dart: onWindowResized -> onWindowResize * Removed temporary diagnostic debugPrint from tabbar_widget.dart. * Updated MainFlutterWindow.swift to obtain the mouse position by creating a dummy CGEvent. The old NSEvent.mouseLocation code is left as a fallback. * The documentation said to be sure to call CFRelease, but apparently it's a build error to do so. :-P * Replaced CGEvent calls in MainFlutterWindow.swift with uses of the CGEvent wrapper struct. * Added argument label to call to CGEvent.init. * Changed mouseLoc from piecewise assignment to assignment of the whole structure, as it is not yet initialized at that point. * Linux platform channel: Refactored bump_mouse, setting the stage for a future Wayland implementation. - Made a new top-level bump_mouse method in bump_mouse.cc/.h. - Moved the X11-specific implementation to bump_mouse_x11 in bump_mouse_x11.cc/h. Reworked the bumpMouse operation to have a boolean return value: - Updated bumpMouse in platform_channel.dart to return a Future<bool> instead of a Future<void>. - Windows platform channel: Updated BumpMouse in win32_desktop.cpp to return a bool value. Updated the method call handler "bumpMouse" branch in flutter_window.cpp to propagate the BumpMouse return value back to the originating MethodCall. - MacOS platform channel: Updated the "bumpMouse" branch in the method call handler in MainFlutterWindow.swift to pass true or false into the 'result()' call. - Linux platform channel: Updated the bump_mouse top-level method and its underlying implementation bump_mouse_x11 to return bool values. Updated the "bumpMouse" branch of host_channel_call_handler in my_application.cc to propagate the result value back up the method channel. - Updated the kWindowBumpMouse branch of the method handler registered in desktop_home_page.dart to propagate a return value from RdplatformChannel.bumpMouse. * Reworked the edge scrolling computations in model.dart to use Vector2 from the vector_math package. Updated pubspec.yaml to declare a dependency on vector_math. * Added an alternative edge scrolling mechanism for when "Bump Mouse" functionality is unavailable: - Added methods setEdgeScrollTimer and cancelEdgeScrollTimer to model.dart, along with a few state fields. - Updated edgeScrollMouse to latch the (x, y) coordinate of the last edge scroll event, in case it will be autorepeating. - Updated edgeScrollMouse to check whether the call to the kWindowBumpMouse method of rustDeskWinManager (and thus the underlying bump_mouse method) succeeded, and to switch to timer-based autorepeat if it fails. Made edgeScrollMouse async to allow awaiting the result of the kWindowBumpMouse method call. - Updated input_model.dart to call cancelEdgeScrollTimer when a new move event is being processed. - Updated remote_page.dart to call cancelEdgeScrollTimer when the pointer exits the area represented by the view. * Fixed scroll percentage math in edgeScrollMouse in model.dart. * Fixed declared return value for Win32Desktop::BumpMouse in win32_desktop.h. * Fixed vector_math dependency version in pubspec.yaml to be compatible with the codebase standard Flutter version. * Added class EdgeScrollFallbackState to model.dart for tracking the state of the edge scroll fallback strategy. Factored out the actual edge scrolling action from CanvasModel.edgeScrollMouse to new method performEdgeScroll so that EdgeScrollFallbackState can call it. Updated edgeScrollMouse to not call performEdgeScroll when it's enabling the fallback strategy. Updated CanvasModel to use EdgeScrollFallbackState instead of directly tracking the state. Removed method setEdgeScrollTimer. Added method initializeEdgeScrollFallback to CanvasModel that takes a TickerProvider. Updated _RemotePageState to include the mixin TickerProviderStateMixin. Updated _RemotePageState.initState to call canvasModel.initializeEdgeScrollFallback. Updated handlePointerDevicePos in input_model.dart to not call cancelEdgeScrollTimer before edgeScrollMouse. Renamed CanvasModel.cancelEdgeScrollTimer to CanvasModel.cancelEdgeScroll. Updated the calculations in CanvasModel.edgeScrollMouse to only factor in the safe zone if BumpMouse is working. (Otherwise the problem with resizing can't possibly occur.) * Updated CanvasModel.edgeScrollMouse in model.dart to handle the situation where only one of the scrollbars is active. Factored extraction of scrollbar data into new function getScrollInfo. * Updated onWindowResize in tabbar_widget.dart to be resilient to RemotePage instances that don't yet have an ffi reference. Added property hasFFI to remote_page.dart. * Removed debug output from model.dart. * PR feedback: - Added filtering to diagnostic output in the method handler in desktop_home_page.dart to exclude the very chatty kWindowBumpMouse-related output. - Removed the diagnostic output from bumpMouse in platform_channel.dart for the same reason. - Updated setScrollPercent to coalesce NaN values for x and y to 0. - Initialized the GError pointer variable passed into fl_method_call_respond_success in linux/my_application.cc to NULL. - Added bounds checking of the argument values in the EncodableList branch of the "bumpMouse" method call handler in windows/runner/flutter_window.cpp. * Added a latch mechanism that keeps edge scrolling disabled until the cursor is observed to be in the inner area bounded by the edge scroll areas: - Added tristate enumerated type EdgeScrollState to model.dart. In addition to inactive and active states, there is state armed which behaves like inactive but can transition to active when conditions are met. - Added a field to CanvasModel of type EdgeScrollState. Added methods disableEdgeScroll and rearmEdgeScroll. - Updated enterView to call canvasModel.rearmEdgeScroll and leaveView to call canvasModel.disableEdgeScroll in remote_page.dart. - Updated edgeScrollMouse to check the state, disabling edge scrolling when the state is not active and transitioning from armed to active when the mouse is in the interior space. - Removed the notifyResize/_suppressEdgeScroll mechanism from CanvasModel in model.dart as it is no longer necessary. - Removed the "safe zone" mechanism from CanvasModel.edgeScrollMouse in model.dart as it is no longer necessary. - Switched the onWindowResize handler in DesktopTabState in tabbar_widget.dart back to onWindowResized, now that it is no longer delivering canvasModel.notifyResize to all RemotePage tabs. * Fixed memory leak: Added call to free GError object returned by Flutter API in the event of an error. * PR feedback: - Copilot: Use type annotations. - Copilot: Condition to stop edge scrolling when fallback strategy is in use and the mouse is moved back to the centre. - Copilot: Check FLValue type before calling fl_value_get_int. - Copilot: Support list-style method channel dispatch in "bumpMouse" handler for macos as the linux and windows implementations already do. - Naming convention for constants. - Left-over variable from previous strategy: _suppressEdgeScroll. - Unnecessary extra parentheses in edge scroll area conditions. * Removed property suppressEdgeScroll referencing now-removed field _suppressEdgeScroll in model.dart. Removed accidental extra blank line in MainFlutterWindow.swift. * Switched CanvasModel.setScrollPercent to use double.isFinite instead of double.isNaN to test for proper numerical values. * PR feedback: - Copilot: Use Vector2.length2 instead of Vector2.length to avoid an unnecessary sqrt in comparison with zero. - Copilot: Baleet unnecessary semicolons from Swift code. * PR feedback: - Copilot: Check argList.count before indexing it * Oops with the semicolons again. * Edge scroll, active local cursor Signed-off-by: fufesou <linlong1266@gmail.com> * Remove duplicated condition checks Signed-off-by: fufesou <linlong1266@gmail.com> * Chore Signed-off-by: fufesou <linlong1266@gmail.com> * PR feedback: - Copilot: Removed unused property hasFFI from remote_page.dart. - Copilot: Updated updateScrollStyle in model.dart to be resilient to the possibility of bind.sessionGetScrollStyle returning null. * Factored local cursor updates out of CanvasModel.moveDesktopMouse in model.dart, adding new methods activateLocalCursor and updateLocalCursor. Updated handlePointerDevicePos in input_model.dart to call canvasModel.updateLocalCursor on every mouse event. Updated initState in remote_page.dart to schedule a call to canvasModel.activateLocalCursor as a first-image callback. * Updated the explanation for rounding away from 0 in edgeScrollMouse in model.dart. --------- Signed-off-by: Jonathan Gilbert <logic@deltaq.org> Signed-off-by: fufesou <linlong1266@gmail.com> Co-authored-by: fufesou <linlong1266@gmail.com>
898 lines
29 KiB
Dart
898 lines
29 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
import 'package:flutter_hbb/models/state_model.dart';
|
|
|
|
import '../../consts.dart';
|
|
import '../../common/widgets/overlay.dart';
|
|
import '../../common/widgets/remote_input.dart';
|
|
import '../../common.dart';
|
|
import '../../common/widgets/dialog.dart';
|
|
import '../../common/widgets/toolbar.dart';
|
|
import '../../models/model.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../../common/shared_state.dart';
|
|
import '../../utils/image.dart';
|
|
import '../widgets/remote_toolbar.dart';
|
|
import '../widgets/kb_layout_type_chooser.dart';
|
|
import '../widgets/tabbar_widget.dart';
|
|
|
|
import 'package:flutter_hbb/native/custom_cursor.dart'
|
|
if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
|
|
|
|
final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
|
|
|
|
// Used to skip session close if "move to new window" is clicked.
|
|
final Map<String, bool> closeSessionOnDispose = {};
|
|
|
|
class RemotePage extends StatefulWidget {
|
|
RemotePage({
|
|
Key? key,
|
|
required this.id,
|
|
required this.toolbarState,
|
|
this.sessionId,
|
|
this.tabWindowId,
|
|
this.password,
|
|
this.display,
|
|
this.displays,
|
|
this.tabController,
|
|
this.switchUuid,
|
|
this.forceRelay,
|
|
this.isSharedPassword,
|
|
}) : super(key: key) {
|
|
initSharedStates(id);
|
|
}
|
|
|
|
final String id;
|
|
final SessionID? sessionId;
|
|
final int? tabWindowId;
|
|
final int? display;
|
|
final List<int>? displays;
|
|
final String? password;
|
|
final ToolbarState toolbarState;
|
|
final String? switchUuid;
|
|
final bool? forceRelay;
|
|
final bool? isSharedPassword;
|
|
final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null);
|
|
final DesktopTabController? tabController;
|
|
|
|
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
|
|
|
|
@override
|
|
State<RemotePage> createState() {
|
|
final state = _RemotePageState(id);
|
|
_lastState.value = state;
|
|
return state;
|
|
}
|
|
}
|
|
|
|
class _RemotePageState extends State<RemotePage>
|
|
with AutomaticKeepAliveClientMixin, MultiWindowListener, TickerProviderStateMixin {
|
|
Timer? _timer;
|
|
String keyboardMode = "legacy";
|
|
bool _isWindowBlur = false;
|
|
final _cursorOverImage = false.obs;
|
|
late RxBool _showRemoteCursor;
|
|
late RxBool _zoomCursor;
|
|
late RxBool _remoteCursorMoved;
|
|
late RxBool _keyboardEnabled;
|
|
|
|
var _blockableOverlayState = BlockableOverlayState();
|
|
|
|
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
|
|
|
|
// We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
|
|
// to identify the toolbar instance and its callback function.
|
|
int? _instanceIdOnEnterOrLeaveImage4Toolbar;
|
|
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
|
|
|
late FFI _ffi;
|
|
|
|
SessionID get sessionId => _ffi.sessionId;
|
|
|
|
_RemotePageState(String id) {
|
|
_initStates(id);
|
|
}
|
|
|
|
void _initStates(String id) {
|
|
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
|
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ffi = FFI(widget.sessionId);
|
|
Get.put<FFI>(_ffi, tag: widget.id);
|
|
_ffi.imageModel.addCallbackOnFirstImage((String peerId) {
|
|
_ffi.canvasModel.activateLocalCursor();
|
|
showKBLayoutTypeChooserIfNeeded(
|
|
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
|
|
_ffi.recordingModel
|
|
.updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
|
|
});
|
|
_ffi.canvasModel.initializeEdgeScrollFallback(this);
|
|
_ffi.start(
|
|
widget.id,
|
|
password: widget.password,
|
|
isSharedPassword: widget.isSharedPassword,
|
|
switchUuid: widget.switchUuid,
|
|
forceRelay: widget.forceRelay,
|
|
tabWindowId: widget.tabWindowId,
|
|
display: widget.display,
|
|
displays: widget.displays,
|
|
);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
|
_ffi.dialogManager
|
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
|
});
|
|
if (!isLinux) {
|
|
WakelockPlus.enable();
|
|
}
|
|
|
|
_ffi.ffiModel.updateEventListener(sessionId, widget.id);
|
|
if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
|
|
_ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
|
_ffi.dialogManager.loadMobileActionsOverlayVisible();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
// Session option should be set after models.dart/FFI.start
|
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
|
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
|
sessionId: sessionId, arg: kOptionZoomCursor);
|
|
});
|
|
DesktopMultiWindow.addListener(this);
|
|
// if (!_isCustomCursorInited) {
|
|
// customCursorController.registerNeedUpdateCursorCallback(
|
|
// (String? lastKey, String? currentKey) async {
|
|
// if (_firstEnterImage.value) {
|
|
// _firstEnterImage.value = false;
|
|
// return true;
|
|
// }
|
|
// return lastKey == null || lastKey != currentKey;
|
|
// });
|
|
// _isCustomCursorInited = true;
|
|
// }
|
|
|
|
_blockableOverlayState.applyFfi(_ffi);
|
|
// Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
widget.tabController?.onSelected?.call(widget.id);
|
|
});
|
|
}
|
|
|
|
@override
|
|
void onWindowBlur() {
|
|
super.onWindowBlur();
|
|
// On windows, we use `focus` way to handle keyboard better.
|
|
// Now on Linux, there's some rdev issues which will break the input.
|
|
// We disable the `focus` way for non-Windows temporarily.
|
|
if (isWindows) {
|
|
_isWindowBlur = true;
|
|
// unfocus the primary-focus when the whole window is lost focus,
|
|
// and let OS to handle events instead.
|
|
_rawKeyFocusNode.unfocus();
|
|
}
|
|
stateGlobal.isFocused.value = false;
|
|
}
|
|
|
|
@override
|
|
void onWindowFocus() {
|
|
super.onWindowFocus();
|
|
// See [onWindowBlur].
|
|
if (isWindows) {
|
|
_isWindowBlur = false;
|
|
}
|
|
stateGlobal.isFocused.value = true;
|
|
}
|
|
|
|
@override
|
|
void onWindowRestore() {
|
|
super.onWindowRestore();
|
|
// On windows, we use `onWindowRestore` way to handle window restore from
|
|
// a minimized state.
|
|
if (isWindows) {
|
|
_isWindowBlur = false;
|
|
}
|
|
if (!isLinux) {
|
|
WakelockPlus.enable();
|
|
}
|
|
}
|
|
|
|
// When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
|
|
@override
|
|
void onWindowMaximize() {
|
|
super.onWindowMaximize();
|
|
if (!isLinux) {
|
|
WakelockPlus.enable();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onWindowMinimize() {
|
|
super.onWindowMinimize();
|
|
if (!isLinux) {
|
|
WakelockPlus.disable();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onWindowEnterFullScreen() {
|
|
super.onWindowEnterFullScreen();
|
|
if (isMacOS) {
|
|
stateGlobal.setFullscreen(true);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onWindowLeaveFullScreen() {
|
|
super.onWindowLeaveFullScreen();
|
|
if (isMacOS) {
|
|
stateGlobal.setFullscreen(false);
|
|
}
|
|
}
|
|
|
|
@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 session $sessionId ${widget.id}");
|
|
_ffi.textureModel.onRemotePageDispose(closeSession);
|
|
if (closeSession) {
|
|
// ensure we leave this session, this is a double check
|
|
_ffi.inputModel.enterOrLeave(false);
|
|
}
|
|
DesktopMultiWindow.removeListener(this);
|
|
_ffi.dialogManager.hideMobileActionsOverlay();
|
|
_ffi.imageModel.disposeImage();
|
|
_ffi.cursorModel.disposeImages();
|
|
_rawKeyFocusNode.dispose();
|
|
await _ffi.close(closeSession: closeSession);
|
|
_timer?.cancel();
|
|
_ffi.dialogManager.dismissAll();
|
|
if (closeSession) {
|
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
|
overlays: SystemUiOverlay.values);
|
|
}
|
|
if (!isLinux) {
|
|
await WakelockPlus.disable();
|
|
}
|
|
await Get.delete<FFI>(tag: widget.id);
|
|
removeSharedStates(widget.id);
|
|
}
|
|
|
|
Widget emptyOverlay() => BlockableOverlay(
|
|
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
|
|
/// see override build() in [BlockableOverlay]
|
|
state: _blockableOverlayState,
|
|
underlying: Container(
|
|
color: Colors.transparent,
|
|
),
|
|
);
|
|
|
|
Widget buildBody(BuildContext context) {
|
|
remoteToolbar(BuildContext context) => RemoteToolbar(
|
|
id: widget.id,
|
|
ffi: _ffi,
|
|
state: widget.toolbarState,
|
|
onEnterOrLeaveImageSetter: (id, func) {
|
|
_instanceIdOnEnterOrLeaveImage4Toolbar = id;
|
|
_onEnterOrLeaveImage4Toolbar = func;
|
|
},
|
|
onEnterOrLeaveImageCleaner: (id) {
|
|
// If _instanceIdOnEnterOrLeaveImage4Toolbar != id
|
|
// it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
|
|
if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
|
|
_instanceIdOnEnterOrLeaveImage4Toolbar = null;
|
|
_onEnterOrLeaveImage4Toolbar = null;
|
|
}
|
|
},
|
|
setRemoteState: setState,
|
|
);
|
|
|
|
bodyWidget() {
|
|
return Stack(
|
|
children: [
|
|
Container(
|
|
color: kColorCanvas,
|
|
child: RawKeyFocusScope(
|
|
focusNode: _rawKeyFocusNode,
|
|
onFocusChange: (bool imageFocused) {
|
|
debugPrint(
|
|
"onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
|
|
// See [onWindowBlur].
|
|
if (isWindows) {
|
|
if (_isWindowBlur) {
|
|
imageFocused = false;
|
|
Future.delayed(Duration.zero, () {
|
|
_rawKeyFocusNode.unfocus();
|
|
});
|
|
}
|
|
if (imageFocused) {
|
|
_ffi.inputModel.enterOrLeave(true);
|
|
} else {
|
|
_ffi.inputModel.enterOrLeave(false);
|
|
}
|
|
}
|
|
},
|
|
inputModel: _ffi.inputModel,
|
|
child: getBodyForDesktop(context))),
|
|
Stack(
|
|
children: [
|
|
_ffi.ffiModel.pi.isSet.isTrue &&
|
|
_ffi.ffiModel.waitForFirstImage.isTrue
|
|
? emptyOverlay()
|
|
: () {
|
|
if (!_ffi.ffiModel.isPeerAndroid) {
|
|
return Offstage();
|
|
} else {
|
|
return Obx(() => Offstage(
|
|
offstage: _ffi.dialogManager
|
|
.mobileActionsOverlayVisible.isFalse,
|
|
child: Overlay(initialEntries: [
|
|
makeMobileActionsOverlayEntry(
|
|
() => _ffi.dialogManager
|
|
.setMobileActionsOverlayVisible(false),
|
|
ffi: _ffi,
|
|
)
|
|
]),
|
|
));
|
|
}
|
|
}(),
|
|
// Use Overlay to enable rebuild every time on menu button click.
|
|
_ffi.ffiModel.pi.isSet.isTrue
|
|
? Overlay(
|
|
initialEntries: [OverlayEntry(builder: remoteToolbar)])
|
|
: remoteToolbar(context),
|
|
_ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
backgroundColor: Theme.of(context).colorScheme.background,
|
|
body: Obx(() {
|
|
final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
|
|
_ffi.ffiModel.waitForFirstImage.isFalse;
|
|
if (imageReady) {
|
|
// If the privacy mode(disable physical displays) is switched,
|
|
// we should not dismiss the dialog immediately.
|
|
if (DateTime.now().difference(togglePrivacyModeTime) >
|
|
const Duration(milliseconds: 3000)) {
|
|
// `dismissAll()` is to ensure that the state is clean.
|
|
// It's ok to call dismissAll() here.
|
|
_ffi.dialogManager.dismissAll();
|
|
// Recreate the block state to refresh the state.
|
|
_blockableOverlayState = BlockableOverlayState();
|
|
_blockableOverlayState.applyFfi(_ffi);
|
|
}
|
|
// Block the whole `bodyWidget()` when dialog shows.
|
|
return BlockableOverlay(
|
|
underlying: bodyWidget(),
|
|
state: _blockableOverlayState,
|
|
);
|
|
} else {
|
|
// `_blockableOverlayState` is not recreated here.
|
|
// The toolbar's block state won't work properly when reconnecting, but that's okay.
|
|
return bodyWidget();
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return WillPopScope(
|
|
onWillPop: () async {
|
|
clientClose(sessionId, _ffi.dialogManager);
|
|
return false;
|
|
},
|
|
child: MultiProvider(providers: [
|
|
ChangeNotifierProvider.value(value: _ffi.ffiModel),
|
|
ChangeNotifierProvider.value(value: _ffi.imageModel),
|
|
ChangeNotifierProvider.value(value: _ffi.cursorModel),
|
|
ChangeNotifierProvider.value(value: _ffi.canvasModel),
|
|
ChangeNotifierProvider.value(value: _ffi.recordingModel),
|
|
], child: buildBody(context)));
|
|
}
|
|
|
|
void enterView(PointerEnterEvent evt) {
|
|
_ffi.canvasModel.rearmEdgeScroll();
|
|
|
|
_cursorOverImage.value = true;
|
|
_firstEnterImage.value = true;
|
|
if (_onEnterOrLeaveImage4Toolbar != null) {
|
|
try {
|
|
_onEnterOrLeaveImage4Toolbar!(true);
|
|
} catch (e) {
|
|
//
|
|
}
|
|
}
|
|
// See [onWindowBlur].
|
|
if (!isWindows) {
|
|
if (!_rawKeyFocusNode.hasFocus) {
|
|
_rawKeyFocusNode.requestFocus();
|
|
}
|
|
_ffi.inputModel.enterOrLeave(true);
|
|
}
|
|
}
|
|
|
|
void leaveView(PointerExitEvent evt) {
|
|
_ffi.canvasModel.disableEdgeScroll();
|
|
|
|
if (_ffi.ffiModel.keyboard) {
|
|
_ffi.inputModel.tryMoveEdgeOnExit(evt.position);
|
|
}
|
|
|
|
_cursorOverImage.value = false;
|
|
_firstEnterImage.value = false;
|
|
if (_onEnterOrLeaveImage4Toolbar != null) {
|
|
try {
|
|
_onEnterOrLeaveImage4Toolbar!(false);
|
|
} catch (e) {
|
|
//
|
|
}
|
|
}
|
|
// See [onWindowBlur].
|
|
if (!isWindows) {
|
|
_ffi.inputModel.enterOrLeave(false);
|
|
}
|
|
}
|
|
|
|
Widget _buildRawTouchAndPointerRegion(
|
|
Widget child,
|
|
PointerEnterEventListener? onEnter,
|
|
PointerExitEventListener? onExit,
|
|
) {
|
|
return RawTouchGestureDetectorRegion(
|
|
child: _buildRawPointerMouseRegion(child, onEnter, onExit),
|
|
ffi: _ffi,
|
|
);
|
|
}
|
|
|
|
Widget _buildRawPointerMouseRegion(
|
|
Widget child,
|
|
PointerEnterEventListener? onEnter,
|
|
PointerExitEventListener? onExit,
|
|
) {
|
|
return RawPointerMouseRegion(
|
|
onEnter: onEnter,
|
|
onExit: onExit,
|
|
onPointerDown: (event) {
|
|
// A double check for blur status.
|
|
// Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
|
|
// Sometimes the system does not send the necessary focus event to flutter. We should manually
|
|
// handle this inconsistent status by setting `_isWindowBlur` to false. So we can
|
|
// ensure the grab-key thread is running when our users are clicking the remote canvas.
|
|
if (_isWindowBlur) {
|
|
debugPrint(
|
|
"Unexpected status: onPointerDown is triggered while the remote window is in blur status");
|
|
_isWindowBlur = false;
|
|
}
|
|
if (!_rawKeyFocusNode.hasFocus) {
|
|
_rawKeyFocusNode.requestFocus();
|
|
}
|
|
},
|
|
inputModel: _ffi.inputModel,
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget getBodyForDesktop(BuildContext context) {
|
|
var paints = <Widget>[
|
|
MouseRegion(onEnter: (evt) {
|
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
|
|
}, onExit: (evt) {
|
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
|
}, child: LayoutBuilder(builder: (context, constraints) {
|
|
final c = Provider.of<CanvasModel>(context, listen: false);
|
|
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
|
final peerDisplay = CurrentDisplayState.find(widget.id);
|
|
return Obx(
|
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
|
? Container(color: Colors.transparent)
|
|
: Obx(() {
|
|
widget.toolbarState.initShow(sessionId);
|
|
_ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
|
|
return ImagePaint(
|
|
id: widget.id,
|
|
zoomCursor: _zoomCursor,
|
|
cursorOverImage: _cursorOverImage,
|
|
keyboardEnabled: _keyboardEnabled,
|
|
remoteCursorMoved: _remoteCursorMoved,
|
|
listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
|
|
child, enterView, leaveView),
|
|
ffi: _ffi,
|
|
);
|
|
}),
|
|
);
|
|
}))
|
|
];
|
|
|
|
if (!_ffi.canvasModel.cursorEmbedded) {
|
|
paints
|
|
.add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
|
|
? Offstage()
|
|
: CursorPaint(
|
|
id: widget.id,
|
|
zoomCursor: _zoomCursor,
|
|
)));
|
|
}
|
|
paints.add(
|
|
Positioned(
|
|
top: 10,
|
|
right: 10,
|
|
child: _buildRawTouchAndPointerRegion(
|
|
QualityMonitor(_ffi.qualityMonitorModel), null, null),
|
|
),
|
|
);
|
|
return Stack(
|
|
children: paints,
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
class ImagePaint extends StatefulWidget {
|
|
final FFI ffi;
|
|
final String id;
|
|
final RxBool zoomCursor;
|
|
final RxBool cursorOverImage;
|
|
final RxBool keyboardEnabled;
|
|
final RxBool remoteCursorMoved;
|
|
final Widget Function(Widget)? listenerBuilder;
|
|
|
|
ImagePaint(
|
|
{Key? key,
|
|
required this.ffi,
|
|
required this.id,
|
|
required this.zoomCursor,
|
|
required this.cursorOverImage,
|
|
required this.keyboardEnabled,
|
|
required this.remoteCursorMoved,
|
|
this.listenerBuilder})
|
|
: super(key: key);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _ImagePaintState();
|
|
}
|
|
|
|
class _ImagePaintState extends State<ImagePaint> {
|
|
bool _lastRemoteCursorMoved = false;
|
|
|
|
String get id => widget.id;
|
|
RxBool get zoomCursor => widget.zoomCursor;
|
|
RxBool get cursorOverImage => widget.cursorOverImage;
|
|
RxBool get keyboardEnabled => widget.keyboardEnabled;
|
|
RxBool get remoteCursorMoved => widget.remoteCursorMoved;
|
|
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final m = Provider.of<ImageModel>(context);
|
|
var c = Provider.of<CanvasModel>(context);
|
|
final s = c.scale;
|
|
|
|
bool isViewAdaptive() => c.viewStyle.style == kRemoteViewStyleAdaptive;
|
|
bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
|
|
|
|
mouseRegion({child}) => Obx(() {
|
|
double getCursorScale() {
|
|
var c = Provider.of<CanvasModel>(context);
|
|
var cursorScale = 1.0;
|
|
if (isWindows) {
|
|
// debug win10
|
|
if (zoomCursor.value && isViewAdaptive()) {
|
|
cursorScale = s * c.devicePixelRatio;
|
|
}
|
|
} else {
|
|
if (zoomCursor.value || isViewOriginal()) {
|
|
cursorScale = s;
|
|
}
|
|
}
|
|
return cursorScale;
|
|
}
|
|
|
|
return MouseRegion(
|
|
cursor: cursorOverImage.isTrue
|
|
? c.cursorEmbedded
|
|
? SystemMouseCursors.none
|
|
: keyboardEnabled.isTrue
|
|
? (() {
|
|
if (remoteCursorMoved.isTrue) {
|
|
_lastRemoteCursorMoved = true;
|
|
return SystemMouseCursors.none;
|
|
} else {
|
|
if (_lastRemoteCursorMoved) {
|
|
_lastRemoteCursorMoved = false;
|
|
_firstEnterImage.value = true;
|
|
}
|
|
return _buildCustomCursor(
|
|
context, getCursorScale());
|
|
}
|
|
}())
|
|
: _buildDisabledCursor(context, getCursorScale())
|
|
: MouseCursor.defer,
|
|
onHover: (evt) {},
|
|
child: child);
|
|
});
|
|
if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) {
|
|
final paintWidth = c.getDisplayWidth() * s;
|
|
final paintHeight = c.getDisplayHeight() * s;
|
|
final paintSize = Size(paintWidth, paintHeight);
|
|
final paintWidget =
|
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
|
? _BuildPaintTextureRender(
|
|
c, s, Offset.zero, paintSize, isViewOriginal())
|
|
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
|
return NotificationListener<ScrollNotification>(
|
|
onNotification: (notification) {
|
|
c.updateScrollPercent();
|
|
return false;
|
|
},
|
|
child: mouseRegion(
|
|
child: Obx(() => _buildCrossScrollbarFromLayout(
|
|
context,
|
|
_buildListener(paintWidget),
|
|
c.size,
|
|
paintSize,
|
|
c.scrollHorizontal,
|
|
c.scrollVertical,
|
|
)),
|
|
));
|
|
} else {
|
|
if (c.size.width > 0 && c.size.height > 0) {
|
|
final paintWidget =
|
|
m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
|
|
? _BuildPaintTextureRender(
|
|
c,
|
|
s,
|
|
Offset(
|
|
isLinux ? c.x.toInt().toDouble() : c.x,
|
|
isLinux ? c.y.toInt().toDouble() : c.y,
|
|
),
|
|
c.size,
|
|
isViewOriginal())
|
|
: _buildScrollAutoNonTextureRender(m, c, s);
|
|
return mouseRegion(child: _buildListener(paintWidget));
|
|
} else {
|
|
return Container();
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildScrollbarNonTextureRender(
|
|
ImageModel m, Size imageSize, double s) {
|
|
return CustomPaint(
|
|
size: imageSize,
|
|
painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
|
|
);
|
|
}
|
|
|
|
Widget _buildScrollAutoNonTextureRender(
|
|
ImageModel m, CanvasModel c, double s) {
|
|
return CustomPaint(
|
|
size: Size(c.size.width, c.size.height),
|
|
painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
|
|
);
|
|
}
|
|
|
|
Widget _BuildPaintTextureRender(
|
|
CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
|
|
final ffiModel = c.parent.target!.ffiModel;
|
|
final displays = ffiModel.pi.getCurDisplays();
|
|
final children = <Widget>[];
|
|
final rect = ffiModel.rect;
|
|
if (rect == null) {
|
|
return Container();
|
|
}
|
|
final curDisplay = ffiModel.pi.currentDisplay;
|
|
for (var i = 0; i < displays.length; i++) {
|
|
final textureId = widget.ffi.textureModel
|
|
.getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
|
|
if (true) {
|
|
// both "textureId.value != -1" and "true" seems ok
|
|
children.add(Positioned(
|
|
left: (displays[i].x - rect.left) * s + offset.dx,
|
|
top: (displays[i].y - rect.top) * s + offset.dy,
|
|
width: displays[i].width * s,
|
|
height: displays[i].height * s,
|
|
child: Obx(() => Texture(
|
|
textureId: textureId.value,
|
|
filterQuality:
|
|
isViewOriginal ? FilterQuality.none : FilterQuality.low,
|
|
)),
|
|
));
|
|
}
|
|
}
|
|
return SizedBox(
|
|
width: size.width,
|
|
height: size.height,
|
|
child: Stack(children: children),
|
|
);
|
|
}
|
|
|
|
MouseCursor _buildCustomCursor(BuildContext context, double scale) {
|
|
final cursor = Provider.of<CursorModel>(context);
|
|
final cache = cursor.cache ?? preDefaultCursor.cache;
|
|
return buildCursorOfCache(cursor, scale, cache);
|
|
}
|
|
|
|
MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
|
|
final cursor = Provider.of<CursorModel>(context);
|
|
final cache = preForbiddenCursor.cache;
|
|
return buildCursorOfCache(cursor, scale, cache);
|
|
}
|
|
|
|
Widget _buildCrossScrollbarFromLayout(
|
|
BuildContext context,
|
|
Widget child,
|
|
Size layoutSize,
|
|
Size size,
|
|
ScrollController horizontal,
|
|
ScrollController vertical,
|
|
) {
|
|
var widget = child;
|
|
if (layoutSize.width < size.width) {
|
|
widget = ScrollConfiguration(
|
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
|
child: SingleChildScrollView(
|
|
controller: horizontal,
|
|
scrollDirection: Axis.horizontal,
|
|
physics: cursorOverImage.isTrue
|
|
? const NeverScrollableScrollPhysics()
|
|
: null,
|
|
child: widget,
|
|
),
|
|
);
|
|
} else {
|
|
widget = Row(
|
|
children: [
|
|
Container(
|
|
width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
|
|
),
|
|
widget,
|
|
],
|
|
);
|
|
}
|
|
if (layoutSize.height < size.height) {
|
|
widget = ScrollConfiguration(
|
|
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
|
child: SingleChildScrollView(
|
|
controller: vertical,
|
|
physics: cursorOverImage.isTrue
|
|
? const NeverScrollableScrollPhysics()
|
|
: null,
|
|
child: widget,
|
|
),
|
|
);
|
|
} else {
|
|
widget = Column(
|
|
children: [
|
|
Container(
|
|
height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
|
|
),
|
|
widget,
|
|
],
|
|
);
|
|
}
|
|
if (layoutSize.width < size.width) {
|
|
widget = RawScrollbar(
|
|
thickness: kScrollbarThickness,
|
|
thumbColor: Colors.grey,
|
|
controller: horizontal,
|
|
thumbVisibility: false,
|
|
trackVisibility: false,
|
|
notificationPredicate: layoutSize.height < size.height
|
|
? (notification) => notification.depth == 1
|
|
: defaultScrollNotificationPredicate,
|
|
child: widget,
|
|
);
|
|
}
|
|
if (layoutSize.height < size.height) {
|
|
widget = RawScrollbar(
|
|
thickness: kScrollbarThickness,
|
|
thumbColor: Colors.grey,
|
|
controller: vertical,
|
|
thumbVisibility: false,
|
|
trackVisibility: false,
|
|
child: widget,
|
|
);
|
|
}
|
|
|
|
return Container(
|
|
child: widget,
|
|
width: layoutSize.width,
|
|
height: layoutSize.height,
|
|
);
|
|
}
|
|
|
|
Widget _buildListener(Widget child) {
|
|
if (listenerBuilder != null) {
|
|
return listenerBuilder!(child);
|
|
} else {
|
|
return child;
|
|
}
|
|
}
|
|
}
|
|
|
|
class CursorPaint extends StatelessWidget {
|
|
final String id;
|
|
final RxBool zoomCursor;
|
|
|
|
const CursorPaint({
|
|
Key? key,
|
|
required this.id,
|
|
required this.zoomCursor,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final m = Provider.of<CursorModel>(context);
|
|
final c = Provider.of<CanvasModel>(context);
|
|
double hotx = m.hotx;
|
|
double hoty = m.hoty;
|
|
if (m.image == null) {
|
|
if (preDefaultCursor.image != null) {
|
|
hotx = preDefaultCursor.image!.width / 2;
|
|
hoty = preDefaultCursor.image!.height / 2;
|
|
}
|
|
}
|
|
|
|
double cx = c.x;
|
|
double cy = c.y;
|
|
if (c.viewStyle.style == kRemoteViewStyleOriginal &&
|
|
c.scrollStyle == ScrollStyle.scrollbar) {
|
|
final rect = c.parent.target!.ffiModel.rect;
|
|
if (rect == null) {
|
|
// unreachable!
|
|
debugPrint('unreachable! The displays rect is null.');
|
|
return Container();
|
|
}
|
|
if (cx < 0) {
|
|
final imageWidth = rect.width * c.scale;
|
|
cx = -imageWidth * c.scrollX;
|
|
}
|
|
if (cy < 0) {
|
|
final imageHeight = rect.height * c.scale;
|
|
cy = -imageHeight * c.scrollY;
|
|
}
|
|
}
|
|
|
|
double x = (m.x - hotx) * c.scale + cx;
|
|
double y = (m.y - hoty) * c.scale + cy;
|
|
double scale = 1.0;
|
|
final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal;
|
|
if (zoomCursor.value || isViewOriginal) {
|
|
x = m.x - hotx + cx / c.scale;
|
|
y = m.y - hoty + cy / c.scale;
|
|
scale = c.scale;
|
|
}
|
|
|
|
return CustomPaint(
|
|
painter: ImagePainter(
|
|
image: m.image ?? preDefaultCursor.image,
|
|
x: x,
|
|
y: y,
|
|
scale: scale,
|
|
),
|
|
);
|
|
}
|
|
}
|