mirror of
https://github.com/rustdesk/rustdesk
synced 2026-04-21 13:27:19 +00:00
- Skip side button events in view-only sessions (consistent with other mouse entry points) - Release held side buttons on session close to avoid stuck buttons on the remote - Drop unpaired 'up' events instead of falling back to the active model, which could send to the wrong session
1997 lines
61 KiB
Dart
1997 lines
61 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_hbb/main.dart';
|
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
|
import 'package:get/get.dart';
|
|
|
|
import '../../models/model.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../../models/state_model.dart';
|
|
import 'relative_mouse_model.dart';
|
|
import '../common.dart';
|
|
import '../consts.dart';
|
|
|
|
/// Mouse button enum.
|
|
enum MouseButtons { left, right, wheel, back, forward }
|
|
|
|
const _kMouseEventDown = 'mousedown';
|
|
const _kMouseEventUp = 'mouseup';
|
|
const _kMouseEventMove = 'mousemove';
|
|
|
|
class CanvasCoords {
|
|
double x = 0;
|
|
double y = 0;
|
|
double scale = 1.0;
|
|
double scrollX = 0;
|
|
double scrollY = 0;
|
|
ScrollStyle scrollStyle = ScrollStyle.scrollauto;
|
|
Size size = Size.zero;
|
|
|
|
CanvasCoords();
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'x': x,
|
|
'y': y,
|
|
'scale': scale,
|
|
'scrollX': scrollX,
|
|
'scrollY': scrollY,
|
|
'scrollStyle': scrollStyle.toJson(),
|
|
'size': {
|
|
'w': size.width,
|
|
'h': size.height,
|
|
}
|
|
};
|
|
}
|
|
|
|
static CanvasCoords fromJson(Map<String, dynamic> json) {
|
|
final model = CanvasCoords();
|
|
model.x = json['x'];
|
|
model.y = json['y'];
|
|
model.scale = json['scale'];
|
|
model.scrollX = json['scrollX'];
|
|
model.scrollY = json['scrollY'];
|
|
model.scrollStyle =
|
|
ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto);
|
|
model.size = Size(json['size']['w'], json['size']['h']);
|
|
return model;
|
|
}
|
|
|
|
static CanvasCoords fromCanvasModel(CanvasModel model) {
|
|
final coords = CanvasCoords();
|
|
coords.x = model.x;
|
|
coords.y = model.y;
|
|
coords.scale = model.scale;
|
|
coords.scrollX = model.scrollX;
|
|
coords.scrollY = model.scrollY;
|
|
coords.scrollStyle = model.scrollStyle;
|
|
coords.size = model.size;
|
|
return coords;
|
|
}
|
|
}
|
|
|
|
class CursorCoords {
|
|
Offset offset = Offset.zero;
|
|
|
|
CursorCoords();
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'offset_x': offset.dx,
|
|
'offset_y': offset.dy,
|
|
};
|
|
}
|
|
|
|
static CursorCoords fromJson(Map<String, dynamic> json) {
|
|
final model = CursorCoords();
|
|
model.offset = Offset(json['offset_x'], json['offset_y']);
|
|
return model;
|
|
}
|
|
|
|
static CursorCoords fromCursorModel(CursorModel model) {
|
|
final coords = CursorCoords();
|
|
coords.offset = model.offset;
|
|
return coords;
|
|
}
|
|
}
|
|
|
|
class RemoteWindowCoords {
|
|
RemoteWindowCoords(
|
|
this.windowRect, this.canvas, this.cursor, this.remoteRect);
|
|
Rect windowRect;
|
|
CanvasCoords canvas;
|
|
CursorCoords cursor;
|
|
Rect remoteRect;
|
|
Offset relativeOffset = Offset.zero;
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'canvas': canvas.toJson(),
|
|
'cursor': cursor.toJson(),
|
|
'windowRect': rectToJson(windowRect),
|
|
'remoteRect': rectToJson(remoteRect),
|
|
};
|
|
}
|
|
|
|
static Map<String, dynamic> rectToJson(Rect r) {
|
|
return {
|
|
'l': r.left,
|
|
't': r.top,
|
|
'w': r.width,
|
|
'h': r.height,
|
|
};
|
|
}
|
|
|
|
static Rect rectFromJson(Map<String, dynamic> json) {
|
|
return Rect.fromLTWH(
|
|
json['l'],
|
|
json['t'],
|
|
json['w'],
|
|
json['h'],
|
|
);
|
|
}
|
|
|
|
RemoteWindowCoords.fromJson(Map<String, dynamic> json)
|
|
: windowRect = rectFromJson(json['windowRect']),
|
|
canvas = CanvasCoords.fromJson(json['canvas']),
|
|
cursor = CursorCoords.fromJson(json['cursor']),
|
|
remoteRect = rectFromJson(json['remoteRect']);
|
|
}
|
|
|
|
extension ToString on MouseButtons {
|
|
String get value {
|
|
switch (this) {
|
|
case MouseButtons.left:
|
|
return 'left';
|
|
case MouseButtons.right:
|
|
return 'right';
|
|
case MouseButtons.wheel:
|
|
return 'wheel';
|
|
case MouseButtons.back:
|
|
return 'back';
|
|
case MouseButtons.forward:
|
|
return 'forward';
|
|
}
|
|
}
|
|
}
|
|
|
|
class PointerEventToRust {
|
|
final String kind;
|
|
final String type;
|
|
final dynamic value;
|
|
|
|
PointerEventToRust(this.kind, this.type, this.value);
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'k': kind,
|
|
'v': {
|
|
't': type,
|
|
'v': value,
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
class ToReleaseRawKeys {
|
|
RawKeyEvent? lastLShiftKeyEvent;
|
|
RawKeyEvent? lastRShiftKeyEvent;
|
|
RawKeyEvent? lastLCtrlKeyEvent;
|
|
RawKeyEvent? lastRCtrlKeyEvent;
|
|
RawKeyEvent? lastLAltKeyEvent;
|
|
RawKeyEvent? lastRAltKeyEvent;
|
|
RawKeyEvent? lastLCommandKeyEvent;
|
|
RawKeyEvent? lastRCommandKeyEvent;
|
|
RawKeyEvent? lastSuperKeyEvent;
|
|
|
|
reset() {
|
|
lastLShiftKeyEvent = null;
|
|
lastRShiftKeyEvent = null;
|
|
lastLCtrlKeyEvent = null;
|
|
lastRCtrlKeyEvent = null;
|
|
lastLAltKeyEvent = null;
|
|
lastRAltKeyEvent = null;
|
|
lastLCommandKeyEvent = null;
|
|
lastRCommandKeyEvent = null;
|
|
lastSuperKeyEvent = null;
|
|
}
|
|
|
|
updateKeyDown(LogicalKeyboardKey logicKey, RawKeyDownEvent e) {
|
|
if (e.isAltPressed) {
|
|
if (logicKey == LogicalKeyboardKey.altLeft) {
|
|
lastLAltKeyEvent = e;
|
|
} else if (logicKey == LogicalKeyboardKey.altRight) {
|
|
lastRAltKeyEvent = e;
|
|
}
|
|
} else if (e.isControlPressed) {
|
|
if (logicKey == LogicalKeyboardKey.controlLeft) {
|
|
lastLCtrlKeyEvent = e;
|
|
} else if (logicKey == LogicalKeyboardKey.controlRight) {
|
|
lastRCtrlKeyEvent = e;
|
|
}
|
|
} else if (e.isShiftPressed) {
|
|
if (logicKey == LogicalKeyboardKey.shiftLeft) {
|
|
lastLShiftKeyEvent = e;
|
|
} else if (logicKey == LogicalKeyboardKey.shiftRight) {
|
|
lastRShiftKeyEvent = e;
|
|
}
|
|
} else if (e.isMetaPressed) {
|
|
if (logicKey == LogicalKeyboardKey.metaLeft) {
|
|
lastLCommandKeyEvent = e;
|
|
} else if (logicKey == LogicalKeyboardKey.metaRight) {
|
|
lastRCommandKeyEvent = e;
|
|
} else if (logicKey == LogicalKeyboardKey.superKey) {
|
|
lastSuperKeyEvent = e;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateKeyUp(LogicalKeyboardKey logicKey, RawKeyUpEvent e) {
|
|
if (e.isAltPressed) {
|
|
if (logicKey == LogicalKeyboardKey.altLeft) {
|
|
lastLAltKeyEvent = null;
|
|
} else if (logicKey == LogicalKeyboardKey.altRight) {
|
|
lastRAltKeyEvent = null;
|
|
}
|
|
} else if (e.isControlPressed) {
|
|
if (logicKey == LogicalKeyboardKey.controlLeft) {
|
|
lastLCtrlKeyEvent = null;
|
|
} else if (logicKey == LogicalKeyboardKey.controlRight) {
|
|
lastRCtrlKeyEvent = null;
|
|
}
|
|
} else if (e.isShiftPressed) {
|
|
if (logicKey == LogicalKeyboardKey.shiftLeft) {
|
|
lastLShiftKeyEvent = null;
|
|
} else if (logicKey == LogicalKeyboardKey.shiftRight) {
|
|
lastRShiftKeyEvent = null;
|
|
}
|
|
} else if (e.isMetaPressed) {
|
|
if (logicKey == LogicalKeyboardKey.metaLeft) {
|
|
lastLCommandKeyEvent = null;
|
|
} else if (logicKey == LogicalKeyboardKey.metaRight) {
|
|
lastRCommandKeyEvent = null;
|
|
} else if (logicKey == LogicalKeyboardKey.superKey) {
|
|
lastSuperKeyEvent = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
release(KeyEventResult Function(RawKeyEvent e) handleRawKeyEvent) {
|
|
for (final key in [
|
|
lastLShiftKeyEvent,
|
|
lastRShiftKeyEvent,
|
|
lastLCtrlKeyEvent,
|
|
lastRCtrlKeyEvent,
|
|
lastLAltKeyEvent,
|
|
lastRAltKeyEvent,
|
|
lastLCommandKeyEvent,
|
|
lastRCommandKeyEvent,
|
|
lastSuperKeyEvent,
|
|
]) {
|
|
if (key != null) {
|
|
handleRawKeyEvent(RawKeyUpEvent(
|
|
data: key.data,
|
|
character: key.character,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ToReleaseKeys {
|
|
KeyEvent? lastLShiftKeyEvent;
|
|
KeyEvent? lastRShiftKeyEvent;
|
|
KeyEvent? lastLCtrlKeyEvent;
|
|
KeyEvent? lastRCtrlKeyEvent;
|
|
KeyEvent? lastLAltKeyEvent;
|
|
KeyEvent? lastRAltKeyEvent;
|
|
KeyEvent? lastLCommandKeyEvent;
|
|
KeyEvent? lastRCommandKeyEvent;
|
|
KeyEvent? lastSuperKeyEvent;
|
|
|
|
reset() {
|
|
lastLShiftKeyEvent = null;
|
|
lastRShiftKeyEvent = null;
|
|
lastLCtrlKeyEvent = null;
|
|
lastRCtrlKeyEvent = null;
|
|
lastLAltKeyEvent = null;
|
|
lastRAltKeyEvent = null;
|
|
lastLCommandKeyEvent = null;
|
|
lastRCommandKeyEvent = null;
|
|
lastSuperKeyEvent = null;
|
|
}
|
|
|
|
release(KeyEventResult Function(KeyEvent e) handleKeyEvent) {
|
|
for (final key in [
|
|
lastLShiftKeyEvent,
|
|
lastRShiftKeyEvent,
|
|
lastLCtrlKeyEvent,
|
|
lastRCtrlKeyEvent,
|
|
lastLAltKeyEvent,
|
|
lastRAltKeyEvent,
|
|
lastLCommandKeyEvent,
|
|
lastRCommandKeyEvent,
|
|
lastSuperKeyEvent,
|
|
]) {
|
|
if (key != null) {
|
|
handleKeyEvent(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class InputModel {
|
|
// Side mouse button support for Linux.
|
|
// Flutter's Linux embedder drops X11 button 8/9 events, so we capture them
|
|
// natively via GDK and forward through the platform channel.
|
|
static InputModel? _activeSideButtonModel;
|
|
// Tracks per-button which model received a side button down event, so the
|
|
// matching up event is routed there even if the pointer has left the view
|
|
// or a different button was pressed in between.
|
|
static final Map<MouseButtons, InputModel> _sideButtonDownModels = {};
|
|
static bool _sideButtonChannelInitialized = false;
|
|
|
|
static void initSideButtonChannel() {
|
|
if (!Platform.isLinux) return;
|
|
if (_sideButtonChannelInitialized) return;
|
|
_sideButtonChannelInitialized = true;
|
|
|
|
const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons');
|
|
channel.setMethodCallHandler((call) async {
|
|
if (call.method == 'onSideMouseButton') {
|
|
final args = call.arguments as Map<dynamic, dynamic>;
|
|
final button = args['button'] as String;
|
|
final type = args['type'] as String;
|
|
final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward;
|
|
|
|
if (type == 'down') {
|
|
final model = _activeSideButtonModel;
|
|
if (model != null &&
|
|
!(model.isViewOnly && !model.showMyCursor)) {
|
|
_sideButtonDownModels[mb] = model;
|
|
await model.sendMouse(type, mb);
|
|
}
|
|
} else {
|
|
// Only route 'up' when we recorded the matching 'down';
|
|
// dropping avoids sending unpaired 'up' to an unrelated session.
|
|
final model = _sideButtonDownModels.remove(mb);
|
|
if (model != null) {
|
|
await model.sendMouse(type, mb);
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
});
|
|
}
|
|
|
|
/// Clear any static references to this model (prevents stale routing).
|
|
/// Releases any held side buttons on the peer so closing a session
|
|
/// mid-press does not leave a stuck button.
|
|
void disposeSideButtonTracking() {
|
|
if (_activeSideButtonModel == this) _activeSideButtonModel = null;
|
|
final held = _sideButtonDownModels.entries
|
|
.where((e) => e.value == this)
|
|
.map((e) => e.key)
|
|
.toList();
|
|
for (final mb in held) {
|
|
_sideButtonDownModels.remove(mb);
|
|
unawaited(sendMouse('up', mb));
|
|
}
|
|
}
|
|
|
|
final WeakReference<FFI> parent;
|
|
String keyboardMode = '';
|
|
|
|
// keyboard
|
|
var shift = false;
|
|
var ctrl = false;
|
|
var alt = false;
|
|
var command = false;
|
|
|
|
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
|
|
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
|
|
|
|
// trackpad
|
|
var _trackpadLastDelta = Offset.zero;
|
|
var _stopFling = true;
|
|
var _fling = false;
|
|
Timer? _flingTimer;
|
|
final _flingBaseDelay = 30;
|
|
final _trackpadAdjustPeerLinux = 0.06;
|
|
// This is an experience value.
|
|
final _trackpadAdjustMacToWin = 2.50;
|
|
// Ignore directional locking for very small deltas on both axes (including
|
|
// tiny single-axis movement) to avoid over-filtering near zero.
|
|
static const double _trackpadAxisNoiseThreshold = 0.2;
|
|
// Lock to dominant axis only when one axis is clearly stronger.
|
|
// 1.6 means the dominant axis must be >= 60% larger than the other.
|
|
static const double _trackpadAxisLockRatio = 1.6;
|
|
int _trackpadSpeed = kDefaultTrackpadSpeed;
|
|
double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0;
|
|
var _trackpadScrollUnsent = Offset.zero;
|
|
|
|
// Mobile relative mouse delta accumulators (for slow/fine movements).
|
|
double _mobileDeltaRemainderX = 0.0;
|
|
double _mobileDeltaRemainderY = 0.0;
|
|
|
|
var _lastScale = 1.0;
|
|
|
|
bool _pointerMovedAfterEnter = false;
|
|
bool _pointerInsideImage = false;
|
|
|
|
// mouse
|
|
final isPhysicalMouse = false.obs;
|
|
int _lastButtons = 0;
|
|
Offset lastMousePos = Offset.zero;
|
|
int _lastWheelTsUs = 0;
|
|
|
|
// Wheel acceleration thresholds.
|
|
static const int _wheelAccelFastThresholdUs = 40000; // 40ms
|
|
static const int _wheelAccelMediumThresholdUs = 80000; // 80ms
|
|
static const double _wheelBurstVelocityThreshold =
|
|
0.002; // delta units per microsecond
|
|
// Wheel burst acceleration (empirical tuning).
|
|
// Applies only to fast, non-smooth bursts to preserve single-step scrolling.
|
|
// Flutter uses microseconds for dt, so velocity is in delta/us.
|
|
|
|
// Relative mouse mode (for games/3D apps).
|
|
final relativeMouseMode = false.obs;
|
|
late final RelativeMouseModel _relativeMouse;
|
|
// Callback to cancel external throttle timer when relative mouse mode is disabled.
|
|
VoidCallback? onRelativeMouseModeDisabled;
|
|
// Disposer for the relativeMouseMode observer (to prevent memory leaks).
|
|
Worker? _relativeMouseModeDisposer;
|
|
|
|
bool _queryOtherWindowCoords = false;
|
|
Rect? _windowRect;
|
|
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
|
|
|
late final SessionID sessionId;
|
|
|
|
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
|
String get id => parent.target?.id ?? '';
|
|
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
|
String get peerVersion => parent.target?.ffiModel.pi.version ?? '';
|
|
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
|
bool get showMyCursor => parent.target!.ffiModel.showMyCursor;
|
|
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
|
bool get isViewCamera => parent.target!.connType == ConnType.viewCamera;
|
|
int get trackpadSpeed => _trackpadSpeed;
|
|
bool get useEdgeScroll =>
|
|
parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge;
|
|
|
|
/// Check if the connected server supports relative mouse mode.
|
|
bool get isRelativeMouseModeSupported => _relativeMouse.isSupported;
|
|
|
|
InputModel(this.parent) {
|
|
sessionId = parent.target!.sessionId;
|
|
_relativeMouse = RelativeMouseModel(
|
|
sessionId: sessionId,
|
|
enabled: relativeMouseMode,
|
|
keyboardPerm: () => keyboardPerm,
|
|
isViewCamera: () => isViewCamera,
|
|
peerVersion: () => peerVersion,
|
|
peerPlatform: () => peerPlatform,
|
|
modify: (msg) => modify(msg),
|
|
getPointerInsideImage: () => _pointerInsideImage,
|
|
setPointerInsideImage: (inside) => _pointerInsideImage = inside,
|
|
);
|
|
_relativeMouse.onDisabled = () => onRelativeMouseModeDisabled?.call();
|
|
|
|
// Sync relative mouse mode state to global state for UI components (e.g., tab bar hint).
|
|
_relativeMouseModeDisposer = ever(relativeMouseMode, (bool value) {
|
|
final peerId = id;
|
|
if (peerId.isNotEmpty) {
|
|
stateGlobal.relativeMouseModeState[peerId] = value;
|
|
}
|
|
});
|
|
}
|
|
|
|
// https://github.com/flutter/flutter/issues/157241
|
|
// Infer CapsLock state from the character output.
|
|
// This is needed because Flutter's HardwareKeyboard.lockModesEnabled may report
|
|
// incorrect CapsLock state on iOS.
|
|
bool _getIosCapsFromCharacter(KeyEvent e) {
|
|
if (!isIOS) return false;
|
|
final ch = e.character;
|
|
return _getIosCapsFromCharacterImpl(
|
|
ch, HardwareKeyboard.instance.isShiftPressed);
|
|
}
|
|
|
|
// RawKeyEvent version of _getIosCapsFromCharacter.
|
|
bool _getIosCapsFromRawCharacter(RawKeyEvent e) {
|
|
if (!isIOS) return false;
|
|
final ch = e.character;
|
|
return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed);
|
|
}
|
|
|
|
// Shared implementation for inferring CapsLock state from character.
|
|
// Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É).
|
|
//
|
|
// Limitations:
|
|
// 1. This inference assumes the client and server use the same keyboard layout.
|
|
// If layouts differ (e.g., client uses EN, server uses DE), the character output
|
|
// may not match expectations. For example, ';' on EN layout maps to 'ö' on DE
|
|
// layout, making it impossible to correctly infer CapsLock state from the
|
|
// character alone.
|
|
// 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it
|
|
// produces lowercase). This method cannot handle that case correctly.
|
|
bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) {
|
|
if (ch == null || ch.length != 1) return false;
|
|
// Use Dart's built-in Unicode-aware case detection
|
|
final upper = ch.toUpperCase();
|
|
final lower = ch.toLowerCase();
|
|
final isUpper = upper == ch && lower != ch;
|
|
final isLower = lower == ch && upper != ch;
|
|
// Skip non-letter characters (e.g., numbers, symbols, CJK characters without case)
|
|
if (!isUpper && !isLower) return false;
|
|
return isUpper != shiftPressed;
|
|
}
|
|
|
|
int _buildLockModes(bool iosCapsLock) {
|
|
const capslock = 1;
|
|
const numlock = 2;
|
|
const scrolllock = 3;
|
|
int lockModes = 0;
|
|
if (isIOS) {
|
|
if (iosCapsLock) {
|
|
lockModes |= (1 << capslock);
|
|
}
|
|
// Ignore "NumLock/ScrollLock" on iOS for now.
|
|
} else {
|
|
if (HardwareKeyboard.instance.lockModesEnabled
|
|
.contains(KeyboardLockMode.capsLock)) {
|
|
lockModes |= (1 << capslock);
|
|
}
|
|
if (HardwareKeyboard.instance.lockModesEnabled
|
|
.contains(KeyboardLockMode.numLock)) {
|
|
lockModes |= (1 << numlock);
|
|
}
|
|
if (HardwareKeyboard.instance.lockModesEnabled
|
|
.contains(KeyboardLockMode.scrollLock)) {
|
|
lockModes |= (1 << scrolllock);
|
|
}
|
|
}
|
|
return lockModes;
|
|
}
|
|
|
|
// This function must be called after the peer info is received.
|
|
// Because `sessionGetKeyboardMode` relies on the peer version.
|
|
updateKeyboardMode() async {
|
|
// * Currently mobile does not enable map mode
|
|
if (isDesktop || isWebDesktop) {
|
|
keyboardMode = await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
|
|
kKeyLegacyMode;
|
|
}
|
|
}
|
|
|
|
/// Updates the trackpad speed based on the session value.
|
|
///
|
|
/// The expected format of the retrieved value is a string that can be parsed into a double.
|
|
/// If parsing fails or the value is out of bounds (less than `kMinTrackpadSpeed` or greater
|
|
/// than `kMaxTrackpadSpeed`), the trackpad speed is reset to the default
|
|
/// value (`kDefaultTrackpadSpeed`).
|
|
///
|
|
/// Bounds:
|
|
/// - Minimum: `kMinTrackpadSpeed`
|
|
/// - Maximum: `kMaxTrackpadSpeed`
|
|
/// - Default: `kDefaultTrackpadSpeed`
|
|
Future<void> updateTrackpadSpeed() async {
|
|
_trackpadSpeed =
|
|
(await bind.sessionGetTrackpadSpeed(sessionId: sessionId) ??
|
|
kDefaultTrackpadSpeed);
|
|
if (_trackpadSpeed < kMinTrackpadSpeed ||
|
|
_trackpadSpeed > kMaxTrackpadSpeed) {
|
|
_trackpadSpeed = kDefaultTrackpadSpeed;
|
|
}
|
|
_trackpadSpeedInner = _trackpadSpeed / 100.0;
|
|
}
|
|
|
|
void handleKeyDownEventModifiers(KeyEvent e) {
|
|
KeyUpEvent upEvent(e) => KeyUpEvent(
|
|
physicalKey: e.physicalKey,
|
|
logicalKey: e.logicalKey,
|
|
timeStamp: e.timeStamp,
|
|
);
|
|
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
|
if (!alt) {
|
|
alt = true;
|
|
}
|
|
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
|
|
if (!alt) {
|
|
alt = true;
|
|
}
|
|
toReleaseKeys.lastLAltKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
|
|
if (!ctrl) {
|
|
ctrl = true;
|
|
}
|
|
toReleaseKeys.lastLCtrlKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
|
|
if (!ctrl) {
|
|
ctrl = true;
|
|
}
|
|
toReleaseKeys.lastRCtrlKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
|
if (!shift) {
|
|
shift = true;
|
|
}
|
|
toReleaseKeys.lastLShiftKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
|
|
if (!shift) {
|
|
shift = true;
|
|
}
|
|
toReleaseKeys.lastRShiftKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
|
|
if (!command) {
|
|
command = true;
|
|
}
|
|
toReleaseKeys.lastLCommandKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
|
|
if (!command) {
|
|
command = true;
|
|
}
|
|
toReleaseKeys.lastRCommandKeyEvent = upEvent(e);
|
|
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
|
|
if (!command) {
|
|
command = true;
|
|
}
|
|
toReleaseKeys.lastSuperKeyEvent = upEvent(e);
|
|
}
|
|
}
|
|
|
|
void handleKeyUpEventModifiers(KeyEvent e) {
|
|
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
|
alt = false;
|
|
toReleaseKeys.lastLAltKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.altRight) {
|
|
alt = false;
|
|
toReleaseKeys.lastRAltKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.controlLeft) {
|
|
ctrl = false;
|
|
toReleaseKeys.lastLCtrlKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.controlRight) {
|
|
ctrl = false;
|
|
toReleaseKeys.lastRCtrlKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.shiftLeft) {
|
|
shift = false;
|
|
toReleaseKeys.lastLShiftKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.shiftRight) {
|
|
shift = false;
|
|
toReleaseKeys.lastRShiftKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.metaLeft) {
|
|
command = false;
|
|
toReleaseKeys.lastLCommandKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.metaRight) {
|
|
command = false;
|
|
toReleaseKeys.lastRCommandKeyEvent = null;
|
|
} else if (e.logicalKey == LogicalKeyboardKey.superKey) {
|
|
command = false;
|
|
toReleaseKeys.lastSuperKeyEvent = null;
|
|
}
|
|
}
|
|
|
|
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
|
if (isViewOnly) return KeyEventResult.handled;
|
|
if (isViewCamera) return KeyEventResult.handled;
|
|
if (!isInputSourceFlutter) {
|
|
if (isDesktop) {
|
|
return KeyEventResult.handled;
|
|
} else if (isWeb) {
|
|
return KeyEventResult.ignored;
|
|
}
|
|
}
|
|
|
|
if (_relativeMouse.handleRawKeyEvent(e)) {
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
bool iosCapsLock = false;
|
|
if (isIOS && e is RawKeyDownEvent) {
|
|
iosCapsLock = _getIosCapsFromRawCharacter(e);
|
|
}
|
|
|
|
final key = e.logicalKey;
|
|
if (e is RawKeyDownEvent) {
|
|
if (!e.repeat) {
|
|
if (e.isAltPressed && !alt) {
|
|
alt = true;
|
|
} else if (e.isControlPressed && !ctrl) {
|
|
ctrl = true;
|
|
} else if (e.isShiftPressed && !shift) {
|
|
shift = true;
|
|
} else if (e.isMetaPressed && !command) {
|
|
command = true;
|
|
}
|
|
}
|
|
toReleaseRawKeys.updateKeyDown(key, e);
|
|
}
|
|
if (e is RawKeyUpEvent) {
|
|
if (key == LogicalKeyboardKey.altLeft ||
|
|
key == LogicalKeyboardKey.altRight) {
|
|
alt = false;
|
|
} else if (key == LogicalKeyboardKey.controlLeft ||
|
|
key == LogicalKeyboardKey.controlRight) {
|
|
ctrl = false;
|
|
} else if (key == LogicalKeyboardKey.shiftRight ||
|
|
key == LogicalKeyboardKey.shiftLeft) {
|
|
shift = false;
|
|
} else if (key == LogicalKeyboardKey.metaLeft ||
|
|
key == LogicalKeyboardKey.metaRight ||
|
|
key == LogicalKeyboardKey.superKey) {
|
|
command = false;
|
|
}
|
|
|
|
toReleaseRawKeys.updateKeyUp(key, e);
|
|
}
|
|
|
|
// * Currently mobile does not enable map mode
|
|
if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) {
|
|
mapKeyboardModeRaw(e, iosCapsLock);
|
|
} else {
|
|
legacyKeyboardModeRaw(e);
|
|
}
|
|
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
KeyEventResult handleKeyEvent(KeyEvent e) {
|
|
if (isViewOnly) return KeyEventResult.handled;
|
|
if (isViewCamera) return KeyEventResult.handled;
|
|
if (!isInputSourceFlutter) {
|
|
if (isDesktop) {
|
|
return KeyEventResult.handled;
|
|
} else if (isWeb) {
|
|
return KeyEventResult.ignored;
|
|
}
|
|
}
|
|
if (isWindows || isLinux) {
|
|
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
|
|
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||
|
|
e.physicalKey == PhysicalKeyboardKey.metaRight) {
|
|
return KeyEventResult.handled;
|
|
}
|
|
}
|
|
|
|
if (_relativeMouse.handleKeyEvent(
|
|
e,
|
|
ctrlPressed: ctrl,
|
|
shiftPressed: shift,
|
|
altPressed: alt,
|
|
commandPressed: command,
|
|
)) {
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
bool iosCapsLock = false;
|
|
if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) {
|
|
iosCapsLock = _getIosCapsFromCharacter(e);
|
|
}
|
|
|
|
if (e is KeyUpEvent) {
|
|
handleKeyUpEventModifiers(e);
|
|
} else if (e is KeyDownEvent) {
|
|
handleKeyDownEventModifiers(e);
|
|
}
|
|
|
|
bool isMobileAndMapMode = false;
|
|
if (isMobile) {
|
|
// Do not use map mode if mobile -> Android. Android does not support map mode for now.
|
|
// Because simulating the physical key events(uhid) which requires root permission is not supported.
|
|
if (peerPlatform != kPeerPlatformAndroid) {
|
|
if (isIOS) {
|
|
isMobileAndMapMode = true;
|
|
} else {
|
|
// The physicalKey.usbHidUsage may be not correct for soft keyboard on Android.
|
|
// iOS does not have this issue.
|
|
// 1. Open the soft keyboard on Android
|
|
// 2. Switch to input method like zh/ko/ja
|
|
// 3. Click Backspace and Enter on the soft keyboard or physical keyboard
|
|
// 4. The physicalKey.usbHidUsage is not correct.
|
|
// PhysicalKeyboardKey#8ac83(usbHidUsage: "0x1100000042", debugName: "Key with ID 0x1100000042")
|
|
// LogicalKeyboardKey#2604c(keyId: "0x10000000d", keyLabel: "Enter", debugName: "Enter")
|
|
//
|
|
// The correct PhysicalKeyboardKey should be
|
|
// PhysicalKeyboardKey#e14a9(usbHidUsage: "0x00070028", debugName: "Enter")
|
|
// https://github.com/flutter/flutter/issues/157771
|
|
// We cannot use the debugName to determine the key is correct or not, because it's null in release mode.
|
|
// The normal `usbHidUsage` for keyboard shoud be between [0x00000010, 0x000c029f]
|
|
// https://github.com/flutter/flutter/blob/c051b69e2a2224300e20d93dbd15f4b91e8844d1/packages/flutter/lib/src/services/keyboard_key.g.dart#L5332 - 5600
|
|
final isNormalHsbHidUsage = (e.physicalKey.usbHidUsage >> 20) == 0;
|
|
isMobileAndMapMode = isNormalHsbHidUsage &&
|
|
// No need to check `!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel)`
|
|
// But we still add it for more reliability.
|
|
!['Backspace', 'Enter'].contains(e.logicalKey.keyLabel);
|
|
}
|
|
}
|
|
}
|
|
final isDesktopAndMapMode =
|
|
isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode);
|
|
if (isMobileAndMapMode || isDesktopAndMapMode) {
|
|
// FIXME: e.character is wrong for dead keys, eg: ^ in de
|
|
newKeyboardMode(
|
|
e.character ?? '',
|
|
e.physicalKey.usbHidUsage & 0xFFFF,
|
|
// Show repeat event be converted to "release+press" events?
|
|
e is KeyDownEvent || e is KeyRepeatEvent,
|
|
iosCapsLock);
|
|
} else {
|
|
legacyKeyboardMode(e);
|
|
}
|
|
|
|
return KeyEventResult.handled;
|
|
}
|
|
|
|
/// Send Key Event
|
|
void newKeyboardMode(
|
|
String character, int usbHid, bool down, bool iosCapsLock) {
|
|
final lockModes = _buildLockModes(iosCapsLock);
|
|
bind.sessionHandleFlutterKeyEvent(
|
|
sessionId: sessionId,
|
|
character: character,
|
|
usbHid: usbHid,
|
|
lockModes: lockModes,
|
|
downOrUp: down);
|
|
}
|
|
|
|
void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) {
|
|
int positionCode = -1;
|
|
int platformCode = -1;
|
|
bool down;
|
|
|
|
if (e.data is RawKeyEventDataMacOs) {
|
|
RawKeyEventDataMacOs newData = e.data as RawKeyEventDataMacOs;
|
|
positionCode = newData.keyCode;
|
|
platformCode = newData.keyCode;
|
|
} else if (e.data is RawKeyEventDataWindows) {
|
|
RawKeyEventDataWindows newData = e.data as RawKeyEventDataWindows;
|
|
positionCode = newData.scanCode;
|
|
platformCode = newData.keyCode;
|
|
} else if (e.data is RawKeyEventDataLinux) {
|
|
RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux;
|
|
// scanCode and keyCode of RawKeyEventDataLinux are incorrect.
|
|
// 1. scanCode means keycode
|
|
// 2. keyCode means keysym
|
|
positionCode = newData.scanCode;
|
|
platformCode = newData.keyCode;
|
|
} else if (e.data is RawKeyEventDataAndroid) {
|
|
RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid;
|
|
positionCode = newData.scanCode + 8;
|
|
platformCode = newData.keyCode;
|
|
} else {}
|
|
|
|
if (e is RawKeyDownEvent) {
|
|
down = true;
|
|
} else {
|
|
down = false;
|
|
}
|
|
inputRawKey(
|
|
e.character ?? '', platformCode, positionCode, down, iosCapsLock);
|
|
}
|
|
|
|
/// Send raw Key Event
|
|
void inputRawKey(String name, int platformCode, int positionCode, bool down,
|
|
bool iosCapsLock) {
|
|
final lockModes = _buildLockModes(iosCapsLock);
|
|
bind.sessionHandleFlutterRawKeyEvent(
|
|
sessionId: sessionId,
|
|
name: name,
|
|
platformCode: platformCode,
|
|
positionCode: positionCode,
|
|
lockModes: lockModes,
|
|
downOrUp: down);
|
|
}
|
|
|
|
void legacyKeyboardModeRaw(RawKeyEvent e) {
|
|
if (e is RawKeyDownEvent) {
|
|
if (e.repeat) {
|
|
sendRawKey(e, press: true);
|
|
} else {
|
|
sendRawKey(e, down: true);
|
|
}
|
|
}
|
|
if (e is RawKeyUpEvent) {
|
|
sendRawKey(e);
|
|
}
|
|
}
|
|
|
|
void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) {
|
|
// for maximum compatibility
|
|
final label = physicalKeyMap[e.physicalKey.usbHidUsage] ??
|
|
logicalKeyMap[e.logicalKey.keyId] ??
|
|
e.logicalKey.keyLabel;
|
|
inputKey(label, down: down, press: press ?? false);
|
|
}
|
|
|
|
void legacyKeyboardMode(KeyEvent e) {
|
|
if (e is KeyDownEvent) {
|
|
sendKey(e, down: true);
|
|
} else if (e is KeyRepeatEvent) {
|
|
sendKey(e, press: true);
|
|
} else if (e is KeyUpEvent) {
|
|
sendKey(e);
|
|
}
|
|
}
|
|
|
|
void sendKey(KeyEvent e, {bool? down, bool? press}) {
|
|
// for maximum compatibility
|
|
final label = physicalKeyMap[e.physicalKey.usbHidUsage] ??
|
|
logicalKeyMap[e.logicalKey.keyId] ??
|
|
e.logicalKey.keyLabel;
|
|
inputKey(label, down: down, press: press ?? false);
|
|
}
|
|
|
|
/// Send key stroke event.
|
|
/// [down] indicates the key's state(down or up).
|
|
/// [press] indicates a click event(down and up).
|
|
void inputKey(String name, {bool? down, bool? press}) {
|
|
if (!keyboardPerm) return;
|
|
if (isViewCamera) return;
|
|
bind.sessionInputKey(
|
|
sessionId: sessionId,
|
|
name: name,
|
|
down: down ?? false,
|
|
press: press ?? true,
|
|
alt: alt,
|
|
ctrl: ctrl,
|
|
shift: shift,
|
|
command: command);
|
|
}
|
|
|
|
static Map<String, dynamic> getMouseEventMove() => {
|
|
'type': _kMouseEventMove,
|
|
'buttons': 0,
|
|
};
|
|
|
|
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
|
|
final Map<String, dynamic> out = {};
|
|
|
|
bool hasStaleButtonsOnMouseUp =
|
|
type == _kMouseEventUp && evt.buttons == _lastButtons;
|
|
|
|
// Check update event type and set buttons to be sent.
|
|
int buttons = _lastButtons;
|
|
if (type == _kMouseEventMove) {
|
|
// flutter may emit move event if one button is pressed and another button
|
|
// is pressing or releasing.
|
|
if (evt.buttons != _lastButtons) {
|
|
// For simplicity
|
|
// Just consider 3 - 1 ((Left + Right buttons) - Left button)
|
|
// Do not consider 2 - 1 (Right button - Left button)
|
|
// or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons))
|
|
// and so on
|
|
buttons = evt.buttons - _lastButtons;
|
|
if (buttons > 0) {
|
|
type = _kMouseEventDown;
|
|
} else {
|
|
type = _kMouseEventUp;
|
|
buttons = -buttons;
|
|
}
|
|
}
|
|
} else {
|
|
if (evt.buttons != 0) {
|
|
buttons = evt.buttons;
|
|
}
|
|
}
|
|
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
|
|
|
|
out['buttons'] = buttons;
|
|
out['type'] = type;
|
|
return out;
|
|
}
|
|
|
|
/// Send a mouse tap event(down and up).
|
|
Future<void> tap(MouseButtons button) async {
|
|
await sendMouse('down', button);
|
|
await sendMouse('up', button);
|
|
}
|
|
|
|
Future<void> tapDown(MouseButtons button) async {
|
|
await sendMouse('down', button);
|
|
}
|
|
|
|
Future<void> tapUp(MouseButtons button) async {
|
|
await sendMouse('up', button);
|
|
}
|
|
|
|
/// Send scroll event with scroll distance [y].
|
|
Future<void> scroll(int y) async {
|
|
if (isViewCamera) return;
|
|
await bind.sessionSendMouse(
|
|
sessionId: sessionId,
|
|
msg: json
|
|
.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()})));
|
|
}
|
|
|
|
/// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command].
|
|
void resetModifiers() {
|
|
shift = ctrl = alt = command = false;
|
|
}
|
|
|
|
/// Modify the given modifier map [evt] based on current modifier key status.
|
|
Map<String, dynamic> modify(Map<String, dynamic> evt) {
|
|
if (ctrl) evt['ctrl'] = 'true';
|
|
if (shift) evt['shift'] = 'true';
|
|
if (alt) evt['alt'] = 'true';
|
|
if (command) evt['command'] = 'true';
|
|
return evt;
|
|
}
|
|
|
|
/// Send mouse press event.
|
|
Future<void> sendMouse(String type, MouseButtons button) async {
|
|
if (!keyboardPerm) return;
|
|
if (isViewCamera) return;
|
|
await bind.sessionSendMouse(
|
|
sessionId: sessionId,
|
|
msg: json.encode(modify({'type': type, 'buttons': button.value})));
|
|
}
|
|
|
|
void enterOrLeave(bool enter) {
|
|
toReleaseKeys.release(handleKeyEvent);
|
|
toReleaseRawKeys.release(handleRawKeyEvent);
|
|
_pointerMovedAfterEnter = false;
|
|
_pointerInsideImage = enter;
|
|
_lastWheelTsUs = 0;
|
|
|
|
// Track active model for side button events (Linux).
|
|
if (enter) {
|
|
_activeSideButtonModel = this;
|
|
} else if (_activeSideButtonModel == this) {
|
|
_activeSideButtonModel = null;
|
|
}
|
|
|
|
// Fix status
|
|
if (!enter) {
|
|
resetModifiers();
|
|
}
|
|
_relativeMouse.onEnterOrLeaveImage(enter);
|
|
_flingTimer?.cancel();
|
|
if (!isInputSourceFlutter) {
|
|
bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter);
|
|
}
|
|
if (!isWeb && enter) {
|
|
bind.setCurSessionId(sessionId: sessionId);
|
|
}
|
|
}
|
|
|
|
/// Send mouse movement event with distance in [x] and [y].
|
|
Future<void> moveMouse(double x, double y) async {
|
|
if (!keyboardPerm) return;
|
|
if (isViewCamera) return;
|
|
var x2 = x.toInt();
|
|
var y2 = y.toInt();
|
|
await bind.sessionSendMouse(
|
|
sessionId: sessionId,
|
|
msg: json.encode(modify({'x': '$x2', 'y': '$y2'})));
|
|
}
|
|
|
|
/// Send relative mouse movement for mobile clients (virtual joystick).
|
|
/// This method is for touch-based controls that want to send delta values.
|
|
/// Uses the 'move_relative' type which bypasses absolute position tracking.
|
|
///
|
|
/// Accumulates fractional deltas to avoid losing slow/fine movements.
|
|
/// Only sends events when relative mouse mode is enabled and supported.
|
|
Future<void> sendMobileRelativeMouseMove(double dx, double dy) async {
|
|
if (!keyboardPerm) return;
|
|
if (isViewCamera) return;
|
|
// Only send relative mouse events when relative mode is enabled and supported.
|
|
if (!isRelativeMouseModeSupported || !relativeMouseMode.value) return;
|
|
_mobileDeltaRemainderX += dx;
|
|
_mobileDeltaRemainderY += dy;
|
|
final x = _mobileDeltaRemainderX.truncate();
|
|
final y = _mobileDeltaRemainderY.truncate();
|
|
_mobileDeltaRemainderX -= x;
|
|
_mobileDeltaRemainderY -= y;
|
|
if (x == 0 && y == 0) return;
|
|
await bind.sessionSendMouse(
|
|
sessionId: sessionId,
|
|
msg: json.encode(modify({
|
|
'type': 'move_relative',
|
|
'x': '$x',
|
|
'y': '$y',
|
|
})));
|
|
}
|
|
|
|
/// Update the pointer lock center position based on current window frame.
|
|
Future<void> updatePointerLockCenter({Offset? localCenter}) {
|
|
return _relativeMouse.updatePointerLockCenter(localCenter: localCenter);
|
|
}
|
|
|
|
/// Get the current image widget size (for comparison to avoid unnecessary updates).
|
|
Size? get imageWidgetSize => _relativeMouse.imageWidgetSize;
|
|
|
|
/// Update the image widget size for center calculation.
|
|
void updateImageWidgetSize(Size size) {
|
|
_relativeMouse.updateImageWidgetSize(size);
|
|
}
|
|
|
|
void toggleRelativeMouseMode() {
|
|
_relativeMouse.toggleRelativeMouseMode();
|
|
}
|
|
|
|
bool setRelativeMouseMode(bool enabled) {
|
|
return _relativeMouse.setRelativeMouseMode(enabled);
|
|
}
|
|
|
|
/// Exit relative mouse mode and release all modifier keys to the remote.
|
|
/// This is called when the user presses the exit shortcut (Ctrl+Alt on Win/Linux, Cmd+G on macOS).
|
|
/// We need to send key-up events for all modifiers because the shortcut itself may have
|
|
/// blocked some key events, leaving the remote in a state where modifiers are stuck.
|
|
void exitRelativeMouseModeWithKeyRelease() {
|
|
if (!_relativeMouse.enabled.value) return;
|
|
|
|
// First, send release events for all modifier keys to the remote.
|
|
// This ensures the remote doesn't have stuck modifier keys after exiting.
|
|
// Use press: false, down: false to send key-up events without modifiers attached.
|
|
final modifiersToRelease = [
|
|
'Control_L',
|
|
'Control_R',
|
|
'Alt_L',
|
|
'Alt_R',
|
|
'Shift_L',
|
|
'Shift_R',
|
|
'Meta_L', // Command/Super left
|
|
'Meta_R', // Command/Super right
|
|
];
|
|
|
|
for (final key in modifiersToRelease) {
|
|
bind.sessionInputKey(
|
|
sessionId: sessionId,
|
|
name: key,
|
|
down: false,
|
|
press: false,
|
|
alt: false,
|
|
ctrl: false,
|
|
shift: false,
|
|
command: false,
|
|
);
|
|
}
|
|
|
|
// Reset local modifier state
|
|
resetModifiers();
|
|
|
|
// Now exit relative mouse mode
|
|
_relativeMouse.setRelativeMouseMode(false);
|
|
}
|
|
|
|
void disposeRelativeMouseMode() {
|
|
_relativeMouse.dispose();
|
|
onRelativeMouseModeDisabled = null;
|
|
// Cancel the relative mouse mode observer and clean up global state.
|
|
_relativeMouseModeDisposer?.dispose();
|
|
_relativeMouseModeDisposer = null;
|
|
final peerId = id;
|
|
if (peerId.isNotEmpty) {
|
|
stateGlobal.relativeMouseModeState.remove(peerId);
|
|
}
|
|
}
|
|
|
|
void onWindowBlur() {
|
|
_relativeMouse.onWindowBlur();
|
|
}
|
|
|
|
void onWindowFocus() {
|
|
_relativeMouse.onWindowFocus();
|
|
}
|
|
|
|
void onPointHoverImage(PointerHoverEvent e) {
|
|
_stopFling = true;
|
|
if (isViewOnly && !showMyCursor) return;
|
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
|
|
|
// May fix https://github.com/rustdesk/rustdesk/issues/13009
|
|
if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) {
|
|
// iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected.
|
|
// Ignore this event to prevent cursor jumping.
|
|
debugPrint('Ignored synthesized hover at (0,0) on iOS');
|
|
return;
|
|
}
|
|
|
|
// Only update pointer region when relative mouse mode is enabled.
|
|
// This avoids unnecessary tracking when not in relative mode.
|
|
if (_relativeMouse.enabled.value) {
|
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
|
}
|
|
|
|
if (!isPhysicalMouse.value) {
|
|
isPhysicalMouse.value = true;
|
|
}
|
|
if (isPhysicalMouse.value) {
|
|
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
|
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
|
edgeScroll: useEdgeScroll);
|
|
}
|
|
}
|
|
}
|
|
|
|
void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
|
|
_lastScale = 1.0;
|
|
_stopFling = true;
|
|
if (isViewOnly) return;
|
|
if (isViewCamera) return;
|
|
if (peerPlatform == kPeerPlatformAndroid) {
|
|
handlePointerEvent('touch', kMouseEventTypePanStart, e.position);
|
|
}
|
|
}
|
|
|
|
// https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
|
|
void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
|
|
if (isViewOnly) return;
|
|
if (isViewCamera) return;
|
|
if (peerPlatform != kPeerPlatformAndroid) {
|
|
final scale = ((e.scale - _lastScale) * 1000).toInt();
|
|
_lastScale = e.scale;
|
|
|
|
if (scale != 0) {
|
|
bind.sessionSendPointer(
|
|
sessionId: sessionId,
|
|
msg: json.encode(
|
|
PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
|
|
.toJson()));
|
|
return;
|
|
}
|
|
}
|
|
|
|
var delta = e.panDelta * _trackpadSpeedInner;
|
|
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
|
|
delta *= _trackpadAdjustMacToWin;
|
|
}
|
|
delta = _filterTrackpadDeltaAxis(delta);
|
|
_trackpadLastDelta = delta;
|
|
|
|
var x = delta.dx.toInt();
|
|
var y = delta.dy.toInt();
|
|
if (peerPlatform == kPeerPlatformLinux) {
|
|
_trackpadScrollUnsent += (delta * _trackpadAdjustPeerLinux);
|
|
x = _trackpadScrollUnsent.dx.truncate();
|
|
y = _trackpadScrollUnsent.dy.truncate();
|
|
_trackpadScrollUnsent -= Offset(x.toDouble(), y.toDouble());
|
|
} else {
|
|
if (x == 0 && y == 0) {
|
|
final thr = 0.1;
|
|
if (delta.dx.abs() > delta.dy.abs()) {
|
|
x = delta.dx > thr ? 1 : (delta.dx < -thr ? -1 : 0);
|
|
} else {
|
|
y = delta.dy > thr ? 1 : (delta.dy < -thr ? -1 : 0);
|
|
}
|
|
}
|
|
}
|
|
if (x != 0 || y != 0) {
|
|
if (peerPlatform == kPeerPlatformAndroid) {
|
|
handlePointerEvent('touch', kMouseEventTypePanUpdate,
|
|
Offset(x.toDouble(), y.toDouble()));
|
|
} else {
|
|
if (isViewCamera) return;
|
|
bind.sessionSendMouse(
|
|
sessionId: sessionId,
|
|
msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
|
|
}
|
|
}
|
|
}
|
|
|
|
Offset _filterTrackpadDeltaAxis(Offset delta) {
|
|
final absDx = delta.dx.abs();
|
|
final absDy = delta.dy.abs();
|
|
// Keep diagonal intent when movement is tiny on both axes.
|
|
if (absDx < _trackpadAxisNoiseThreshold &&
|
|
absDy < _trackpadAxisNoiseThreshold) {
|
|
return delta;
|
|
}
|
|
// Dominant-axis lock to reduce accidental cross-axis scrolling noise.
|
|
if (absDy >= absDx * _trackpadAxisLockRatio) {
|
|
return Offset(0, delta.dy);
|
|
}
|
|
if (absDx >= absDy * _trackpadAxisLockRatio) {
|
|
return Offset(delta.dx, 0);
|
|
}
|
|
return delta;
|
|
}
|
|
|
|
void _scheduleFling(double x, double y, int delay) {
|
|
if (isViewCamera) return;
|
|
if ((x == 0 && y == 0) || _stopFling) {
|
|
_fling = false;
|
|
return;
|
|
}
|
|
|
|
_flingTimer = Timer(Duration(milliseconds: delay), () {
|
|
if (_stopFling) {
|
|
_fling = false;
|
|
return;
|
|
}
|
|
|
|
final d = 0.97;
|
|
x *= d;
|
|
y *= d;
|
|
|
|
// Try set delta (x,y) and delay.
|
|
var dx = x.toInt();
|
|
var dy = y.toInt();
|
|
if (parent.target?.ffiModel.pi.platform == kPeerPlatformLinux) {
|
|
dx = (x * _trackpadAdjustPeerLinux).toInt();
|
|
dy = (y * _trackpadAdjustPeerLinux).toInt();
|
|
}
|
|
|
|
var delay = _flingBaseDelay;
|
|
|
|
if (dx == 0 && dy == 0) {
|
|
_fling = false;
|
|
return;
|
|
}
|
|
|
|
bind.sessionSendMouse(
|
|
sessionId: sessionId,
|
|
msg: '{"type": "trackpad", "x": "$dx", "y": "$dy"}');
|
|
_scheduleFling(x, y, delay);
|
|
});
|
|
}
|
|
|
|
void waitLastFlingDone() {
|
|
if (_fling) {
|
|
_stopFling = true;
|
|
}
|
|
for (var i = 0; i < 5; i++) {
|
|
if (!_fling) {
|
|
break;
|
|
}
|
|
sleep(Duration(milliseconds: 10));
|
|
}
|
|
_flingTimer?.cancel();
|
|
}
|
|
|
|
void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
|
|
if (isViewCamera) return;
|
|
if (peerPlatform == kPeerPlatformAndroid) {
|
|
handlePointerEvent('touch', kMouseEventTypePanEnd, e.position);
|
|
return;
|
|
}
|
|
|
|
bind.sessionSendPointer(
|
|
sessionId: sessionId,
|
|
msg: json.encode(
|
|
PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
|
|
|
|
waitLastFlingDone();
|
|
_stopFling = false;
|
|
|
|
// 2.0 is an experience value
|
|
double minFlingValue = 2.0 * _trackpadSpeedInner;
|
|
if (isMacOS && peerPlatform == kPeerPlatformWindows) {
|
|
minFlingValue *= _trackpadAdjustMacToWin;
|
|
}
|
|
if (_trackpadLastDelta.dx.abs() > minFlingValue ||
|
|
_trackpadLastDelta.dy.abs() > minFlingValue) {
|
|
_fling = true;
|
|
_scheduleFling(
|
|
_trackpadLastDelta.dx, _trackpadLastDelta.dy, _flingBaseDelay);
|
|
}
|
|
_trackpadLastDelta = Offset.zero;
|
|
}
|
|
|
|
// iOS Magic Mouse duplicate event detection.
|
|
// When using Magic Mouse on iPad, iOS may emit both mouse and touch events
|
|
// for the same click in certain areas (like top-left corner).
|
|
int _lastMouseDownTimeMs = 0;
|
|
ui.Offset _lastMouseDownPos = ui.Offset.zero;
|
|
|
|
/// Check if a touch tap event should be ignored because it's a duplicate
|
|
/// of a recent mouse event (iOS Magic Mouse issue).
|
|
bool shouldIgnoreTouchTap(ui.Offset pos) {
|
|
if (!isIOS) return false;
|
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
|
final dt = nowMs - _lastMouseDownTimeMs;
|
|
final distance = (_lastMouseDownPos - pos).distance;
|
|
// If touch tap is within 2000ms and 80px of the last mouse down,
|
|
// it's likely a duplicate event from the same Magic Mouse click.
|
|
if (dt >= 0 && dt < 2000 && distance < 80.0) {
|
|
debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void onPointDownImage(PointerDownEvent e) {
|
|
debugPrint("onPointDownImage ${e.kind}");
|
|
_stopFling = true;
|
|
if (isDesktop) _queryOtherWindowCoords = true;
|
|
_remoteWindowCoords = [];
|
|
_windowRect = null;
|
|
if (isViewOnly && !showMyCursor) return;
|
|
if (isViewCamera) return;
|
|
|
|
// Track mouse down events for duplicate detection on iOS.
|
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
|
if (e.kind == ui.PointerDeviceKind.mouse) {
|
|
_lastMouseDownTimeMs = nowMs;
|
|
_lastMouseDownPos = e.position;
|
|
}
|
|
|
|
if (_relativeMouse.enabled.value) {
|
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
|
}
|
|
|
|
if (e.kind != ui.PointerDeviceKind.mouse) {
|
|
if (isPhysicalMouse.value) {
|
|
isPhysicalMouse.value = false;
|
|
}
|
|
}
|
|
if (isPhysicalMouse.value) {
|
|
// In relative mouse mode, send button events without position.
|
|
// Use _relativeMouse.enabled.value consistently with the guard above.
|
|
if (_relativeMouse.enabled.value) {
|
|
_relativeMouse
|
|
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventDown));
|
|
} else {
|
|
handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
|
|
}
|
|
}
|
|
}
|
|
|
|
void onPointUpImage(PointerUpEvent e) {
|
|
if (isDesktop) _queryOtherWindowCoords = false;
|
|
if (isViewOnly && !showMyCursor) return;
|
|
if (isViewCamera) return;
|
|
|
|
if (_relativeMouse.enabled.value) {
|
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
|
}
|
|
|
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
|
if (isPhysicalMouse.value) {
|
|
// In relative mouse mode, send button events without position.
|
|
// Use _relativeMouse.enabled.value consistently with the guard above.
|
|
if (_relativeMouse.enabled.value) {
|
|
_relativeMouse
|
|
.sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventUp));
|
|
} else {
|
|
handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
|
|
}
|
|
}
|
|
}
|
|
|
|
void onPointMoveImage(PointerMoveEvent e) {
|
|
if (isViewOnly && !showMyCursor) return;
|
|
if (isViewCamera) return;
|
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
|
|
|
if (_relativeMouse.enabled.value) {
|
|
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
|
|
}
|
|
|
|
if (_queryOtherWindowCoords) {
|
|
Future.delayed(Duration.zero, () async {
|
|
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
|
});
|
|
_queryOtherWindowCoords = false;
|
|
}
|
|
if (isPhysicalMouse.value) {
|
|
if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) {
|
|
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position,
|
|
edgeScroll: useEdgeScroll);
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
|
|
List<RemoteWindowCoords> remoteWindowCoords) async {
|
|
final coords =
|
|
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
|
|
final wc = WindowController.fromWindowId(kWindowId!);
|
|
try {
|
|
final frame = await wc.getFrame();
|
|
for (final c in coords) {
|
|
c.relativeOffset = Offset(
|
|
c.windowRect.left - frame.left, c.windowRect.top - frame.top);
|
|
remoteWindowCoords.add(c);
|
|
}
|
|
return frame;
|
|
} catch (e) {
|
|
// Unreachable code
|
|
debugPrint("Failed to get frame of window $kWindowId, it may be hidden");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Handle scroll/wheel events.
|
|
/// Note: Scroll events intentionally use absolute positioning even in relative mouse mode.
|
|
/// This is because scroll events don't need relative positioning - they represent
|
|
/// scroll deltas that are independent of cursor position. Games and 3D applications
|
|
/// handle scroll events the same way regardless of mouse mode.
|
|
void onPointerSignalImage(PointerSignalEvent e) {
|
|
if (isViewOnly) return;
|
|
if (isViewCamera) return;
|
|
if (e is PointerScrollEvent) {
|
|
final rawDx = e.scrollDelta.dx;
|
|
final rawDy = e.scrollDelta.dy;
|
|
final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs();
|
|
final isSmooth = dominantDelta < 1;
|
|
final nowUs = DateTime.now().microsecondsSinceEpoch;
|
|
final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs;
|
|
_lastWheelTsUs = nowUs;
|
|
int accel = 1;
|
|
if (!isSmooth &&
|
|
dtUs > 0 &&
|
|
dtUs <= _wheelAccelMediumThresholdUs &&
|
|
(isWindows || isLinux) &&
|
|
peerPlatform == kPeerPlatformMacOS) {
|
|
final velocity = dominantDelta / dtUs;
|
|
if (velocity >= _wheelBurstVelocityThreshold) {
|
|
if (dtUs < _wheelAccelFastThresholdUs) {
|
|
accel = 3;
|
|
} else {
|
|
accel = 2;
|
|
}
|
|
}
|
|
}
|
|
var dx = rawDx.toInt();
|
|
var dy = rawDy.toInt();
|
|
if (rawDx.abs() > rawDy.abs()) {
|
|
dy = 0;
|
|
} else {
|
|
dx = 0;
|
|
}
|
|
if (dx > 0) {
|
|
dx = -accel;
|
|
} else if (dx < 0) {
|
|
dx = accel;
|
|
}
|
|
if (dy > 0) {
|
|
dy = -accel;
|
|
} else if (dy < 0) {
|
|
dy = accel;
|
|
}
|
|
bind.sessionSendMouse(
|
|
sessionId: sessionId,
|
|
msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}');
|
|
}
|
|
}
|
|
|
|
void refreshMousePos() => handleMouse({
|
|
'buttons': 0,
|
|
'type': _kMouseEventMove,
|
|
}, lastMousePos, edgeScroll: useEdgeScroll);
|
|
|
|
void tryMoveEdgeOnExit(Offset pos) => handleMouse(
|
|
{
|
|
'buttons': 0,
|
|
'type': _kMouseEventMove,
|
|
},
|
|
pos,
|
|
onExit: true,
|
|
);
|
|
|
|
static double tryGetNearestRange(double v, double min, double max, double n) {
|
|
if (v < min && v >= min - n) {
|
|
v = min;
|
|
}
|
|
if (v > max && v <= max + n) {
|
|
v = max;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
Offset setNearestEdge(double x, double y, Rect rect) {
|
|
double left = x - rect.left;
|
|
double right = rect.right - 1 - x;
|
|
double top = y - rect.top;
|
|
double bottom = rect.bottom - 1 - y;
|
|
if (left < right && left < top && left < bottom) {
|
|
x = rect.left;
|
|
}
|
|
if (right < left && right < top && right < bottom) {
|
|
x = rect.right - 1;
|
|
}
|
|
if (top < left && top < right && top < bottom) {
|
|
y = rect.top;
|
|
}
|
|
if (bottom < left && bottom < right && bottom < top) {
|
|
y = rect.bottom - 1;
|
|
}
|
|
return Offset(x, y);
|
|
}
|
|
|
|
void handlePointerEvent(String kind, String type, Offset offset) {
|
|
double x = offset.dx;
|
|
double y = offset.dy;
|
|
if (_checkPeerControlProtected(x, y)) {
|
|
return;
|
|
}
|
|
// Only touch events are handled for now. So we can just ignore buttons.
|
|
// to-do: handle mouse events
|
|
|
|
late final dynamic evtValue;
|
|
if (type == kMouseEventTypePanUpdate) {
|
|
evtValue = {
|
|
'x': x.toInt(),
|
|
'y': y.toInt(),
|
|
};
|
|
} else {
|
|
final isMoveTypes = [kMouseEventTypePanStart, kMouseEventTypePanEnd];
|
|
final pos = handlePointerDevicePos(
|
|
kPointerEventKindTouch,
|
|
x,
|
|
y,
|
|
isMoveTypes.contains(type),
|
|
type,
|
|
);
|
|
if (pos == null) {
|
|
return;
|
|
}
|
|
evtValue = {
|
|
'x': pos.x.toInt(),
|
|
'y': pos.y.toInt(),
|
|
};
|
|
}
|
|
|
|
final evt = PointerEventToRust(kind, type, evtValue).toJson();
|
|
if (isViewCamera) return;
|
|
bind.sessionSendPointer(
|
|
sessionId: sessionId, msg: json.encode(modify(evt)));
|
|
}
|
|
|
|
bool _checkPeerControlProtected(double x, double y) {
|
|
final cursorModel = parent.target!.cursorModel;
|
|
if (cursorModel.isPeerControlProtected) {
|
|
lastMousePos = ui.Offset(x, y);
|
|
return true;
|
|
}
|
|
|
|
if (!cursorModel.gotMouseControl) {
|
|
bool selfGetControl =
|
|
(x - lastMousePos.dx).abs() > kMouseControlDistance ||
|
|
(y - lastMousePos.dy).abs() > kMouseControlDistance;
|
|
if (selfGetControl) {
|
|
cursorModel.gotMouseControl = true;
|
|
} else {
|
|
lastMousePos = ui.Offset(x, y);
|
|
return true;
|
|
}
|
|
}
|
|
lastMousePos = ui.Offset(x, y);
|
|
return false;
|
|
}
|
|
|
|
Map<String, dynamic>? processEventToPeer(
|
|
Map<String, dynamic> evt,
|
|
Offset offset, {
|
|
bool onExit = false,
|
|
bool moveCanvas = true,
|
|
bool edgeScroll = false,
|
|
}) {
|
|
if (isViewCamera) return null;
|
|
double x = offset.dx;
|
|
double y = max(0.0, offset.dy);
|
|
if (_checkPeerControlProtected(x, y)) {
|
|
return null;
|
|
}
|
|
|
|
var type = kMouseEventTypeDefault;
|
|
var isMove = false;
|
|
switch (evt['type']) {
|
|
case _kMouseEventDown:
|
|
type = kMouseEventTypeDown;
|
|
break;
|
|
case _kMouseEventUp:
|
|
type = kMouseEventTypeUp;
|
|
break;
|
|
case _kMouseEventMove:
|
|
_pointerMovedAfterEnter = true;
|
|
isMove = true;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
evt['type'] = type;
|
|
|
|
if (type == kMouseEventTypeDown && !_pointerMovedAfterEnter) {
|
|
// Move mouse to the position of the down event first.
|
|
lastMousePos = ui.Offset(x, y);
|
|
refreshMousePos();
|
|
}
|
|
|
|
final pos = handlePointerDevicePos(
|
|
kPointerEventKindMouse,
|
|
x,
|
|
y,
|
|
isMove,
|
|
type,
|
|
onExit: onExit,
|
|
buttons: evt['buttons'],
|
|
moveCanvas: moveCanvas,
|
|
edgeScroll: edgeScroll,
|
|
);
|
|
if (pos == null) {
|
|
return null;
|
|
}
|
|
if (type != '') {
|
|
evt['x'] = '0';
|
|
evt['y'] = '0';
|
|
} else {
|
|
evt['x'] = '${pos.x.toInt()}';
|
|
evt['y'] = '${pos.y.toInt()}';
|
|
}
|
|
|
|
final buttons = evt['buttons'];
|
|
if (buttons is int) {
|
|
evt['buttons'] = mouseButtonsToPeer(buttons);
|
|
} else {
|
|
// Log warning if buttons exists but is not an int (unexpected caller).
|
|
// Keep empty string fallback for missing buttons to preserve move/hover behavior.
|
|
if (buttons != null) {
|
|
debugPrint(
|
|
'[InputModel] processEventToPeer: unexpected buttons type: ${buttons.runtimeType}, value: $buttons');
|
|
}
|
|
evt['buttons'] = '';
|
|
}
|
|
return evt;
|
|
}
|
|
|
|
Map<String, dynamic>? handleMouse(
|
|
Map<String, dynamic> evt,
|
|
Offset offset, {
|
|
bool onExit = false,
|
|
bool moveCanvas = true,
|
|
bool edgeScroll = false,
|
|
}) {
|
|
final evtToPeer = processEventToPeer(evt, offset,
|
|
onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
|
if (evtToPeer != null) {
|
|
bind.sessionSendMouse(
|
|
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
|
|
}
|
|
return evtToPeer;
|
|
}
|
|
|
|
Point? handlePointerDevicePos(
|
|
String kind,
|
|
double x,
|
|
double y,
|
|
bool isMove,
|
|
String evtType, {
|
|
bool onExit = false,
|
|
int buttons = kPrimaryMouseButton,
|
|
bool moveCanvas = true,
|
|
bool edgeScroll = false,
|
|
}) {
|
|
final ffiModel = parent.target!.ffiModel;
|
|
CanvasCoords canvas =
|
|
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
|
|
Rect? rect = ffiModel.rect;
|
|
|
|
if (isMove) {
|
|
if (_remoteWindowCoords.isNotEmpty &&
|
|
_windowRect != null &&
|
|
!_isInCurrentWindow(x, y)) {
|
|
final coords =
|
|
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
|
|
if (coords != null) {
|
|
isMove = false;
|
|
canvas = coords.canvas;
|
|
rect = coords.remoteRect;
|
|
x -= isWindows
|
|
? coords.relativeOffset.dx / devicePixelRatio
|
|
: coords.relativeOffset.dx;
|
|
y -= isWindows
|
|
? coords.relativeOffset.dy / devicePixelRatio
|
|
: coords.relativeOffset.dy;
|
|
}
|
|
}
|
|
}
|
|
|
|
y -= CanvasModel.topToEdge;
|
|
x -= CanvasModel.leftToEdge;
|
|
if (isMove) {
|
|
final canvasModel = parent.target!.canvasModel;
|
|
|
|
if (edgeScroll) {
|
|
canvasModel.edgeScrollMouse(x, y);
|
|
} else if (moveCanvas) {
|
|
canvasModel.moveDesktopMouse(x, y);
|
|
}
|
|
|
|
canvasModel.updateLocalCursor(x, y);
|
|
}
|
|
|
|
return _handlePointerDevicePos(
|
|
kind,
|
|
x,
|
|
y,
|
|
isMove,
|
|
canvas,
|
|
rect,
|
|
evtType,
|
|
onExit: onExit,
|
|
buttons: buttons,
|
|
);
|
|
}
|
|
|
|
bool _isInCurrentWindow(double x, double y) {
|
|
var w = _windowRect!.width;
|
|
var h = _windowRect!.height;
|
|
if (isWindows) {
|
|
w /= devicePixelRatio;
|
|
h /= devicePixelRatio;
|
|
}
|
|
return x >= 0 && y >= 0 && x <= w && y <= h;
|
|
}
|
|
|
|
static RemoteWindowCoords? findRemoteCoords(double x, double y,
|
|
List<RemoteWindowCoords> remoteWindowCoords, double devicePixelRatio) {
|
|
if (isWindows) {
|
|
x *= devicePixelRatio;
|
|
y *= devicePixelRatio;
|
|
}
|
|
for (final c in remoteWindowCoords) {
|
|
if (x >= c.relativeOffset.dx &&
|
|
y >= c.relativeOffset.dy &&
|
|
x <= c.relativeOffset.dx + c.windowRect.width &&
|
|
y <= c.relativeOffset.dy + c.windowRect.height) {
|
|
return c;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Point? _handlePointerDevicePos(
|
|
String kind,
|
|
double x,
|
|
double y,
|
|
bool moveInCanvas,
|
|
CanvasCoords canvas,
|
|
Rect? rect,
|
|
String evtType, {
|
|
bool onExit = false,
|
|
int buttons = kPrimaryMouseButton,
|
|
}) {
|
|
if (rect == null) {
|
|
return null;
|
|
}
|
|
|
|
final nearThr = 3;
|
|
var nearRight = (canvas.size.width - x) < nearThr;
|
|
var nearBottom = (canvas.size.height - y) < nearThr;
|
|
final imageWidth = rect.width * canvas.scale;
|
|
final imageHeight = rect.height * canvas.scale;
|
|
if (canvas.scrollStyle != ScrollStyle.scrollauto) {
|
|
x += imageWidth * canvas.scrollX;
|
|
y += imageHeight * canvas.scrollY;
|
|
|
|
// boxed size is a center widget
|
|
if (canvas.size.width > imageWidth) {
|
|
x -= ((canvas.size.width - imageWidth) / 2);
|
|
}
|
|
if (canvas.size.height > imageHeight) {
|
|
y -= ((canvas.size.height - imageHeight) / 2);
|
|
}
|
|
} else {
|
|
x -= canvas.x;
|
|
y -= canvas.y;
|
|
}
|
|
|
|
x /= canvas.scale;
|
|
y /= canvas.scale;
|
|
if (canvas.scale > 0 && canvas.scale < 1) {
|
|
final step = 1.0 / canvas.scale - 1;
|
|
if (nearRight) {
|
|
x += step;
|
|
}
|
|
if (nearBottom) {
|
|
y += step;
|
|
}
|
|
}
|
|
x += rect.left;
|
|
y += rect.top;
|
|
|
|
if (onExit) {
|
|
final pos = setNearestEdge(x, y, rect);
|
|
x = pos.dx;
|
|
y = pos.dy;
|
|
}
|
|
|
|
return InputModel.getPointInRemoteRect(
|
|
true, peerPlatform, kind, evtType, x, y, rect,
|
|
buttons: buttons);
|
|
}
|
|
|
|
static Point<double>? getPointInRemoteRect(
|
|
bool isLocalDesktop,
|
|
String? peerPlatform,
|
|
String kind,
|
|
String evtType,
|
|
double evtX,
|
|
double evtY,
|
|
Rect rect,
|
|
{int buttons = kPrimaryMouseButton}) {
|
|
double minX = rect.left;
|
|
// https://github.com/rustdesk/rustdesk/issues/6678
|
|
// For Windows, [0,maxX], [0,maxY] should be set to enable window snapping.
|
|
double maxX = (rect.left + rect.width) -
|
|
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
|
|
double minY = rect.top;
|
|
double maxY = (rect.top + rect.height) -
|
|
(peerPlatform == kPeerPlatformWindows ? 0 : 1);
|
|
evtX = InputModel.tryGetNearestRange(evtX, minX, maxX, 5);
|
|
evtY = InputModel.tryGetNearestRange(evtY, minY, maxY, 5);
|
|
if (isLocalDesktop) {
|
|
if (kind == kPointerEventKindMouse) {
|
|
if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
|
|
// If left mouse up, no early return.
|
|
if (!(buttons == kPrimaryMouseButton &&
|
|
evtType == kMouseEventTypeUp)) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
bool evtXInRange = evtX >= minX && evtX <= maxX;
|
|
bool evtYInRange = evtY >= minY && evtY <= maxY;
|
|
if (!(evtXInRange || evtYInRange)) {
|
|
return null;
|
|
}
|
|
if (evtX < minX) {
|
|
evtX = minX;
|
|
} else if (evtX > maxX) {
|
|
evtX = maxX;
|
|
}
|
|
if (evtY < minY) {
|
|
evtY = minY;
|
|
} else if (evtY > maxY) {
|
|
evtY = maxY;
|
|
}
|
|
}
|
|
|
|
return Point(evtX, evtY);
|
|
}
|
|
|
|
/// Web only
|
|
void listenToMouse(bool yesOrNo) {
|
|
if (yesOrNo) {
|
|
platformFFI.startDesktopWebListener();
|
|
} else {
|
|
platformFFI.stopDesktopWebListener();
|
|
}
|
|
}
|
|
|
|
void onMobileBack() {
|
|
final minBackButtonVersion = "1.3.8";
|
|
final peerVersion =
|
|
parent.target?.ffiModel.pi.version ?? minBackButtonVersion;
|
|
var btn = MouseButtons.back;
|
|
// For compatibility with old versions
|
|
if (versionCmp(peerVersion, minBackButtonVersion) < 0) {
|
|
btn = MouseButtons.right;
|
|
}
|
|
tap(btn);
|
|
}
|
|
|
|
void onMobileHome() => tap(MouseButtons.wheel);
|
|
Future<void> onMobileApps() async {
|
|
sendMouse('down', MouseButtons.wheel);
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
sendMouse('up', MouseButtons.wheel);
|
|
}
|
|
|
|
// Simulate a key press event.
|
|
// `usbHidUsage` is the USB HID usage code of the key.
|
|
Future<void> tapHidKey(int usbHidUsage) async {
|
|
newKeyboardMode(kKeyFlutterKey, usbHidUsage, true, false);
|
|
await Future.delayed(Duration(milliseconds: 100));
|
|
newKeyboardMode(kKeyFlutterKey, usbHidUsage, false, false);
|
|
}
|
|
|
|
Future<void> onMobileVolumeUp() async =>
|
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeUp.usbHidUsage & 0xFFFF);
|
|
Future<void> onMobileVolumeDown() async =>
|
|
await tapHidKey(PhysicalKeyboardKey.audioVolumeDown.usbHidUsage & 0xFFFF);
|
|
Future<void> onMobilePower() async =>
|
|
await tapHidKey(PhysicalKeyboardKey.power.usbHidUsage & 0xFFFF);
|
|
}
|