remote_page.dart 29 KB

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