Skip to content

Commit

Permalink
Merge pull request #2524 from cwensley/curtis/mac-gtk-mouseleave-when…
Browse files Browse the repository at this point in the history
…-unloaded

Mac/Gtk: Ensure MouseLeave is called when control is unloaded
  • Loading branch information
cwensley authored Jul 14, 2023
2 parents c217f19 + 2aaa043 commit 8d4f334
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 23 deletions.
40 changes: 38 additions & 2 deletions src/Eto.Gtk/Forms/GtkControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ static class GtkControl
public static readonly object TabIndex_Key = new object();
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 uint? DefaultBorderWidth;
}

Expand Down Expand Up @@ -141,6 +142,12 @@ protected virtual void SetSize(Size size)
ContainerControl.SetSizeRequest(size.Width, size.Height);
InvalidateMeasure();
}

internal bool DeferMouseLeave
{
get => Widget.Properties.Get<bool>(GtkControl.DeferMouseLeave_Key);
set => Widget.Properties.Set(GtkControl.DeferMouseLeave_Key, value);
}

public virtual bool Enabled
{
Expand All @@ -154,11 +161,13 @@ public virtual bool Enabled
}
set
{
DeferMouseLeave = true;
#if GTK3
ContainerControl.Sensitive = value;
#else
TriggerEnabled(Enabled, value, true);
#endif
DeferMouseLeave = false;
}
}

Expand Down Expand Up @@ -358,6 +367,7 @@ public virtual void OnLoadComplete(EventArgs e)

public virtual void OnUnLoad(EventArgs e)
{
Connector.TriggerMouseLeaveIfNeeded();
}

protected virtual void RealizedSetup()
Expand Down Expand Up @@ -407,6 +417,7 @@ public override void AttachEvent(string id)
case Eto.Forms.Control.MouseLeaveEvent:
EventControl.AddEvents((int)Gdk.EventMask.LeaveNotifyMask);
EventControl.LeaveNotifyEvent += Connector.HandleControlLeaveNotifyEvent;
HandleEvent(Eto.Forms.Control.MouseEnterEvent);
break;
case Eto.Forms.Control.MouseMoveEvent:
EventControl.AddEvents((int)Gdk.EventMask.ButtonMotionMask);
Expand Down Expand Up @@ -474,6 +485,8 @@ protected class GtkControlConnector : WeakConnector
DragEffects _dragEffects;
DataObject _dragData;
bool _isDrop;
bool _mouseEntered;

protected DragEventArgs DragArgs { get; private set; }

new GtkControl<TControl, TWidget, TCallback> Handler { get { return (GtkControl<TControl, TWidget, TCallback>)base.Handler; } }
Expand Down Expand Up @@ -527,7 +540,30 @@ public void HandleControlLeaveNotifyEvent(object o, Gtk.LeaveNotifyEventArgs arg
var p = new PointF((float)args.Event.X, (float)args.Event.Y);
Keys modifiers = args.Event.State.ToEtoKey();
MouseButtons buttons = MouseButtons.None;

_mouseEntered = false;
if (handler.DeferMouseLeave)
{
Application.Instance.AsyncInvoke(() =>
{
if (!handler.Widget.IsDisposed)
handler.Callback.OnMouseLeave(handler.Widget, new MouseEventArgs(buttons, modifiers, p));
});
}
else
{
handler.Callback.OnMouseLeave(handler.Widget, new MouseEventArgs(buttons, modifiers, p));
}
}

public void TriggerMouseLeaveIfNeeded()
{
var handler = Handler;
if (handler == null || !_mouseEntered)
return;
_mouseEntered = false;
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));
}

Expand All @@ -544,7 +580,7 @@ public void HandleControlEnterNotifyEvent(object o, Gtk.EnterNotifyEventArgs arg
var p = new PointF((float)args.Event.X, (float)args.Event.Y);
Keys modifiers = args.Event.State.ToEtoKey();
MouseButtons buttons = MouseButtons.None;

_mouseEntered = true;
handler.Callback.OnMouseEnter(handler.Widget, new MouseEventArgs(buttons, modifiers, p));
}

