Gesture support
This commit is contained in:
@@ -9,7 +9,7 @@ namespace Microsoft.Maui.Platform.Linux.Handlers;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manages gesture recognition and processing for MAUI views on Linux.
|
/// Manages gesture recognition and processing for MAUI views on Linux.
|
||||||
/// Handles tap, pan, swipe, and pointer gestures.
|
/// Handles tap, pan, swipe, pinch, and pointer gestures.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class GestureManager
|
public static class GestureManager
|
||||||
{
|
{
|
||||||
@@ -22,6 +22,8 @@ public static class GestureManager
|
|||||||
public DateTime StartTime { get; set; }
|
public DateTime StartTime { get; set; }
|
||||||
public bool IsPanning { get; set; }
|
public bool IsPanning { get; set; }
|
||||||
public bool IsPressed { get; set; }
|
public bool IsPressed { get; set; }
|
||||||
|
public bool IsPinching { get; set; }
|
||||||
|
public double PinchScale { get; set; } = 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum PointerEventType
|
private enum PointerEventType
|
||||||
@@ -34,6 +36,7 @@ public static class GestureManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static MethodInfo? _sendTappedMethod;
|
private static MethodInfo? _sendTappedMethod;
|
||||||
|
private static MethodInfo? _sendPinchMethod;
|
||||||
private static readonly Dictionary<View, (DateTime lastTap, int tapCount)> _tapTracking = new Dictionary<View, (DateTime, int)>();
|
private static readonly Dictionary<View, (DateTime lastTap, int tapCount)> _tapTracking = new Dictionary<View, (DateTime, int)>();
|
||||||
private static readonly Dictionary<View, GestureTrackingState> _gestureState = new Dictionary<View, GestureTrackingState>();
|
private static readonly Dictionary<View, GestureTrackingState> _gestureState = new Dictionary<View, GestureTrackingState>();
|
||||||
|
|
||||||
@@ -41,6 +44,7 @@ public static class GestureManager
|
|||||||
private const double SwipeMaxTime = 500.0;
|
private const double SwipeMaxTime = 500.0;
|
||||||
private const double SwipeDirectionThreshold = 0.5;
|
private const double SwipeDirectionThreshold = 0.5;
|
||||||
private const double PanMinDistance = 10.0;
|
private const double PanMinDistance = 10.0;
|
||||||
|
private const double PinchScrollScale = 0.1; // Scale factor per scroll unit
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes a tap gesture on the specified view.
|
/// Processes a tap gesture on the specified view.
|
||||||
@@ -504,6 +508,142 @@ public static class GestureManager
|
|||||||
return null!;
|
return null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a scroll event that may be a pinch gesture (Ctrl+Scroll).
|
||||||
|
/// Returns true if the scroll was consumed as a pinch gesture.
|
||||||
|
/// </summary>
|
||||||
|
public static bool ProcessScrollAsPinch(View? view, double x, double y, double deltaY, bool isCtrlPressed)
|
||||||
|
{
|
||||||
|
if (view == null || !isCtrlPressed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if view has a pinch gesture recognizer
|
||||||
|
if (!HasPinchGestureRecognizer(view))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get or create gesture state
|
||||||
|
if (!_gestureState.TryGetValue(view, out var state))
|
||||||
|
{
|
||||||
|
state = new GestureTrackingState
|
||||||
|
{
|
||||||
|
StartX = x,
|
||||||
|
StartY = y,
|
||||||
|
CurrentX = x,
|
||||||
|
CurrentY = y,
|
||||||
|
StartTime = DateTime.UtcNow,
|
||||||
|
PinchScale = 1.0
|
||||||
|
};
|
||||||
|
_gestureState[view] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate new scale based on scroll delta
|
||||||
|
double scaleDelta = 1.0 + (deltaY * PinchScrollScale);
|
||||||
|
state.PinchScale *= scaleDelta;
|
||||||
|
|
||||||
|
// Clamp scale to reasonable bounds
|
||||||
|
state.PinchScale = Math.Clamp(state.PinchScale, 0.1, 10.0);
|
||||||
|
|
||||||
|
GestureStatus status;
|
||||||
|
if (!state.IsPinching)
|
||||||
|
{
|
||||||
|
state.IsPinching = true;
|
||||||
|
status = GestureStatus.Started;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status = GestureStatus.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessPinchGesture(view, state.PinchScale, x, y, status);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ends an ongoing pinch gesture.
|
||||||
|
/// </summary>
|
||||||
|
public static void EndPinchGesture(View? view)
|
||||||
|
{
|
||||||
|
if (view == null) return;
|
||||||
|
|
||||||
|
if (_gestureState.TryGetValue(view, out var state) && state.IsPinching)
|
||||||
|
{
|
||||||
|
ProcessPinchGesture(view, state.PinchScale, state.CurrentX, state.CurrentY, GestureStatus.Completed);
|
||||||
|
state.IsPinching = false;
|
||||||
|
state.PinchScale = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ProcessPinchGesture(View view, double scale, double originX, double originY, GestureStatus status)
|
||||||
|
{
|
||||||
|
var recognizers = view.GestureRecognizers;
|
||||||
|
if (recognizers == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in recognizers)
|
||||||
|
{
|
||||||
|
var pinchRecognizer = item as PinchGestureRecognizer;
|
||||||
|
if (pinchRecognizer == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Pinch gesture: status={status}, scale={scale:F2}, origin=({originX:F0},{originY:F0})");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Cache the method lookup
|
||||||
|
if (_sendPinchMethod == null)
|
||||||
|
{
|
||||||
|
_sendPinchMethod = typeof(PinchGestureRecognizer).GetMethod("SendPinch",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_sendPinchMethod != null)
|
||||||
|
{
|
||||||
|
// SendPinch(IView sender, double scale, Point scaleOrigin, GestureStatus status)
|
||||||
|
var scaleOrigin = new Point(originX / view.Width, originY / view.Height);
|
||||||
|
_sendPinchMethod.Invoke(pinchRecognizer, new object[]
|
||||||
|
{
|
||||||
|
view,
|
||||||
|
scale,
|
||||||
|
scaleOrigin,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
Console.WriteLine("[GestureManager] SendPinch invoked successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendPinch failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the view has a pinch gesture recognizer.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasPinchGestureRecognizer(View? view)
|
||||||
|
{
|
||||||
|
if (view?.GestureRecognizers == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (var recognizer in view.GestureRecognizers)
|
||||||
|
{
|
||||||
|
if (recognizer is PinchGestureRecognizer)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if the view has a swipe gesture recognizer.
|
/// Checks if the view has a swipe gesture recognizer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -560,4 +700,146 @@ public static class GestureManager
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the view has a drag gesture recognizer.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasDragGestureRecognizer(View? view)
|
||||||
|
{
|
||||||
|
if (view?.GestureRecognizers == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (var recognizer in view.GestureRecognizers)
|
||||||
|
{
|
||||||
|
if (recognizer is DragGestureRecognizer)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the view has a drop gesture recognizer.
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasDropGestureRecognizer(View? view)
|
||||||
|
{
|
||||||
|
if (view?.GestureRecognizers == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
foreach (var recognizer in view.GestureRecognizers)
|
||||||
|
{
|
||||||
|
if (recognizer is DropGestureRecognizer)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initiates a drag operation from the specified view.
|
||||||
|
/// </summary>
|
||||||
|
public static void StartDrag(View? view, double x, double y)
|
||||||
|
{
|
||||||
|
if (view == null) return;
|
||||||
|
|
||||||
|
var recognizers = view.GestureRecognizers;
|
||||||
|
if (recognizers == null) return;
|
||||||
|
|
||||||
|
foreach (var item in recognizers)
|
||||||
|
{
|
||||||
|
var dragRecognizer = item as DragGestureRecognizer;
|
||||||
|
if (dragRecognizer == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Starting drag from {view.GetType().Name}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Create DragStartingEventArgs and invoke SendDragStarting
|
||||||
|
var method = typeof(DragGestureRecognizer).GetMethod("SendDragStarting",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(dragRecognizer, new object[] { view });
|
||||||
|
Console.WriteLine("[GestureManager] SendDragStarting invoked successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendDragStarting failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a drag enter event on the specified view.
|
||||||
|
/// </summary>
|
||||||
|
public static void ProcessDragEnter(View? view, double x, double y, object? data)
|
||||||
|
{
|
||||||
|
if (view == null) return;
|
||||||
|
|
||||||
|
var recognizers = view.GestureRecognizers;
|
||||||
|
if (recognizers == null) return;
|
||||||
|
|
||||||
|
foreach (var item in recognizers)
|
||||||
|
{
|
||||||
|
var dropRecognizer = item as DropGestureRecognizer;
|
||||||
|
if (dropRecognizer == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Drag enter on {view.GetType().Name}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = typeof(DropGestureRecognizer).GetMethod("SendDragOver",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(dropRecognizer, new object[] { view });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendDragOver failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes a drop event on the specified view.
|
||||||
|
/// </summary>
|
||||||
|
public static void ProcessDrop(View? view, double x, double y, object? data)
|
||||||
|
{
|
||||||
|
if (view == null) return;
|
||||||
|
|
||||||
|
var recognizers = view.GestureRecognizers;
|
||||||
|
if (recognizers == null) return;
|
||||||
|
|
||||||
|
foreach (var item in recognizers)
|
||||||
|
{
|
||||||
|
var dropRecognizer = item as DropGestureRecognizer;
|
||||||
|
if (dropRecognizer == null) continue;
|
||||||
|
|
||||||
|
Console.WriteLine($"[GestureManager] Drop on {view.GetType().Name}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var method = typeof(DropGestureRecognizer).GetMethod("SendDrop",
|
||||||
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||||
|
|
||||||
|
if (method != null)
|
||||||
|
{
|
||||||
|
method.Invoke(dropRecognizer, new object[] { view });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[GestureManager] SendDrop failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ public static class LinuxMauiAppBuilderExtensions
|
|||||||
builder.Services.TryAddSingleton<NotificationService>();
|
builder.Services.TryAddSingleton<NotificationService>();
|
||||||
builder.Services.TryAddSingleton<SystemTrayService>();
|
builder.Services.TryAddSingleton<SystemTrayService>();
|
||||||
builder.Services.TryAddSingleton(_ => MonitorService.Instance);
|
builder.Services.TryAddSingleton(_ => MonitorService.Instance);
|
||||||
|
builder.Services.TryAddSingleton<DragDropService>();
|
||||||
|
|
||||||
// Register GTK host service
|
// Register GTK host service
|
||||||
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
|
builder.Services.TryAddSingleton(_ => GtkHostService.Instance);
|
||||||
|
|||||||
@@ -929,16 +929,31 @@ public class LinuxApplication : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnGtkScrolled(object? sender, (double X, double Y, double DeltaX, double DeltaY) e)
|
private void OnGtkScrolled(object? sender, (double X, double Y, double DeltaX, double DeltaY, uint State) e)
|
||||||
{
|
{
|
||||||
if (_rootView == null) return;
|
if (_rootView == null) return;
|
||||||
|
|
||||||
|
// Convert GDK state to KeyModifiers
|
||||||
|
var modifiers = ConvertGdkStateToModifiers(e.State);
|
||||||
|
bool isCtrlPressed = (modifiers & KeyModifiers.Control) != 0;
|
||||||
|
|
||||||
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
var hitView = _rootView.HitTest((float)e.X, (float)e.Y);
|
||||||
|
|
||||||
|
// Check for pinch gesture (Ctrl+Scroll) first
|
||||||
|
if (isCtrlPressed && hitView?.MauiView != null)
|
||||||
|
{
|
||||||
|
if (Handlers.GestureManager.ProcessScrollAsPinch(hitView.MauiView, e.X, e.Y, e.DeltaY, true))
|
||||||
|
{
|
||||||
|
_gtkWindow?.RequestRedraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (hitView != null)
|
while (hitView != null)
|
||||||
{
|
{
|
||||||
if (hitView is SkiaScrollView scrollView)
|
if (hitView is SkiaScrollView scrollView)
|
||||||
{
|
{
|
||||||
var args = new ScrollEventArgs((float)e.X, (float)e.Y, (float)e.DeltaX, (float)e.DeltaY);
|
var args = new ScrollEventArgs((float)e.X, (float)e.Y, (float)e.DeltaX, (float)e.DeltaY, modifiers);
|
||||||
scrollView.OnScroll(args);
|
scrollView.OnScroll(args);
|
||||||
_gtkWindow?.RequestRedraw();
|
_gtkWindow?.RequestRedraw();
|
||||||
break;
|
break;
|
||||||
@@ -947,6 +962,25 @@ public class LinuxApplication : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static KeyModifiers ConvertGdkStateToModifiers(uint state)
|
||||||
|
{
|
||||||
|
var modifiers = KeyModifiers.None;
|
||||||
|
// GDK modifier masks
|
||||||
|
const uint GDK_SHIFT_MASK = 1 << 0;
|
||||||
|
const uint GDK_CONTROL_MASK = 1 << 2;
|
||||||
|
const uint GDK_MOD1_MASK = 1 << 3; // Alt
|
||||||
|
const uint GDK_SUPER_MASK = 1 << 26;
|
||||||
|
const uint GDK_LOCK_MASK = 1 << 1; // Caps Lock
|
||||||
|
|
||||||
|
if ((state & GDK_SHIFT_MASK) != 0) modifiers |= KeyModifiers.Shift;
|
||||||
|
if ((state & GDK_CONTROL_MASK) != 0) modifiers |= KeyModifiers.Control;
|
||||||
|
if ((state & GDK_MOD1_MASK) != 0) modifiers |= KeyModifiers.Alt;
|
||||||
|
if ((state & GDK_SUPER_MASK) != 0) modifiers |= KeyModifiers.Super;
|
||||||
|
if ((state & GDK_LOCK_MASK) != 0) modifiers |= KeyModifiers.CapsLock;
|
||||||
|
|
||||||
|
return modifiers;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnGtkTextInput(object? sender, string text)
|
private void OnGtkTextInput(object? sender, string text)
|
||||||
{
|
{
|
||||||
if (_focusedView != null)
|
if (_focusedView != null)
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
|||||||
public event EventHandler<(double X, double Y)>? PointerMoved;
|
public event EventHandler<(double X, double Y)>? PointerMoved;
|
||||||
public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyPressed;
|
public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyPressed;
|
||||||
public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyReleased;
|
public event EventHandler<(uint KeyVal, uint KeyCode, uint State)>? KeyReleased;
|
||||||
public event EventHandler<(double X, double Y, double DeltaX, double DeltaY)>? Scrolled;
|
public event EventHandler<(double X, double Y, double DeltaX, double DeltaY, uint State)>? Scrolled;
|
||||||
public event EventHandler<string>? TextInput;
|
public event EventHandler<string>? TextInput;
|
||||||
|
|
||||||
public GtkSkiaSurfaceWidget(int width, int height)
|
public GtkSkiaSurfaceWidget(int width, int height)
|
||||||
@@ -314,8 +314,8 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
|||||||
|
|
||||||
private bool OnScroll(IntPtr widget, IntPtr eventData, IntPtr userData)
|
private bool OnScroll(IntPtr widget, IntPtr eventData, IntPtr userData)
|
||||||
{
|
{
|
||||||
var (x, y, deltaX, deltaY) = ParseScrollEvent(eventData);
|
var (x, y, deltaX, deltaY, state) = ParseScrollEvent(eventData);
|
||||||
Scrolled?.Invoke(this, (x, y, deltaX, deltaY));
|
Scrolled?.Invoke(this, (x, y, deltaX, deltaY, state));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
|||||||
return (evt.keyval, evt.hardware_keycode, evt.state);
|
return (evt.keyval, evt.hardware_keycode, evt.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (double x, double y, double deltaX, double deltaY) ParseScrollEvent(IntPtr eventData)
|
private static (double x, double y, double deltaX, double deltaY, uint state) ParseScrollEvent(IntPtr eventData)
|
||||||
{
|
{
|
||||||
var evt = Marshal.PtrToStructure<GdkEventScroll>(eventData);
|
var evt = Marshal.PtrToStructure<GdkEventScroll>(eventData);
|
||||||
double deltaX = 0.0;
|
double deltaX = 0.0;
|
||||||
@@ -366,7 +366,7 @@ public sealed class GtkSkiaSurfaceWidget : IDisposable
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (evt.x, evt.y, deltaX, deltaY);
|
return (evt.x, evt.y, deltaX, deltaY, evt.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void GrabFocus()
|
public void GrabFocus()
|
||||||
|
|||||||
@@ -1738,14 +1738,21 @@ public class ScrollEventArgs : EventArgs
|
|||||||
public float Y { get; }
|
public float Y { get; }
|
||||||
public float DeltaX { get; }
|
public float DeltaX { get; }
|
||||||
public float DeltaY { get; }
|
public float DeltaY { get; }
|
||||||
|
public KeyModifiers Modifiers { get; }
|
||||||
public bool Handled { get; set; }
|
public bool Handled { get; set; }
|
||||||
|
|
||||||
public ScrollEventArgs(float x, float y, float deltaX, float deltaY)
|
/// <summary>
|
||||||
|
/// Gets whether the Control key is pressed during this scroll event.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsControlPressed => (Modifiers & KeyModifiers.Control) != 0;
|
||||||
|
|
||||||
|
public ScrollEventArgs(float x, float y, float deltaX, float deltaY, KeyModifiers modifiers = KeyModifiers.None)
|
||||||
{
|
{
|
||||||
X = x;
|
X = x;
|
||||||
Y = y;
|
Y = y;
|
||||||
DeltaX = deltaX;
|
DeltaX = deltaX;
|
||||||
DeltaY = deltaY;
|
DeltaY = deltaY;
|
||||||
|
Modifiers = modifiers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user