feat: mobile, virtual mouse (#12911)

* feat: mobile, virtual mouse

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat: mobile, virtual mouse, mouse mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* refact: mobile, virtual mouse, mouse mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat: mobile, virtual mouse mode

Signed-off-by: fufesou <linlong1266@gmail.com>

* feat: mobile virtual mouse, options

Signed-off-by: fufesou <linlong1266@gmail.com>

---------

Signed-off-by: fufesou <linlong1266@gmail.com>
Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com>
This commit is contained in:
fufesou
2025-10-08 20:23:55 -04:00
committed by GitHub
parent 02f455b0cc
commit 0f3a03aab7
56 changed files with 2714 additions and 49 deletions

View File

@@ -75,6 +75,9 @@ bool _ignoreDevicePixelRatio = true;
int windowsBuildNumber = 0;
DesktopType? desktopType;
// Tolerance used for floating-point position comparisons to avoid precision errors.
const double _kPositionEpsilon = 1e-6;
bool get isMainDesktopWindow =>
desktopType == DesktopType.main || desktopType == DesktopType.cm;
@@ -106,6 +109,10 @@ enum DesktopType {
portForward,
}
bool isDoubleEqual(double a, double b) {
return (a - b).abs() < _kPositionEpsilon;
}
class IconFont {
static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar';
@@ -1852,6 +1859,8 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
return Size(restoreWidth, restoreHeight);
}
// Consider using Rect.contains() instead,
// though the implementation is not exactly the same.
bool isPointInRect(Offset point, Rect rect) {
return point.dx >= rect.left &&
point.dx <= rect.right &&

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
enum GestureState {
none,
@@ -96,6 +97,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
if (onTwoFingerScaleEnd != null) {
onTwoFingerScaleEnd!(d);
}
if (isSpecialHoldDragActive) {
// If we are in special drag mode, we need to reset the state.
// Otherwise, the next `onTwoFingerScaleUpdate()` will handle a wrong `focalPoint`.
_currentState = GestureState.none;
return;
}
break;
case GestureState.threeFingerVerticalDrag:
debugPrint("ThreeFingerState.vertical onEnd");

View File

@@ -51,6 +51,13 @@ class RawKeyFocusScope extends StatelessWidget {
}
}
// For virtual mouse when using the mouse mode on mobile.
// Special hold-drag mode: one finger holds a button (left/right button), another finger pans.
// This flag is to override the scale gesture to a pan gesture.
bool isSpecialHoldDragActive = false;
// Cache the last focal point to calculate deltas in special hold-drag mode.
Offset _lastSpecialHoldDragFocalPoint = Offset.zero;
class RawTouchGestureDetectorRegion extends StatefulWidget {
final Widget child;
final FFI ffi;
@@ -97,6 +104,10 @@ class _RawTouchGestureDetectorRegionState
bool _touchModePanStarted = false;
Offset _doubleFinerTapPosition = Offset.zero;
// For mouse mode, we need to block the events when the cursor is in a blocked area.
// So we need to cache the last tap down position.
Offset? _lastTapDownPositionForMouseMode;
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
InputModel get inputModel => widget.inputModel;
@@ -112,7 +123,15 @@ class _RawTouchGestureDetectorRegionState
}
bool isNotTouchBasedDevice() {
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
return !kTouchBasedDeviceKinds.contains(lastDeviceKind);
}
// Mobile, mouse mode.
// Check if should block the mouse tap event (`_lastTapDownPositionForMouseMode`).
bool shouldBlockMouseModeEvent() {
return _lastTapDownPositionForMouseMode != null &&
ffi.cursorModel.shouldBlock(_lastTapDownPositionForMouseMode!.dx,
_lastTapDownPositionForMouseMode!.dy);
}
onTapDown(TapDownDetails d) async {
@@ -124,6 +143,8 @@ class _RawTouchGestureDetectorRegionState
_lastPosOfDoubleTapDown = d.localPosition;
// Desktop or mobile "Touch mode"
_lastTapDownDetails = d;
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
}
}
@@ -150,6 +171,11 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (!handleTouch) {
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
// Using `_lastTapDownPositionForMouseMode` instead.
if (shouldBlockMouseModeEvent()) {
return;
}
// Mobile, "Mouse mode"
await inputModel.tap(MouseButtons.left);
}
@@ -163,6 +189,8 @@ class _RawTouchGestureDetectorRegionState
if (handleTouch) {
_lastPosOfDoubleTapDown = d.localPosition;
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
}
}
@@ -177,6 +205,12 @@ class _RawTouchGestureDetectorRegionState
!ffi.cursorModel.isInRemoteRect(_lastPosOfDoubleTapDown)) {
return;
}
// Check if the position is in a blocked area when using the mouse mode.
if (!handleTouch) {
if (shouldBlockMouseModeEvent()) {
return;
}
}
await inputModel.tap(MouseButtons.left);
await inputModel.tap(MouseButtons.left);
}
@@ -198,6 +232,8 @@ class _RawTouchGestureDetectorRegionState
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
await inputModel.tapDown(MouseButtons.left);
}
} else {
_lastTapDownPositionForMouseMode = d.localPosition;
}
}
@@ -222,6 +258,10 @@ class _RawTouchGestureDetectorRegionState
if (!isMoved) {
return;
}
} else {
if (shouldBlockMouseModeEvent()) {
return;
}
}
await inputModel.tap(MouseButtons.right);
} else {
@@ -274,6 +314,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (!handleTouch) {
if (isSpecialHoldDragActive) return;
await inputModel.sendMouse('down', MouseButtons.left);
}
}
@@ -283,6 +324,7 @@ class _RawTouchGestureDetectorRegionState
return;
}
if (!handleTouch) {
if (isSpecialHoldDragActive) return;
await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
}
}
@@ -377,12 +419,26 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
if (isSpecialHoldDragActive) {
// Initialize the last focal point to calculate deltas manually.
_lastSpecialHoldDragFocalPoint = d.focalPoint;
}
}
onTwoFingerScaleUpdate(ScaleUpdateDetails d) async {
if (isNotTouchBasedDevice()) {
return;
}
// If in special drag mode, perform a pan instead of a scale.
if (isSpecialHoldDragActive) {
// Calculate delta manually to avoid the jumpy behavior.
final delta = d.focalPoint - _lastSpecialHoldDragFocalPoint;
_lastSpecialHoldDragFocalPoint = d.focalPoint;
await ffi.cursorModel.updatePan(delta * 2.0, d.focalPoint, handleTouch);
return;
}
if ((isDesktop || isWebDesktop)) {
final scale = ((d.scale - _scale) * 1000).toInt();
_scale = d.scale;
@@ -420,7 +476,9 @@ class _RawTouchGestureDetectorRegionState
// No idea why we need to set the view style to "" here.
// bind.sessionSetViewStyle(sessionId: sessionId, value: "");
}
await inputModel.sendMouse('up', MouseButtons.left);
if (!isSpecialHoldDragActive) {
await inputModel.sendMouse('up', MouseButtons.left);
}
}
get onHoldDragCancel => null;

