123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908 |
- import 'dart:async';
- import 'package:desktop_multi_window/desktop_multi_window.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/services.dart';
- import 'package:get/get.dart';
- import 'package:provider/provider.dart';
- import 'package:wakelock_plus/wakelock_plus.dart';
- import 'package:flutter_improved_scrolling/flutter_improved_scrolling.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 {
- 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) {
- showKBLayoutTypeChooserIfNeeded(
- _ffi.ffiModel.pi.platform, _ffi.dialogManager);
- _ffi.recordingModel
- .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId));
- });
- _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) {
- _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) {
- 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.scrollbar) {
- 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,
- ) {
- final scrollConfig = CustomMouseWheelScrollConfig(
- scrollDuration: kDefaultScrollDuration,
- scrollCurve: Curves.linearToEaseOut,
- mouseWheelTurnsThrottleTimeMs:
- kDefaultMouseWheelThrottleDuration.inMilliseconds,
- scrollAmountMultiplier: kDefaultScrollAmountMultiplier);
- 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 = ImprovedScrolling(
- scrollController: horizontal,
- enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
- customMouseWheelScrollConfig: scrollConfig,
- child: 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 = ImprovedScrolling(
- scrollController: vertical,
- enableCustomMouseWheelScrolling: cursorOverImage.isFalse,
- customMouseWheelScrollConfig: scrollConfig,
- child: 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,
- ),
- );
- }
- }
|