remote_page.dart 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. import 'dart:async';
  2. import 'package:desktop_multi_window/desktop_multi_window.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:get/get.dart';
  6. import 'package:provider/provider.dart';
  7. import 'package:wakelock_plus/wakelock_plus.dart';
  8. import 'package:flutter_hbb/models/state_model.dart';
  9. import '../../consts.dart';
  10. import '../../common/widgets/overlay.dart';
  11. import '../../common/widgets/remote_input.dart';
  12. import '../../common.dart';
  13. import '../../common/widgets/dialog.dart';
  14. import '../../common/widgets/toolbar.dart';
  15. import '../../models/model.dart';
  16. import '../../models/platform_model.dart';
  17. import '../../common/shared_state.dart';
  18. import '../../utils/image.dart';
  19. import '../widgets/remote_toolbar.dart';
  20. import '../widgets/kb_layout_type_chooser.dart';
  21. import '../widgets/tabbar_widget.dart';
  22. import 'package:flutter_hbb/native/custom_cursor.dart'
  23. if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart';
  24. final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
  25. // Used to skip session close if "move to new window" is clicked.
  26. final Map<String, bool> closeSessionOnDispose = {};
  27. class RemotePage extends StatefulWidget {
  28. RemotePage({
  29. Key? key,
  30. required this.id,
  31. required this.toolbarState,
  32. this.sessionId,
  33. this.tabWindowId,
  34. this.password,
  35. this.display,
  36. this.displays,
  37. this.tabController,
  38. this.switchUuid,
  39. this.forceRelay,
  40. this.isSharedPassword,
  41. }) : super(key: key) {
  42. initSharedStates(id);
  43. }
  44. final String id;
  45. final SessionID? sessionId;
  46. final int? tabWindowId;
  47. final int? display;
  48. final List<int>? displays;
  49. final String? password;
  50. final ToolbarState toolbarState;
  51. final String? switchUuid;
  52. final bool? forceRelay;
  53. final bool? isSharedPassword;
  54. final SimpleWrapper<State<RemotePage>?> _lastState = SimpleWrapper(null);
  55. final DesktopTabController? tabController;
  56. FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
  57. @override
  58. State<RemotePage> createState() {
  59. final state = _RemotePageState(id);
  60. _lastState.value = state;
  61. return state;
  62. }
  63. }
  64. class _RemotePageState extends State<RemotePage>
  65. with AutomaticKeepAliveClientMixin, MultiWindowListener {
  66. Timer? _timer;
  67. String keyboardMode = "legacy";
  68. bool _isWindowBlur = false;
  69. final _cursorOverImage = false.obs;
  70. late RxBool _showRemoteCursor;
  71. late RxBool _zoomCursor;
  72. late RxBool _remoteCursorMoved;
  73. late RxBool _keyboardEnabled;
  74. var _blockableOverlayState = BlockableOverlayState();
  75. final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
  76. // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar`
  77. // to identify the toolbar instance and its callback function.
  78. int? _instanceIdOnEnterOrLeaveImage4Toolbar;
  79. Function(bool)? _onEnterOrLeaveImage4Toolbar;
  80. late FFI _ffi;
  81. SessionID get sessionId => _ffi.sessionId;
  82. _RemotePageState(String id) {
  83. _initStates(id);
  84. }
  85. void _initStates(String id) {
  86. _zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
  87. _showRemoteCursor = ShowRemoteCursorState.find(id);
  88. _keyboardEnabled = KeyboardEnabledState.find(id);
  89. _remoteCursorMoved = RemoteCursorMovedState.find(id);
  90. }
  91. @override
  92. void initState() {
  93. super.initState();
  94. _ffi = FFI(widget.sessionId);
  95. Get.put<FFI>(_ffi, tag: widget.id);
  96. _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
  97. showKBLayoutTypeChooserIfNeeded(
  98. _ffi.ffiModel.pi.platform, _ffi.dialogManager);
  99. _ffi.recordingModel
  100. .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
  101. });
  102. _ffi.start(
  103. widget.id,
  104. password: widget.password,
  105. isSharedPassword: widget.isSharedPassword,
  106. switchUuid: widget.switchUuid,
  107. forceRelay: widget.forceRelay,
  108. tabWindowId: widget.tabWindowId,
  109. display: widget.display,
  110. displays: widget.displays,
  111. );
  112. WidgetsBinding.instance.addPostFrameCallback((_) {
  113. SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
  114. _ffi.dialogManager
  115. .showLoading(translate('Connecting...'), onCancel: closeConnection);
  116. });
  117. if (!isLinux) {
  118. WakelockPlus.enable();
  119. }
  120. _ffi.ffiModel.updateEventListener(sessionId, widget.id);
  121. if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
  122. _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
  123. _ffi.dialogManager.loadMobileActionsOverlayVisible();
  124. WidgetsBinding.instance.addPostFrameCallback((_) {
  125. // Session option should be set after models.dart/FFI.start
  126. _showRemoteCursor.value = bind.sessionGetToggleOptionSync(
  127. sessionId: sessionId, arg: 'show-remote-cursor');
  128. _zoomCursor.value = bind.sessionGetToggleOptionSync(
  129. sessionId: sessionId, arg: kOptionZoomCursor);
  130. });
  131. DesktopMultiWindow.addListener(this);
  132. // if (!_isCustomCursorInited) {
  133. // customCursorController.registerNeedUpdateCursorCallback(
  134. // (String? lastKey, String? currentKey) async {
  135. // if (_firstEnterImage.value) {
  136. // _firstEnterImage.value = false;
  137. // return true;
  138. // }
  139. // return lastKey == null || lastKey != currentKey;
  140. // });
  141. // _isCustomCursorInited = true;
  142. // }
  143. _blockableOverlayState.applyFfi(_ffi);
  144. // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState.
  145. WidgetsBinding.instance.addPostFrameCallback((_) {
  146. widget.tabController?.onSelected?.call(widget.id);
  147. });
  148. }
  149. @override
  150. void onWindowBlur() {
  151. super.onWindowBlur();
  152. // On windows, we use `focus` way to handle keyboard better.
  153. // Now on Linux, there's some rdev issues which will break the input.
  154. // We disable the `focus` way for non-Windows temporarily.
  155. if (isWindows) {
  156. _isWindowBlur = true;
  157. // unfocus the primary-focus when the whole window is lost focus,
  158. // and let OS to handle events instead.
  159. _rawKeyFocusNode.unfocus();
  160. }
  161. stateGlobal.isFocused.value = false;
  162. }
  163. @override
  164. void onWindowFocus() {
  165. super.onWindowFocus();
  166. // See [onWindowBlur].
  167. if (isWindows) {
  168. _isWindowBlur = false;
  169. }
  170. stateGlobal.isFocused.value = true;
  171. }
  172. @override
  173. void onWindowRestore() {
  174. super.onWindowRestore();
  175. // On windows, we use `onWindowRestore` way to handle window restore from
  176. // a minimized state.
  177. if (isWindows) {
  178. _isWindowBlur = false;
  179. }
  180. if (!isLinux) {
  181. WakelockPlus.enable();
  182. }
  183. }
  184. // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not.
  185. @override
  186. void onWindowMaximize() {
  187. super.onWindowMaximize();
  188. if (!isLinux) {
  189. WakelockPlus.enable();
  190. }
  191. }
  192. @override
  193. void onWindowMinimize() {
  194. super.onWindowMinimize();
  195. if (!isLinux) {
  196. WakelockPlus.disable();
  197. }
  198. }
  199. @override
  200. void onWindowEnterFullScreen() {
  201. super.onWindowEnterFullScreen();
  202. if (isMacOS) {
  203. stateGlobal.setFullscreen(true);
  204. }
  205. }
  206. @override
  207. void onWindowLeaveFullScreen() {
  208. super.onWindowLeaveFullScreen();
  209. if (isMacOS) {
  210. stateGlobal.setFullscreen(false);
  211. }
  212. }
  213. @override
  214. Future<void> dispose() async {
  215. final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
  216. // https://github.com/flutter/flutter/issues/64935
  217. super.dispose();
  218. debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
  219. _ffi.textureModel.onRemotePageDispose(closeSession);
  220. if (closeSession) {
  221. // ensure we leave this session, this is a double check
  222. _ffi.inputModel.enterOrLeave(false);
  223. }
  224. DesktopMultiWindow.removeListener(this);
  225. _ffi.dialogManager.hideMobileActionsOverlay();
  226. _ffi.imageModel.disposeImage();
  227. _ffi.cursorModel.disposeImages();
  228. _rawKeyFocusNode.dispose();
  229. await _ffi.close(closeSession: closeSession);
  230. _timer?.cancel();
  231. _ffi.dialogManager.dismissAll();
  232. if (closeSession) {
  233. await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
  234. overlays: SystemUiOverlay.values);
  235. }
  236. if (!isLinux) {
  237. await WakelockPlus.disable();
  238. }
  239. await Get.delete<FFI>(tag: widget.id);
  240. removeSharedStates(widget.id);
  241. }
  242. Widget emptyOverlay() => BlockableOverlay(
  243. /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
  244. /// see override build() in [BlockableOverlay]
  245. state: _blockableOverlayState,
  246. underlying: Container(
  247. color: Colors.transparent,
  248. ),
  249. );
  250. Widget buildBody(BuildContext context) {
  251. remoteToolbar(BuildContext context) => RemoteToolbar(
  252. id: widget.id,
  253. ffi: _ffi,
  254. state: widget.toolbarState,
  255. onEnterOrLeaveImageSetter: (id, func) {
  256. _instanceIdOnEnterOrLeaveImage4Toolbar = id;
  257. _onEnterOrLeaveImage4Toolbar = func;
  258. },
  259. onEnterOrLeaveImageCleaner: (id) {
  260. // If _instanceIdOnEnterOrLeaveImage4Toolbar != id
  261. // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar.
  262. if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) {
  263. _instanceIdOnEnterOrLeaveImage4Toolbar = null;
  264. _onEnterOrLeaveImage4Toolbar = null;
  265. }
  266. },
  267. setRemoteState: setState,
  268. );
  269. bodyWidget() {
  270. return Stack(
  271. children: [
  272. Container(
  273. color: kColorCanvas,
  274. child: RawKeyFocusScope(
  275. focusNode: _rawKeyFocusNode,
  276. onFocusChange: (bool imageFocused) {
  277. debugPrint(
  278. "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
  279. // See [onWindowBlur].
  280. if (isWindows) {
  281. if (_isWindowBlur) {
  282. imageFocused = false;
  283. Future.delayed(Duration.zero, () {
  284. _rawKeyFocusNode.unfocus();
  285. });
  286. }
  287. if (imageFocused) {
  288. _ffi.inputModel.enterOrLeave(true);
  289. } else {
  290. _ffi.inputModel.enterOrLeave(false);
  291. }
  292. }
  293. },
  294. inputModel: _ffi.inputModel,
  295. child: getBodyForDesktop(context))),
  296. Stack(
  297. children: [
  298. _ffi.ffiModel.pi.isSet.isTrue &&
  299. _ffi.ffiModel.waitForFirstImage.isTrue
  300. ? emptyOverlay()
  301. : () {
  302. if (!_ffi.ffiModel.isPeerAndroid) {
  303. return Offstage();
  304. } else {
  305. return Obx(() => Offstage(
  306. offstage: _ffi.dialogManager
  307. .mobileActionsOverlayVisible.isFalse,
  308. child: Overlay(initialEntries: [
  309. makeMobileActionsOverlayEntry(
  310. () => _ffi.dialogManager
  311. .setMobileActionsOverlayVisible(false),
  312. ffi: _ffi,
  313. )
  314. ]),
  315. ));
  316. }
  317. }(),
  318. // Use Overlay to enable rebuild every time on menu button click.
  319. _ffi.ffiModel.pi.isSet.isTrue
  320. ? Overlay(
  321. initialEntries: [OverlayEntry(builder: remoteToolbar)])
  322. : remoteToolbar(context),
  323. _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
  324. ],
  325. ),
  326. ],
  327. );
  328. }
  329. return Scaffold(
  330. backgroundColor: Theme.of(context).colorScheme.background,
  331. body: Obx(() {
  332. final imageReady = _ffi.ffiModel.pi.isSet.isTrue &&
  333. _ffi.ffiModel.waitForFirstImage.isFalse;
  334. if (imageReady) {
  335. // If the privacy mode(disable physical displays) is switched,
  336. // we should not dismiss the dialog immediately.
  337. if (DateTime.now().difference(togglePrivacyModeTime) >
  338. const Duration(milliseconds: 3000)) {
  339. // `dismissAll()` is to ensure that the state is clean.
  340. // It's ok to call dismissAll() here.
  341. _ffi.dialogManager.dismissAll();
  342. // Recreate the block state to refresh the state.
  343. _blockableOverlayState = BlockableOverlayState();
  344. _blockableOverlayState.applyFfi(_ffi);
  345. }
  346. // Block the whole `bodyWidget()` when dialog shows.
  347. return BlockableOverlay(
  348. underlying: bodyWidget(),
  349. state: _blockableOverlayState,
  350. );
  351. } else {
  352. // `_blockableOverlayState` is not recreated here.
  353. // The toolbar's block state won't work properly when reconnecting, but that's okay.
  354. return bodyWidget();
  355. }
  356. }),
  357. );
  358. }
  359. @override
  360. Widget build(BuildContext context) {
  361. super.build(context);
  362. return WillPopScope(
  363. onWillPop: () async {
  364. clientClose(sessionId, _ffi.dialogManager);
  365. return false;
  366. },
  367. child: MultiProvider(providers: [
  368. ChangeNotifierProvider.value(value: _ffi.ffiModel),
  369. ChangeNotifierProvider.value(value: _ffi.imageModel),
  370. ChangeNotifierProvider.value(value: _ffi.cursorModel),
  371. ChangeNotifierProvider.value(value: _ffi.canvasModel),
  372. ChangeNotifierProvider.value(value: _ffi.recordingModel),
  373. ], child: buildBody(context)));
  374. }
  375. void enterView(PointerEnterEvent evt) {
  376. _cursorOverImage.value = true;
  377. _firstEnterImage.value = true;
  378. if (_onEnterOrLeaveImage4Toolbar != null) {
  379. try {
  380. _onEnterOrLeaveImage4Toolbar!(true);
  381. } catch (e) {
  382. //
  383. }
  384. }
  385. // See [onWindowBlur].
  386. if (!isWindows) {
  387. if (!_rawKeyFocusNode.hasFocus) {
  388. _rawKeyFocusNode.requestFocus();
  389. }
  390. _ffi.inputModel.enterOrLeave(true);
  391. }
  392. }
  393. void leaveView(PointerExitEvent evt) {
  394. if (_ffi.ffiModel.keyboard) {
  395. _ffi.inputModel.tryMoveEdgeOnExit(evt.position);
  396. }
  397. _cursorOverImage.value = false;
  398. _firstEnterImage.value = false;
  399. if (_onEnterOrLeaveImage4Toolbar != null) {
  400. try {
  401. _onEnterOrLeaveImage4Toolbar!(false);
  402. } catch (e) {
  403. //
  404. }
  405. }
  406. // See [onWindowBlur].
  407. if (!isWindows) {
  408. _ffi.inputModel.enterOrLeave(false);
  409. }
  410. }
  411. Widget _buildRawTouchAndPointerRegion(
  412. Widget child,
  413. PointerEnterEventListener? onEnter,
  414. PointerExitEventListener? onExit,
  415. ) {
  416. return RawTouchGestureDetectorRegion(
  417. child: _buildRawPointerMouseRegion(child, onEnter, onExit),
  418. ffi: _ffi,
  419. );
  420. }
  421. Widget _buildRawPointerMouseRegion(
  422. Widget child,
  423. PointerEnterEventListener? onEnter,
  424. PointerExitEventListener? onExit,
  425. ) {
  426. return RawPointerMouseRegion(
  427. onEnter: onEnter,
  428. onExit: onExit,
  429. onPointerDown: (event) {
  430. // A double check for blur status.
  431. // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
  432. // Sometimes the system does not send the necessary focus event to flutter. We should manually
  433. // handle this inconsistent status by setting `_isWindowBlur` to false. So we can
  434. // ensure the grab-key thread is running when our users are clicking the remote canvas.
  435. if (_isWindowBlur) {
  436. debugPrint(
  437. "Unexpected status: onPointerDown is triggered while the remote window is in blur status");
  438. _isWindowBlur = false;
  439. }
  440. if (!_rawKeyFocusNode.hasFocus) {
  441. _rawKeyFocusNode.requestFocus();
  442. }
  443. },
  444. inputModel: _ffi.inputModel,
  445. child: child,
  446. );
  447. }
  448. Widget getBodyForDesktop(BuildContext context) {
  449. var paints = <Widget>[
  450. MouseRegion(onEnter: (evt) {
  451. if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false);
  452. }, onExit: (evt) {
  453. if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
  454. }, child: LayoutBuilder(builder: (context, constraints) {
  455. final c = Provider.of<CanvasModel>(context, listen: false);
  456. Future.delayed(Duration.zero, () => c.updateViewStyle());
  457. final peerDisplay = CurrentDisplayState.find(widget.id);
  458. return Obx(
  459. () => _ffi.ffiModel.pi.isSet.isFalse
  460. ? Container(color: Colors.transparent)
  461. : Obx(() {
  462. widget.toolbarState.initShow(sessionId);
  463. _ffi.textureModel.updateCurrentDisplay(peerDisplay.value);
  464. return ImagePaint(
  465. id: widget.id,
  466. zoomCursor: _zoomCursor,
  467. cursorOverImage: _cursorOverImage,
  468. keyboardEnabled: _keyboardEnabled,
  469. remoteCursorMoved: _remoteCursorMoved,
  470. listenerBuilder: (child) => _buildRawTouchAndPointerRegion(
  471. child, enterView, leaveView),
  472. ffi: _ffi,
  473. );
  474. }),
  475. );
  476. }))
  477. ];
  478. if (!_ffi.canvasModel.cursorEmbedded) {
  479. paints
  480. .add(Obx(() => _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse
  481. ? Offstage()
  482. : CursorPaint(
  483. id: widget.id,
  484. zoomCursor: _zoomCursor,
  485. )));
  486. }
  487. paints.add(
  488. Positioned(
  489. top: 10,
  490. right: 10,
  491. child: _buildRawTouchAndPointerRegion(
  492. QualityMonitor(_ffi.qualityMonitorModel), null, null),
  493. ),
  494. );
  495. return Stack(
  496. children: paints,
  497. );
  498. }
  499. @override
  500. bool get wantKeepAlive => true;
  501. }
  502. class ImagePaint extends StatefulWidget {
  503. final FFI ffi;
  504. final String id;
  505. final RxBool zoomCursor;
  506. final RxBool cursorOverImage;
  507. final RxBool keyboardEnabled;
  508. final RxBool remoteCursorMoved;
  509. final Widget Function(Widget)? listenerBuilder;
  510. ImagePaint(
  511. {Key? key,
  512. required this.ffi,
  513. required this.id,
  514. required this.zoomCursor,
  515. required this.cursorOverImage,
  516. required this.keyboardEnabled,
  517. required this.remoteCursorMoved,
  518. this.listenerBuilder})
  519. : super(key: key);
  520. @override
  521. State<StatefulWidget> createState() => _ImagePaintState();
  522. }
  523. class _ImagePaintState extends State<ImagePaint> {
  524. bool _lastRemoteCursorMoved = false;
  525. String get id => widget.id;
  526. RxBool get zoomCursor => widget.zoomCursor;
  527. RxBool get cursorOverImage => widget.cursorOverImage;
  528. RxBool get keyboardEnabled => widget.keyboardEnabled;
  529. RxBool get remoteCursorMoved => widget.remoteCursorMoved;
  530. Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
  531. @override
  532. Widget build(BuildContext context) {
  533. final m = Provider.of<ImageModel>(context);
  534. var c = Provider.of<CanvasModel>(context);
  535. final s = c.scale;
  536. bool isViewAdaptive() => c.viewStyle.style == kRemoteViewStyleAdaptive;
  537. bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal;
  538. mouseRegion({child}) => Obx(() {
  539. double getCursorScale() {
  540. var c = Provider.of<CanvasModel>(context);
  541. var cursorScale = 1.0;
  542. if (isWindows) {
  543. // debug win10
  544. if (zoomCursor.value && isViewAdaptive()) {
  545. cursorScale = s * c.devicePixelRatio;
  546. }
  547. } else {
  548. if (zoomCursor.value || isViewOriginal()) {
  549. cursorScale = s;
  550. }
  551. }
  552. return cursorScale;
  553. }
  554. return MouseRegion(
  555. cursor: cursorOverImage.isTrue
  556. ? c.cursorEmbedded
  557. ? SystemMouseCursors.none
  558. : keyboardEnabled.isTrue
  559. ? (() {
  560. if (remoteCursorMoved.isTrue) {
  561. _lastRemoteCursorMoved = true;
  562. return SystemMouseCursors.none;
  563. } else {
  564. if (_lastRemoteCursorMoved) {
  565. _lastRemoteCursorMoved = false;
  566. _firstEnterImage.value = true;
  567. }
  568. return _buildCustomCursor(
  569. context, getCursorScale());
  570. }
  571. }())
  572. : _buildDisabledCursor(context, getCursorScale())
  573. : MouseCursor.defer,
  574. onHover: (evt) {},
  575. child: child);
  576. });
  577. if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
  578. final paintWidth = c.getDisplayWidth() * s;
  579. final paintHeight = c.getDisplayHeight() * s;
  580. final paintSize = Size(paintWidth, paintHeight);
  581. final paintWidget =
  582. m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
  583. ? _BuildPaintTextureRender(
  584. c, s, Offset.zero, paintSize, isViewOriginal())
  585. : _buildScrollbarNonTextureRender(m, paintSize, s);
  586. return NotificationListener<ScrollNotification>(
  587. onNotification: (notification) {
  588. c.updateScrollPercent();
  589. return false;
  590. },
  591. child: mouseRegion(
  592. child: Obx(() => _buildCrossScrollbarFromLayout(
  593. context,
  594. _buildListener(paintWidget),
  595. c.size,
  596. paintSize,
  597. c.scrollHorizontal,
  598. c.scrollVertical,
  599. )),
  600. ));
  601. } else {
  602. if (c.size.width > 0 && c.size.height > 0) {
  603. final paintWidget =
  604. m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender
  605. ? _BuildPaintTextureRender(
  606. c,
  607. s,
  608. Offset(
  609. isLinux ? c.x.toInt().toDouble() : c.x,
  610. isLinux ? c.y.toInt().toDouble() : c.y,
  611. ),
  612. c.size,
  613. isViewOriginal())
  614. : _buildScrollAutoNonTextureRender(m, c, s);
  615. return mouseRegion(child: _buildListener(paintWidget));
  616. } else {
  617. return Container();
  618. }
  619. }
  620. }
  621. Widget _buildScrollbarNonTextureRender(
  622. ImageModel m, Size imageSize, double s) {
  623. return CustomPaint(
  624. size: imageSize,
  625. painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
  626. );
  627. }
  628. Widget _buildScrollAutoNonTextureRender(
  629. ImageModel m, CanvasModel c, double s) {
  630. return CustomPaint(
  631. size: Size(c.size.width, c.size.height),
  632. painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
  633. );
  634. }
  635. Widget _BuildPaintTextureRender(
  636. CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) {
  637. final ffiModel = c.parent.target!.ffiModel;
  638. final displays = ffiModel.pi.getCurDisplays();
  639. final children = <Widget>[];
  640. final rect = ffiModel.rect;
  641. if (rect == null) {
  642. return Container();
  643. }
  644. final curDisplay = ffiModel.pi.currentDisplay;
  645. for (var i = 0; i < displays.length; i++) {
  646. final textureId = widget.ffi.textureModel
  647. .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay);
  648. if (true) {
  649. // both "textureId.value != -1" and "true" seems ok
  650. children.add(Positioned(
  651. left: (displays[i].x - rect.left) * s + offset.dx,
  652. top: (displays[i].y - rect.top) * s + offset.dy,
  653. width: displays[i].width * s,
  654. height: displays[i].height * s,
  655. child: Obx(() => Texture(
  656. textureId: textureId.value,
  657. filterQuality:
  658. isViewOriginal ? FilterQuality.none : FilterQuality.low,
  659. )),
  660. ));
  661. }
  662. }
  663. return SizedBox(
  664. width: size.width,
  665. height: size.height,
  666. child: Stack(children: children),
  667. );
  668. }
  669. MouseCursor _buildCustomCursor(BuildContext context, double scale) {
  670. final cursor = Provider.of<CursorModel>(context);
  671. final cache = cursor.cache ?? preDefaultCursor.cache;
  672. return buildCursorOfCache(cursor, scale, cache);
  673. }
  674. MouseCursor _buildDisabledCursor(BuildContext context, double scale) {
  675. final cursor = Provider.of<CursorModel>(context);
  676. final cache = preForbiddenCursor.cache;
  677. return buildCursorOfCache(cursor, scale, cache);
  678. }
  679. Widget _buildCrossScrollbarFromLayout(
  680. BuildContext context,
  681. Widget child,
  682. Size layoutSize,
  683. Size size,
  684. ScrollController horizontal,
  685. ScrollController vertical,
  686. ) {
  687. var widget = child;
  688. if (layoutSize.width < size.width) {
  689. widget = ScrollConfiguration(
  690. behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
  691. child: SingleChildScrollView(
  692. controller: horizontal,
  693. scrollDirection: Axis.horizontal,
  694. physics: cursorOverImage.isTrue
  695. ? const NeverScrollableScrollPhysics()
  696. : null,
  697. child: widget,
  698. ),
  699. );
  700. } else {
  701. widget = Row(
  702. children: [
  703. Container(
  704. width: ((layoutSize.width - size.width) ~/ 2).toDouble(),
  705. ),
  706. widget,
  707. ],
  708. );
  709. }
  710. if (layoutSize.height < size.height) {
  711. widget = ScrollConfiguration(
  712. behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
  713. child: SingleChildScrollView(
  714. controller: vertical,
  715. physics: cursorOverImage.isTrue
  716. ? const NeverScrollableScrollPhysics()
  717. : null,
  718. child: widget,
  719. ),
  720. );
  721. } else {
  722. widget = Column(
  723. children: [
  724. Container(
  725. height: ((layoutSize.height - size.height) ~/ 2).toDouble(),
  726. ),
  727. widget,
  728. ],
  729. );
  730. }
  731. if (layoutSize.width < size.width) {
  732. widget = RawScrollbar(
  733. thickness: kScrollbarThickness,
  734. thumbColor: Colors.grey,
  735. controller: horizontal,
  736. thumbVisibility: false,
  737. trackVisibility: false,
  738. notificationPredicate: layoutSize.height < size.height
  739. ? (notification) => notification.depth == 1
  740. : defaultScrollNotificationPredicate,
  741. child: widget,
  742. );
  743. }
  744. if (layoutSize.height < size.height) {
  745. widget = RawScrollbar(
  746. thickness: kScrollbarThickness,
  747. thumbColor: Colors.grey,
  748. controller: vertical,
  749. thumbVisibility: false,
  750. trackVisibility: false,
  751. child: widget,
  752. );
  753. }
  754. return Container(
  755. child: widget,
  756. width: layoutSize.width,
  757. height: layoutSize.height,
  758. );
  759. }
  760. Widget _buildListener(Widget child) {
  761. if (listenerBuilder != null) {
  762. return listenerBuilder!(child);
  763. } else {
  764. return child;
  765. }
  766. }
  767. }
  768. class CursorPaint extends StatelessWidget {
  769. final String id;
  770. final RxBool zoomCursor;
  771. const CursorPaint({
  772. Key? key,
  773. required this.id,
  774. required this.zoomCursor,
  775. }) : super(key: key);
  776. @override
  777. Widget build(BuildContext context) {
  778. final m = Provider.of<CursorModel>(context);
  779. final c = Provider.of<CanvasModel>(context);
  780. double hotx = m.hotx;
  781. double hoty = m.hoty;
  782. if (m.image == null) {
  783. if (preDefaultCursor.image != null) {
  784. hotx = preDefaultCursor.image!.width / 2;
  785. hoty = preDefaultCursor.image!.height / 2;
  786. }
  787. }
  788. double cx = c.x;
  789. double cy = c.y;
  790. if (c.viewStyle.style == kRemoteViewStyleOriginal &&
  791. c.scrollStyle == ScrollStyle.scrollbar) {
  792. final rect = c.parent.target!.ffiModel.rect;
  793. if (rect == null) {
  794. // unreachable!
  795. debugPrint('unreachable! The displays rect is null.');
  796. return Container();
  797. }
  798. if (cx < 0) {
  799. final imageWidth = rect.width * c.scale;
  800. cx = -imageWidth * c.scrollX;
  801. }
  802. if (cy < 0) {
  803. final imageHeight = rect.height * c.scale;
  804. cy = -imageHeight * c.scrollY;
  805. }
  806. }
  807. double x = (m.x - hotx) * c.scale + cx;
  808. double y = (m.y - hoty) * c.scale + cy;
  809. double scale = 1.0;
  810. final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal;
  811. if (zoomCursor.value || isViewOriginal) {
  812. x = m.x - hotx + cx / c.scale;
  813. y = m.y - hoty + cy / c.scale;
  814. scale = c.scale;
  815. }
  816. return CustomPaint(
  817. painter: ImagePainter(
  818. image: m.image ?? preDefaultCursor.image,
  819. x: x,
  820. y: y,
  821. scale: scale,
  822. ),
  823. );
  824. }
  825. }