View File

@@ -155,6 +155,9 @@ const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification";
const String kOptionEnableUdpPunch = "enable-udp-punch";
const String kOptionEnableIpv6Punch = "enable-ipv6-punch";
const String kOptionEnableTrustedDevices = "enable-trusted-devices";
const String kOptionShowVirtualMouse = "show-virtual-mouse";
const String kOptionVirtualMouseScale = "virtual-mouse-scale";
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
// network options
const String kOptionAllowWebSocket = "allow-websocket";

View File

@@ -6,6 +6,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_hbb/common/shared_state.dart';
import 'package:flutter_hbb/common/widgets/toolbar.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/mobile/widgets/floating_mouse.dart';
import 'package:flutter_hbb/mobile/widgets/floating_mouse_widgets.dart';
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
@@ -617,6 +619,15 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
if (showCursorPaint) {
paints.add(CursorPaint(widget.id));
}
if (gFFI.ffiModel.touchMode) {
paints.add(FloatingMouse(
ffi: gFFI,
));
} else {
paints.add(FloatingMouseWidgets(
ffi: gFFI,
));
}
return paints;
}()));
}
@@ -789,13 +800,15 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
controller: ScrollController(),
padding: EdgeInsets.symmetric(vertical: 10),
child: GestureHelp(
touchMode: gFFI.ffiModel.touchMode,
onTouchModeChange: (t) {
gFFI.ffiModel.toggleTouchMode();
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
bind.sessionPeerOption(
sessionId: sessionId, name: kOptionTouchMode, value: v);
})));
touchMode: gFFI.ffiModel.touchMode,
onTouchModeChange: (t) {
gFFI.ffiModel.toggleTouchMode();
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
bind.sessionPeerOption(
sessionId: sessionId, name: kOptionTouchMode, value: v);
},
virtualMouseMode: gFFI.ffiModel.virtualMouseMode,
)));
}
// * Currently mobile does not enable map mode

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,880 @@
// These floating mouse widgets are used to simulate a physical mouse
// when "mobile" -> "desktop" in mouse mode.
// This file does not contain whole mouse widgets, it only contains
// parts that help to control, such as wheel scroll and wheel button.
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/remote_input.dart';
import 'package:flutter_hbb/models/input_model.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
// Used for the wheel button and wheel scroll widgets
const double _kSpaceToHorizontalEdge = 25;
const double _wheelWidth = 50;
const double _wheelHeight = 162;
// Used for the left/right button widgets
const double _kSpaceToVerticalEdge = 15;
const double _kSpaceBetweenLeftRightButtons = 40;
const double _kLeftRightButtonWidth = 55;
const double _kLeftRightButtonHeight = 40;
const double _kBorderWidth = 1;
final Color _kDefaultBorderColor = Colors.white.withOpacity(0.7);
final Color _kDefaultColor = Colors.black.withOpacity(0.4);
final Color _kTapDownColor = Colors.blue.withOpacity(0.7);
final Color _kWidgetHighlightColor = Colors.white.withOpacity(0.9);
const int _kInputTimerIntervalMillis = 100;
class FloatingMouseWidgets extends StatefulWidget {
final FFI ffi;
const FloatingMouseWidgets({
super.key,
required this.ffi,
});
@override
State<FloatingMouseWidgets> createState() => _FloatingMouseWidgetsState();
}
class _FloatingMouseWidgetsState extends State<FloatingMouseWidgets> {
InputModel get _inputModel => widget.ffi.inputModel;
CursorModel get _cursorModel => widget.ffi.cursorModel;
late final VirtualMouseMode _virtualMouseMode;
@override
void initState() {
super.initState();
_virtualMouseMode = widget.ffi.ffiModel.virtualMouseMode;
_virtualMouseMode.addListener(_onVirtualMouseModeChanged);
_cursorModel.blockEvents = false;
isSpecialHoldDragActive = false;
}
void _onVirtualMouseModeChanged() {
if (mounted) {
setState(() {});
}
}
@override
void dispose() {
_virtualMouseMode.removeListener(_onVirtualMouseModeChanged);
super.dispose();
_cursorModel.blockEvents = false;
isSpecialHoldDragActive = false;
}
@override
Widget build(BuildContext context) {
final virtualMouseMode = _virtualMouseMode;
if (!virtualMouseMode.showVirtualMouse) {
return const Offstage();
}
return Stack(
children: [
FloatingWheel(
inputModel: _inputModel,
cursorModel: _cursorModel,
),
if (virtualMouseMode.showVirtualJoystick)
VirtualJoystick(cursorModel: _cursorModel),
FloatingLeftRightButton(
isLeft: true,
inputModel: _inputModel,
cursorModel: _cursorModel,
),
FloatingLeftRightButton(
isLeft: false,
inputModel: _inputModel,
cursorModel: _cursorModel,
),
],
);
}
}
class FloatingWheel extends StatefulWidget {
final InputModel inputModel;
final CursorModel cursorModel;
const FloatingWheel(
{super.key, required this.inputModel, required this.cursorModel});
@override
State<FloatingWheel> createState() => _FloatingWheelState();
}
class _FloatingWheelState extends State<FloatingWheel> {
Offset _position = Offset.zero;
bool _isInitialized = false;
Rect? _lastBlockedRect;
bool _isUpDown = false;
bool _isMidDown = false;
bool _isDownDown = false;
Orientation? _previousOrientation;
Timer? _scrollTimer;
InputModel get _inputModel => widget.inputModel;
CursorModel get _cursorModel => widget.cursorModel;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_resetPosition();
});
}
void _resetPosition() {
final size = MediaQuery.of(context).size;
setState(() {
_position = Offset(
size.width - _wheelWidth - _kSpaceToHorizontalEdge,
(size.height - _wheelHeight) / 2,
);
_isInitialized = true;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _updateBlockedRect();
});
}
void _updateBlockedRect() {
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
final newRect =
Rect.fromLTWH(_position.dx, _position.dy, _wheelWidth, _wheelHeight);
_cursorModel.addBlockedRect(newRect);
_lastBlockedRect = newRect;
}
@override
void dispose() {
_scrollTimer?.cancel();
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentOrientation = MediaQuery.of(context).orientation;
if (_previousOrientation != null &&
_previousOrientation != currentOrientation) {
_resetPosition();
}
_previousOrientation = currentOrientation;
}
Widget _buildUpDownButton(
void Function(PointerDownEvent) onPointerDown,
void Function(PointerUpEvent) onPointerUp,
void Function(PointerCancelEvent) onPointerCancel,
bool Function() flagGetter,
BorderRadiusGeometry borderRadius,
IconData iconData) {
return Listener(
onPointerDown: onPointerDown,
onPointerUp: onPointerUp,
onPointerCancel: onPointerCancel,
child: Container(
width: _wheelWidth,
height: 55,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _kDefaultColor,
border: Border.all(
color: flagGetter() ? _kTapDownColor : _kDefaultBorderColor,
width: 1),
borderRadius: borderRadius,
),
child: Icon(iconData, color: _kDefaultBorderColor, size: 32),
),
);
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return Positioned(child: Offstage());
}
return Positioned(
left: _position.dx,
top: _position.dy,
child: _buildWidget(context),
);
}
Widget _buildWidget(BuildContext context) {
return Container(
width: _wheelWidth,
height: _wheelHeight,
child: Column(
children: [
_buildUpDownButton(
(event) {
setState(() {
_isUpDown = true;
});
_startScrollTimer(1);
},
(event) {
setState(() {
_isUpDown = false;
});
_stopScrollTimer();
},
(event) {
setState(() {
_isUpDown = false;
});
_stopScrollTimer();
},
() => _isUpDown,
BorderRadius.vertical(top: Radius.circular(_wheelWidth * 0.5)),
Icons.keyboard_arrow_up,
),
Listener(
onPointerDown: (event) {
setState(() {
_isMidDown = true;
});
_inputModel.tapDown(MouseButtons.wheel);
},
onPointerUp: (event) {
setState(() {
_isMidDown = false;
});
_inputModel.tapUp(MouseButtons.wheel);
},
onPointerCancel: (event) {
setState(() {
_isMidDown = false;
});
_inputModel.tapUp(MouseButtons.wheel);
},
child: Container(
width: _wheelWidth,
height: 52,
decoration: BoxDecoration(
color: _kDefaultColor,
border: Border.symmetric(
vertical: BorderSide(
color:
_isMidDown ? _kTapDownColor : _kDefaultBorderColor,
width: _kBorderWidth)),
),
child: Center(
child: Container(
width: _wheelWidth - 10,
height: _wheelWidth - 10,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 18,
height: 2,
color: _kDefaultBorderColor,
),
SizedBox(height: 6),
Container(
width: 24,
height: 2,
color: _kDefaultBorderColor,
),
SizedBox(height: 6),
Container(
width: 18,
height: 2,
color: _kDefaultBorderColor,
),
],
),
),
),
),
),
),
_buildUpDownButton(
(event) {
setState(() {
_isDownDown = true;
});
_startScrollTimer(-1);
},
(event) {
setState(() {
_isDownDown = false;
});
_stopScrollTimer();
},
(event) {
setState(() {
_isDownDown = false;
});
_stopScrollTimer();
},
() => _isDownDown,
BorderRadius.vertical(bottom: Radius.circular(_wheelWidth * 0.5)),
Icons.keyboard_arrow_down,
),
],
),
);
}
void _startScrollTimer(int direction) {
_scrollTimer?.cancel();
_inputModel.scroll(direction);
_scrollTimer = Timer.periodic(
Duration(milliseconds: _kInputTimerIntervalMillis), (timer) {
_inputModel.scroll(direction);
});
}
void _stopScrollTimer() {
_scrollTimer?.cancel();
_scrollTimer = null;
}
}
class FloatingLeftRightButton extends StatefulWidget {
final bool isLeft;
final InputModel inputModel;
final CursorModel cursorModel;
const FloatingLeftRightButton(
{super.key,
required this.isLeft,
required this.inputModel,
required this.cursorModel});
@override
State<FloatingLeftRightButton> createState() =>
_FloatingLeftRightButtonState();
}
class _FloatingLeftRightButtonState extends State<FloatingLeftRightButton> {
Offset _position = Offset.zero;
bool _isInitialized = false;
bool _isDown = false;
Rect? _lastBlockedRect;
Orientation? _previousOrientation;
Offset _preSavedPos = Offset.zero;
// Gesture ambiguity resolution
Timer? _tapDownTimer;
final Duration _pressTimeout = const Duration(milliseconds: 200);
bool _isDragging = false;
bool get _isLeft => widget.isLeft;
InputModel get _inputModel => widget.inputModel;
CursorModel get _cursorModel => widget.cursorModel;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final currentOrientation = MediaQuery.of(context).orientation;
_previousOrientation = currentOrientation;
_resetPosition(currentOrientation);
});
}
@override
void dispose() {
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
_tapDownTimer?.cancel();
_trySavePosition();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentOrientation = MediaQuery.of(context).orientation;
if (_previousOrientation == null ||
_previousOrientation != currentOrientation) {
_resetPosition(currentOrientation);
}
_previousOrientation = currentOrientation;
}
double _getOffsetX(double w) {
if (_isLeft) {
return (w - _kLeftRightButtonWidth * 2 - _kSpaceBetweenLeftRightButtons) *
0.5;
} else {
return (w + _kSpaceBetweenLeftRightButtons) * 0.5;
}
}
String _getPositionKey(Orientation ori) {
final strLeftRight = _isLeft ? 'l' : 'r';
final strOri = ori == Orientation.landscape ? 'l' : 'p';
return '$strLeftRight$strOri-mouse-btn-pos';
}
static Offset? _loadPositionFromString(String s) {
if (s.isEmpty) {
return null;
}
try {
final m = jsonDecode(s);
return Offset(m['x'], m['y']);
} catch (e) {
debugPrintStack(label: 'Failed to load position "$s" $e');
return null;
}
}
void _trySavePosition() {
if (_previousOrientation == null) return;
if (((_position - _preSavedPos)).distanceSquared < 0.1) return;
final pos = jsonEncode({
'x': _position.dx,
'y': _position.dy,
});
bind.setLocalFlutterOption(
k: _getPositionKey(_previousOrientation!), v: pos);
_preSavedPos = _position;
}
void _restorePosition(Orientation ori) {
final ps = bind.getLocalFlutterOption(k: _getPositionKey(ori));
final pos = _loadPositionFromString(ps);
if (pos == null) {
final size = MediaQuery.of(context).size;
_position = Offset(_getOffsetX(size.width),
size.height - _kSpaceToVerticalEdge - _kLeftRightButtonHeight);
} else {
_position = pos;
_preSavedPos = pos;
}
}
void _resetPosition(Orientation ori) {
setState(() {
_restorePosition(ori);
_isInitialized = true;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _updateBlockedRect();
});
}
void _updateBlockedRect() {
if (_lastBlockedRect != null) {
_cursorModel.removeBlockedRect(_lastBlockedRect!);
}
final newRect = Rect.fromLTWH(_position.dx, _position.dy,
_kLeftRightButtonWidth, _kLeftRightButtonHeight);
_cursorModel.addBlockedRect(newRect);
_lastBlockedRect = newRect;
}
void _onMoveUpdateDelta(Offset delta) {
final context = this.context;
final size = MediaQuery.of(context).size;
Offset newPosition = _position + delta;
double minX = _kSpaceToHorizontalEdge;
double minY = _kSpaceToVerticalEdge;
double maxX = size.width - _kLeftRightButtonWidth - _kSpaceToHorizontalEdge;
double maxY = size.height - _kLeftRightButtonHeight - _kSpaceToVerticalEdge;
newPosition = Offset(
newPosition.dx.clamp(minX, maxX),
newPosition.dy.clamp(minY, maxY),
);
final isPositionChanged = !(isDoubleEqual(newPosition.dx, _position.dx) &&
isDoubleEqual(newPosition.dy, _position.dy));
setState(() {
_position = newPosition;
});
if (isPositionChanged) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _updateBlockedRect();
});
}
}
void _onBodyPointerMoveUpdate(PointerMoveEvent event) {
_cursorModel.blockEvents = true;
// If move, it's a drag, not a tap.
_isDragging = true;
// Cancel the timer to prevent it from being recognized as a tap/hold.
_tapDownTimer?.cancel();
_tapDownTimer = null;
_onMoveUpdateDelta(event.delta);
}
Widget _buildButtonIcon() {
final double w = _kLeftRightButtonWidth * 0.45;
final double h = _kLeftRightButtonHeight * 0.75;
final double borderRadius = w * 0.5;
final double quarterCircleRadius = borderRadius * 0.9;
return Stack(
children: [
Container(
width: w,
height: h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_kLeftRightButtonWidth * 0.225),
color: Colors.white,
),
),
Positioned(
left: _isLeft ? quarterCircleRadius * 0.25 : null,
right: _isLeft ? null : quarterCircleRadius * 0.25,
top: quarterCircleRadius * 0.25,
child: CustomPaint(
size: Size(quarterCircleRadius * 2, quarterCircleRadius * 2),
painter: _QuarterCirclePainter(
color: _kDefaultColor,
isLeft: _isLeft,
radius: quarterCircleRadius,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return Positioned(child: Offstage());
}
return Positioned(
left: _position.dx,
top: _position.dy,
// We can't use the GestureDetector here, because `onTapDown` may be
// triggered sometimes when dragging.
child: Listener(
onPointerMove: _onBodyPointerMoveUpdate,
onPointerDown: (event) async {
_isDragging = false;
setState(() {
_isDown = true;
});
// Start a timer. If it fires, it's a hold.
_tapDownTimer?.cancel();
_tapDownTimer = Timer(_pressTimeout, () {
isSpecialHoldDragActive = true;
() async {
await _cursorModel.syncCursorPosition();
await _inputModel
.tapDown(_isLeft ? MouseButtons.left : MouseButtons.right);
}();
_tapDownTimer = null;
});
},
onPointerUp: (event) {
_cursorModel.blockEvents = false;
setState(() {
_isDown = false;
});
// If timer is active, it's a quick tap.
if (_tapDownTimer != null) {
_tapDownTimer!.cancel();
_tapDownTimer = null;
// Fire tap down and up quickly.
_inputModel
.tapDown(_isLeft ? MouseButtons.left : MouseButtons.right)
.then(
(_) => Future.delayed(const Duration(milliseconds: 50), () {
_inputModel.tapUp(
_isLeft ? MouseButtons.left : MouseButtons.right);
}));
} else {
// If it's not a quick tap, it could be a hold or drag.
// If it was a hold, isSpecialHoldDragActive is true.
if (isSpecialHoldDragActive) {
_inputModel
.tapUp(_isLeft ? MouseButtons.left : MouseButtons.right);
}
}
if (_isDragging) {
_trySavePosition();
}
isSpecialHoldDragActive = false;
},
onPointerCancel: (event) {
_cursorModel.blockEvents = false;
setState(() {
_isDown = false;
});
_tapDownTimer?.cancel();
_tapDownTimer = null;
if (isSpecialHoldDragActive) {
_inputModel.tapUp(_isLeft ? MouseButtons.left : MouseButtons.right);
}
isSpecialHoldDragActive = false;
if (_isDragging) {
_trySavePosition();
}
},
child: Container(
width: _kLeftRightButtonWidth,
height: _kLeftRightButtonHeight,
alignment: Alignment.center,
decoration: BoxDecoration(
color: _kDefaultColor,
border: Border.all(
color: _isDown ? _kTapDownColor : _kDefaultBorderColor,
width: _kBorderWidth),
borderRadius: _isLeft
? BorderRadius.horizontal(
left: Radius.circular(_kLeftRightButtonHeight * 0.5))
: BorderRadius.horizontal(
right: Radius.circular(_kLeftRightButtonHeight * 0.5)),
),
child: _buildButtonIcon(),
),
),
);
}
}
class _QuarterCirclePainter extends CustomPainter {
final Color color;
final bool isLeft;
final double radius;
_QuarterCirclePainter(
{required this.color, required this.isLeft, required this.radius});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..style = PaintingStyle.fill;
final rect = Rect.fromLTWH(0, 0, radius * 2, radius * 2);
if (isLeft) {
canvas.drawArc(rect, -pi, pi / 2, true, paint);
} else {
canvas.drawArc(rect, -pi / 2, pi / 2, true, paint);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
// Virtual joystick sends the absolute movement for now.
// Maybe we need to change it to relative movement in the future.
class VirtualJoystick extends StatefulWidget {
final CursorModel cursorModel;
const VirtualJoystick({super.key, required this.cursorModel});
@override
State<VirtualJoystick> createState() => _VirtualJoystickState();
}
class _VirtualJoystickState extends State<VirtualJoystick> {
Offset _position = Offset.zero;
bool _isInitialized = false;
Offset _offset = Offset.zero;
final double _joystickRadius = 50.0;
final double _thumbRadius = 20.0;
final double _moveStep = 3.0;
final double _speed = 1.0;
// One-shot timer to detect a drag gesture
Timer? _dragStartTimer;
// Periodic timer for continuous movement
Timer? _continuousMoveTimer;
Size? _lastScreenSize;
bool _isPressed = false;
@override
void initState() {
super.initState();
widget.cursorModel.blockEvents = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
_lastScreenSize = MediaQuery.of(context).size;
_resetPosition();
});
}
@override
void dispose() {
_stopSendEventTimer();
widget.cursorModel.blockEvents = false;
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final currentScreenSize = MediaQuery.of(context).size;
if (_lastScreenSize != null && _lastScreenSize != currentScreenSize) {
_resetPosition();
}
_lastScreenSize = currentScreenSize;
}
void _resetPosition() {
final size = MediaQuery.of(context).size;
setState(() {
_position = Offset(
_kSpaceToHorizontalEdge + _joystickRadius,
size.height * 0.5 + _joystickRadius * 1.5,
);
_isInitialized = true;
});
}
Offset _offsetToPanDelta(Offset offset) {
return Offset(
offset.dx / _joystickRadius,
offset.dy / _joystickRadius,
);
}
void _stopSendEventTimer() {
_dragStartTimer?.cancel();
_continuousMoveTimer?.cancel();
_dragStartTimer = null;
_continuousMoveTimer = null;
}
@override
Widget build(BuildContext context) {
if (!_isInitialized) {
return Positioned(child: Offstage());
}
return Positioned(
left: _position.dx - _joystickRadius,
top: _position.dy - _joystickRadius,
child: GestureDetector(
onPanStart: (details) {
setState(() {
_isPressed = true;
});
widget.cursorModel.blockEvents = true;
_updateOffset(details.localPosition);
// 1. Send a single, small pan event immediately for responsiveness.
// The movement is small for a gentle start.
final initialDelta = _offsetToPanDelta(_offset);
if (initialDelta.distance > 0) {
widget.cursorModel.updatePan(initialDelta, Offset.zero, false);
}
// 2. Start a one-shot timer to check if the user is holding for a drag.
_dragStartTimer?.cancel();
_dragStartTimer = Timer(const Duration(milliseconds: 120), () {
// 3. If the timer fires, it's a drag. Start the continuous movement timer.
_continuousMoveTimer?.cancel();
_continuousMoveTimer =
periodic_immediate(const Duration(milliseconds: 20), () async {
if (_offset != Offset.zero) {
widget.cursorModel.updatePan(
_offsetToPanDelta(_offset) * _moveStep * _speed,
Offset.zero,
false);
}
});
});
},
onPanUpdate: (details) {
_updateOffset(details.localPosition);
},
onPanEnd: (details) {
setState(() {
_offset = Offset.zero;
_isPressed = false;
});
widget.cursorModel.blockEvents = false;
// 4. Critical step: On pan end, cancel all timers.
// If it was a flick, this cancels the drag detection before it fires.
// If it was a drag, this stops the continuous movement.
_stopSendEventTimer();
},
child: CustomPaint(
size: Size(_joystickRadius * 2, _joystickRadius * 2),
painter: _JoystickPainter(
_offset, _joystickRadius, _thumbRadius, _isPressed),
),
),
);
}
void _updateOffset(Offset localPosition) {
final center = Offset(_joystickRadius, _joystickRadius);
final offset = localPosition - center;
final distance = offset.distance;
if (distance <= _joystickRadius) {
setState(() {
_offset = offset;
});
} else {
final clampedOffset = offset / distance * _joystickRadius;
setState(() {
_offset = clampedOffset;
});
}
}
}
class _JoystickPainter extends CustomPainter {
final Offset _offset;
final double _joystickRadius;
final double _thumbRadius;
final bool _isPressed;
_JoystickPainter(
this._offset, this._joystickRadius, this._thumbRadius, this._isPressed);
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final joystickColor = _kDefaultColor;
final borderColor = _isPressed ? _kTapDownColor : _kDefaultBorderColor;
final thumbColor = _kWidgetHighlightColor;
final joystickPaint = Paint()
..color = joystickColor
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
final thumbPaint = Paint()
..color = thumbColor
..style = PaintingStyle.fill;
// Draw joystick base and border
canvas.drawCircle(center, _joystickRadius, joystickPaint);
canvas.drawCircle(center, _joystickRadius, borderPaint);
// Draw thumb
final thumbCenter = center + _offset;
canvas.drawCircle(thumbCenter, _thumbRadius, thumbPaint);
}
@override
bool shouldRepaint(covariant _JoystickPainter oldDelegate) {
return oldDelegate._offset != _offset ||
oldDelegate._isPressed != _isPressed;
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:toggle_switch/toggle_switch.dart';
class GestureIcons {
@@ -35,20 +36,27 @@ typedef OnTouchModeChange = void Function(bool);
class GestureHelp extends StatefulWidget {
GestureHelp(
{Key? key, required this.touchMode, required this.onTouchModeChange})
{Key? key,
required this.touchMode,
required this.onTouchModeChange,
required this.virtualMouseMode})
: super(key: key);
final bool touchMode;
final OnTouchModeChange onTouchModeChange;
final VirtualMouseMode virtualMouseMode;
@override
State<StatefulWidget> createState() => _GestureHelpState(touchMode);
State<StatefulWidget> createState() =>
_GestureHelpState(touchMode, virtualMouseMode);
}
class _GestureHelpState extends State<GestureHelp> {
late int _selectedIndex;
late bool _touchMode;
final VirtualMouseMode _virtualMouseMode;
_GestureHelpState(bool touchMode) {
_GestureHelpState(bool touchMode, VirtualMouseMode virtualMouseMode)
: _virtualMouseMode = virtualMouseMode {
_touchMode = touchMode;
_selectedIndex = _touchMode ? 1 : 0;
}
@@ -68,31 +76,144 @@ class _GestureHelpState extends State<GestureHelp> {
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ToggleSwitch(
initialLabelIndex: _selectedIndex,
activeFgColor: Colors.white,
inactiveFgColor: Colors.white60,
activeBgColor: [MyTheme.accent],
inactiveBgColor: Theme.of(context).hintColor,
totalSwitches: 2,
minWidth: 150,
fontSize: 15,
iconSize: 18,
labels: [translate("Mouse mode"), translate("Touch mode")],
icons: [Icons.mouse, Icons.touch_app],
onToggle: (index) {
setState(() {
if (_selectedIndex != index) {
_selectedIndex = index ?? 0;
_touchMode = index == 0 ? false : true;
widget.onTouchModeChange(_touchMode);
}
});
},
Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ToggleSwitch(
initialLabelIndex: _selectedIndex,
activeFgColor: Colors.white,
inactiveFgColor: Colors.white60,
activeBgColor: [MyTheme.accent],
inactiveBgColor: Theme.of(context).hintColor,
totalSwitches: 2,
minWidth: 150,
fontSize: 15,
iconSize: 18,
labels: [
translate("Mouse mode"),
translate("Touch mode")
],
icons: [Icons.mouse, Icons.touch_app],
onToggle: (index) {
setState(() {
if (_selectedIndex != index) {
_selectedIndex = index ?? 0;
_touchMode = index == 0 ? false : true;
widget.onTouchModeChange(_touchMode);
}
});
},
),
Transform.translate(
offset: const Offset(-10.0, 0.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value: _virtualMouseMode.showVirtualMouse,
onChanged: (value) async {
if (value == null) return;
await _virtualMouseMode.toggleVirtualMouse();
setState(() {});
},
),
InkWell(
onTap: () async {
await _virtualMouseMode.toggleVirtualMouse();
setState(() {});
},
child: Text(translate('Show virtual mouse')),
),
],
),
),
if (_touchMode && _virtualMouseMode.showVirtualMouse)
Padding(
// Indent "Virtual mouse size"
padding: const EdgeInsets.only(left: 24.0),
child: SizedBox(
width: 260,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(
top: 0.0, bottom: 0),
child: Text(translate('Virtual mouse size')),
),
Transform.translate(
offset: Offset(-0.0, -6.0),
child: Row(
children: [
Padding(
padding:
const EdgeInsets.only(left: 0.0),
child: Text(translate('Small')),
),
Expanded(
child: Slider(
value: _virtualMouseMode
.virtualMouseScale,
min: 0.8,
max: 1.8,
divisions: 10,
onChanged: (value) {
_virtualMouseMode
.setVirtualMouseScale(value);
setState(() {});
},
),
),
Padding(
padding:
const EdgeInsets.only(right: 16.0),
child: Text(translate('Large')),
),
],
),
),
],
),
),
),
if (!_touchMode && _virtualMouseMode.showVirtualMouse)
Transform.translate(
offset: const Offset(-10.0, -12.0),
child: Padding(
// Indent "Show virtual joystick"
padding: const EdgeInsets.only(left: 24.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Checkbox(
value:
_virtualMouseMode.showVirtualJoystick,
onChanged: (value) async {
if (value == null) return;
await _virtualMouseMode
.toggleVirtualJoystick();
setState(() {});
},
),
InkWell(
onTap: () async {
await _virtualMouseMode
.toggleVirtualJoystick();
setState(() {});
},
child: Text(
translate("Show virtual joystick")),
),
],
)),
),
],
),
),
const SizedBox(height: 30),
Container(
child: Wrap(
spacing: space,

View File

@@ -766,6 +766,11 @@ class InputModel {
command: command);
}
static Map<String, dynamic> getMouseEventMove() => {
'type': _kMouseEventMove,
'buttons': 0,
};
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
@@ -1222,16 +1227,17 @@ class InputModel {
return false;
}
void handleMouse(
Map<String, dynamic>? processEventToPeer(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
}) {
if (isViewCamera) return;
if (isViewCamera) return null;
double x = offset.dx;
double y = max(0.0, offset.dy);
if (_checkPeerControlProtected(x, y)) {
return;
return null;
}
var type = kMouseEventTypeDefault;
@@ -1248,7 +1254,7 @@ class InputModel {
isMove = true;
break;
default:
return;
return null;
}
evt['type'] = type;
@@ -1266,9 +1272,10 @@ class InputModel {
type,
onExit: onExit,
buttons: evt['buttons'],
moveCanvas: moveCanvas,
);
if (pos == null) {
return;
return null;
}
if (type != '') {
evt['x'] = '0';
@@ -1286,7 +1293,22 @@ class InputModel {
kForwardMouseButton: 'forward'
};
evt['buttons'] = mapButtons[evt['buttons']] ?? '';
bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
return evt;
}
Map<String, dynamic>? handleMouse(
Map<String, dynamic> evt,
Offset offset, {
bool onExit = false,
bool moveCanvas = true,
}) {
final evtToPeer =
processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas);
if (evtToPeer != null) {
bind.sessionSendMouse(
sessionId: sessionId, msg: json.encode(modify(evtToPeer)));
}
return evtToPeer;
}
Point? handlePointerDevicePos(
@@ -1297,6 +1319,7 @@ class InputModel {
String evtType, {
bool onExit = false,
int buttons = kPrimaryMouseButton,
bool moveCanvas = true,
}) {
final ffiModel = parent.target!.ffiModel;
CanvasCoords canvas =
@@ -1325,7 +1348,7 @@ class InputModel {
y -= CanvasModel.topToEdge;
x -= CanvasModel.leftToEdge;
if (isMove) {
if (isMove && moveCanvas) {
parent.target!.canvasModel.moveDesktopMouse(x, y);
}

View File

@@ -114,6 +114,7 @@ class FfiModel with ChangeNotifier {
bool? _secure;
bool? _direct;
bool _touchMode = false;
late VirtualMouseMode virtualMouseMode;
Timer? _timer;
var _reconnects = 1;
bool _viewOnly = false;
@@ -166,6 +167,7 @@ class FfiModel with ChangeNotifier {
clear();
sessionId = parent.target!.sessionId;
cachedPeerData.permissions = _permissions;
virtualMouseMode = VirtualMouseMode(this);
}
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
@@ -1109,6 +1111,9 @@ class FfiModel with ChangeNotifier {
sessionId: sessionId, arg: kOptionTouchMode) !=
'';
}
if (isMobile) {
virtualMouseMode.loadOptions();
}
if (connType == ConnType.fileTransfer) {
parent.target?.fileModel.onReady();
} else if (connType == ConnType.terminal) {
@@ -1508,6 +1513,72 @@ class FfiModel with ChangeNotifier {
}
}
class VirtualMouseMode with ChangeNotifier {
bool _showVirtualMouse = false;
double _virtualMouseScale = 1.0;
bool _showVirtualJoystick = false;
bool get showVirtualMouse => _showVirtualMouse;
double get virtualMouseScale => _virtualMouseScale;
bool get showVirtualJoystick => _showVirtualJoystick;
FfiModel ffiModel;
VirtualMouseMode(this.ffiModel);
bool _shouldShow() => !ffiModel.isPeerAndroid;
setShowVirtualMouse(bool b) {
if (b == _showVirtualMouse) return;
if (_shouldShow()) {
_showVirtualMouse = b;
notifyListeners();
}
}
setVirtualMouseScale(double s) {
if (s <= 0) return;
if (s == _virtualMouseScale) return;
_virtualMouseScale = s;
bind.mainSetLocalOption(key: kOptionVirtualMouseScale, value: s.toString());
notifyListeners();
}
setShowVirtualJoystick(bool b) {
if (b == _showVirtualJoystick) return;
if (_shouldShow()) {
_showVirtualJoystick = b;
notifyListeners();
}
}
void loadOptions() {
_showVirtualMouse =
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y';
_virtualMouseScale = double.tryParse(
bind.mainGetLocalOption(key: kOptionVirtualMouseScale)) ??
1.0;
_showVirtualJoystick =
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y';
notifyListeners();
}
Future<void> toggleVirtualMouse() async {
await bind.mainSetLocalOption(
key: kOptionShowVirtualMouse, value: showVirtualMouse ? 'N' : 'Y');
setShowVirtualMouse(
bind.mainGetLocalOption(key: kOptionShowVirtualMouse) == 'Y');
}
Future<void> toggleVirtualJoystick() async {
await bind.mainSetLocalOption(
key: kOptionShowVirtualJoystick,
value: showVirtualJoystick ? 'N' : 'Y');
setShowVirtualJoystick(
bind.mainGetLocalOption(key: kOptionShowVirtualJoystick) == 'Y');
}
}
class ImageModel with ChangeNotifier {
ui.Image? _image;
@@ -2289,9 +2360,25 @@ class CursorModel with ChangeNotifier {
Rect? get keyHelpToolsRectToAdjustCanvas =>
_lastKeyboardIsVisible ? _keyHelpToolsRect : null;
keyHelpToolsVisibilityChanged(Rect? r, bool keyboardIsVisible) {
_keyHelpToolsRect = r;
if (r == null) {
// The blocked rect is used to block the pointer/touch events in the remote page.
final List<Rect> _blockedRects = [];
// Used in shouldBlock().
// _blockEvents is a flag to block pointer/touch events on the remote image.
// It is set to true to prevent accidental touch events in the following scenarios:
// 1. In floating mouse mode, when the scroll circle is shown.
// 2. In floating mouse widgets mode, when the left/right buttons are moving.
// 3. In floating mouse widgets mode, when using the virtual joystick.
// When _blockEvents is true, all pointer/touch events are blocked regardless of the contents of _blockedRects.
// _blockedRects contains specific rectangular regions where events are blocked; these are checked when _blockEvents is false.
// In summary: _blockEvents acts as a global block, while _blockedRects provides fine-grained blocking.
bool _blockEvents = false;
List<Rect> get blockedRects => List.unmodifiable(_blockedRects);
set blockEvents(bool v) => _blockEvents = v;
keyHelpToolsVisibilityChanged(Rect? rect, bool keyboardIsVisible) {
_keyHelpToolsRect = rect;
if (rect == null) {
_lastIsBlocked = false;
} else {
// Block the touch event is safe here.
@@ -2306,6 +2393,14 @@ class CursorModel with ChangeNotifier {
_lastKeyboardIsVisible = keyboardIsVisible;
}
addBlockedRect(Rect rect) {
_blockedRects.add(rect);
}
removeBlockedRect(Rect rect) {
_blockedRects.remove(rect);
}
get lastIsBlocked => _lastIsBlocked;
ui.Image? get image => _image;
@@ -2372,13 +2467,22 @@ class CursorModel with ChangeNotifier {
// mobile Soft keyboard, block touch event from the KeyHelpTools
shouldBlock(double x, double y) {
if (_blockEvents) {
return true;
}
final offset = Offset(x, y);
for (final rect in _blockedRects) {
if (isPointInRect(offset, rect)) {
return true;
}
}
// For help tools rectangle, only block touch event when in touch mode.
if (!(parent.target?.ffiModel.touchMode ?? false)) {
return false;
}
if (_keyHelpToolsRect == null) {
return false;
}
if (isPointInRect(Offset(x, y), _keyHelpToolsRect!)) {
if (_keyHelpToolsRect != null &&
isPointInRect(offset, _keyHelpToolsRect!)) {
return true;
}
return false;
@@ -2398,6 +2502,10 @@ class CursorModel with ChangeNotifier {
return true;
}
Future<void> syncCursorPosition() async {
await parent.target?.inputModel.moveMouse(_x, _y);
}
bool isInRemoteRect(Offset offset) {
return getRemotePosInRect(offset) != null;
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Control lliscant d'escala personalitzada"),
("Decrease", "Disminueix"),
("Increase", "Augmenta"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "自定义缩放滑块"),
("Decrease", "缩小"),
("Increase", "放大"),
("Show virtual mouse", "显示虚拟鼠标"),
("Virtual mouse size", "虚拟鼠标大小"),
("Small", ""),
("Large", ""),
("Show virtual joystick", "显示虚拟摇杆"),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Schieberegler für benutzerdefinierte Skalierung"),
("Decrease", "Verringern"),
("Increase", "Erhöhen"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Control deslizante de escala personalizada"),
("Decrease", "Disminuir"),
("Increase", "Aumentar"),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Curseur déchelle personnalisée"),
("Decrease", "Diminuer"),
("Increase", "Augmenter"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -715,5 +715,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Egyéni méretarány-csúszka"),
("Decrease", "Csökkentés"),
("Increase", "Növelés"),
("Show my cursor", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Cursore scala personalizzata"),
("Decrease", "Diminuisci"),
("Increase", "Aumenta"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "カスタムスケールのスライダー"),
("Decrease", "縮小"),
("Increase", "拡大"),
("Show my cursor", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "사용자 지정 크기 조정 슬라이더"),
("Decrease", "축소"),
("Increase", "확대"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Aangepaste schuifregelaar voor schaal"),
("Decrease", "Verlagen"),
("Increase", "Verhogen"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Controlo deslizante de escala personalizada"),
("Decrease", "Diminuir"),
("Increase", "Aumentar"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Controle deslizante de escala personalizada"),
("Decrease", "Diminuir"),
("Increase", "Aumentar"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Glisor pentru scalare personalizată"),
("Decrease", "Micșorează"),
("Increase", "Mărește"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "Ползунок пользовательского масштаба"),
("Decrease", "Уменьшить"),
("Increase", "Увеличить"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", "自訂縮放滑桿"),
("Decrease", "縮小"),
("Increase", "放大"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}

View File

@@ -714,5 +714,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Custom scale slider", ""),
("Decrease", ""),
("Increase", ""),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual joystick", ""),
].iter().cloned().collect();
}