diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 1fb9c2599..a19986e2c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2948,7 +2948,7 @@ Future updateSystemWindowTheme() async { /// /// Note: not found a general solution for rust based AVFoundation bingding. /// [AVFoundation] crate has compile error. -const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos"); +const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/host"); enum PermissionAuthorizeType { undetermined, diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 19c24a109..35f7e90e9 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -58,6 +58,7 @@ const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; const String kWindowConnect = "connect"; +const String kWindowBumpMouse = "bump_mouse"; const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewFileTransfer = "new_file_transfer"; @@ -326,6 +327,9 @@ const kRemoteScrollStyleAuto = 'scrollauto'; /// [kRemoteScrollStyleBar] Scroll image with scroll bar. const kRemoteScrollStyleBar = 'scrollbar'; +/// [kRemoteScrollStyleEdge] Scroll image auto at edges. +const kRemoteScrollStyleEdge = 'scrolledge'; + /// [kScrollModeDefault] Mouse or touchpad, the default scroll mode. const kScrollModeDefault = 'default'; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 237691159..b8b7c0286 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -18,6 +18,7 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/ui_manager.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -760,9 +761,19 @@ class _DesktopHomePageState extends State 'scaleFactor': screen.scaleFactor, }; + bool isChattyMethod(String methodName) { + switch (methodName) { + case kWindowBumpMouse: return true; + } + + return false; + } + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { - debugPrint( + if (!isChattyMethod(call.method)) { + debugPrint( "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + } if (call.method == kWindowMainWindowOnTop) { windowOnTop(null); } else if (call.method == kWindowGetWindowInfo) { @@ -793,6 +804,10 @@ class _DesktopHomePageState extends State forceRelay: call.arguments['forceRelay'], connToken: call.arguments['connToken'], ); + } else if (call.method == kWindowBumpMouse) { + return RdPlatformChannel.instance.bumpMouse( + dx: call.arguments['dx'], + dy: call.arguments['dy']); } else if (call.method == kWindowEventMoveTabToNewWindow) { final args = call.arguments.split(','); int? windowId; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index cc1f3f271..6d1ef3a8b 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1691,6 +1691,11 @@ class _DisplayState extends State<_Display> { groupValue: groupValue, label: 'ScrollAuto', onChanged: isOptFixed ? null : onChanged), + _Radio(context, + value: kRemoteScrollStyleEdge, + groupValue: groupValue, + label: 'ScrollEdge', + onChanged: isOptFixed ? null : onChanged), _Radio(context, value: kRemoteScrollStyleBar, groupValue: groupValue, diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 912b06b02..8e14b4f1b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/scheduler.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -72,7 +73,7 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State - with AutomaticKeepAliveClientMixin, MultiWindowListener { + with AutomaticKeepAliveClientMixin, MultiWindowListener, TickerProviderStateMixin { Timer? _timer; String keyboardMode = "legacy"; bool _isWindowBlur = false; @@ -112,11 +113,13 @@ class _RemotePageState extends State _ffi = FFI(widget.sessionId); Get.put(_ffi, tag: widget.id); _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + _ffi.canvasModel.activateLocalCursor(); showKBLayoutTypeChooserIfNeeded( _ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.recordingModel .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); }); + _ffi.canvasModel.initializeEdgeScrollFallback(this); _ffi.start( widget.id, password: widget.password, @@ -408,6 +411,8 @@ class _RemotePageState extends State } void enterView(PointerEnterEvent evt) { + _ffi.canvasModel.rearmEdgeScroll(); + _cursorOverImage.value = true; _firstEnterImage.value = true; if (_onEnterOrLeaveImage4Toolbar != null) { @@ -427,6 +432,8 @@ class _RemotePageState extends State } void leaveView(PointerExitEvent evt) { + _ffi.canvasModel.disableEdgeScroll(); + if (_ffi.ffiModel.keyboard) { _ffi.inputModel.tryMoveEdgeOnExit(evt.position); } @@ -625,7 +632,7 @@ class _ImagePaintState extends State { onHover: (evt) {}, child: child); }); - if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { + if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) { final paintWidth = c.getDisplayWidth() * s; final paintHeight = c.getDisplayHeight() * s; final paintSize = Size(paintWidth, paintHeight); diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index a1cc5c8a0..87e6e4327 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -527,7 +527,7 @@ class _ImagePaintState extends State { bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; - if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { + if (c.imageOverflow.isTrue && c.scrollStyle != ScrollStyle.scrollauto) { final paintWidth = c.getDisplayWidth() * s; final paintHeight = c.getDisplayHeight() * s; final paintSize = Size(paintWidth, paintHeight); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 84b741d00..8f5fbca66 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1088,6 +1088,15 @@ class _DisplayMenuState extends State<_DisplayMenu> { : null, ffi: widget.ffi, ), + RdoMenuButton( + child: Text(translate('ScrollEdge')), + value: kRemoteScrollStyleEdge, + groupValue: groupValue, + onChanged: widget.ffi.canvasModel.imageOverflow.value + ? (value) => onChange(value) + : null, + ffi: widget.ffi, + ), RdoMenuButton( child: Text(translate('Scrollbar')), value: kRemoteScrollStyleBar, diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 03a9c7beb..29d0cc0fd 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -42,8 +42,7 @@ class CanvasCoords { 'scale': scale, 'scrollX': scrollX, 'scrollY': scrollY, - 'scrollStyle': - scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar', + 'scrollStyle': scrollStyle.toJson(), 'size': { 'w': size.width, 'h': size.height, @@ -58,9 +57,7 @@ class CanvasCoords { model.scale = json['scale']; model.scrollX = json['scrollX']; model.scrollY = json['scrollY']; - model.scrollStyle = json['scrollStyle'] == 'scrollauto' - ? ScrollStyle.scrollauto - : ScrollStyle.scrollbar; + model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto); model.size = Size(json['size']['w'], json['size']['h']); return model; } @@ -375,6 +372,7 @@ class InputModel { double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; bool get isViewCamera => parent.target!.connType == ConnType.viewCamera; int get trackpadSpeed => _trackpadSpeed; + bool get useEdgeScroll => parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge; InputModel(this.parent) { sessionId = parent.target!.sessionId; @@ -888,7 +886,7 @@ class InputModel { isPhysicalMouse.value = true; } if (isPhysicalMouse.value) { - handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position); + handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll); } } @@ -1076,7 +1074,7 @@ class InputModel { _queryOtherWindowCoords = false; } if (isPhysicalMouse.value) { - handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position); + handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll); } } @@ -1125,7 +1123,7 @@ class InputModel { void refreshMousePos() => handleMouse({ 'buttons': 0, 'type': _kMouseEventMove, - }, lastMousePos); + }, lastMousePos, edgeScroll: useEdgeScroll); void tryMoveEdgeOnExit(Offset pos) => handleMouse( { @@ -1232,6 +1230,7 @@ class InputModel { Offset offset, { bool onExit = false, bool moveCanvas = true, + bool edgeScroll = false, }) { if (isViewCamera) return null; double x = offset.dx; @@ -1273,6 +1272,7 @@ class InputModel { onExit: onExit, buttons: evt['buttons'], moveCanvas: moveCanvas, + edgeScroll: edgeScroll, ); if (pos == null) { return null; @@ -1301,9 +1301,10 @@ class InputModel { Offset offset, { bool onExit = false, bool moveCanvas = true, + bool edgeScroll = false, }) { final evtToPeer = - processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas); + processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll); if (evtToPeer != null) { bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify(evtToPeer))); @@ -1320,6 +1321,7 @@ class InputModel { bool onExit = false, int buttons = kPrimaryMouseButton, bool moveCanvas = true, + bool edgeScroll = false, }) { final ffiModel = parent.target!.ffiModel; CanvasCoords canvas = @@ -1348,8 +1350,16 @@ class InputModel { y -= CanvasModel.topToEdge; x -= CanvasModel.leftToEdge; - if (isMove && moveCanvas) { - parent.target!.canvasModel.moveDesktopMouse(x, y); + if (isMove) { + final canvasModel = parent.target!.canvasModel; + + if (edgeScroll) { + canvasModel.edgeScrollMouse(x, y); + } else if (moveCanvas) { + canvasModel.moveDesktopMouse(x, y); + } + + canvasModel.updateLocalCursor(x, y); } return _handlePointerDevicePos( @@ -1412,7 +1422,7 @@ class InputModel { var nearBottom = (canvas.size.height - y) < nearThr; final imageWidth = rect.width * canvas.scale; final imageHeight = rect.height * canvas.scale; - if (canvas.scrollStyle == ScrollStyle.scrollbar) { + if (canvas.scrollStyle != ScrollStyle.scrollauto) { x += imageWidth * canvas.scrollX; y += imageHeight * canvas.scrollY; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 893a17b26..8e45b69e7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -9,6 +9,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/ab_model.dart'; @@ -36,6 +37,7 @@ import 'package:get/get.dart'; import 'package:uuid/uuid.dart'; import 'package:window_manager/window_manager.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:vector_math/vector_math.dart' show Vector2; import '../common.dart'; import '../utils/image.dart' as img; @@ -1713,8 +1715,56 @@ class ImageModel with ChangeNotifier { } enum ScrollStyle { - scrollbar, - scrollauto, + scrollbar(kRemoteScrollStyleBar), + scrollauto(kRemoteScrollStyleAuto), + scrolledge(kRemoteScrollStyleEdge); + + const ScrollStyle(this.stringValue); + + final String stringValue; + + String toJson() { + return name; + } + + static ScrollStyle fromJson(String json, [ScrollStyle? fallbackValue]) { + switch (json) { + case 'scrollbar': + return scrollbar; + case 'scrollauto': + return scrollauto; + case 'scrolledge': + return scrolledge; + } + + if (fallbackValue != null) { + return fallbackValue; + } + + throw ArgumentError("Unknown ScrollStyle JSON value: '$json'"); + } + + @override + String toString() { + return stringValue; + } + + static ScrollStyle fromString(String string, [ScrollStyle? fallbackValue]) { + switch (string) { + case kRemoteScrollStyleBar: + return scrollbar; + case kRemoteScrollStyleAuto: + return scrollauto; + case kRemoteScrollStyleEdge: + return scrolledge; + } + + if (fallbackValue != null) { + return fallbackValue; + } + + throw ArgumentError("Unknown ScrollStyle string value: '$string'"); + } } class ViewStyle { @@ -1789,6 +1839,60 @@ class ViewStyle { } } +enum EdgeScrollState { + inactive, + armed, + active, +} + +class EdgeScrollFallbackState { + final CanvasModel _owner; + + late Ticker _ticker; + + Duration _lastTotalElapsed = Duration.zero; + bool _nextEventIsFirst = true; + Vector2 _encroachment = Vector2.zero(); + + EdgeScrollFallbackState(this._owner, TickerProvider tickerProvider) { + _ticker = tickerProvider.createTicker(emitTick); + } + + void setEncroachment(Vector2 encroachment) { + _encroachment = encroachment; + } + + void emitTick(Duration totalElapsed) { + if (_nextEventIsFirst) { + _lastTotalElapsed = totalElapsed; + _nextEventIsFirst = false; + } else { + final thisTickElapsed = totalElapsed - _lastTotalElapsed; + + const double kFrameTime = 1000.0 / 60.0; + const double kSpeedFactor = 0.1; + + var delta = _encroachment * + (kSpeedFactor * thisTickElapsed.inMilliseconds / kFrameTime); + + _owner.performEdgeScroll(delta); + + _lastTotalElapsed = totalElapsed; + } + } + + void start() { + if (!_ticker.isActive) { + _nextEventIsFirst = true; + _ticker.start(); + } + } + + void stop() { + _ticker.stop(); + } +} + class CanvasModel with ChangeNotifier { // image offset of canvas double _x = 0; @@ -1810,6 +1914,13 @@ class CanvasModel with ChangeNotifier { // scroll offset y percent double _scrollY = 0.0; ScrollStyle _scrollStyle = ScrollStyle.scrollauto; + // tracks whether edge scroll should be active, prevents spurious + // scrolling when the cursor enters the view from outside + EdgeScrollState _edgeScrollState = EdgeScrollState.inactive; + // fallback strategy for when Bump Mouse isn't available + late EdgeScrollFallbackState _edgeScrollFallbackState; + // to avoid hammering a non-functional Bump Mouse + bool _bumpMouseIsWorking = true; ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); Timer? _timerMobileFocusCanvasCursor; @@ -1840,9 +1951,18 @@ class CanvasModel with ChangeNotifier { _resetScroll() => setScrollPercent(0.0, 0.0); - setScrollPercent(double x, double y) { - _scrollX = x; - _scrollY = y; + void setScrollPercent(double x, double y) { + _scrollX = x.isFinite ? x : 0.0; + _scrollY = y.isFinite ? y : 0.0; + } + + void pushScrollPositionToUI(double scrollPixelX, double scrollPixelY) { + if (_horizontal.hasClients) { + _horizontal.jumpTo(scrollPixelX); + } + if (_vertical.hasClients) { + _vertical.jumpTo(scrollPixelY); + } } ScrollController get scrollHorizontal => _horizontal; @@ -1957,13 +2077,14 @@ class CanvasModel with ChangeNotifier { } tryUpdateScrollStyle(Duration duration, String? style) async { - if (_scrollStyle != ScrollStyle.scrollbar) return; + if (_scrollStyle == ScrollStyle.scrollauto) return; style ??= await bind.sessionGetViewStyle(sessionId: sessionId); if (style != kRemoteViewStyleOriginal && style != kRemoteViewStyleCustom) { return; } _resetScroll(); + Future.delayed(duration, () async { updateScrollPercent(); }); @@ -1971,12 +2092,15 @@ class CanvasModel with ChangeNotifier { updateScrollStyle() async { final style = await bind.sessionGetScrollStyle(sessionId: sessionId); - if (style == kRemoteScrollStyleBar) { - _scrollStyle = ScrollStyle.scrollbar; + + _scrollStyle = style != null + ? ScrollStyle.fromString(style!) + : ScrollStyle.scrollauto; + + if (_scrollStyle != ScrollStyle.scrollauto) { _resetScroll(); - } else { - _scrollStyle = ScrollStyle.scrollauto; } + notifyListeners(); } @@ -2007,7 +2131,33 @@ class CanvasModel with ChangeNotifier { static double get windowBorderWidth => stateGlobal.windowBorderWidth.value; static double get tabBarHeight => stateGlobal.tabBarHeight; - moveDesktopMouse(double x, double y) { + void activateLocalCursor() { + if (isDesktop || isWebDesktop) { + try { + RemoteCursorMovedState.find(id).value = false; + } catch (e) { + // + } + } + } + + void updateLocalCursor(double x, double y) { + // If keyboard is not permitted, do not move cursor when mouse is moving. + if (parent.target != null && parent.target!.ffiModel.keyboard) { + // Draw cursor if is not desktop. + if (!(isDesktop || isWebDesktop)) { + parent.target!.cursorModel.moveLocal(x, y); + } else { + try { + RemoteCursorMovedState.find(id).value = false; + } catch (e) { + // + } + } + } + } + + void moveDesktopMouse(double x, double y) { if (size.width == 0 || size.height == 0) { return; } @@ -2036,20 +2186,132 @@ class CanvasModel with ChangeNotifier { if (dxOffset != 0 || dyOffset != 0) { notifyListeners(); } + } - // If keyboard is not permitted, do not move cursor when mouse is moving. - if (parent.target != null && parent.target!.ffiModel.keyboard) { - // Draw cursor if is not desktop. - if (!(isDesktop || isWebDesktop)) { - parent.target!.cursorModel.moveLocal(x, y); + void initializeEdgeScrollFallback(TickerProvider tickerProvider) { + _edgeScrollFallbackState = EdgeScrollFallbackState(this, tickerProvider); + } + + void disableEdgeScroll() { + _edgeScrollState = EdgeScrollState.inactive; + cancelEdgeScroll(); + } + + void rearmEdgeScroll() { + _edgeScrollState = EdgeScrollState.armed; + } + + void cancelEdgeScroll() { + _edgeScrollFallbackState.stop(); + } + + (Vector2, Vector2) getScrollInfo() { + final scrollPixel = Vector2( + _horizontal.hasClients ? _horizontal.position.pixels : 0, + _vertical.hasClients ? _vertical.position.pixels : 0); + + final max = Vector2( + _horizontal.hasClients ? _horizontal.position.maxScrollExtent : 0, + _vertical.hasClients ? _vertical.position.maxScrollExtent : 0); + + return (scrollPixel, max); + } + + void edgeScrollMouse(double x, double y) async { + if ((_edgeScrollState == EdgeScrollState.inactive) || + (size.width == 0 || size.height == 0) || + !(_horizontal.hasClients || _vertical.hasClients)) { + return; + } + + // Trigger scrolling when the cursor is close to an edge + const double edgeThickness = 100; + + if (_edgeScrollState == EdgeScrollState.armed) { + // Edge scroll is armed to become active once the cursor + // is observed within the rectangle interior to the + // edge scroll regions. If the user has just moved the + // cursor in from outside of the window, edge scrolling + // doesn't happen yet. + final clientArea = Rect.fromLTWH(0, 0, size.width, size.height); + + final innerZone = clientArea.deflate(edgeThickness); + + if (innerZone.contains(Offset(x, y))) { + _edgeScrollState = EdgeScrollState.active; } else { - try { - RemoteCursorMovedState.find(id).value = false; - } catch (e) { - // - } + // Not yet. + return; } } + + var dxOffset = 0.0; + var dyOffset = 0.0; + + if (x < edgeThickness) { + dxOffset = x - edgeThickness; + } else if (x >= size.width - edgeThickness) { + dxOffset = x - (size.width - edgeThickness); + } + + if (y < edgeThickness) { + dyOffset = y - edgeThickness; + } else if (y >= size.height - edgeThickness) { + dyOffset = y - (size.height - edgeThickness); + } + + var encroachment = Vector2(dxOffset, dyOffset); + + var (scrollPixel, max) = getScrollInfo(); + + encroachment.clamp(-scrollPixel, max - scrollPixel); + + if (encroachment.length2 == 0) { + _edgeScrollFallbackState.stop(); + } else { + var bumpAmount = -encroachment; + + // Round away from 0: this ensures that the mouse will be bumped clear of + // whichever edge scroll zone(s) it is in + bumpAmount.x += bumpAmount.x.sign * 0.5; + bumpAmount.y += bumpAmount.y.sign * 0.5; + + var bumpMouseSucceeded = _bumpMouseIsWorking && + (await rustDeskWinManager.call(WindowType.Main, kWindowBumpMouse, + {"dx": bumpAmount.x.round(), "dy": bumpAmount.y.round()})) + .result; + + if (bumpMouseSucceeded) { + performEdgeScroll(encroachment); + } else { + // If we can't BumpMouse, then we switch to slower scrolling with autorepeat + + // Don't keep hammering BumpMouse if it's not working. + _bumpMouseIsWorking = false; + + // Keep scrolling as long as the user is overtop of an edge. + _edgeScrollFallbackState.setEncroachment(encroachment); + _edgeScrollFallbackState.start(); + } + } + } + + void performEdgeScroll(Vector2 delta) { + var (scrollPixel, max) = getScrollInfo(); + + scrollPixel += delta; + + scrollPixel.clamp(Vector2.zero(), max); + + var scrollPixelPercent = scrollPixel.clone(); + + scrollPixelPercent.divide(max); + scrollPixelPercent.scale(100.0); + + setScrollPercent(scrollPixelPercent.x, scrollPixelPercent.y); + pushScrollPositionToUI(scrollPixel.x, scrollPixel.y); + + notifyListeners(); } set scale(v) { diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart index eaea4e79f..9e53ca076 100644 --- a/flutter/lib/utils/platform_channel.dart +++ b/flutter/lib/utils/platform_channel.dart @@ -13,8 +13,18 @@ class RdPlatformChannel { static RdPlatformChannel get instance => _windowUtil; - final MethodChannel _osxMethodChannel = - MethodChannel("org.rustdesk.rustdesk/macos"); + final MethodChannel _hostMethodChannel = + MethodChannel("org.rustdesk.rustdesk/host"); + + /// Bump the position of the mouse cursor, if applicable + Future bumpMouse({required int dx, required int dy}) async { + // No debug output; this call is too chatty. + + bool? result = await _hostMethodChannel + .invokeMethod("bumpMouse", {"dx": dx, "dy": dy}); + + return result ?? false; + } /// Change the theme of the system window Future changeSystemWindowTheme(SystemWindowTheme theme) { @@ -23,13 +33,13 @@ class RdPlatformChannel { print( "[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}"); } - return _osxMethodChannel + return _hostMethodChannel .invokeMethod("setWindowTheme", {"themeName": theme.name}); } /// Terminate .app manually. Future terminate() { assert(isMacOS); - return _osxMethodChannel.invokeMethod("terminate"); + return _hostMethodChannel.invokeMethod("terminate"); } } diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index a9fd84088..d320f403c 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -63,6 +63,8 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") add_executable(${BINARY_NAME} "main.cc" "my_application.cc" + "bump_mouse.cc" + "bump_mouse_x11.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" ) diff --git a/flutter/linux/bump_mouse.cc b/flutter/linux/bump_mouse.cc new file mode 100644 index 000000000..985aa6e81 --- /dev/null +++ b/flutter/linux/bump_mouse.cc @@ -0,0 +1,18 @@ +#include "bump_mouse.h" + +#include "bump_mouse_x11.h" + +#include + +bool bump_mouse(int dx, int dy) +{ + GdkDisplay *display = gdk_display_get_default(); + + if (GDK_IS_X11_DISPLAY(display)) { + return bump_mouse_x11(dx, dy); + } + else { + // Don't know how to support this. + return false; + } +} diff --git a/flutter/linux/bump_mouse.h b/flutter/linux/bump_mouse.h new file mode 100644 index 000000000..0861e44e8 --- /dev/null +++ b/flutter/linux/bump_mouse.h @@ -0,0 +1,3 @@ +#pragma once + +bool bump_mouse(int dx, int dy); diff --git a/flutter/linux/bump_mouse_x11.cc b/flutter/linux/bump_mouse_x11.cc new file mode 100644 index 000000000..7889ea302 --- /dev/null +++ b/flutter/linux/bump_mouse_x11.cc @@ -0,0 +1,30 @@ +#include "bump_mouse.h" + +#include + +#include + +#include + +bool bump_mouse_x11(int dx, int dy) +{ + GdkDevice *mouse_device; + +#if GTK_CHECK_VERSION(3, 20, 0) + auto seat = gdk_display_get_default_seat(gdk_display_get_default()); + + mouse_device = gdk_seat_get_pointer(seat); +#else + auto devman = gdk_display_get_device_manager(gdk_display_get_default()); + + mouse_device = gdk_device_manager_get_client_pointer(devman); +#endif + + GdkScreen *screen; + gint x, y; + + gdk_device_get_position(mouse_device, &screen, &x, &y); + gdk_device_warp(mouse_device, screen, x + dx, y + dy); + + return true; +} diff --git a/flutter/linux/bump_mouse_x11.h b/flutter/linux/bump_mouse_x11.h new file mode 100644 index 000000000..00bbaaad9 --- /dev/null +++ b/flutter/linux/bump_mouse_x11.h @@ -0,0 +1,3 @@ +#pragma once + +bool bump_mouse_x11(int dx, int dy); diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index b9d36a0ce..c84cbddba 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -1,5 +1,7 @@ #include "my_application.h" +#include "bump_mouse.h" + #include #ifdef GDK_WINDOWING_X11 #include @@ -10,10 +12,13 @@ struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; + FlMethodChannel* host_channel; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) +void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data); + GtkWidget *find_gl_area(GtkWidget *widget); void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); @@ -24,10 +29,11 @@ GtkWidget *find_gl_area(GtkWidget *widget); // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); gtk_window_set_decorated(window, FALSE); - // try setting icon for rustdesk, which uses the system cache + // try setting icon for rustdesk, which uses the system cache GtkIconTheme* theme = gtk_icon_theme_get_default(); gint icons[4] = {256, 128, 64, 32}; for (int i = 0; i < 4; i++) { @@ -87,6 +93,17 @@ static void my_application_activate(GApplication* application) { fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->host_channel = fl_method_channel_new( + fl_engine_get_binary_messenger(fl_view_get_engine(view)), + "org.rustdesk.rustdesk/host", + FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler( + self->host_channel, + host_channel_call_handler, + self, + nullptr); + gtk_widget_grab_focus(GTK_WIDGET(view)); } @@ -113,6 +130,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch static void my_application_dispose(GObject* object) { MyApplication* self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + g_clear_object(&self->host_channel); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } @@ -131,6 +149,61 @@ MyApplication* my_application_new() { nullptr)); } +void host_channel_call_handler(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) +{ + if (strcmp(fl_method_call_get_name(method_call), "bumpMouse") == 0) { + FlValue *args = fl_method_call_get_args(method_call); + + FlValue *dxValue = nullptr; + FlValue *dyValue = nullptr; + + switch (fl_value_get_type(args)) + { + case FL_VALUE_TYPE_MAP: + { + dxValue = fl_value_lookup_string(args, "dx"); + dyValue = fl_value_lookup_string(args, "dy"); + + break; + } + case FL_VALUE_TYPE_LIST: + { + int listSize = fl_value_get_length(args); + + dxValue = (listSize >= 1) ? fl_value_get_list_value(args, 0) : nullptr; + dyValue = (listSize >= 2) ? fl_value_get_list_value(args, 1) : nullptr; + + break; + } + + default: break; + } + + int dx = 0, dy = 0; + + if (dxValue && (fl_value_get_type(dxValue) == FL_VALUE_TYPE_INT)) { + dx = fl_value_get_int(dxValue); + } + + if (dyValue && (fl_value_get_type(dyValue) == FL_VALUE_TYPE_INT)) { + dy = fl_value_get_int(dyValue); + } + + bool result = bump_mouse(dx, dy); + + FlValue *result_value = fl_value_new_bool(result); + + GError *error = nullptr; + + if (!fl_method_call_respond_success(method_call, result_value, &error)) { + g_warning("Failed to send Flutter Platform Channel response: %s", error->message); + g_error_free(error); + } + + fl_value_unref(result_value); + } +} + GtkWidget *find_gl_area(GtkWidget *widget) { if (GTK_IS_GL_AREA(widget)) { @@ -160,7 +233,7 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view) GtkWidget *gl_area = NULL; printf("Try setting transparent\n"); - + gl_area = find_gl_area(GTK_WIDGET(view)); if (gl_area != NULL) { gtk_gl_area_set_has_alpha(GTK_GL_AREA(gl_area), TRUE); diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index b0e20d6ae..d27d7f228 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -29,7 +29,7 @@ class MainFlutterWindow: NSWindow { // register self method handler let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin") setMethodHandler(registrar: registrar) - + RegisterGeneratedPlugins(registry: flutterViewController) FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in @@ -50,22 +50,22 @@ class MainFlutterWindow: NSWindow { WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin")) TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin")) } - + super.awakeFromNib() } - + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { super.order(place, relativeTo: otherWin) hiddenWindowAtLaunch() } - + /// Override window theme. public func setWindowInterfaceMode(window: NSWindow, themeName: String) { window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua) } - + public func setMethodHandler(registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/macos", binaryMessenger: registrar.messenger) + let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger) channel.setMethodCallHandler({ (call, result) -> Void in switch call.method { @@ -99,6 +99,58 @@ class MainFlutterWindow: NSWindow { result(granted) }) break + case "bumpMouse": + var dx = 0 + var dy = 0 + + if let argMap = call.arguments as? [String: Any] { + dx = (argMap["dx"] as? Int) ?? 0 + dy = (argMap["dy"] as? Int) ?? 0 + } + else if let argList = call.arguments as? [Any] { + dx = argList.count >= 1 ? (argList[0] as? Int) ?? 0 : 0 + dy = argList.count >= 2 ? (argList[1] as? Int) ?? 0 : 0 + } + + var mouseLoc: CGPoint + + if let dummyEvent = CGEvent(source: nil) { // can this ever fail? + mouseLoc = dummyEvent.location + } + else if let screenFrame = NSScreen.screens.first?.frame { + // NeXTStep: Origin is lower-left of primary screen, positive is up + // Cocoa Core Graphics: Origin is upper-left of primary screen, positive is down + let nsMouseLoc = NSEvent.mouseLocation + + mouseLoc = CGPoint( + x: nsMouseLoc.x, + y: NSHeight(screenFrame) - nsMouseLoc.y) + } + else { + result(false) + break + } + + let newLoc = CGPoint(x: mouseLoc.x + CGFloat(dx), y: mouseLoc.y + CGFloat(dy)) + + CGDisplayMoveCursorToPoint(0, newLoc) + + // By default, Cocoa suppresses mouse events briefly after a call to warp the + // cursor to a new location. This is good if you want to draw the user's + // attention to the fact that the mouse is now in a particular location, but + // it's bad in this case; we get called as part of the handling of edge + // scrolling, which means the mouse is typically still in motion, and we want + // the cursor to keep moving smoothly uninterrupted. + // + // This function's main action is to toggle whether the mouse cursor is + // associated with the mouse position, but setting it to true when it's + // already true has the side-effect of cancelling this motion suppression. + CGAssociateMouseAndMouseCursorPosition(1 /* true */) + + result(true) + + break + default: result(FlutterMethodNotImplemented) } diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0697b0f12..f8e2f44df 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -109,6 +109,7 @@ dependencies: xterm: 4.0.0 sqflite: 2.2.0 google_fonts: ^6.2.1 + vector_math: ^2.1.4 dev_dependencies: icons_launcher: ^2.0.4 diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp index 3ccbdd4f4..903fb2fa0 100644 --- a/flutter/windows/runner/flutter_window.cpp +++ b/flutter/windows/runner/flutter_window.cpp @@ -1,13 +1,24 @@ #include "flutter_window.h" -#include - #include #include #include #include "flutter/generated_plugin_registrant.h" +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "win32_desktop.h" + FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -29,6 +40,48 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + + flutter::MethodChannel<> channel( + flutter_controller_->engine()->messenger(), + "org.rustdesk.rustdesk/host", + &flutter::StandardMethodCodec::GetInstance()); + + channel.SetMethodCallHandler( + [](const flutter::MethodCall<>& call, std::unique_ptr> result) { + if (call.method_name() == "bumpMouse") { + auto arguments = call.arguments(); + + int dx = 0, dy = 0; + + if (std::holds_alternative(*arguments)) { + auto argsMap = std::get(*arguments); + + auto dxIt = argsMap.find(flutter::EncodableValue("dx")); + auto dyIt = argsMap.find(flutter::EncodableValue("dy")); + + if ((dxIt != argsMap.end()) && std::holds_alternative(dxIt->second)) { + dx = std::get(dxIt->second); + } + if ((dyIt != argsMap.end()) && std::holds_alternative(dyIt->second)) { + dy = std::get(dyIt->second); + } + } else if (std::holds_alternative(*arguments)) { + auto argsList = std::get(*arguments); + + if ((argsList.size() >= 1) && std::holds_alternative(argsList[0])) { + dx = std::get(argsList[0]); + } + if ((argsList.size() >= 2) && std::holds_alternative(argsList[1])) { + dy = std::get(argsList[1]); + } + } + + bool succeeded = Win32Desktop::BumpMouse(dx, dy); + + result->Success(succeeded); + } + }); + DesktopMultiWindowSetWindowCreatedCallback([](void *controller) { auto *flutter_view_controller = reinterpret_cast(controller); diff --git a/flutter/windows/runner/win32_desktop.cpp b/flutter/windows/runner/win32_desktop.cpp index 70ba31c75..4274f6ec5 100644 --- a/flutter/windows/runner/win32_desktop.cpp +++ b/flutter/windows/runner/win32_desktop.cpp @@ -66,4 +66,17 @@ namespace Win32Desktop size.width = std::min(size.width, workarea_bottom_right.x - origin.x); size.height = std::min(size.height, workarea_bottom_right.y - origin.y); } + + bool BumpMouse(int dx, int dy) + { + POINT pos; + + if (GetCursorPos(&pos)) + { + SetCursorPos(pos.x + dx, pos.y + dy); + return true; + } + + return false; + } } diff --git a/flutter/windows/runner/win32_desktop.h b/flutter/windows/runner/win32_desktop.h index 164770b47..8a07478e5 100644 --- a/flutter/windows/runner/win32_desktop.h +++ b/flutter/windows/runner/win32_desktop.h @@ -7,6 +7,7 @@ namespace Win32Desktop { void GetWorkArea(Win32Window::Point& origin, Win32Window::Size& size); void FitToWorkArea(Win32Window::Point& origin, Win32Window::Size& size); + bool BumpMouse(int dx, int dy); } #endif // RUNNER_WIN32_DESKTOP_H_ diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 0d62bd10a..6f92da46a 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "إظهار عصا التحكم الافتراضية"), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 39d1bb1a3..06ed18b44 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 82b368f59..ab73ad990 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 835d06024..f9e0f2296 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Mostra el joystick virtual"), ("Edit note", "Edita la nota"), ("Alias", "Alias"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 471b72cca..f6a1b7c03 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "显示虚拟摇杆"), ("Edit note", "编辑备注"), ("Alias", "别名"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index a80f74168..069bf13ff 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 6dbc0049d..a8b34b4fd 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index fa6ace8a4..03e4d0463 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Virtuellen Joystick anzeigen"), ("Edit note", "Hinweis bearbeiten"), ("Alias", "Alias"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index d0fcdd8e3..61fe674b1 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0670929aa..a22fd331e 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index ebfbeb859..857a95730 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Mostrar joystick virtual"), ("Edit note", "Editar nota"), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index bfc5530e6..18c63028c 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 36f658419..84ceaebb6 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index e39901ae6..393773b25 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "نمایش جوی‌استیک مجازی"), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 045709c16..4d03dd5c0 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Afficher le joystick virtuel"), ("Edit note", "Modifier la note"), ("Alias", "Alias"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index a6d6a7eea..2b243ce7a 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 643f78eb7..c2092789b 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index a80b579a4..4b02796a1 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index c118cc85b..bb35f417b 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Virtuális vezérlő megjelenítése"), ("Edit note", "Jegyzet szerkesztése"), ("Alias", "Álnév"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7dc279e2d..2aada65ff 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 9906003e2..231ef4d17 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Visualizza joystick virtuale"), ("Edit note", "Modifica nota"), ("Alias", "Alias"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5dd18428f..1962c2c29 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "仮想ジョイスティックを表示する"), ("Edit note", "メモを編集"), ("Alias", "エイリアス"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d1c345469..3345d94c6 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "가상 조이스틱 표시"), ("Edit note", "노트 편집"), ("Alias", "별명"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 209c8eef7..6ee142fca 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 42c5b0082..5a481119b 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 09dfb83b0..cea1cce4b 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 3e00d3f26..00df82d59 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 7a35d03c9..6ecdc113f 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Virtuele joystick weergeven"), ("Edit note", "Opmerking bewerken"), ("Alias", "Alias"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e2e385b58..82f9ca8bd 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Pokaz wirtualny joystick"), ("Edit note", "Edytuj notatkę"), ("Alias", "Alias"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index b2f7b2e07..5734d2029 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 42ec471b1..4b210090c 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 0f1516a90..c6dff88ea 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 84cba99de..5b2b9430f 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Показать виртуальный джойстик"), ("Edit note", "Изменить заметку"), ("Alias", "Псевдоним"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 0af391d01..174e15d7d 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 1769b6130..0e354eb06 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 63610909b..dc5215fdf 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 0477a0198..8ca030cf0 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 0e1227f89..92f616d81 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index d88c48cf7..88a38b4fc 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 97ff0266e..cb54af842 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 4a0d6b14f..4aef3bee9 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index d3894efd0..462b824f1 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 51f221752..4208d77ea 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e1f203a51..284dcb2ba 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "顯示虛擬搖桿"), ("Edit note", "編輯備註"), ("Alias", "別名"), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 336021b3a..9dcb34ef1 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 308c84502..0a8c0d5b5 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -721,5 +721,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", ""), ("Edit note", ""), ("Alias", ""), + ("ScrollEdge", ""), ].iter().cloned().collect(); }