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

Round 3 of unit test conversions and modernization work #276

Open
wants to merge 56 commits into
base: v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
313e7ec
Much more efficient array insertion
dodexahedron Dec 29, 2023
61b60c4
Avoid allocating an iterator
dodexahedron Dec 29, 2023
22c1c05
Add attribute for static analysis
dodexahedron Dec 29, 2023
9be334e
Only allocate one dictionary and use it
dodexahedron Dec 29, 2023
bd12396
This can't be null
dodexahedron Dec 29, 2023
c2e768c
Collection expression
dodexahedron Dec 29, 2023
e06d46d
Eliminate more allocations
dodexahedron Dec 29, 2023
1949364
Deconstruct and make prettier
dodexahedron Dec 29, 2023
3238547
This can now be private
dodexahedron Dec 29, 2023
f1825a1
XmlDoc for this (mostly copied from the old method
dodexahedron Dec 29, 2023
c9cb983
Null propagation removes an indentation level here 🥳
dodexahedron Dec 29, 2023
724f8d3
Simplify with collection expression
dodexahedron Dec 29, 2023
81bba54
OK Dependabot! We hear you!
dodexahedron Dec 30, 2023
f7e2554
Add an unregister method
dodexahedron Dec 31, 2023
e6555eb
Convert and improve this big test.
dodexahedron Dec 31, 2023
6ce10d6
Use using to dispose of this
dodexahedron Dec 31, 2023
4ae4e45
First usage of the new unregister method
dodexahedron Dec 31, 2023
27c0582
Sorted members by design guides ONLY
dodexahedron Dec 31, 2023
faece78
Annotate this test method
dodexahedron Dec 31, 2023
abab514
A few pre-conditions
dodexahedron Dec 31, 2023
bbbae8e
Pre-condition has guaranteed this can't be null
dodexahedron Dec 31, 2023
2458a4a
This is guaranteed by the pre-conditions
dodexahedron Dec 31, 2023
c2ea14c
Test the operation itself is ok
dodexahedron Dec 31, 2023
5fb0915
Convert and simplify conditions
dodexahedron Dec 31, 2023
494385d
Track UndoStack and RedoStack
dodexahedron Dec 31, 2023
29ce307
Finish up with the rest of the test
dodexahedron Dec 31, 2023
879db74
Annotate tested type
dodexahedron Dec 31, 2023
44bd86f
Add disposal for this MenuBar and add a note for later
dodexahedron Dec 31, 2023
c89f5ad
Refactoring this guy to be clearer (WIP)
dodexahedron Dec 31, 2023
dd96623
More refactoring to make this clearer
dodexahedron Dec 31, 2023
846edf4
There we go
dodexahedron Dec 31, 2023
8aa8234
And update the usages! (done)
dodexahedron Dec 31, 2023
b64c9b1
These are actually pre-conditions
dodexahedron Dec 31, 2023
661a091
Be sure these don't throw
dodexahedron Dec 31, 2023
c929629
Finish converting and improving this test
dodexahedron Dec 31, 2023
8cc4048
Introduce a type so the menubars can be auto-disposed by usings
dodexahedron Dec 31, 2023
cbda19b
Convert these and change to pre-conditions
dodexahedron Dec 31, 2023
b810696
Finish converting and improving this test
dodexahedron Dec 31, 2023
c8cb5f7
Convert MoveMenuItemLeft_CannotMoveRootItems
dodexahedron Dec 31, 2023
1aec466
Convert these to pre-conditions
dodexahedron Dec 31, 2023
5625d36
Make sure nothing throws and IsImpossible is false
dodexahedron Dec 31, 2023
277eb2b
Convert and add a reference check
dodexahedron Dec 31, 2023
f632cb9
Convert and simplify
dodexahedron Dec 31, 2023
240dac4
Convert and simplify the rest of this test
dodexahedron Dec 31, 2023
97c5722
Convert and upgrade this test.
dodexahedron Dec 31, 2023
2fc019e
Convert this test
dodexahedron Dec 31, 2023
7c64fee
Add tests to ensure the helpers are valid and to reduce redundancy
dodexahedron Dec 31, 2023
0396cbc
Remove code that is now redundant
dodexahedron Dec 31, 2023
cdc3020
Some more code that can go away with the new tests
dodexahedron Dec 31, 2023
f4b27b2
Convert the last test method
dodexahedron Dec 31, 2023
acdd558
Silence warnings from static analysis about possible null dereferences
dodexahedron Dec 31, 2023
e3d8b78
These can all be static lambdas
dodexahedron Dec 31, 2023
e3023a4
Re-ordering code after member name changes and new additions (just mo…
dodexahedron Dec 31, 2023
616335e
One more rename/sort and make the record private sealed
dodexahedron Dec 31, 2023
9b19f17
Collection expressions
dodexahedron Dec 31, 2023
4784f20
Merge branch 'v2' into convert-unit-tests-to-constraint-model-3
dodexahedron Jan 3, 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
85 changes: 54 additions & 31 deletions src/MenuTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ public void Register(MenuBar mb)
this.bars.Add(mb);
}

/// <summary>
/// Unregisters listeners for <paramref name="mb"/>.
/// </summary>
/// <param name="mb"><see cref="MenuBar"/> to stop tracking.</param>
public void UnregisterMenuBar( MenuBar? mb )
{
if ( !bars.TryTake( out mb ) )
{
return;
}

mb.MenuAllClosed -= MenuAllClosed;
mb.MenuOpened -= MenuOpened;
mb.MenuClosing -= MenuClosing;
}

/// <summary>
/// <para>
/// Searches child items of all MenuBars tracked by this class
Expand All @@ -63,7 +79,7 @@ public void Register(MenuBar mb)
/// <returns>The immediate parent of <paramref name="item"/>.</returns>
/// <remarks>Result may be a top level menu (e.g. File, View)
/// or a sub-menu parent (e.g. View=>Windows).</remarks>
public MenuBarItem? GetParent( MenuItem item, out MenuBar? hostBar )
private MenuBarItem? GetParent( MenuItem item, out MenuBar? hostBar )
{
foreach (var bar in this.bars)
{
Expand All @@ -83,7 +99,23 @@ public void Register(MenuBar mb)
return null;
}

public bool TryGetParent(MenuItem item, [NotNullWhen(true)]out MenuBar? hostBar, [NotNullWhen(true)] out MenuBarItem? parentItem)
/// <summary>
/// Searches child items of all MenuBars tracked by this class to try and find the parent of the item passed.
/// </summary>
/// <param name="item">The item whose parent you want to find.</param>
/// <param name="hostBar">
/// When this method returns true, the <see cref="MenuBar" /> that owns <paramref name="item" />.<br /> Otherwise, <see langword="null" /> if
/// not found or parent not registered (see <see cref="Register(MenuBar)" />).
/// </param>
/// <param name="parentItem">
/// When this method returns <see langword="true" />, the immediate parent of <paramref name="item" />.<br /> Otherwise,
/// <see langword="null" />
/// </param>
/// <remarks>
/// Search is recursive and dips into sub-menus.<br /> For sub-menus it is the immediate parent that is returned.
/// </remarks>
/// <returns>A <see langword="bool" /> indicating if the search was successful or not.</returns>
public bool TryGetParent( MenuItem item, [NotNullWhen( true )] out MenuBar? hostBar, [NotNullWhen( true )] out MenuBarItem? parentItem )
{
var parentCandidate = GetParent( item, out hostBar );
if ( parentCandidate is null )
Expand All @@ -106,22 +138,21 @@ public bool TryGetParent(MenuItem item, [NotNullWhen(true)]out MenuBar? hostBar,
/// the substitution object (<see cref="MenuItem"/>). See
/// <see cref="ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem, out MenuItem?)"/>
/// for more information.</returns>
public Dictionary<MenuBarItem, MenuItem> ConvertEmptyMenus()
public Dictionary<MenuBarItem, MenuItem> ConvertEmptyMenus( )
{
var toReturn = new Dictionary<MenuBarItem, MenuItem>();

Dictionary<MenuBarItem, MenuItem> dictionary = [];
foreach (var b in this.bars)
{
foreach (var bi in b.Menus)
{
foreach (var converted in this.ConvertEmptyMenus(b, bi))
foreach ( ( MenuBarItem? convertedMenuBarItem, MenuItem? convertedMenuItem ) in this.ConvertEmptyMenus( dictionary, b, bi ) )
{
toReturn.Add(converted.Key, converted.Value);
dictionary.TryAdd( convertedMenuBarItem, convertedMenuItem );
}
}
}

return toReturn;
return dictionary;
}

/// <summary>
Expand All @@ -137,12 +168,12 @@ public Dictionary<MenuBarItem, MenuItem> ConvertEmptyMenus()
/// <param name="added">The result of the conversion (same text, same index etc but
/// <see cref="MenuItem"/> instead of <see cref="MenuBarItem"/>).</param>
/// <returns><see langword="true"/> if conversion was possible (menu was empty and belonged to tracked menu).</returns>
internal static bool ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem bar, out MenuItem? added)
internal static bool ConvertMenuBarItemToRegularItemIfEmpty( MenuBarItem bar, [NotNullWhen( true )] out MenuItem? added )
{
added = null;

// bar still has more children so don't convert
if (bar.Children.Any())
if ( bar.Children.Length != 0 )
{
return false;
}
Expand All @@ -152,48 +183,40 @@ internal static bool ConvertMenuBarItemToRegularItemIfEmpty(MenuBarItem bar, out
return false;
}

var children = parent.Children.ToList<MenuItem>();
var idx = children.IndexOf(bar);
int idx = Array.IndexOf( parent.Children, bar );

if (idx < 0)
{
return false;
}

// bar has no children so convert to MenuItem
added = new MenuItem { Title = bar.Title };
added.Data = bar.Data;
added.Shortcut = bar.Shortcut;

children.RemoveAt(idx);
children.Insert(idx, added);

parent.Children = children.ToArray();
parent.Children[ idx ] = added = new( )
{
Title = bar.Title,
Data = bar.Data,
Shortcut = bar.Shortcut
};

return true;
}

/// <inheritdoc cref="ConvertEmptyMenus()"/>
private Dictionary<MenuBarItem, MenuItem> ConvertEmptyMenus(MenuBar bar, MenuBarItem mbi)
private Dictionary<MenuBarItem, MenuItem> ConvertEmptyMenus(Dictionary<MenuBarItem,MenuItem> dictionary, MenuBar bar, MenuBarItem mbi)
{
var toReturn = new Dictionary<MenuBarItem, MenuItem>();

foreach (var c in mbi.Children.OfType<MenuBarItem>())
{
this.ConvertEmptyMenus(bar, c);
if ( ConvertMenuBarItemToRegularItemIfEmpty( c, out var added))
this.ConvertEmptyMenus(dictionary,bar, c);
if ( ConvertMenuBarItemToRegularItemIfEmpty( c, out MenuItem? added))
{
if (added != null)
{
toReturn.Add(c, added);
}
dictionary.TryAdd( c, added );

bar.CloseMenu();
bar.OpenMenu();
}
}

return toReturn;
return dictionary;
}

private void MenuClosing(object? sender, MenuClosingEventArgs obj)
Expand All @@ -204,7 +227,7 @@ private void MenuClosing(object? sender, MenuClosingEventArgs obj)
private void MenuOpened(object? sender, MenuOpenedEventArgs obj)
{
this.CurrentlyOpenMenuItem = obj.MenuItem;
this.ConvertEmptyMenus();
this.ConvertEmptyMenus( );
}

private void MenuAllClosed(object? sender, EventArgs e)
Expand Down
55 changes: 27 additions & 28 deletions src/Operations/MenuOperations/RemoveMenuItemOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ public override void Undo()
return;
}

var children = this.Parent.Children.ToList<MenuItem>();

children.Insert(this.removedAtIdx, this.OperateOn);
this.Parent.Children = children.ToArray();
this.Parent.Children =
[
.. Parent.Children[ .. removedAtIdx ],
this.OperateOn,
.. Parent.Children[ removedAtIdx .. ]
];
this.Bar?.SetNeedsDisplay();

// if any MenuBarItem were converted to vanilla MenuItem
Expand Down Expand Up @@ -123,12 +125,12 @@ protected override bool DoImpl()
return false;
}

var children = this.Parent.Children.ToList<MenuItem>();

this.removedAtIdx = Math.Max(0, children.IndexOf(this.OperateOn));

children.Remove(this.OperateOn);
this.Parent.Children = children.ToArray();
this.removedAtIdx = Math.Max( 0, Array.IndexOf( Parent.Children, OperateOn ) );
this.Parent.Children =
[
.. Parent.Children[ ..removedAtIdx ],
.. Parent.Children[ ( removedAtIdx + 1 ).. ]
];
this.Bar?.SetNeedsDisplay();

if (this.Bar != null)
Expand All @@ -137,27 +139,24 @@ protected override bool DoImpl()
}

