Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #3678. ContextMenu accesses disposed MenuBar. #3681

Merged
merged 23 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1f4ea74
Fixes #3678. ContextMenu accesses disposed MenuBar.
BDisp Aug 21, 2024
aae9074
Preserve always the last menu opened.
BDisp Aug 22, 2024
2d44258
Re-added MenuAllClosed event.
BDisp Aug 22, 2024
168cb2a
Ensures call CleanUp if Application.MouseGrabView hold another MenuBa…
BDisp Aug 22, 2024
4ece78d
Fix unit tests.
BDisp Aug 22, 2024
49d3342
Passing the offending object instead of the View type.
BDisp Aug 22, 2024
d46d9ed
Add that to the dictionary to shut respeller up.
dodexahedron Aug 23, 2024
8552218
Address a few warnings
dodexahedron Aug 23, 2024
e60c37a
A question
dodexahedron Aug 23, 2024
bb083a8
Just a little more cleanup
dodexahedron Aug 23, 2024
d78d9fd
Combine these and comment
dodexahedron Aug 23, 2024
b9b46b9
Slight additional cleanup
dodexahedron Aug 23, 2024
73b6c51
More minor cleanup
dodexahedron Aug 23, 2024
8e3af56
Nullable != true ===> is not true
dodexahedron Aug 23, 2024
1cbcd53
Unconditional break at the top is just a part of the while statement …
dodexahedron Aug 23, 2024
60d03c0
Merge pull request #193 from dodexahedron/v2_3678_review_pass_1
BDisp Aug 23, 2024
a1a486f
Merge branch 'v2_develop' into v2_3678_contextmenu-disposed-menubar-fix
BDisp Aug 23, 2024
df2cf35
Fixes #3687. ColorPicker isn't respecting the current UI culture.
BDisp Aug 23, 2024
b12b346
Merge branch 'v2_3687_colorpicker-current-ui-culture-fix' into v2_367…
BDisp Aug 23, 2024
fc3164f
Fix @dodexahedron erroneous code.
BDisp Aug 23, 2024
8a3fb2f
Cleanup comments.
BDisp Aug 23, 2024
ad9e3c6
Merge branch 'v2_develop' into v2_3678_contextmenu-disposed-menubar-fix
tig Aug 23, 2024
2941c2d
Merge branch 'v2_develop' into v2_3678_contextmenu-disposed-menubar-fix
tig Aug 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 39 additions & 43 deletions Terminal.Gui/Application/Application.Mouse.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
#nullable enable
namespace Terminal.Gui;

public static partial class Application // Mouse handling
{
#region Mouse handling
Expand Down Expand Up @@ -36,16 +37,13 @@ public static partial class Application // Mouse handling
/// <param name="view">View that will receive all mouse events until <see cref="UngrabMouse"/> is invoked.</param>
public static void GrabMouse (View? view)
{
if (view is null)
if (view is null || OnGrabbingMouse (view))
{
return;
}

if (!OnGrabbingMouse (view))
{
OnGrabbedMouse (view);
MouseGrabView = view;
}
OnGrabbedMouse (view);
MouseGrabView = view;
}

/// <summary>Releases the mouse grab, so mouse events will be routed to the view on which the mouse is.</summary>
Expand All @@ -56,6 +54,10 @@ public static void UngrabMouse ()
return;
}

#if DEBUG_IDISPOSABLE
ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
BDisp marked this conversation as resolved.
Show resolved Hide resolved
#endif

if (!OnUnGrabbingMouse (MouseGrabView))
{
View view = MouseGrabView;
Expand All @@ -64,6 +66,7 @@ public static void UngrabMouse ()
}
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static bool OnGrabbingMouse (View? view)
{
if (view is null)
Expand All @@ -77,6 +80,7 @@ private static bool OnGrabbingMouse (View? view)
return evArgs.Cancel;
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static bool OnUnGrabbingMouse (View? view)
{
if (view is null)
Expand All @@ -90,6 +94,7 @@ private static bool OnUnGrabbingMouse (View? view)
return evArgs.Cancel;
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static void OnGrabbedMouse (View? view)
{
if (view is null)
Expand All @@ -100,6 +105,7 @@ private static void OnGrabbedMouse (View? view)
GrabbedMouse?.Invoke (view, new (view));
}

/// <exception cref="Exception">A delegate callback throws an exception.</exception>
private static void OnUnGrabbedMouse (View? view)
{
if (view is null)
Expand All @@ -110,8 +116,6 @@ private static void OnUnGrabbedMouse (View? view)
UnGrabbedMouse?.Invoke (view, new (view));
}

#nullable enable

// Used by OnMouseEvent to track the last view that was clicked on.
internal static View? MouseEnteredView { get; set; }

Expand Down Expand Up @@ -166,51 +170,49 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)
if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false)
{
// The mouse has moved outside the bounds of the view that grabbed the mouse
MouseGrabView?.NewMouseLeaveEvent (mouseEvent);
MouseGrabView.NewMouseLeaveEvent (mouseEvent);
}

