| 
2 | 2 | using System;  | 
3 | 3 | using Android.Views;  | 
4 | 4 | using Microsoft.Maui.Graphics;  | 
 | 5 | +using System.Runtime.Versioning;  | 
5 | 6 | using AView = Android.Views.View;  | 
6 | 7 | 
 
  | 
7 | 8 | namespace Microsoft.Maui.Controls.Platform  | 
8 | 9 | {  | 
9 | 10 | 	internal class PointerGestureHandler : Java.Lang.Object, AView.IOnHoverListener  | 
10 | 11 | 	{  | 
 | 12 | +		// Tracks the last button pressed so we can use it for subsequent Move/Up/Cancel  | 
 | 13 | +		ButtonsMask? _activeButton;  | 
 | 14 | + | 
11 | 15 | 		internal PointerGestureHandler(Func<View> getView, Func<AView> getControl)  | 
12 | 16 | 		{  | 
13 | 17 | 			GetView = getView;  | 
@@ -50,6 +54,133 @@ public bool OnHover(AView control, MotionEvent e)  | 
50 | 54 | 			return false;  | 
51 | 55 | 		}  | 
52 | 56 | 
 
  | 
 | 57 | +		// This method is called by InnerGestureListener to handle touch events for pointer gestures  | 
 | 58 | +		public bool OnTouch(MotionEvent e)  | 
 | 59 | +		{  | 
 | 60 | +			var view = GetView();  | 
 | 61 | + | 
 | 62 | +			if (view == null)  | 
 | 63 | +				return false;  | 
 | 64 | + | 
 | 65 | +			var control = GetControl();  | 
 | 66 | +			if (control == null)  | 
 | 67 | +				return false;  | 
 | 68 | + | 
 | 69 | +			var platformPointerArgs = new PlatformPointerEventArgs(control, e);  | 
 | 70 | + | 
 | 71 | +			foreach (var gesture in view.GetCompositeGestureRecognizers())  | 
 | 72 | +			{  | 
 | 73 | +				if (gesture is PointerGestureRecognizer pgr)  | 
 | 74 | +				{  | 
 | 75 | +					// Determine the button for this action. For Move/Up/Cancel prefer the active button, if any.  | 
 | 76 | +					ButtonsMask current = GetPressedButton(e);  | 
 | 77 | +					ButtonsMask effectiveButton = current;  | 
 | 78 | + | 
 | 79 | +					switch (e.Action)  | 
 | 80 | +					{  | 
 | 81 | +						case MotionEventActions.Down:  | 
 | 82 | +							// Primary button goes through Down/Up  | 
 | 83 | +							_activeButton = current;  | 
 | 84 | +							effectiveButton = current;  | 
 | 85 | +							if (!CheckButtonMask(pgr, effectiveButton))  | 
 | 86 | +								continue;  | 
 | 87 | +							pgr.SendPointerPressed(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);  | 
 | 88 | +							break;  | 
 | 89 | +						case MotionEventActions.Move:  | 
 | 90 | +							// Keep reporting the button that initiated the press if one is active  | 
 | 91 | +							effectiveButton = _activeButton ?? current;  | 
 | 92 | +							if (!CheckButtonMask(pgr, effectiveButton))  | 
 | 93 | +								continue;  | 
 | 94 | +							pgr.SendPointerMoved(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);  | 
 | 95 | +							break;  | 
 | 96 | +						case MotionEventActions.Up:  | 
 | 97 | +							// ACTION_UP does not carry ActionButton. Use the active one if available.  | 
 | 98 | +							effectiveButton = _activeButton ?? current;  | 
 | 99 | +							if (!CheckButtonMask(pgr, effectiveButton))  | 
 | 100 | +								continue;  | 
 | 101 | +							pgr.SendPointerReleased(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);  | 
 | 102 | +							// Clear active button after release  | 
 | 103 | +							_activeButton = null;  | 
 | 104 | +							break;  | 
 | 105 | +						case MotionEventActions.Cancel:  | 
 | 106 | +							// Treat cancel similar to release for active button, then exit  | 
 | 107 | +							effectiveButton = _activeButton ?? current;  | 
 | 108 | +							if (!CheckButtonMask(pgr, effectiveButton))  | 
 | 109 | +								continue;  | 
 | 110 | +							pgr.SendPointerExited(view, (relativeTo) => e.CalculatePosition(GetView(), relativeTo), platformPointerArgs, effectiveButton);  | 
 | 111 | +							_activeButton = null;  | 
 | 112 | +							break;  | 
 | 113 | +					}  | 
 | 114 | +				}  | 
 | 115 | +			}  | 
 | 116 | + | 
 | 117 | +			return false;  | 
 | 118 | +		}  | 
 | 119 | + | 
 | 120 | +		ButtonsMask GetPressedButton(MotionEvent motionEvent)  | 
 | 121 | +		{  | 
 | 122 | +			if (motionEvent == null)  | 
 | 123 | +				return ButtonsMask.Primary;  | 
 | 124 | + | 
 | 125 | +			var action = motionEvent.Action;  | 
 | 126 | + | 
 | 127 | +			// For explicit button change events (mouse/pen), use ActionButton to determine which button changed  | 
 | 128 | +			if (OperatingSystem.IsAndroidVersionAtLeast(23) &&  | 
 | 129 | +				(action == MotionEventActions.ButtonPress || action == MotionEventActions.ButtonRelease))  | 
 | 130 | +			{  | 
 | 131 | +#pragma warning disable CA1416 // Validate platform compatibility  | 
 | 132 | +				var actionButton = motionEvent.ActionButton; // Which button changed for this event  | 
 | 133 | +				if ((actionButton & MotionEventButtonState.Secondary) == MotionEventButtonState.Secondary)  | 
 | 134 | +					return ButtonsMask.Secondary;  | 
 | 135 | +				if ((actionButton & MotionEventButtonState.Primary) == MotionEventButtonState.Primary)  | 
 | 136 | +					return ButtonsMask.Primary;  | 
 | 137 | +#pragma warning restore CA1416 // Validate platform compatibility  | 
 | 138 | +			}  | 
 | 139 | + | 
 | 140 | +			// Otherwise, infer from current ButtonState (covers Move/Down/Up and API < 23)  | 
 | 141 | +			var buttonState = motionEvent.ButtonState;  | 
 | 142 | + | 
 | 143 | +			// Check for secondary button (right mouse button)  | 
 | 144 | +			if ((buttonState & MotionEventButtonState.Secondary) == MotionEventButtonState.Secondary)  | 
 | 145 | +			{  | 
 | 146 | +				return ButtonsMask.Secondary;  | 
 | 147 | +			}  | 
 | 148 | + | 
 | 149 | +			// Check for stylus secondary button on API 23+  | 
 | 150 | +			if (OperatingSystem.IsAndroidVersionAtLeast(23))  | 
 | 151 | +			{  | 
 | 152 | +#pragma warning disable CA1416 // Validate platform compatibility  | 
 | 153 | +				if (CheckStylusSecondaryButton(buttonState))  | 
 | 154 | +#pragma warning restore CA1416 // Validate platform compatibility  | 
 | 155 | +				{  | 
 | 156 | +					return ButtonsMask.Secondary;  | 
 | 157 | +				}  | 
 | 158 | +			}  | 
 | 159 | + | 
 | 160 | +			// Default to primary button  | 
 | 161 | +			return ButtonsMask.Primary;  | 
 | 162 | +		}  | 
 | 163 | + | 
 | 164 | +		[SupportedOSPlatform("android23.0")]  | 
 | 165 | +		bool CheckStylusSecondaryButton(MotionEventButtonState buttonState)  | 
 | 166 | +		{  | 
 | 167 | +			return (buttonState & MotionEventButtonState.StylusSecondary) == MotionEventButtonState.StylusSecondary;  | 
 | 168 | +		}  | 
 | 169 | + | 
 | 170 | +		bool CheckButtonMask(PointerGestureRecognizer recognizer, ButtonsMask currentButton)  | 
 | 171 | +		{  | 
 | 172 | +			// If no buttons specified (enum backing value is 0), default to Primary only  | 
 | 173 | +			if ((int)recognizer.Buttons == 0)  | 
 | 174 | +				return currentButton == ButtonsMask.Primary;  | 
 | 175 | + | 
 | 176 | +			if (currentButton == ButtonsMask.Secondary)  | 
 | 177 | +			{  | 
 | 178 | +				return (recognizer.Buttons & ButtonsMask.Secondary) == ButtonsMask.Secondary;  | 
 | 179 | +			}  | 
 | 180 | + | 
 | 181 | +			return (recognizer.Buttons & ButtonsMask.Primary) == ButtonsMask.Primary;  | 
 | 182 | +		}  | 
 | 183 | + | 
53 | 184 | 		public void SetupHandlerForPointer()  | 
54 | 185 | 		{  | 
55 | 186 | 			var view = GetView();  | 
 | 
0 commit comments