// if a top level menu now has no children
if (this.Bar != null)
var empty = this.Bar?.Menus.Where(bi => bi.Children.Length == 0).ToArray();
if (empty?.Any() == true)
{
var empty = this.Bar.Menus.Where(bi => bi.Children.Length == 0).ToArray();
if (empty.Any())
{
// remember where they were
this.prunedEmptyTopLevelMenus = empty.ToDictionary(e => Array.IndexOf(this.Bar.Menus, e), v => v);
// remember where they were
this.prunedEmptyTopLevelMenus = empty.ToDictionary(e => Array.IndexOf(this.Bar.Menus, e), v => v);

// and remove them
this.Bar.Menus = this.Bar.Menus.Except(this.prunedEmptyTopLevelMenus.Values).ToArray();
}
// and remove them
this.Bar.Menus = this.Bar.Menus.Except(this.prunedEmptyTopLevelMenus.Values).ToArray();
}

// if we just removed the last menu header
// leaving a completely blank menu bar
if (this.Bar.Menus.Length == 0 && this.Bar.SuperView != null)
{
// remove the bar completely
this.Bar.CloseMenu();
this.barRemovedFrom = this.Bar.SuperView;
this.barRemovedFrom.Remove(this.Bar);
}
// if we just removed the last menu header
// leaving a completely blank menu bar
if (this.Bar?.Menus.Length == 0 && this.Bar.SuperView != null)
{
// remove the bar completely
this.Bar.CloseMenu();
this.barRemovedFrom = this.Bar.SuperView;
this.barRemovedFrom.Remove(this.Bar);
}

return true;
Expand Down
15 changes: 7 additions & 8 deletions src/ViewFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static class ViewFactory
internal const string DefaultMenuItemText = "Edit Me";

internal static readonly Type[] KnownUnsupportedTypes =
{
[
typeof( Toplevel ),
typeof( Dialog ),
typeof( FileDialog ),
Expand All @@ -42,7 +42,7 @@ public static class ViewFactory
// BUG These seem to cause stack overflows in CreateSubControlDesigns (see TestAddView_RoundTrip)
typeof( Wizard ),
typeof( WizardStep )
};
];

/// <summary>
/// Gets a new instance of a default <see cref="MenuBarItem" />[], to include as the default initial
Expand All @@ -57,12 +57,11 @@ internal static MenuBarItem[] DefaultMenuBarItems
{
get
{
return new[]
{
new MenuBarItem(
"_File (F9)",
new[] { new MenuItem( DefaultMenuItemText, string.Empty, ( ) => { } ) } )
};
return
[
new( "_File (F9)",
[ new MenuItem( DefaultMenuItemText, string.Empty, static ( ) => { } ) ] )
];
}
}

Expand Down
Loading