//System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");
if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true)
if (MouseGrabView.NewMouseEvent (viewRelativeMouseEvent) is true)
{
return;
}
}

if (view is { WantContinuousButtonPressed: true })
// We can combine this into the switch expression to reduce cognitive complexity even more and likely
// avoid one or two of these checks in the process, as well.
WantContinuousButtonPressedView = view switch
{
{ WantContinuousButtonPressed: true } => view,
_ => null
};

if (view is not Adornment
&& (view is null || view == ApplicationOverlapped.OverlappedTop)
&& Current is { Modal: false }
&& ApplicationOverlapped.OverlappedTop != null
&& mouseEvent.Flags is not MouseFlags.ReportMousePosition and not 0)
{
WantContinuousButtonPressedView = view;
}
else
{
WantContinuousButtonPressedView = null;
}
// This occurs when there are multiple overlapped "tops"
// E.g. "Mdi" - in the Background Worker Scenario
View? top = ApplicationOverlapped.FindDeepestTop (Top!, mouseEvent.Position);
view = View.FindDeepestView (top, mouseEvent.Position);

if (view is not Adornment)
{
if ((view is null || view == ApplicationOverlapped.OverlappedTop)
&& Current is { Modal: false }
&& ApplicationOverlapped.OverlappedTop != null
&& mouseEvent.Flags != MouseFlags.ReportMousePosition
&& mouseEvent.Flags != 0)
if (view is { } && view != ApplicationOverlapped.OverlappedTop && top != Current && top is { })
{
// This occurs when there are multiple overlapped "tops"
// E.g. "Mdi" - in the Background Worker Scenario
View? top = ApplicationOverlapped.FindDeepestTop (Top!, mouseEvent.Position);
view = View.FindDeepestView (top, mouseEvent.Position);

if (view is { } && view != ApplicationOverlapped.OverlappedTop && top != Current && top is { })
{
ApplicationOverlapped.MoveCurrent ((Toplevel)top);
}
ApplicationOverlapped.MoveCurrent ((Toplevel)top);
}
}

// May be null before the prior condition or the condition may set it as null.
// So, the checking must be outside the prior condition.
if (view is null)
{
return;
}

MouseEvent? me = null;
MouseEvent? me;

if (view is Adornment adornment)
{
Expand All @@ -236,8 +238,7 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)
View = view
};
}

if (me is null)
else
{
return;
}
Expand All @@ -263,13 +264,8 @@ internal static void OnMouseEvent (MouseEvent mouseEvent)

//Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}");

while (view.NewMouseEvent (me) != true)
while (view.NewMouseEvent (me) is not true && MouseGrabView is not { })
{
if (MouseGrabView is { })
{
break;
}

if (view is Adornment adornmentView)
{
view = adornmentView.Parent!.SuperView;
Expand Down
6 changes: 3 additions & 3 deletions Terminal.Gui/Drawing/ColorStrings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static class ColorStrings
public static string? GetW3CColorName (Color color)
{
// Fetch the color name from the resource file
return _resourceManager.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentCulture);
return _resourceManager.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentUICulture);
}

