mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-17 14:07:28 +08:00
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:
@@ -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 &&
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
1209
flutter/lib/mobile/widgets/floating_mouse.dart
Normal file
1209
flutter/lib/mobile/widgets/floating_mouse.dart
Normal file
File diff suppressed because it is too large
Load Diff
880
flutter/lib/mobile/widgets/floating_mouse_widgets.dart
Normal file
880
flutter/lib/mobile/widgets/floating_mouse_widgets.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user