Expand Down
24 changes: 16 additions & 8 deletions src/Eto.Mac/Forms/MacView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public void MouseExited(NSEvent theEvent)
{
var h = Handler;
if (h == null || !h.Enabled) return;
h.Callback.OnMouseLeave(h.Widget, MacConversions.GetMouseEvent(h, theEvent, false));
entered = false;
h.Callback.OnMouseLeave(h.Widget, MacConversions.GetMouseEvent(h, theEvent, false));
}

[Export("scrollWheel:")]
Expand All @@ -53,19 +53,26 @@ public void ScrollWheel(NSEvent theEvent)
h.Callback.OnMouseWheel(h.Widget, MacConversions.GetMouseEvent(h, theEvent, true));
}

public void FireMouseLeaveIfNeeded()
public void FireMouseLeaveIfNeeded(bool async)
{
var h = Handler;
if (h == null || h.Enabled || !entered) return;
if (h == null || !entered) return;
entered = false;
Application.Instance.AsyncInvoke(() =>
if (async)
{
if (!h.Widget.IsDisposed)
Application.Instance.AsyncInvoke(() =>
{
if (h.Widget.IsDisposed)
return;
var theEvent = NSApplication.SharedApplication.CurrentEvent;
h.Callback.OnMouseLeave(h.Widget, MacConversions.GetMouseEvent(h, theEvent, false));
}
});
});
}
else
{
var theEvent = NSApplication.SharedApplication.CurrentEvent;
h.Callback.OnMouseLeave(h.Widget, MacConversions.GetMouseEvent(h, theEvent, false));
}
}

}
Expand Down Expand Up @@ -1059,7 +1066,7 @@ void SetEnabled(bool parentEnabled, bool? newValue)
Callback.OnEnabledChanged(Widget, EventArgs.Empty);

if (!newEnabled)
mouseDelegate?.FireMouseLeaveIfNeeded();
mouseDelegate?.FireMouseLeaveIfNeeded(true);
}
}

Expand Down Expand Up @@ -1161,6 +1168,7 @@ public virtual void OnLoadComplete(EventArgs e)

public virtual void OnUnLoad(EventArgs e)
{
mouseDelegate?.FireMouseLeaveIfNeeded(false);
}