/// <summary>
Expand All @@ -30,7 +30,7 @@ public static class ColorStrings
/// <returns></returns>
public static IEnumerable<string> GetW3CColorNames ()
{
foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentCulture, true, true)!)
foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentUICulture, true, true)!)
{
string keyName = entry.Key.ToString () ?? string.Empty;

Expand All @@ -50,7 +50,7 @@ public static IEnumerable<string> GetW3CColorNames ()
public static bool TryParseW3CColorName (string name, out Color color)
{
// Iterate through all resource entries to find the matching color name
foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentCulture, true, true)!)
foreach (DictionaryEntry entry in _resourceManager.GetResourceSet (CultureInfo.CurrentUICulture, true, true)!)
{
if (entry.Value is string colorName && colorName.Equals (name, StringComparison.OrdinalIgnoreCase))
{
Expand Down
25 changes: 14 additions & 11 deletions Terminal.Gui/Views/Menu/ContextMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,26 +105,25 @@ public MouseFlags MouseFlags
/// <summary>Disposes the context menu object.</summary>
public void Dispose ()
{
if (IsShow)
{
_menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
_menuBar.Dispose ();
_menuBar = null;
IsShow = false;
}
_menuBar.MenuAllClosed -= MenuBar_MenuAllClosed;
Application.UngrabMouse ();
_menuBar?.Dispose ();
_menuBar = null;
IsShow = false;

if (_container is { })
{
_container.Closing -= Container_Closing;
_container.Deactivate -= Container_Deactivate;
_container.Disposing -= Container_Disposing;
}
}

/// <summary>Hides (closes) the ContextMenu.</summary>
public void Hide ()
{
_menuBar?.CleanUp ();
Dispose ();
IsShow = false;
}

/// <summary>Event invoked when the <see cref="ContextMenu.Key"/> is changed.</summary>
Expand All @@ -139,11 +138,13 @@ public void Show ()
if (_menuBar is { })
{
Hide ();
Dispose ();
}

_container = Application.Current;
_container.Closing += Container_Closing;
_container!.Closing += Container_Closing;
_container.Deactivate += Container_Deactivate;
_container.Disposing += Container_Disposing;
Rectangle frame = Application.Screen;
Point position = Position;

Expand Down Expand Up @@ -219,7 +220,9 @@ public void Show ()
_menuBar.OpenMenu ();
}

private void Container_Deactivate (object sender, ToplevelEventArgs e) { Hide (); }
private void Container_Closing (object sender, ToplevelClosingEventArgs obj) { Hide (); }
private void MenuBar_MenuAllClosed (object sender, EventArgs e) { Dispose (); }
private void Container_Deactivate (object sender, ToplevelEventArgs e) { Hide (); }
private void Container_Disposing (object sender, EventArgs e) { Dispose (); }

private void MenuBar_MenuAllClosed (object sender, EventArgs e) { Hide (); }
}
10 changes: 10 additions & 0 deletions Terminal.Gui/Views/Menu/MenuBar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,16 @@ internal void CleanUp ()
}

SetNeedsDisplay ();

if (Application.MouseGrabView is { } && Application.MouseGrabView is MenuBar && Application.MouseGrabView != this)
{
var menuBar = Application.MouseGrabView as MenuBar;

if (menuBar!.IsMenuOpen)
{
menuBar.CleanUp ();
}
}
Application.UngrabMouse ();
_isCleaning = false;
}
Expand Down
1 change: 1 addition & 0 deletions Terminal.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=langword/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Roslynator/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevel/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Ungrab/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unsynchronized/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=BUGBUG/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>
17 changes: 15 additions & 2 deletions UICatalog/Scenarios/ContextMenus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,22 @@ public override void Main ()
Application.MouseEvent -= ApplicationMouseEvent;
};

var menu = new MenuBar
{
Menus =
[
new (
"File",
new MenuItem [] { new ("Quit", "", () => Application.RequestStop (), null, null, Application.QuitKey) })
]
};

var top = new Toplevel ();
top.Add (appWindow, menu);

// Run - Start the application.
Application.Run (appWindow);
appWindow.Dispose ();
Application.Run (top);
top.Dispose ();

// Shutdown - Calling Application.Shutdown is required.
Application.Shutdown ();
Expand Down
9 changes: 6 additions & 3 deletions UnitTests/Views/ContextMenuTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,8 @@ public void Key_Open_And_Close_The_ContextMenu ()
Assert.True (Application.OnKeyDown (ContextMenu.DefaultKey));
Assert.True (tf.ContextMenu.MenuBar.IsMenuOpen);
Assert.True (Application.OnKeyDown (ContextMenu.DefaultKey));
Assert.Null (tf.ContextMenu.MenuBar);
// The last context menu bar opened is always preserved
Assert.NotNull (tf.ContextMenu.MenuBar);
top.Dispose ();
}

Expand Down Expand Up @@ -1390,7 +1391,8 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus ()
Assert.True (tf1.HasFocus);
Assert.False (tf2.HasFocus);
Assert.Equal (2, win.Subviews.Count);
Assert.Null (tf2.ContextMenu.MenuBar);
// The last context menu bar opened is always preserved
Assert.NotNull (tf2.ContextMenu.MenuBar);
Assert.Equal (win.Focused, tf1);
Assert.Null (Application.MouseGrabView);
Assert.Equal (tf1, Application.MouseEnteredView);
Expand All @@ -1400,7 +1402,8 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus ()
Assert.False (tf1.HasFocus);
Assert.True (tf2.HasFocus);
Assert.Equal (2, win.Subviews.Count);
Assert.Null (tf2.ContextMenu.MenuBar);
// The last context menu bar opened is always preserved
Assert.NotNull (tf2.ContextMenu.MenuBar);
Assert.Equal (win.Focused, tf2);
Assert.Null (Application.MouseGrabView);
Assert.Equal (tf2, Application.MouseEnteredView);
Expand Down
Loading