Skip to content

Commit 8d37e77

Browse files
author
tznind
committed
Fix Enter on MenuBar context menu crash
Fix hit detection of what top level MenuBar item was right clicked Serialize Separator as Line differently
1 parent f66a9df commit 8d37e77

8 files changed

Lines changed: 59 additions & 82 deletions

File tree

src/Design.cs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -278,16 +278,7 @@ public IEnumerable<Property> GetDesignableProperties()
278278
return this.designableProperties;
279279
}
280280

281-
/// <summary>
282-
/// Returns all operations not to do with setting properties. Often these
283-
/// are view specific e.g. add/remove column from a <see cref="TableView"/>.
284-
/// </summary>
285-
/// <returns>All view specific <see cref="Operation"/> supported on <see cref="View"/> Type.
286-
/// Does not return regular <see cref="Property"/> changing operations.</returns>
287-
public IEnumerable<IOperation> GetExtraOperations()
288-
{
289-
return this.GetExtraOperations(Point.Empty);
290-
}
281+
291282

292283
/// <summary>
293284
/// Returns all <see cref="Operation"/> that can be performed on the view at position <paramref name="pos"/>
@@ -297,8 +288,10 @@ public IEnumerable<IOperation> GetExtraOperations()
297288
/// may inform what operations are returned (e.g. right clicking a specific table view column). Otherwise
298289
/// <see cref="Point.Empty"/>.</param>
299290
/// <returns>All view specific <see cref="IOperation"/> that are supported at the <paramref name="pos"/>.</returns>
300-
public IEnumerable<IOperation> GetExtraOperations(Point pos)
291+
public IEnumerable<IOperation> GetExtraOperations(Mouse? mouse = null)
301292
{
293+
var pos = mouse == null ? Point.Empty: View.ScreenToViewport(mouse.Position.Value);
294+
302295
// Extra TableView operations
303296
if (this.View is TableView tv)
304297
{
@@ -358,7 +351,7 @@ public IEnumerable<IOperation> GetExtraOperations(Point pos)
358351
{
359352
yield return new AddMenuOperation(App, this, null);
360353

361-
var menu = pos.IsEmpty ? mb.GetSelectedMenuItem() : mb.ScreenToMenuBarItem(pos.X);
354+
var menu = mouse == null ? mb.GetSelectedMenuItem() : mb.ScreenToMenuBarItem(App, mouse);
362355

363356
if (menu != null)
364357
{

src/MenuBarExtensions.cs

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Terminal.Gui;
2-
using Terminal.Gui.Text;
2+
using Terminal.Gui.App;
3+
using Terminal.Gui.Input;
34
using Terminal.Gui.Views;
4-
using TerminalGuiDesigner.Operations.MenuOperations;
55

66
namespace TerminalGuiDesigner;
77

@@ -10,6 +10,8 @@ namespace TerminalGuiDesigner;
1010
/// </summary>
1111
public static class MenuBarExtensions
1212
{
13+
/// <summary>The title used to mark a <see cref="MenuItem"/> as a separator in the designer.</summary>
14+
public const string SeparatorTitle = "---";
1315
/// <summary>
1416
/// Gets the top level selected <see cref="MenuBarItem"/> in the <paramref name="menuBar"/>
1517
/// or null if it is not open/no selection is set. Note that this is the top level menu item only
@@ -19,20 +21,16 @@ public static class MenuBarExtensions
1921
/// <returns>Selected <see cref="MenuItem"/> or null if none.</returns>
2022
public static MenuBarItem? GetSelectedMenuItem(this MenuBar menuBar)
2123
{
22-
int selected = menuBar.GetNonNullNonPublicFieldValue<int, MenuBar>( "selected" );
24+
if (menuBar.Focused is MenuBarItem mbi)
25+
return mbi;
2326

24-
if (selected < 0 || selected >= menuBar.SubViews.OfType<MenuBarItem>().Count())
25-
{
26-
return null;
27-
}
28-
29-
return menuBar.SubViews.OfType<MenuBarItem>().ElementAt(selected);
27+
return menuBar.SubViews.OfType<MenuBarItem>().FirstOrDefault();
3028
}
3129

3230
/// <summary>
3331
/// Walks all menus in <paramref name="menuBar"/> and replaces any <see cref="Line"/> views
3432
/// (produced by code generation for separators) with sentinel <see cref="MenuItem"/> instances
35-
/// whose <see cref="MenuItem.Title"/> is <see cref="ConvertMenuItemToSeperatorOperation.SeparatorTitle"/>.
33+
/// whose <see cref="MenuItem.Title"/> is <see cref="SeparatorTitle"/>.
3634
/// Call this after loading a <see cref="MenuBar"/> from generated code.
3735
/// </summary>
3836
public static void ConvertLineSeparatorsToSentinels(this MenuBar menuBar)
@@ -60,7 +58,7 @@ private static void ConvertLineSeparatorsInMenu(Menu menu)
6058
foreach (var v in allViews)
6159
{
6260
menu.Add(v is Line
63-
? new MenuItem { Title = ConvertMenuItemToSeperatorOperation.SeparatorTitle }
61+
? new MenuItem { Title = SeparatorTitle }
6462
: v);
6563
}
6664
}
@@ -85,48 +83,25 @@ private static void ConvertLineSeparatorsInMenu(Menu menu)
8583
/// <param name="menuBar"><see cref="MenuBar"/> you want to find the clicked <see cref="MenuBarItem"/> (top level menu) for.</param>
8684
/// <param name="screenX">Screen coordinate of the click in X.</param>
8785
/// <returns>The <see cref="MenuBarItem"/> under the mouse at this position or null (only considers X).</returns>
88-
public static MenuBarItem? ScreenToMenuBarItem(this MenuBar menuBar, int screenX)
86+
public static MenuBarItem? ScreenToMenuBarItem(this MenuBar menuBar, IApplication application, Mouse mouse)
8987
{
90-
// These might be changed in Terminal.Gui library
91-
// TODO: Maybe load these from a config file, so we aren't at TG's mercy
92-
const int initialWhitespace = 1;
93-
const int afterEachItemWhitespace = 2;
88+
var hit = menuBar.HitTest(application,mouse, out _, out _);
9489

95-
if (menuBar.SubViews.OfType<MenuBarItem>().Count() == 0)
90+
if(hit == null)
9691
{
9792
return null;
9893
}
9994

100-
var clientPoint = menuBar.ScreenToViewport(new Point(screenX, 0));
101-
102-
// if click is not in our client area
103-
if (clientPoint.X < initialWhitespace)
95+
if (hit is MenuBarItem mbi)
10496
{
105-
return null;
97+
return mbi;
10698
}
107-
108-
// Calculate the x display positions of each menu
109-
int distance = initialWhitespace;
110-
Dictionary<int, MenuBarItem?> menuXLocations = new();
111-
112-
foreach (var mb in menuBar.SubViews.OfType<MenuBarItem>())
113-
{
114-
menuXLocations.Add(distance, mb);
115-
distance += mb.Title.GetColumns() + afterEachItemWhitespace;
116-
}
117-
118-
// anything after this is not a click on a menu
119-
menuXLocations.Add(distance, null);
120-
121-
// LastOrDefault does not work with Dictionaries, if we somehow still have a point outside bounds
122-
// of anything then just return null;
123-
if (!menuXLocations.Any(m => m.Key <= clientPoint.X))
99+
if (hit.SuperView is MenuBarItem super)
124100
{
125-
return null;
101+
return super;
126102
}
127103

128-
// Return the last menu item that begins rendering before this X point
129-
return menuXLocations.Last(m => m.Key <= clientPoint.X).Value;
104+
return null;
130105
}
131106

132107
/// <summary>

src/MenuTracker.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,10 @@ private void ConvertEmptySubMenus(MenuItem menuItem)
277277
return potentialParent;
278278
}
279279

280-
// Recursively check each child's submenu
280+
// Recursively check each child's submenu (SubMenu for MenuItem, PopoverMenu for MenuBarItem)
281281
foreach (var child in children)
282282
{
283-
if (child.SubMenu != null)
283+
if (child is MenuBarItem || child.SubMenu != null)
284284
{
285285
var result = FindParentRecursive(item, child);
286286
if (result != null)

src/Operations/MenuOperations/ConvertMenuItemToSeperatorOperation.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@ namespace TerminalGuiDesigner.Operations.MenuOperations;
88
/// <para>
99
/// Converts a <see cref="MenuItem"/> into a Separator (horizontal line in menu).
1010
/// In the designer the separator is stored as a <see cref="MenuItem"/> with
11-
/// <see cref="SeparatorTitle"/> as its <see cref="MenuItem.Title"/>.
11+
/// <see cref="MenuBarExtensions.SeparatorTitle"/> as its <see cref="MenuItem.Title"/>.
1212
/// When code is generated it becomes a <c>new Line { Orientation = Orientation.Horizontal }</c>
1313
/// and on load those Line views are converted back to sentinel MenuItems.
1414
/// </para>
1515
/// </summary>
1616
public class ConvertMenuItemToSeperatorOperation : MenuItemOperation
1717
{
18-
/// <summary>The title used to mark a MenuItem as a separator in the designer.</summary>
19-
public const string SeparatorTitle = "---";
20-
2118
private string? originalTitle;
2219

2320
/// <summary>
@@ -28,7 +25,7 @@ public class ConvertMenuItemToSeperatorOperation : MenuItemOperation
2825
public ConvertMenuItemToSeperatorOperation(IApplication app, MenuItem toConvert)
2926
: base(app, toConvert)
3027
{
31-
if (toConvert.Title?.ToString() == SeparatorTitle)
28+
if (toConvert.Title?.ToString() == MenuBarExtensions.SeparatorTitle)
3229
{
3330
IsImpossible = true;
3431
}
@@ -61,7 +58,7 @@ protected override bool DoImpl()
6158
}
6259

6360
this.originalTitle = this.OperateOn.Title?.ToString();
64-
this.OperateOn.Title = SeparatorTitle;
61+
this.OperateOn.Title = MenuBarExtensions.SeparatorTitle;
6562
this.Bar?.SetNeedsDraw();
6663
return true;
6764
}

src/Operations/MenuOperations/MoveMenuItemRightOperation.cs

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,39 +73,51 @@ protected override bool DoImpl()
7373
return false;
7474
}
7575

76-
// When user hits shift right
7776
var children = this.Parent.GetMenuItems(out _);
7877
var currentItemIdx = children.IndexOf(this.OperateOn);
7978
var aboveIdx = currentItemIdx - 1;
8079

81-
// and there is an item above
8280
if (aboveIdx < 0)
8381
{
8482
return false;
8583
}
8684

87-
// Get or create a submenu on the item above
8885
var itemAbove = children[aboveIdx];
89-
if (itemAbove.SubMenu == null)
90-
{
91-
itemAbove.SubMenu = new Menu();
92-
}
9386

94-
// Remove us from current menu
87+
// Remove us from current menu first
9588
this.Parent.RemoveMenuItem(this.OperateOn);
9689

97-
// Add us to the submenu of the item above
98-
if (this.InsertionIndex != null)
90+
if (itemAbove is MenuBarItem existingMbi)
9991
{
100-
itemAbove.InsertMenuItem(this.InsertionIndex.Value, this.OperateOn);
92+
// Item above already has a sub-menu — add to it
93+
if (this.InsertionIndex != null)
94+
{
95+
existingMbi.InsertMenuItem(this.InsertionIndex.Value, this.OperateOn);
96+
}
97+
else
98+
{
99+
var subItems = existingMbi.GetMenuItems(out _);
100+
subItems.Add(this.OperateOn);
101+
existingMbi.SetMenuItems(subItems);
102+
}
101103
}
102104
else
103105
{
104-
itemAbove.SubMenu.Add(this.OperateOn);
106+
// Convert plain MenuItem to MenuBarItem with OperateOn as first child.
107+
// Re-read children since RemoveMenuItem may have shifted indexes.
108+
children = this.Parent.GetMenuItems(out _);
109+
var newAboveIdx = children.IndexOf(itemAbove);
110+
111+
var newMbi = new MenuBarItem(itemAbove.Title, new MenuItem[] { this.OperateOn });
112+
newMbi.Data = itemAbove.Data;
113+
newMbi.Key = itemAbove.Key;
114+
115+
children.RemoveAt(newAboveIdx);
116+
children.Insert(newAboveIdx, newMbi);
117+
this.Parent.SetMenuItems(children);
105118
}
106119

107120
this.Bar?.SetNeedsDraw();
108-
109121
return true;
110122
}
111123

src/Operations/OperationFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ private IEnumerable<IOperation> CreateOperations(Mouse? m, Design d)
109109
{
110110
var ops = m == null || !m.Position.HasValue?
111111
d.GetExtraOperations() :
112-
d.GetExtraOperations(d.View.ScreenToViewport(m.Position.Value));
112+
d.GetExtraOperations(m);
113113

114114
foreach (var extra in ops.Where(c => !c.IsImpossible))
115115
{

src/ToCode/MenuBarItemsToCode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ private void ToCode(CodeDomArgs args, MenuItem child, out string fieldName)
8282
foreach (var mi in child.GetMenuItems(out wasPopover))
8383
{
8484
// Separators are stored as MenuItem("---") in the designer but emitted as Line in code
85-
if (mi.Title?.ToString() == Operations.MenuOperations.ConvertMenuItemToSeperatorOperation.SeparatorTitle)
85+
if (mi.Title?.ToString() == MenuBarExtensions.SeparatorTitle)
8686
{
8787
children.Add("new Line { Orientation = Terminal.Gui.ViewBase.Orientation.Horizontal }");
8888
continue;

tests/Operations/ConvertMenuItemToSeperatorOperationTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public void TestConvertToSeperator_RoundTrip_Do()
2020
op.Do();
2121

2222
ClassicAssert.AreEqual(1, fileMenu.GetMenuItems(out _).Count);
23-
ClassicAssert.AreEqual(ConvertMenuItemToSeperatorOperation.SeparatorTitle, fileMenu.GetMenuItems(out _)[0].Title.ToString());
23+
ClassicAssert.AreEqual(MenuBarExtensions.SeparatorTitle, fileMenu.GetMenuItems(out _)[0].Title.ToString());
2424

2525
}, out _);
2626

2727
var mbInFileMenu = mbIn.SubViews.OfType<MenuBarItem>().First();
2828
ClassicAssert.AreEqual(1, mbInFileMenu.GetMenuItems(out _).Count);
29-
ClassicAssert.AreEqual(ConvertMenuItemToSeperatorOperation.SeparatorTitle, mbInFileMenu.GetMenuItems(out _)[0].Title.ToString());
29+
ClassicAssert.AreEqual(MenuBarExtensions.SeparatorTitle, mbInFileMenu.GetMenuItems(out _)[0].Title.ToString());
3030
}
3131

3232
[Test]
@@ -40,7 +40,7 @@ public void TestConvertToSeperator_RoundTrip_UnDo()
4040
var op = new ConvertMenuItemToSeperatorOperation(App, orig);
4141
op.Do();
4242
ClassicAssert.AreEqual(1, fileMenu.GetMenuItems(out _).Count);
43-
ClassicAssert.AreEqual(ConvertMenuItemToSeperatorOperation.SeparatorTitle, fileMenu.GetMenuItems(out _)[0].Title.ToString());
43+
ClassicAssert.AreEqual(MenuBarExtensions.SeparatorTitle, fileMenu.GetMenuItems(out _)[0].Title.ToString());
4444

4545
op.Undo();
4646
ClassicAssert.AreEqual(1, fileMenu.GetMenuItems(out _).Count);
@@ -50,7 +50,7 @@ public void TestConvertToSeperator_RoundTrip_UnDo()
5050
op.Undo();
5151
op.Redo();
5252
ClassicAssert.AreEqual(1, fileMenu.GetMenuItems(out _).Count);
53-
ClassicAssert.AreEqual(ConvertMenuItemToSeperatorOperation.SeparatorTitle, fileMenu.GetMenuItems(out _)[0].Title.ToString());
53+
ClassicAssert.AreEqual(MenuBarExtensions.SeparatorTitle, fileMenu.GetMenuItems(out _)[0].Title.ToString());
5454

5555
op.Undo();
5656
op.Undo();
@@ -64,6 +64,6 @@ public void TestConvertToSeperator_RoundTrip_UnDo()
6464
var mbInFileMenu = mbIn.SubViews.OfType<MenuBarItem>().First();
6565
ClassicAssert.AreEqual(1, mbInFileMenu.GetMenuItems(out _).Count);
6666
ClassicAssert.IsNotNull(mbInFileMenu.GetMenuItems(out _)[0]);
67-
ClassicAssert.AreNotEqual(ConvertMenuItemToSeperatorOperation.SeparatorTitle, mbInFileMenu.GetMenuItems(out _)[0].Title.ToString());
67+
ClassicAssert.AreNotEqual(MenuBarExtensions.SeparatorTitle, mbInFileMenu.GetMenuItems(out _)[0].Title.ToString());
6868
}
6969
}

0 commit comments

Comments
 (0)