public virtual void OnKeyDown(KeyEventArgs e) => Callback.OnKeyDown(Widget, e);
Expand Down
90 changes: 77 additions & 13 deletions test/Eto.Test/UnitTests/Forms/Controls/ControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,40 +453,104 @@ public void ControlsShouldNotGetMouseOrFocusEventsWhenParentDisabled(IControlTyp
[TestCaseSource(nameof(GetControlTypes))]
public void ControlShouldFireMouseLeaveIfEnteredThenDisabled(IControlTypeInfo<Control> info)
{
bool mouseLeaveCalled = false;
bool mouseEnterCalled = false;
int mouseLeaveCalled = 0;
int mouseEnterCalled = 0;
bool mouseLeaveCalledBeforeMouseDown = false;
bool mouseLeaveCalledAfterDisabled = false;
bool mouseDownCalled = false;
int mouseDownCalled = 0;
bool formClosing = false;
bool mouseLeaveCalledAfterFormClosed = false;
int enabledChanged = 0;
bool enabledChangedFiredAfterMouseLeave = false;
ManualForm("Click on the control", form =>
{
form.Closing += (sender, e) =>
{
formClosing = true;
};

var control = info.CreatePopulatedControl();
control.MouseEnter += (sender, e) =>
{
mouseEnterCalled = true;
mouseEnterCalled++;
};
control.MouseLeave += (sender, e) =>
{
mouseLeaveCalled = true;
if (mouseDownCalled)
form.Close();
mouseLeaveCalled++;
mouseLeaveCalledAfterFormClosed |= formClosing;
if (mouseDownCalled > 0)
Application.Instance.AsyncInvoke(form.Close);
};
control.MouseDown += (sender, e) =>
{
mouseDownCalled = true;
mouseLeaveCalledBeforeMouseDown = mouseLeaveCalled;
mouseDownCalled++;
mouseLeaveCalledBeforeMouseDown = mouseLeaveCalled > 0;
control.Enabled = false;
mouseLeaveCalledAfterDisabled = mouseLeaveCalled;
mouseLeaveCalledAfterDisabled = mouseLeaveCalled > 0;
e.Handled = true;
};
control.EnabledChanged += (sender, e) =>
{
enabledChanged++;
enabledChangedFiredAfterMouseLeave = mouseLeaveCalled > 0;

};
return control;
});
Assert.IsTrue(mouseEnterCalled, "#1.1 - MouseEnter did not get called");
Assert.IsTrue(mouseLeaveCalled, "#1.2 - MouseLeave did not get called");
Assert.AreEqual(1, mouseEnterCalled, "#1.1 - MouseEnter should be called exactly once");
Assert.AreEqual(1, mouseLeaveCalled, "#1.2 - MouseLeave should be called exactly once");
Assert.IsFalse(mouseLeaveCalledBeforeMouseDown, "#1.3 - MouseLeave should not have been called before MouseDown");
Assert.IsFalse(mouseLeaveCalledAfterDisabled, "#1.4 - MouseLeave should not be called during Enabled=false, but sometime after the MouseDown completes");
Assert.IsTrue(mouseDownCalled, "#1.5 - MouseDown didn't get called. Did you click the control?");
Assert.AreEqual(1, mouseDownCalled, "#1.5 - MouseDown should get called exactly once. Did you click the control?");
Assert.IsFalse(mouseLeaveCalledAfterFormClosed, "#1.6 - MouseLeave should be called immediately when clicked, not when the form is closed");
Assert.AreEqual(1, enabledChanged, "#1.7 - EnabledChanged should be called exactly once");
Assert.IsFalse(enabledChangedFiredAfterMouseLeave, "#1.8 - MouseLeave should be fired after EnabledChanged event");
}

[ManualTest]
[TestCaseSource(nameof(GetControlTypes))]
public void ControlShouldFireMouseLeaveWhenUnloaded(IControlTypeInfo<Control> info)
{
int mouseLeaveCalled = 0;
int mouseEnterCalled = 0;
bool mouseLeaveCalledBeforeMouseDown = false;
int mouseDownCalled = 0;
bool formClosing = false;
bool mouseLeaveCalledAfterFormClosed = false;
ManualForm("Click on the control", form =>
{
form.Closing += (sender, e) =>
{
formClosing = true;
};

var control = info.CreatePopulatedControl();
control.MouseEnter += (sender, e) =>
{
mouseEnterCalled++;
};
control.MouseLeave += (sender, e) =>
{
mouseLeaveCalled++;
mouseLeaveCalledAfterFormClosed |= formClosing;
if (mouseDownCalled > 0)
Application.Instance.AsyncInvoke(form.Close);
};
control.MouseDown += (sender, e) =>
{
mouseDownCalled++;
mouseLeaveCalledBeforeMouseDown = mouseLeaveCalled > 0;
e.Handled = true;

control.VisualParent.Remove(control);
};
return control;
});
Assert.AreEqual(1, mouseEnterCalled, "#1.1 - MouseEnter should be called exactly once");
Assert.AreEqual(1, mouseLeaveCalled, "#1.2 - MouseLeave should be called exactly once");
Assert.IsFalse(mouseLeaveCalledBeforeMouseDown, "#1.3 - MouseLeave should not have been called before MouseDown");
Assert.AreEqual(1, mouseDownCalled, "#1.5 - MouseDown should get called exactly once. Did you click the control?");
Assert.IsFalse(mouseLeaveCalledAfterFormClosed, "#1.6 - MouseLeave should be called immediately when clicked, not when the form is closed");
}
}
}

0 comments on commit 8d4f334

Please sign in to comment.