From f6c6aeb7b60a72b4423ee32dce93fb8bf82dae17 Mon Sep 17 00:00:00 2001 From: Curtis Wensley Date: Wed, 17 Jan 2024 16:06:09 -0800 Subject: [PATCH] Add Control.IsMouseCaptured, CaptureMouse, and ReleaseMouseCapture APIs --- src/Eto.Gtk/Forms/GtkControl.cs | 114 ++++++++++- src/Eto.Gtk/NativeMethods.cs | 38 ++++ src/Eto.Gtk/NativeMethods.tt | 4 +- src/Eto.Mac/Forms/MacView.cs | 192 ++++++++++++++---- src/Eto.Mac/MacConversions.cs | 18 +- src/Eto.WinForms/Forms/ApplicationHandler.cs | 3 +- src/Eto.WinForms/Forms/HwndFormHandler.cs | 4 + src/Eto.WinForms/Forms/WindowsControl.cs | 33 ++- src/Eto.Wpf/Drawing/GraphicsPathHandler.cs | 2 + src/Eto.Wpf/Forms/WpfFrameworkElement.cs | 26 ++- src/Eto/Forms/Controls/Control.cs | 50 +++++ .../Forms/Controls/ThemedControlHandler.cs | 7 + .../UnitTests/Forms/Behaviors/MouseTests.cs | 169 ++++++++++++++- 13 files changed, 594 insertions(+), 66 deletions(-) mode change 100644 => 100755 src/Eto/Forms/Controls/ThemedControlHandler.cs diff --git a/src/Eto.Gtk/Forms/GtkControl.cs b/src/Eto.Gtk/Forms/GtkControl.cs index b60462610d..e1dc83a518 100644 --- a/src/Eto.Gtk/Forms/GtkControl.cs +++ b/src/Eto.Gtk/Forms/GtkControl.cs @@ -2,6 +2,7 @@ namespace Eto.GtkSharp.Forms { public interface IGtkControl { + Control Widget { get; } Point CurrentLocation { get; set; } Size UserPreferredSize { get; } @@ -17,6 +18,9 @@ public interface IGtkControl #if GTK2 void TriggerEnabled(bool oldEnabled, bool newEnabled, bool force = false); #endif + + void TriggerMouseEnterIfNeeded(); + void TriggerMouseLeaveIfNeeded(); } public static class GtkControlExtensions @@ -58,7 +62,13 @@ static class GtkControl public static readonly object Cursor_Key = new object(); public static readonly object AllowDrop_Key = new object(); public static readonly object DeferMouseLeave_Key = new object(); + public static readonly object IsMouseCaptured_Key = new object(); + public static readonly object LastEnteredControls_Key = new object(); + public static readonly object ShouldTranslatePoints_Key = new object(); public static uint? DefaultBorderWidth; + + public static HashSet EnteredControls = new HashSet(); + public static bool ShouldCaptureMouse; } public abstract class GtkControl : WidgetHandler, Control.IHandler, IGtkControl @@ -535,7 +545,7 @@ public void HandleControlLeaveNotifyEvent(object o, Gtk.LeaveNotifyEventArgs arg return; // ignore child events - if (args.Event.Detail == Gdk.NotifyType.Inferior) + if (args.Event.Detail == Gdk.NotifyType.Inferior || !_mouseEntered || args.Event.Window != handler.EventControl.GetWindow()) return; var p = new PointF((float)args.Event.X, (float)args.Event.Y); Keys modifiers = args.Event.State.ToEtoKey(); @@ -546,26 +556,43 @@ public void HandleControlLeaveNotifyEvent(object o, Gtk.LeaveNotifyEventArgs arg Application.Instance.AsyncInvoke(() => { if (!handler.Widget.IsDisposed) + { + GtkControl.EnteredControls.Remove(handler); handler.Callback.OnMouseLeave(handler.Widget, new MouseEventArgs(buttons, modifiers, p)); + } }); } else { + GtkControl.EnteredControls.Remove(handler); handler.Callback.OnMouseLeave(handler.Widget, new MouseEventArgs(buttons, modifiers, p)); } } - + public void TriggerMouseLeaveIfNeeded() { var handler = Handler; if (handler == null || !_mouseEntered) return; _mouseEntered = false; + GtkControl.EnteredControls.Remove(handler); var p = handler.Widget.PointFromScreen(Mouse.Position); Keys modifiers = Keyboard.Modifiers; MouseButtons buttons = MouseButtons.None; handler.Callback.OnMouseLeave(handler.Widget, new MouseEventArgs(buttons, modifiers, p)); } + public void TriggerMouseEnterIfNeeded() + { + var handler = Handler; + if (handler == null || _mouseEntered) + return; + _mouseEntered = true; + GtkControl.EnteredControls.Add(handler); + var p = handler.Widget.PointFromScreen(Mouse.Position); + Keys modifiers = Keyboard.Modifiers; + MouseButtons buttons = MouseButtons.None; + handler.Callback.OnMouseEnter(handler.Widget, new MouseEventArgs(buttons, modifiers, p)); + } [GLib.ConnectBefore] public void HandleControlEnterNotifyEvent(object o, Gtk.EnterNotifyEventArgs args) @@ -575,12 +602,13 @@ public void HandleControlEnterNotifyEvent(object o, Gtk.EnterNotifyEventArgs arg return; // ignore child events - if (args.Event.Detail == Gdk.NotifyType.Inferior) + if (args.Event.Detail == Gdk.NotifyType.Inferior || _mouseEntered || args.Event.Window != handler.EventControl.GetWindow()) return; var p = new PointF((float)args.Event.X, (float)args.Event.Y); Keys modifiers = args.Event.State.ToEtoKey(); MouseButtons buttons = MouseButtons.None; _mouseEntered = true; + GtkControl.EnteredControls.Add(handler); handler.Callback.OnMouseEnter(handler.Widget, new MouseEventArgs(buttons, modifiers, p)); } @@ -607,6 +635,7 @@ public void HandleButtonReleaseEvent(object o, Gtk.ButtonReleaseEventArgs args) if (handler == null) return; + handler.IsMouseCaptured = false; var p = new PointF((float)args.Event.X, (float)args.Event.Y); p = handler.TranslatePoint(args.Event.Window, p); Keys modifiers = args.Event.State.ToEtoKey(); @@ -624,6 +653,8 @@ public void HandleButtonPressEvent(object sender, Gtk.ButtonPressEventArgs args) if (handler == null) return; + handler.IsMouseCaptured = false; + GtkControl.ShouldCaptureMouse = true; var p = new PointF((float)args.Event.X, (float)args.Event.Y); p = handler.TranslatePoint(args.Event.Window, p); Keys modifiers = args.Event.State.ToEtoKey(); @@ -641,6 +672,8 @@ public void HandleButtonPressEvent(object sender, Gtk.ButtonPressEventArgs args) handler.EventControl.GrabFocus(); if (args.RetVal != null && (bool)args.RetVal == true) return; + if (mouseArgs.Handled && GtkControl.ShouldCaptureMouse) + handler.IsMouseCaptured = true; args.RetVal = mouseArgs.Handled; } @@ -936,8 +969,13 @@ public virtual void HandleStateFlagsChangedForEnabled(object o, Gtk.StateFlagsCh } #endif } + - public virtual bool ShouldTranslatePoints => false; + public virtual bool ShouldTranslatePoints + { + get => Widget.Properties.Get(GtkControl.ShouldTranslatePoints_Key); + private set => Widget.Properties.Set(GtkControl.ShouldTranslatePoints_Key, value); + } public virtual PointF TranslatePoint(Gdk.Window window, PointF p) { @@ -1187,5 +1225,73 @@ public virtual void UpdateLayout() // is this the best way to force a layout pass? I can't find anything else.. ContainerControl.Toplevel?.SizeAllocate(ContainerControl.Toplevel.Allocation); } + + public bool IsMouseCaptured + { + get => Widget.Properties.Get(GtkControl.IsMouseCaptured_Key, false) || EventControl.HasGrab; + private set => Widget.Properties.Set(GtkControl.IsMouseCaptured_Key, value); + } + + Control IGtkControl.Widget => Widget; + + public void TriggerMouseEnterIfNeeded() => Connector.TriggerMouseEnterIfNeeded(); + public void TriggerMouseLeaveIfNeeded() => Connector.TriggerMouseLeaveIfNeeded(); + + public bool CaptureMouse() + { + NativeMethods.gtk_grab_add(EventControl.Handle); + var ret = EventControl.HasGrab; + + // var status = Gdk.Display.Default.DefaultSeat.Grab(EventControl.GetWindow(), Gdk.SeatCapabilities.Pointer, false, null, null, null); + // var ret = status == Gdk.GrabStatus.Success || status == Gdk.GrabStatus.AlreadyGrabbed; + IsMouseCaptured = ret; + if (ret) + { + GtkControl.ShouldCaptureMouse = false; + ShouldTranslatePoints = true; + var lastEntered = Widget.Properties.Create>(GtkControl.LastEnteredControls_Key); + lastEntered.Clear(); + var parents = Widget.Parents.Select(r => r.Handler).OfType().ToList(); + foreach (var entered in GtkControl.EnteredControls) + { + if (parents.Contains(entered)) + continue; + entered.TriggerMouseLeaveIfNeeded(); + lastEntered.Add(entered); + } + foreach (var parent in parents) + parent.TriggerMouseEnterIfNeeded(); + TriggerMouseEnterIfNeeded(); + } + + return ret; + } + + public void ReleaseMouseCapture() + { + if (IsMouseCaptured) + { + ShouldTranslatePoints = false; + NativeMethods.gtk_grab_remove(EventControl.Handle); + // Gdk.Display.Default.DefaultSeat.Ungrab(); // doesn't work?! + IsMouseCaptured = false; + var lastEntered = Widget.Properties.Create>(GtkControl.LastEnteredControls_Key); + var mouseLocation = Mouse.Position; + if (!Widget.RectangleToScreen(new Rectangle(Widget.Size)).Contains(mouseLocation)) + TriggerMouseLeaveIfNeeded(); + var parents = Widget.Parents.Select(r => r.Handler).OfType().ToList(); + foreach (var parent in parents) + { + if (!parent.Widget.RectangleToScreen(new Rectangle(parent.Widget.Size)).Contains(mouseLocation)) + parent.TriggerMouseLeaveIfNeeded(); + } + + foreach (var last in lastEntered) + { + if (last.Widget.RectangleToScreen(new Rectangle(last.Widget.Size)).Contains(mouseLocation)) + last.TriggerMouseEnterIfNeeded(); + } + } + } } } diff --git a/src/Eto.Gtk/NativeMethods.cs b/src/Eto.Gtk/NativeMethods.cs index 9dd509ecca..f99eb67d78 100644 --- a/src/Eto.Gtk/NativeMethods.cs +++ b/src/Eto.Gtk/NativeMethods.cs @@ -220,6 +220,12 @@ static NMWindows() [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] public extern static uint gtk_get_micro_version(); + [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] + public extern static void gtk_grab_add(IntPtr widget); + + [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] + public extern static void gtk_grab_remove(IntPtr widget); + [DllImport(libgdk, CallingConvention = CallingConvention.Cdecl)] public extern static bool gdk_cairo_get_clip_rectangle(IntPtr context, IntPtr rect); @@ -439,6 +445,12 @@ static NMLinux() [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] public extern static uint gtk_get_micro_version(); + [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] + public extern static void gtk_grab_add(IntPtr widget); + + [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] + public extern static void gtk_grab_remove(IntPtr widget); + [DllImport(libgdk, CallingConvention = CallingConvention.Cdecl)] public extern static bool gdk_cairo_get_clip_rectangle(IntPtr context, IntPtr rect); @@ -658,6 +670,12 @@ static NMMac() [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] public extern static uint gtk_get_micro_version(); + [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] + public extern static void gtk_grab_add(IntPtr widget); + + [DllImport(libgtk, CallingConvention = CallingConvention.Cdecl)] + public extern static void gtk_grab_remove(IntPtr widget); + [DllImport(libgdk, CallingConvention = CallingConvention.Cdecl)] public extern static bool gdk_cairo_get_clip_rectangle(IntPtr context, IntPtr rect); @@ -988,6 +1006,26 @@ public static uint gtk_get_micro_version() return NMWindows.gtk_get_micro_version(); } + public static void gtk_grab_add(IntPtr widget) + { + if (EtoEnvironment.Platform.IsLinux) + NMLinux.gtk_grab_add(widget); + else if (EtoEnvironment.Platform.IsMac) + NMMac.gtk_grab_add(widget); + else + NMWindows.gtk_grab_add(widget); + } + + public static void gtk_grab_remove(IntPtr widget) + { + if (EtoEnvironment.Platform.IsLinux) + NMLinux.gtk_grab_remove(widget); + else if (EtoEnvironment.Platform.IsMac) + NMMac.gtk_grab_remove(widget); + else + NMWindows.gtk_grab_remove(widget); + } + public static IntPtr webkit_web_view_new() { if (EtoEnvironment.Platform.IsLinux) diff --git a/src/Eto.Gtk/NativeMethods.tt b/src/Eto.Gtk/NativeMethods.tt index 2927c3dec8..4f20f063f3 100755 --- a/src/Eto.Gtk/NativeMethods.tt +++ b/src/Eto.Gtk/NativeMethods.tt @@ -70,7 +70,9 @@ var gtkmethods = new[] "IntPtr gtk_button_get_event_window(IntPtr button)", "uint gtk_get_major_version()", "uint gtk_get_minor_version()", - "uint gtk_get_micro_version()" + "uint gtk_get_micro_version()", + "void gtk_grab_add(IntPtr widget)", + "void gtk_grab_remove(IntPtr widget)", }; var gdkmethods = new[] diff --git a/src/Eto.Mac/Forms/MacView.cs b/src/Eto.Mac/Forms/MacView.cs index 17024eb09a..bac74458bb 100644 --- a/src/Eto.Mac/Forms/MacView.cs +++ b/src/Eto.Mac/Forms/MacView.cs @@ -9,10 +9,12 @@ namespace Eto.Mac.Forms { class MouseDelegate : NSObject { - WeakReference widget; - bool entered; + WeakReference _widget; + bool _entered; - public IMacViewHandler Handler { get { return (IMacViewHandler)widget.Target; } set { widget = new WeakReference(value); } } + public IMacViewHandler Handler { get => (IMacViewHandler)_widget.Target; set => _widget = new WeakReference(value); } + + public static HashSet EnteredControls = new HashSet(); [Export("mouseMoved:")] public void MouseMoved(NSEvent theEvent) @@ -25,10 +27,13 @@ public void MouseMoved(NSEvent theEvent) [Export("mouseEntered:")] public void MouseEntered(NSEvent theEvent) { - entered = true; + // we could be entered already after using CaptureMouse() var h = Handler; - if (h == null || !h.Enabled) return; + if (h == null || !h.Enabled || _entered) return; + _entered = true; + // Debug.WriteLine($"MouseEnter: {h.Widget.GetType()}"); h.Callback.OnMouseEnter(h.Widget, MacConversions.GetMouseEvent(h, theEvent, false)); + EnteredControls.Add(this); } [Export("cursorUpdate:")] @@ -40,9 +45,11 @@ public void CursorUpdate(NSEvent theEvent) public void MouseExited(NSEvent theEvent) { var h = Handler; - if (h == null || !h.Enabled) return; - entered = false; + if (h == null || !h.Enabled || !_entered) return; + _entered = false; + // Debug.WriteLine($"MouseLeave: {h.Widget.GetType()}"); h.Callback.OnMouseLeave(h.Widget, MacConversions.GetMouseEvent(h, theEvent, false)); + EnteredControls.Remove(this); } [Export("scrollWheel:")] @@ -56,8 +63,8 @@ public void ScrollWheel(NSEvent theEvent) public void FireMouseLeaveIfNeeded(bool async) { var h = Handler; - if (h == null || !entered) return; - entered = false; + if (h == null || !_entered) return; + _entered = false; var theEvent = NSApplication.SharedApplication.CurrentEvent; var args = MacConversions.GetMouseEvent(h, theEvent, false); if (async) @@ -78,8 +85,8 @@ public void FireMouseLeaveIfNeeded(bool async) public void FireMouseEnterIfNeeded(bool async) { var h = Handler; - if (h == null || entered) return; - entered = true; + if (h == null || _entered) return; + _entered = true; var theEvent = NSApplication.SharedApplication.CurrentEvent; var args = MacConversions.GetMouseEvent(h, theEvent, false); if (async) @@ -137,12 +144,14 @@ public interface IMacViewHandler : IMacControlHandler CGPoint GetAlignmentPointForFramePoint(CGPoint point); CGRect GetAlignmentRectForFrame(CGRect frame); bool OnAcceptsFirstMouse(NSEvent theEvent); - bool TriggerMouseCallback(); + bool TriggerMouseCallback(NSEvent theEvent = null, bool includeMouseDown = true); MouseEventArgs TriggerMouseDown(NSObject obj, IntPtr sel, NSEvent theEvent); MouseEventArgs TriggerMouseUp(NSObject obj, IntPtr sel, NSEvent theEvent); void UpdateTrackingAreas(); void OnViewDidMoveToWindow(); bool AutoAttachNative { get; set; } + void FireMouseEnterIfNeeded(); + void FireMouseLeaveIfNeeded(); } static partial class MacView @@ -589,6 +598,8 @@ static bool ValidateSystemUserInterfaceItem(IntPtr sender, IntPtr sel, IntPtr it /// public static bool InMouseTrackingLoop; + public static IMacViewHandler CapturedControl; + public static IntPtr selViewDidMoveToWindow = Selector.GetHandle("viewDidMoveToWindow"); internal static MarshalDelegates.Action_IntPtr_IntPtr TriggerViewDidMoveToWindow_Delegate = TriggerViewDidMoveToWindow; @@ -927,14 +938,24 @@ public DragEventArgs GetDragEventArgs(NSDraggingInfo info, object customControl) /// Triggers a mouse callback from a different event. /// e.g. when an NSButton is clicked it is triggered from a mouse up event. /// - public bool TriggerMouseCallback() + public bool TriggerMouseCallback(NSEvent evt = null, bool includeMouseDown = true) { // trigger mouse up event since it's buried by cocoa - var evt = NSApplication.SharedApplication.CurrentEvent; + evt ??= NSApplication.SharedApplication.CurrentEvent; if (evt == null) return false; switch (evt.Type) { + case NSEventType.LeftMouseDown: + case NSEventType.RightMouseDown: + case NSEventType.OtherMouseDown: + { + if (!includeMouseDown) + return false; + var args = MacConversions.GetMouseEvent(this, evt, false); + Callback.OnMouseDown(Widget, args); + return args.Handled; + } case NSEventType.LeftMouseUp: case NSEventType.RightMouseUp: case NSEventType.OtherMouseUp: @@ -942,6 +963,7 @@ public bool TriggerMouseCallback() var args = MacConversions.GetMouseEvent(this, evt, false); Callback.OnMouseUp(Widget, args); SuppressMouseTriggerCallback = true; + MacView.CapturedControl = null; return args.Handled; } case NSEventType.LeftMouseDragged: @@ -1580,7 +1602,7 @@ public virtual MouseEventArgs TriggerMouseDown(NSObject obj, IntPtr sel, NSEvent // some controls use event loops until mouse up, so we need to trigger the mouse up here. if (!SuppressMouseTriggerCallback) - TriggerMouseCallback(); + TriggerMouseCallback(theEvent, includeMouseDown: false); } else if (UseMouseTrackingLoop && MacView.InMouseTrackingLoop) { @@ -1588,37 +1610,51 @@ public virtual MouseEventArgs TriggerMouseDown(NSObject obj, IntPtr sel, NSEvent // e.g. if a child control that you started to click + drag on is removed then all future events // to the parent are no longer forwarded. // See MouseTests.EventsFromParentShouldWorkWhenChildRemoved - var app = NSApplication.SharedApplication; - // Console.WriteLine("Entered MouseTrackingLoop"); - do - { - var evt = app.NextEvent(NSEventMask.AnyEvent, NSDate.DistantFuture, MouseTrackingRunLoopMode, true); + DoMouseTrackingLoop(true); + } + MacView.InMouseTrackingLoop = false; + return args; + } - var evtType = evt.Type; - switch (evt.Type) - { - case NSEventType.LeftMouseDragged: - case NSEventType.RightMouseDragged: - case NSEventType.OtherMouseDragged: - TriggerMouseCallback(); - break; - case NSEventType.LeftMouseUp: - case NSEventType.RightMouseUp: - case NSEventType.OtherMouseUp: - TriggerMouseCallback(); + private void DoMouseTrackingLoop(bool autoRelease) + { + var app = NSApplication.SharedApplication; + MacView.CapturedControl = this; + bool continueLoop; + // Console.WriteLine("Entered MouseTrackingLoop"); + do + { + var evt = app.NextEvent(NSEventMask.AnyEvent, NSDate.DistantFuture, MouseTrackingRunLoopMode, true); + + switch (evt.Type) + { + case NSEventType.LeftMouseUp: + case NSEventType.RightMouseUp: + case NSEventType.OtherMouseUp: + TriggerMouseCallback(evt); + if (autoRelease) + { MacView.InMouseTrackingLoop = false; - break; - default: - // not a mouse event, send it along. - app.SendEvent(evt); - break; - } + MacView.CapturedControl = null; + } + break; + case NSEventType.LeftMouseDragged: + case NSEventType.RightMouseDragged: + case NSEventType.OtherMouseDragged: + case NSEventType.LeftMouseDown: + case NSEventType.RightMouseDown: + case NSEventType.OtherMouseDown: + TriggerMouseCallback(evt); + break; + default: + // not a mouse event, send it along. + app.SendEvent(evt); + break; } - while (MacView.InMouseTrackingLoop); - // Console.WriteLine("Exited MouseTrackingLoop"); + continueLoop = autoRelease ? MacView.InMouseTrackingLoop : CaptureLoopEnabled; } - MacView.InMouseTrackingLoop = false; - return args; + while (continueLoop); + // Console.WriteLine("Exited MouseTrackingLoop"); } public virtual MouseEventArgs TriggerMouseUp(NSObject obj, IntPtr sel, NSEvent theEvent) @@ -1678,6 +1714,78 @@ public virtual void OnViewDidMoveToWindow() else Widget.AttachNative(); } + + public bool IsMouseCaptured => MacView.CapturedControl == this; + + public bool CaptureMouse() + { + if (!Widget.Loaded || !Widget.Visible) + return false; + + // already captured? + if (MacView.CapturedControl == this && CaptureLoopEnabled) + return true; + + // ensure we release capture of any previous control + if (MacView.CapturedControl != this) + MacView.CapturedControl?.Widget.ReleaseMouseCapture(); + + MacView.CapturedControl = this; + MacView.InMouseTrackingLoop = false; + // Do this asynchronously as this is not a blocking API + Application.Instance.AsyncInvoke(DoMouseCaptureLoop); + return true; + } + + public void FireMouseEnterIfNeeded() => mouseDelegate?.FireMouseEnterIfNeeded(false); + public void FireMouseLeaveIfNeeded() => mouseDelegate?.FireMouseLeaveIfNeeded(false); + + private void DoMouseCaptureLoop() + { + // fire mouse leave of current control(s) + var enteredControls = MouseDelegate.EnteredControls.ToList(); + var parentControls = Widget.Parents.Select(r => r.Handler).OfType().ToList(); + foreach (var ctl in enteredControls) + { + if (ctl.Handler == this || parentControls.Contains(ctl.Handler)) + continue; + ctl.FireMouseLeaveIfNeeded(false); + } + foreach (var parent in parentControls) + { + parent.FireMouseEnterIfNeeded(); + } + FireMouseEnterIfNeeded(); + CaptureLoopEnabled = true; + DoMouseTrackingLoop(false); + var mousePosition = Mouse.Position; + + if (!Widget.RectangleToScreen(new RectangleF(Widget.Size)).Contains(mousePosition)) + FireMouseLeaveIfNeeded(); + + foreach (var parent in parentControls) + { + if (!parent.Widget.RectangleToScreen(new RectangleF(parent.Widget.Size)).Contains(mousePosition)) + parent.FireMouseLeaveIfNeeded(); + } + // fire mouse enter of previous control if still in bounds + foreach (var ctl in enteredControls) + { + if (ctl.Handler == this) + continue; + var widget = ctl.Handler?.Widget; + if (widget != null && widget.RectangleToScreen(new RectangleF(widget.Size)).Contains(mousePosition)) + ctl.FireMouseEnterIfNeeded(false); + } + } + + bool CaptureLoopEnabled; + + public void ReleaseMouseCapture() + { + CaptureLoopEnabled = false; + MacView.CapturedControl = null; + } } } diff --git a/src/Eto.Mac/MacConversions.cs b/src/Eto.Mac/MacConversions.cs index 3efcc65329..26d437f778 100644 --- a/src/Eto.Mac/MacConversions.cs +++ b/src/Eto.Mac/MacConversions.cs @@ -196,7 +196,23 @@ public static MouseEventArgs GetMouseEvent(IMacViewHandler handler, NSEvent theE var view = handler.ContainerControl; var pt = theEvent.LocationInWindow; pt = handler.GetAlignmentPointForFramePoint(pt); - point = pt.ToEto(view); + if (theEvent.Window == null) + { + // flip to top down first + pt.Y = NSScreen.Screens[0].Frame.Height - pt.Y; + point = handler.Widget.PointFromScreen(pt.ToEto()); + } + else if (view.Window != theEvent.Window) + { + var loc = theEvent.Window.Frame.Location.ToEto(); + pt.X += loc.X; + pt.Y += loc.Y; + pt.Y = NSScreen.Screens[0].Frame.Height - pt.Y; + point = handler.Widget.PointFromScreen(pt.ToEto()); + } + else + point = pt.ToEto(view); + if (includeWheel) delta = new SizeF((float)theEvent.DeltaX, (float)theEvent.DeltaY); modifiers = theEvent.ModifierFlags.ToEto(); diff --git a/src/Eto.WinForms/Forms/ApplicationHandler.cs b/src/Eto.WinForms/Forms/ApplicationHandler.cs index 6c4d66ba41..3c94ec79ef 100644 --- a/src/Eto.WinForms/Forms/ApplicationHandler.cs +++ b/src/Eto.WinForms/Forms/ApplicationHandler.cs @@ -147,8 +147,9 @@ void SetOptions() bubble.AddBubbleMouseEvent((c, cb, e) => cb.OnMouseMove(c, e), null, Win32.WM.MOUSEMOVE); bubble.AddBubbleMouseEvents((c, cb, e) => { + WindowsControl.SkipMouseCapture = false; cb.OnMouseDown(c, e); - if (e.Handled && c.Handler is IWindowsControl handler && handler.ShouldCaptureMouse) + if (e.Handled && c.Handler is IWindowsControl handler && handler.ShouldCaptureMouse && !WindowsControl.SkipMouseCapture) { handler.ContainerControl.Capture = true; handler.MouseCaptured = true; diff --git a/src/Eto.WinForms/Forms/HwndFormHandler.cs b/src/Eto.WinForms/Forms/HwndFormHandler.cs index 46efaed021..18b57036f5 100755 --- a/src/Eto.WinForms/Forms/HwndFormHandler.cs +++ b/src/Eto.WinForms/Forms/HwndFormHandler.cs @@ -623,5 +623,9 @@ public SizeF GetPreferredSize(SizeF availableSize) public void Print() => throw new NotImplementedException(); public void UpdateLayout() => throw new NotImplementedException(); + + public bool IsMouseCaptured => throw new NotImplementedException(); + public bool CaptureMouse() => throw new NotImplementedException(); + public void ReleaseMouseCapture() => throw new NotImplementedException(); } } diff --git a/src/Eto.WinForms/Forms/WindowsControl.cs b/src/Eto.WinForms/Forms/WindowsControl.cs index 00018fb02d..77063e43b0 100644 --- a/src/Eto.WinForms/Forms/WindowsControl.cs +++ b/src/Eto.WinForms/Forms/WindowsControl.cs @@ -97,6 +97,7 @@ static class WindowsControl public static readonly object UseShellDropManager_Key = new object(); public static readonly object MouseCaptured_Key = new object(); + public static bool SkipMouseCapture { get; set; } internal static Control DragSourceControl { get; set; } } @@ -520,13 +521,13 @@ void HandleMouseUp(Object sender, swf.MouseEventArgs e) if (MouseCaptured) { MouseCaptured = false; - Control.Capture = false; + ContainerControl.Capture = false; } var args = e.ToEto(Control); Callback.OnMouseUp(Widget, args); - if (args.Handled && Control.Capture) - Control.Capture = false; + if (args.Handled && ContainerControl.Capture) + ContainerControl.Capture = false; } void HandleMouseMove(Object sender, swf.MouseEventArgs e) @@ -537,10 +538,11 @@ void HandleMouseMove(Object sender, swf.MouseEventArgs e) void HandleMouseDown(object sender, swf.MouseEventArgs e) { var ev = e.ToEto(Control); + WindowsControl.SkipMouseCapture = false; Callback.OnMouseDown(Widget, ev); - if (ev.Handled && ShouldCaptureMouse) + if (ev.Handled && ShouldCaptureMouse && !WindowsControl.SkipMouseCapture) { - Control.Capture = true; + ContainerControl.Capture = true; MouseCaptured = true; } } @@ -992,7 +994,6 @@ SwfShellDropBehavior DropBehavior get => Widget.Properties.Get(typeof(SwfShellDropBehavior)); set => Widget.Properties.Set(typeof(SwfShellDropBehavior), value); } - public void DoDragDrop(DataObject data, DragEffects allowedEffects, Image image, PointF cursorOffset) { var dataObject = data.ToSwf(); @@ -1033,5 +1034,25 @@ public void Print() } public void UpdateLayout() => ContainerControl.PerformLayout(); + + public bool IsMouseCaptured => ContainerControl.Capture; + public bool CaptureMouse() + { + ContainerControl.Capture = true; + var ret = MouseCaptured = IsMouseCaptured; + if (ret) + { + // fire mouse enter for parents + // var parentControls = Widget.Parents.Select(r => r.Handler).OfType().ToList(); + WindowsControl.SkipMouseCapture = true; + } + return ret; + } + public void ReleaseMouseCapture() + { + ContainerControl.Capture = false; + MouseCaptured = false; + } + } } diff --git a/src/Eto.Wpf/Drawing/GraphicsPathHandler.cs b/src/Eto.Wpf/Drawing/GraphicsPathHandler.cs index 48ef9d7fe3..f680d70cb1 100644 --- a/src/Eto.Wpf/Drawing/GraphicsPathHandler.cs +++ b/src/Eto.Wpf/Drawing/GraphicsPathHandler.cs @@ -64,6 +64,8 @@ public void StartFigure() public void AddLines(IEnumerable points) { var pointsList = points as IList ?? points.ToArray(); + if (pointsList.Count == 0) + return; ConnectTo(pointsList.First().ToWpf()); var wpfPoints = from p in pointsList select p.ToWpf(); diff --git a/src/Eto.Wpf/Forms/WpfFrameworkElement.cs b/src/Eto.Wpf/Forms/WpfFrameworkElement.cs index cd6841ef50..35e9e8084c 100755 --- a/src/Eto.Wpf/Forms/WpfFrameworkElement.cs +++ b/src/Eto.Wpf/Forms/WpfFrameworkElement.cs @@ -816,7 +816,7 @@ protected virtual void HandleMouseUp(object sender, swi.MouseButtonEventArgs e) Callback.OnMouseUp(Widget, args); e.Handled = args.Handled; - + // If the mouse was captured intrinsically we need to release capture otherwise it hangs the app // since the caller is overriding default behaviour. if (e.Handled && Control.IsMouseCaptured) @@ -829,7 +829,7 @@ void HandleMouseDoubleClick(object sender, swi.MouseButtonEventArgs e) Callback.OnMouseDoubleClick(Widget, args); e.Handled = args.Handled; } - + protected virtual void HandleLostMouseCapture(object sender, swi.MouseEventArgs e) { if (isMouseCaptured) @@ -845,8 +845,8 @@ protected virtual void HandleLostMouseCapture(object sender, swi.MouseEventArgs protected virtual void HandleMouseDown(object sender, swi.MouseButtonEventArgs e) { - isMouseCaptured = false; - var args = e.ToEto(ContainerControl); + isMouseCaptured = false; + var args = e.ToEto(ContainerControl); if (!(Control is swc.Control) && e.ClickCount == 2) Callback.OnMouseDoubleClick(Widget, args); if (!args.Handled) @@ -884,7 +884,7 @@ public virtual void OnLoad(EventArgs e) SetDefaultScale(); } } - + protected virtual void SetDefaultScale() => SetScale(true, true); void Control_Loaded(object sender, sw.RoutedEventArgs e) @@ -973,7 +973,7 @@ public PointF PointFromScreen(PointF point) return point; point = point.LogicalToScreen(Widget.ParentWindow?.Screen); - + if (Win32.IsSystemDpiAware) { var logicalPixelSize = Win32.GetLogicalPixelSize(SwfScreen); @@ -1000,7 +1000,7 @@ public PointF PointToScreen(PointF point) { if (!ContainerControl.IsLoaded) return point; - + // ensure we're connected to a presentation source var presentationSource = sw.PresentationSource.FromVisual(ContainerControl) as HwndSource; if (presentationSource == null) @@ -1014,7 +1014,7 @@ public PointF PointToScreen(PointF point) point = point / systemDpi * logicalPixelSize; pt = Win32.ExecuteInDpiAwarenessContext(() => ContainerControl.PointToScreen(point.ToWpf())).ToEto(); - + // WPF does not take into account the location of the element in the form.. var rootVisual = ContainerControl.GetVisualParents().OfType().Last(); var location = ContainerControl.TranslatePoint(new sw.Point(0, 0), rootVisual).ToEto(); @@ -1073,7 +1073,7 @@ public void DoDragDrop(DataObject data, DragEffects allowedAction, Image image, WpfFrameworkElement.DragSourceControl = null; sw.DragSourceHelper.UnregisterDefaultDragSource(Control); - + var args = new DragEventArgs(Widget, data, allowedAction, PointFromScreen(Mouse.Position), Keyboard.Modifiers, Mouse.Buttons); args.Effects = effects.ToEto(); Callback.OnDragEnd(Widget, args); @@ -1164,5 +1164,13 @@ public void UpdateLayout() // update the layout ContainerControl.UpdateLayout(); } + + public bool IsMouseCaptured => ContainerControl.IsMouseCaptured; + public bool CaptureMouse() + { + WpfFrameworkElementHelper.ShouldCaptureMouse = false; + return ContainerControl.CaptureMouse(); + } + public void ReleaseMouseCapture() => ContainerControl.ReleaseMouseCapture(); } } diff --git a/src/Eto/Forms/Controls/Control.cs b/src/Eto/Forms/Controls/Control.cs index 004f76b6f4..b519141dd2 100755 --- a/src/Eto/Forms/Controls/Control.cs +++ b/src/Eto/Forms/Controls/Control.cs @@ -863,6 +863,31 @@ internal virtual void InternalEnsureLayout() { } + /// + /// Gets a value indicating this control currently has mouse capture + /// + /// + /// Mouse capture can happen during a handled MouseDown event until MouseUp, + /// or it can be captured explicitly via . + /// + public bool IsMouseCaptured => Handler.IsMouseCaptured; + + /// + /// Captures all mouse events to this control. + /// + /// + /// This captures all mouse events until is called. + /// + /// Note that not all platforms will allow a mouse capture unless the mouse is currently down. + /// + /// true if the mouse was captured, false otherwise. + public bool CaptureMouse() => Handler.CaptureMouse(); + + /// + /// Releases the mouse capture after a call to . + /// + public void ReleaseMouseCapture() => Handler.ReleaseMouseCapture(); + /// /// Gets or sets the width of the control size. /// @@ -2030,6 +2055,31 @@ public void OnEnabledChanged(Control widget, EventArgs e) /// This is useful when you need to know the dimensions of the control immediately. /// void UpdateLayout(); + + /// + /// Gets a value indicating this control currently has mouse capture + /// + /// + /// Mouse capture can happen during a handled MouseDown event until MouseUp, + /// or it can be captured explicitly via . + /// + bool IsMouseCaptured { get; } + + /// + /// Captures all mouse events to this control. + /// + /// + /// This captures all mouse events until is called. + /// + /// Note that not all platforms will allow a mouse capture unless the mouse is currently down. + /// + /// true if the mouse was captured, false otherwise. + bool CaptureMouse(); + + /// + /// Releases the mouse capture after a call to . + /// + void ReleaseMouseCapture(); } #endregion } \ No newline at end of file diff --git a/src/Eto/Forms/Controls/ThemedControlHandler.cs b/src/Eto/Forms/Controls/ThemedControlHandler.cs old mode 100644 new mode 100755 index 425a495756..61d373e809 --- a/src/Eto/Forms/Controls/ThemedControlHandler.cs +++ b/src/Eto/Forms/Controls/ThemedControlHandler.cs @@ -466,6 +466,13 @@ public override void AttachEvent(string id) /// public void UpdateLayout() => Control.UpdateLayout(); + /// + public bool IsMouseCaptured => Control.IsMouseCaptured; + /// + public bool CaptureMouse() => Control.CaptureMouse(); + /// + public void ReleaseMouseCapture() => Control.ReleaseMouseCapture(); + #endregion } \ No newline at end of file diff --git a/test/Eto.Test/UnitTests/Forms/Behaviors/MouseTests.cs b/test/Eto.Test/UnitTests/Forms/Behaviors/MouseTests.cs index 5a85f3357e..61d03fdfe7 100644 --- a/test/Eto.Test/UnitTests/Forms/Behaviors/MouseTests.cs +++ b/test/Eto.Test/UnitTests/Forms/Behaviors/MouseTests.cs @@ -103,10 +103,10 @@ StackLayout CreateClickableBox() child.BackgroundColor = child.BackgroundColor == Colors.Blue ? Colors.Green : Colors.Blue; e.Handled = true; }; - + return new StackLayout(parent); } - + var section1 = CreateClickableBox(); var section2 = CreateClickableBox(); @@ -119,5 +119,170 @@ StackLayout CreateClickableBox() Padding = 8 }; }); + + [Test, ManualTest] + public void MouseShouldBeCapturedByOtherControl() + { + bool wasCaptured = false; + bool isMouseCaptured = false; + bool parent1Entered = false; + bool parent2Entered = false; + bool parent2ShouldBeEntered = false; + bool parent1ShouldNotBeEntered = false; + bool panel1ShouldNeverBeCaptured = false; + bool panel2ShouldNeverHaveMouseMoveWithoutCapturing = false; + var points = new List(); + ManualForm("Click and drag on panel 1, events should go to panel 2.\nThen click and drag on panel 2, it should remain green", form => + { + var panel2 = new Drawable + { + CanFocus = true, + Size = new Size(200, 200), + BackgroundColor = Colors.Green, + Content = "Panel 2" + }; + + var panel1 = new Drawable + { + CanFocus = true, + Size = new Size(200, 200), + BackgroundColor = Colors.Blue, + Content = "Panel 1" + }; + panel1.MouseEnter += (sender, e) => Debug.WriteLine("Panel1.MouseEnter"); + panel1.MouseLeave += (sender, e) => Debug.WriteLine("Panel1.MouseLeave"); + panel1.MouseUp += (sender, e) => Debug.WriteLine("Panel1.MouseUp"); + panel1.MouseMove += (sender, e) => + { + if (panel1.IsMouseCaptured) + { + panel1.BackgroundColor = Colors.Red; + panel1ShouldNeverBeCaptured = true; + form.Close(); + } + Debug.WriteLine("Panel1.MouseMove"); + }; + panel1.MouseDown += (sender, e) => + { + Debug.WriteLine("Panel1.MouseDown"); + wasCaptured = panel2.CaptureMouse(); + isMouseCaptured = panel2.IsMouseCaptured; + if (!wasCaptured || !isMouseCaptured) + form.Close(); + e.Handled = true; + points = new List(); + }; + panel1.KeyDown += (sender, e) => + { + Debug.WriteLine("Panel1.KeyDown"); + if (panel2.IsMouseCaptured) + { + panel2.ReleaseMouseCapture(); + } + else + { + wasCaptured = panel2.CaptureMouse(); + isMouseCaptured = panel2.IsMouseCaptured; + if (!wasCaptured || !isMouseCaptured) + form.Close(); + points = new List(); + } + e.Handled = true; + }; + + panel2.MouseEnter += (sender, e) => Debug.WriteLine("Panel2.MouseEnter"); + panel2.MouseLeave += (sender, e) => Debug.WriteLine("Panel2.MouseLeave"); + panel2.MouseDown += (sender, e) => + { + Debug.WriteLine("Panel2.MouseDown"); + // wasCaptured = panel2.CaptureMouse(); + // isMouseCaptured = panel2.IsMouseCaptured; + points = new List + { + e.Location + }; + panel2.Invalidate(); + // if (!wasCaptured || !isMouseCaptured) + // form.Close(); + e.Handled = true; // should capture the mouse + }; + panel2.MouseUp += (sender, e) => + { + Debug.WriteLine("Panel2.MouseUp"); + panel2.ReleaseMouseCapture(); + }; + panel2.MouseMove += (sender, e) => + { + Debug.WriteLine("Panel2.MouseMove"); + if (panel2.IsMouseCaptured) + { + // note: WinForms does not bubble enter/leave events currently + if (!parent2Entered && !Platform.Instance.IsWinForms) + { + form.Close(); + return; + } + parent2ShouldBeEntered = true; + if (parent1Entered && !Platform.Instance.IsWinForms) + { + form.Close(); + return; + } + parent1ShouldNotBeEntered = true; + points.Add(e.Location); + panel2.Invalidate(); + } + else if (e.Buttons != MouseButtons.None) + { + panel2.BackgroundColor = Colors.Red; + panel2ShouldNeverHaveMouseMoveWithoutCapturing = true; + form.Close(); + } + }; + panel2.Paint += (sender, e) => + { + if (points.Count > 1) + e.Graphics.DrawLines(Colors.White, points); + }; + + + var parent1 = new Panel { Content = panel1 }; + parent1.MouseEnter += (sender, e) => + { + parent1Entered = true; + Debug.WriteLine($"parent1.MouseEnter"); + }; + parent1.MouseLeave += (sender, e) => + { + parent1Entered = false; + Debug.WriteLine($"parent1.MouseLeave"); + }; + + var parent2 = new Panel { Content = panel2 }; + parent2.MouseEnter += (sender, e) => + { + parent2Entered = true; + Debug.WriteLine($"parent2.MouseEnter"); + }; + parent2.MouseLeave += (sender, e) => + { + parent2Entered = false; + Debug.WriteLine($"parent2.MouseLeave"); + }; + + var layout = new TableLayout( + new TableRow(null, parent1, null, parent2, null) + ); + + form.Shown += (sender, e) => panel1.Focus(); + return layout; + }); + Assert.IsTrue(wasCaptured, "#1 - Mouse was not able to be captured"); + Assert.IsTrue(isMouseCaptured, "#2 - Control.IsMouseCaptured should be true after CaptureMouse() is successful"); + Assert.IsTrue(parent2ShouldBeEntered, "#3 - Parent2 should be entered when mouse is captured on Panel2"); + Assert.IsTrue(parent1ShouldNotBeEntered, "#4 - Parent1 should not be entered when mouse is captured on Panel2"); + Assert.IsFalse(panel1ShouldNeverBeCaptured, "#5 - Panel2 should never be captured"); + Assert.IsFalse(panel2ShouldNeverHaveMouseMoveWithoutCapturing, "#6 - Panel2 should not have a MouseMove with a button down without being captured"); + } } } \ No newline at end of file