diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs
index eda91faf6..b2b6aa41a 100644
--- a/Terminal.Gui/Application/Application.Keyboard.cs
+++ b/Terminal.Gui/Application/Application.Keyboard.cs
@@ -13,6 +13,17 @@ public static partial class Application // Keyboard handling
/// if the key was handled.
public static bool RaiseKeyDownEvent (Key key)
{
+ Logging.Debug ($"{key}");
+
+ // TODO: Add a way to ignore certain keys, esp for debugging.
+ //#if DEBUG
+ // if (key == Key.Empty.WithAlt || key == Key.Empty.WithCtrl)
+ // {
+ // Logging.Debug ($"Ignoring {key}");
+ // return false;
+ // }
+ //#endif
+
// TODO: This should match standard event patterns
KeyDown?.Invoke (null, key);
diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs
index 72cffe368..acc5cbc95 100644
--- a/Terminal.Gui/Application/Application.Mouse.cs
+++ b/Terminal.Gui/Application/Application.Mouse.cs
@@ -63,7 +63,7 @@ public static partial class Application // Mouse handling
}
#if DEBUG_IDISPOSABLE
- if (View.DebugIDisposable)
+ if (View.EnableDebugIDisposableAsserts)
{
ObjectDisposedException.ThrowIf (MouseGrabView.WasDisposed, MouseGrabView);
}
@@ -154,7 +154,7 @@ public static partial class Application // Mouse handling
if (deepestViewUnderMouse is { })
{
#if DEBUG_IDISPOSABLE
- if (View.DebugIDisposable && deepestViewUnderMouse.WasDisposed)
+ if (View.EnableDebugIDisposableAsserts && deepestViewUnderMouse.WasDisposed)
{
throw new ObjectDisposedException (deepestViewUnderMouse.GetType ().FullName);
}
@@ -174,8 +174,11 @@ public static partial class Application // Mouse handling
&& Popover?.GetActivePopover () as View is { Visible: true } visiblePopover
&& View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false)
{
-
- visiblePopover.Visible = false;
+ // TODO: Build a use/test case for the popover not handling Quit
+ if (visiblePopover.InvokeCommand (Command.Quit) is true && visiblePopover.Visible)
+ {
+ visiblePopover.Visible = false;
+ }
// Recurse once so the event can be handled below the popover
RaiseMouseEvent (mouseEvent);
@@ -297,7 +300,7 @@ public static partial class Application // Mouse handling
if (MouseGrabView is { })
{
#if DEBUG_IDISPOSABLE
- if (View.DebugIDisposable && MouseGrabView.WasDisposed)
+ if (View.EnableDebugIDisposableAsserts && MouseGrabView.WasDisposed)
{
throw new ObjectDisposedException (MouseGrabView.GetType ().FullName);
}
diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs
index f4a4cf44e..d69a34e92 100644
--- a/Terminal.Gui/Application/Application.Run.cs
+++ b/Terminal.Gui/Application/Application.Run.cs
@@ -98,7 +98,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
var rs = new RunState (toplevel);
#if DEBUG_IDISPOSABLE
- if (View.DebugIDisposable && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
+ if (View.EnableDebugIDisposableAsserts && Top is { } && toplevel != Top && !TopLevels.Contains (Top))
{
// This assertion confirm if the Top was already disposed
Debug.Assert (Top.WasDisposed);
@@ -193,6 +193,11 @@ public static partial class Application // Run (Begin, Run, End, Stop)
toplevel.EndInit (); // Calls Layout
}
+ // Call ConfigurationManager Apply here to ensure all subscribers to ConfigurationManager.Applied
+ // can update their state appropriately.
+ // BUGBUG: DO NOT DO THIS. Leave this commented out until we can figure out how to do this right
+ //Apply ();
+
// Try to set initial focus to any TabStop
if (!toplevel.HasFocus)
{
@@ -426,7 +431,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
internal static void LayoutAndDrawImpl (bool forceDraw = false)
{
- List tops = [..TopLevels];
+ List tops = [.. TopLevels];
if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
{
@@ -479,7 +484,10 @@ public static partial class Application // Run (Begin, Run, End, Stop)
for (state.Toplevel.Running = true; state.Toplevel?.Running == true;)
{
- MainLoop!.Running = true;
+ if (MainLoop is { })
+ {
+ MainLoop.Running = true;
+ }
if (EndAfterFirstIteration && !firstIteration)
{
@@ -489,7 +497,10 @@ public static partial class Application // Run (Begin, Run, End, Stop)
firstIteration = RunIteration (ref state, firstIteration);
}
- MainLoop!.Running = false;
+ if (MainLoop is { })
+ {
+ MainLoop.Running = false;
+ }
// Run one last iteration to consume any outstanding input events from Driver
// This is important for remaining OnKeyUp events.
@@ -505,7 +516,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
public static bool RunIteration (ref RunState state, bool firstIteration = false)
{
// If the driver has events pending do an iteration of the driver MainLoop
- if (MainLoop!.Running && MainLoop.EventsPending ())
+ if (MainLoop is { Running: true } && MainLoop.EventsPending ())
{
// Notify Toplevel it's ready
if (firstIteration)
@@ -529,7 +540,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
if (PositionCursor ())
{
- Driver!.UpdateCursor ();
+ Driver?.UpdateCursor ();
}
return firstIteration;
@@ -564,7 +575,14 @@ public static partial class Application // Run (Begin, Run, End, Stop)
{
ArgumentNullException.ThrowIfNull (runState);
- Popover?.Hide (Popover?.GetActivePopover ());
+ if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover)
+ {
+ // TODO: Build a use/test case for the popover not handling Quit
+ if (visiblePopover.InvokeCommand (Command.Quit) is true && visiblePopover.Visible)
+ {
+ visiblePopover.Visible = false;
+ }
+ }
runState.Toplevel.OnUnloaded ();
diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs
index 07bec47c7..f11104c8c 100644
--- a/Terminal.Gui/Application/Application.cs
+++ b/Terminal.Gui/Application/Application.cs
@@ -53,6 +53,7 @@ public static partial class Application
{
return string.Empty;
}
+
var sb = new StringBuilder ();
Cell [,] contents = driver?.Contents!;
@@ -139,7 +140,7 @@ public static partial class Application
// starts running and after Shutdown returns.
internal static void ResetState (bool ignoreDisposed = false)
{
- Application.Navigation = new ApplicationNavigation ();
+ Navigation = new ();
// Shutdown is the bookend for Init. As such it needs to clean up all resources
// Init created. Apps that do any threading will need to code defensively for this.
@@ -151,8 +152,11 @@ public static partial class Application
if (Popover?.GetActivePopover () is View popover)
{
+ // This forcefully closes the popover; invoking Command.Quit would be more graceful
+ // but since this is shutdown, doing this is ok.
popover.Visible = false;
}
+
Popover?.Dispose ();
Popover = null;
@@ -160,9 +164,9 @@ public static partial class Application
#if DEBUG_IDISPOSABLE
// Don't dispose the Top. It's up to caller dispose it
- if (View.DebugIDisposable && !ignoreDisposed && Top is { })
+ if (View.EnableDebugIDisposableAsserts && !ignoreDisposed && Top is { })
{
- Debug.Assert (Top.WasDisposed);
+ Debug.Assert (Top.WasDisposed, $"Title = {Top.Title}, Id = {Top.Id}");
// If End wasn't called _cachedRunStateToplevel may be null
if (_cachedRunStateToplevel is { })
@@ -223,7 +227,6 @@ public static partial class Application
Navigation = null;
-
KeyBindings.Clear ();
AddKeyBindings ();
@@ -234,10 +237,9 @@ public static partial class Application
SynchronizationContext.SetSynchronizationContext (null);
}
-
///
/// Adds specified idle handler function to main iteration processing. The handler function will be called
/// once per iteration of the main loop after other events have been handled.
///
- public static void AddIdle (Func func) => ApplicationImpl.Instance.AddIdle (func);
+ public static void AddIdle (Func func) { ApplicationImpl.Instance.AddIdle (func); }
}
diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs
index 8bbc17ccb..04c5eed6f 100644
--- a/Terminal.Gui/Application/ApplicationImpl.cs
+++ b/Terminal.Gui/Application/ApplicationImpl.cs
@@ -34,7 +34,7 @@ public class ApplicationImpl : IApplication
[RequiresDynamicCode ("AOT")]
public virtual void Init (IConsoleDriver? driver = null, string? driverName = null)
{
- Application.InternalInit (driver, driverName);
+ Application.InternalInit (driver, driverName);
}
///
@@ -166,34 +166,35 @@ public class ApplicationImpl : IApplication
try
{
#endif
- resume = false;
- RunState runState = Application.Begin (view);
+ resume = false;
+ RunState runState = Application.Begin (view);
- // If EndAfterFirstIteration is true then the user must dispose of the runToken
- // by using NotifyStopRunState event.
- Application.RunLoop (runState);
+ // If EndAfterFirstIteration is true then the user must dispose of the runToken
+ // by using NotifyStopRunState event.
+ Application.RunLoop (runState);
- if (runState.Toplevel is null)
- {
+ if (runState.Toplevel is null)
+ {
#if DEBUG_IDISPOSABLE
- if (View.DebugIDisposable)
+ if (View.EnableDebugIDisposableAsserts)
{
Debug.Assert (Application.TopLevels.Count == 0);
}
#endif
- runState.Dispose ();
+ runState.Dispose ();
- return;
- }
+ return;
+ }
- if (!Application.EndAfterFirstIteration)
- {
- Application.End (runState);
- }
+ if (!Application.EndAfterFirstIteration)
+ {
+ Application.End (runState);
+ }
#if !DEBUG
}
catch (Exception error)
{
+ Logging.Warning ($"Release Build Exception: {error}");
if (errorHandler is null)
{
throw;
@@ -225,7 +226,7 @@ public class ApplicationImpl : IApplication
{
bool init = Application.Initialized;
- Application.OnInitializedChanged(this, new (in init));
+ Application.OnInitializedChanged (this, new (in init));
}
}
@@ -270,7 +271,7 @@ public class ApplicationImpl : IApplication
///
public virtual void AddIdle (Func func)
{
- if(Application.MainLoop is null)
+ if (Application.MainLoop is null)
{
throw new NotInitializedException ("Cannot add idle before main loop is initialized");
}
@@ -294,7 +295,7 @@ public class ApplicationImpl : IApplication
///
public virtual bool RemoveTimeout (object token)
- {
+ {
return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false;
}
diff --git a/Terminal.Gui/Application/ApplicationPopover.cs b/Terminal.Gui/Application/ApplicationPopover.cs
index 1124faefb..b8238140b 100644
--- a/Terminal.Gui/Application/ApplicationPopover.cs
+++ b/Terminal.Gui/Application/ApplicationPopover.cs
@@ -103,6 +103,7 @@ public sealed class ApplicationPopover : IDisposable
if (popover is View newPopover)
{
+ Register (popover);
if (!newPopover.IsInitialized)
{
newPopover.BeginInit ();
@@ -145,6 +146,7 @@ public sealed class ApplicationPopover : IDisposable
if (activePopover is { Visible: true })
{
+ Logging.Debug ($"Active - Calling NewKeyDownEvent ({key}) on {activePopover.Title}");
if (activePopover.NewKeyDownEvent (key))
{
return true;
@@ -163,6 +165,7 @@ public sealed class ApplicationPopover : IDisposable
}
// hotKeyHandled = popoverView.InvokeCommandsBoundToHotKey (key);
+ Logging.Debug ($"Inactive - Calling NewKeyDownEvent ({key}) on {popoverView.Title}");
hotKeyHandled = popoverView.NewKeyDownEvent (key);
if (hotKeyHandled is true)
diff --git a/Terminal.Gui/Application/PopoverBaseImpl.cs b/Terminal.Gui/Application/PopoverBaseImpl.cs
index 64b90532c..dfa05c897 100644
--- a/Terminal.Gui/Application/PopoverBaseImpl.cs
+++ b/Terminal.Gui/Application/PopoverBaseImpl.cs
@@ -41,7 +41,7 @@ public abstract class PopoverBaseImpl : View, IPopover
{
if (!Visible)
{
- return null;
+ return false;
}
Visible = false;
@@ -54,11 +54,22 @@ public abstract class PopoverBaseImpl : View, IPopover
protected override bool OnVisibleChanging ()
{
bool ret = base.OnVisibleChanging ();
- if (!ret && !Visible)
+ if (ret is not true)
{
- // Whenever visible is changing to true, we need to resize;
- // it's our only chance because we don't get laid out until we're visible
- Layout (Application.Screen.Size);
+ if (!Visible)
+ {
+ // Whenever visible is changing to true, we need to resize;
+ // it's our only chance because we don't get laid out until we're visible
+ Layout (Application.Screen.Size);
+ }
+ else
+ {
+ // Whenever visible is changing to false, we need to reset the focus
+ if (ApplicationNavigation.IsInHierarchy(this, Application.Navigation?.GetFocused ()))
+ {
+ Application.Navigation?.SetFocused (Application.Top?.MostFocused);
+ }
+ }
}
return ret;
diff --git a/Terminal.Gui/Application/RunState.cs b/Terminal.Gui/Application/RunState.cs
index e0b6fdc30..503055892 100644
--- a/Terminal.Gui/Application/RunState.cs
+++ b/Terminal.Gui/Application/RunState.cs
@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+using System.Collections.Concurrent;
+
+namespace Terminal.Gui;
/// The execution state for a view.
public class RunState : IDisposable
@@ -22,10 +24,7 @@ public class RunState : IDisposable
Dispose (true);
GC.SuppressFinalize (this);
#if DEBUG_IDISPOSABLE
- if (View.DebugIDisposable)
- {
- WasDisposed = true;
- }
+ WasDisposed = true;
#endif
}
@@ -45,22 +44,32 @@ public class RunState : IDisposable
}
#if DEBUG_IDISPOSABLE
- /// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
- public bool WasDisposed;
+ ///
+ /// Gets whether was called on this RunState or not.
+ /// For debug purposes to verify objects are being disposed properly.
+ /// Only valid when DEBUG_IDISPOSABLE is defined.
+ ///
+ public bool WasDisposed { get; private set; }
- /// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
- public int DisposedCount = 0;
+ ///
+ /// Gets the number of times was called on this object.
+ /// For debug purposes to verify objects are being disposed properly.
+ /// Only valid when DEBUG_IDISPOSABLE is defined.
+ ///
+ public int DisposedCount { get; private set; } = 0;
- /// For debug (see DEBUG_IDISPOSABLE define) purposes; the runstate instances that have been created
- public static List Instances = new ();
+ ///
+ /// Gets the list of RunState objects that have been created and not yet disposed.
+ /// Note, this is a static property and will affect all RunState objects.
+ /// For debug purposes to verify objects are being disposed properly.
+ /// Only valid when DEBUG_IDISPOSABLE is defined.
+ ///
+ public static ConcurrentBag Instances { get; private set; } = [];
/// Creates a new RunState object.
public RunState ()
{
- if (View.DebugIDisposable)
- {
- Instances.Add (this);
- }
+ Instances.Add (this);
}
#endif
}
diff --git a/Terminal.Gui/Configuration/AttributeJsonConverter.cs b/Terminal.Gui/Configuration/AttributeJsonConverter.cs
index ff1797221..ba291c75a 100644
--- a/Terminal.Gui/Configuration/AttributeJsonConverter.cs
+++ b/Terminal.Gui/Configuration/AttributeJsonConverter.cs
@@ -92,7 +92,7 @@ internal class AttributeJsonConverter : JsonConverter
}
}
- throw new JsonException ();
+ throw new JsonException ("Attribute");
}
public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options)
diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs
index b2b5f4766..82c01336a 100644
--- a/Terminal.Gui/Configuration/ConfigurationManager.cs
+++ b/Terminal.Gui/Configuration/ConfigurationManager.cs
@@ -258,6 +258,7 @@ public static class ConfigurationManager
Settings?.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!, ConfigLocations.AppResources);
}
+ // TODO: Determine if Runtime should be applied last.
if (Locations.HasFlag (ConfigLocations.Runtime) && !string.IsNullOrEmpty (RuntimeConfig))
{
Settings?.Update (RuntimeConfig, "ConfigurationManager.RuntimeConfig", ConfigLocations.Runtime);
diff --git a/Terminal.Gui/Configuration/ScopeJsonConverter.cs b/Terminal.Gui/Configuration/ScopeJsonConverter.cs
index d1d6e475e..3c5a2c856 100644
--- a/Terminal.Gui/Configuration/ScopeJsonConverter.cs
+++ b/Terminal.Gui/Configuration/ScopeJsonConverter.cs
@@ -96,6 +96,8 @@ internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccess
// Logging.Trace ($"scopeT Read: {ex}");
}
}
+ //Logging.Warning ($"{propertyName} = {scope! [propertyName].PropertyValue}");
+
}
else
{
@@ -147,7 +149,7 @@ internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccess
}
}
- throw new JsonException ();
+ throw new JsonException ("ScopeJsonConverter");
}
public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
index a83b84921..c19c5e1be 100644
--- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
+++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs
@@ -6,7 +6,7 @@ namespace Terminal.Gui;
internal class NetEvents : IDisposable
{
- private readonly CancellationTokenSource _netEventsDisposed = new CancellationTokenSource ();
+ private CancellationTokenSource? _netEventsDisposed = new CancellationTokenSource ();
//CancellationTokenSource _waitForStartCancellationTokenSource;
private readonly ManualResetEventSlim _winChange = new (false);
@@ -597,6 +597,7 @@ internal class NetEvents : IDisposable
{
_netEventsDisposed?.Cancel ();
_netEventsDisposed?.Dispose ();
+ _netEventsDisposed = null;
try
{
diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
index e6461b144..af5e912c2 100644
--- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
+++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
@@ -162,7 +162,7 @@ public class ApplicationV2 : ApplicationImpl
///
public override void Run (Toplevel view, Func? errorHandler = null)
{
- Logging.Logger.LogInformation ($"Run '{view}'");
+ Logging.Information ($"Run '{view}'");
ArgumentNullException.ThrowIfNull (view);
if (!Application.Initialized)
@@ -172,10 +172,12 @@ public class ApplicationV2 : ApplicationImpl
Application.Top = view;
- Application.Begin (view);
+ RunState rs = Application.Begin (view);
- // TODO : how to know when we are done?
- while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view)
+ Application.Top.Running = true;
+
+ // QUESTION: how to know when we are done? - ANSWER: Running == false
+ while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view && view.Running)
{
if (_coordinator is null)
{
@@ -184,6 +186,9 @@ public class ApplicationV2 : ApplicationImpl
_coordinator.RunIteration ();
}
+
+ Logging.Information ($"Run - Calling End");
+ Application.End (rs);
}
///
@@ -197,7 +202,7 @@ public class ApplicationV2 : ApplicationImpl
///
public override void RequestStop (Toplevel? top)
{
- Logging.Logger.LogInformation ($"RequestStop '{top}'");
+ Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
top ??= Application.Top;
@@ -214,22 +219,9 @@ public class ApplicationV2 : ApplicationImpl
return;
}
+ // All RequestStop does is set the Running property to false - In the next iteration
+ // this will be detected
top.Running = false;
-
- // TODO: This definition of stop seems sketchy
- Application.TopLevels.TryPop (out _);
-
- if (Application.TopLevels.Count > 0)
- {
- Application.Top = Application.TopLevels.Peek ();
- }
- else
- {
- Application.Top = null;
- }
-
- // Notify that it is closed
- top.OnClosed (top);
}
///
diff --git a/Terminal.Gui/Drawing/Attribute.cs b/Terminal.Gui/Drawing/Attribute.cs
index a36601ba1..28097f377 100644
--- a/Terminal.Gui/Drawing/Attribute.cs
+++ b/Terminal.Gui/Drawing/Attribute.cs
@@ -4,6 +4,11 @@ using System.Text.Json.Serialization;
namespace Terminal.Gui;
+
+// TODO: Add support for other attributes (bold, underline, etc.) once the platform drivers support them.
+// TODO: See https://github.com/gui-cs/Terminal.Gui/issues/457
+
+
/// Attributes represent how text is styled when displayed in the terminal.
///
/// provides a platform independent representation of colors (and someday other forms of
diff --git a/Terminal.Gui/Drawing/Color/ColorScheme.cs b/Terminal.Gui/Drawing/Color/ColorScheme.cs
index 2e83a4c14..adb47f143 100644
--- a/Terminal.Gui/Drawing/Color/ColorScheme.cs
+++ b/Terminal.Gui/Drawing/Color/ColorScheme.cs
@@ -4,6 +4,9 @@ using System.Text.Json.Serialization;
namespace Terminal.Gui;
+// TODO: Rename "ColorScheme"->"AttributeScheme" given we'll soon have non-color information in Attributes?
+// TODO: See https://github.com/gui-cs/Terminal.Gui/issues/457
+
/// Defines a standard set of s for common visible elements in a .
///
///
diff --git a/Terminal.Gui/Drawing/Region.cs b/Terminal.Gui/Drawing/Region.cs
index 2e5131975..1cfea8638 100644
--- a/Terminal.Gui/Drawing/Region.cs
+++ b/Terminal.Gui/Drawing/Region.cs
@@ -556,72 +556,122 @@ public class Region
/// A list of merged rectangles.
internal static List MergeRectangles (List rectangles, bool minimize)
{
- if (rectangles.Count == 0)
+ if (rectangles.Count <= 1)
{
- return [];
+ return rectangles.ToList ();
}
- // Sweep-line algorithm to merge rectangles
- List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2); // Pre-allocate
-
+ // Generate events
+ List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2);
foreach (Rectangle r in rectangles)
{
if (!r.IsEmpty)
{
- events.Add ((r.Left, true, r.Top, r.Bottom)); // Start event
- events.Add ((r.Right, false, r.Top, r.Bottom)); // End event
+ events.Add ((r.Left, true, r.Top, r.Bottom));
+ events.Add ((r.Right, false, r.Top, r.Bottom));
}
}
if (events.Count == 0)
{
- return []; // Return empty list if no non-empty rectangles exist
+ return [];
}
+ // Sort events:
+ // 1. Primarily by x-coordinate.
+ // 2. Secondary: End events before Start events at the same x.
+ // 3. Tertiary: By yTop coordinate as a tie-breaker.
+ // 4. Quaternary: By yBottom coordinate as a final tie-breaker.
events.Sort (
(a, b) =>
{
+ // 1. Sort by X
int cmp = a.x.CompareTo (b.x);
+ if (cmp != 0) return cmp;
- if (cmp != 0)
- {
- return cmp;
- }
+ // 2. Sort End events before Start events
+ bool aIsEnd = !a.isStart;
+ bool bIsEnd = !b.isStart;
+ cmp = aIsEnd.CompareTo (bIsEnd); // True (End) comes after False (Start)
+ if (cmp != 0) return -cmp; // Reverse: End (true) should come before Start (false)
- return a.isStart.CompareTo (b.isStart); // Start events before end events at same x
+ // 3. Tie-breaker: Sort by yTop
+ cmp = a.yTop.CompareTo (b.yTop);
+ if (cmp != 0) return cmp;
+
+ // 4. Final Tie-breaker: Sort by yBottom
+ return a.yBottom.CompareTo (b.yBottom);
});
List merged = [];
+ // Use a dictionary to track active intervals and their overlap counts
+ Dictionary<(int yTop, int yBottom), int> activeCounts = new ();
+ // Comparer for sorting intervals when needed
+ var intervalComparer = Comparer<(int yTop, int yBottom)>.Create (
+ (a, b) =>
+ {
+ int cmp = a.yTop.CompareTo (b.yTop);
+ return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom);
+ });
- SortedSet<(int yTop, int yBottom)> active = new (
- Comparer<(int yTop, int yBottom)>.Create (
- (a, b) =>
- {
- int cmp = a.yTop.CompareTo (b.yTop);
-
- return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom);
- }));
- int lastX = events [0].x;
-
- foreach ((int x, bool isStart, int yTop, int yBottom) evt in events)
+ // Helper to get the current active intervals (where count > 0) as a SortedSet
+ SortedSet<(int yTop, int yBottom)> GetActiveIntervals ()
{
- // Output rectangles for the previous segment if there are active rectangles
- if (active.Count > 0 && evt.x > lastX)
+ var set = new SortedSet<(int yTop, int yBottom)> (intervalComparer);
+ foreach (var kvp in activeCounts)
{
- merged.AddRange (MergeVerticalIntervals (active, lastX, evt.x));
+ if (kvp.Value > 0)
+ {
+ set.Add (kvp.Key);
+ }
+ }
+ return set;
+ }
+
+ // Group events by x-coordinate to process all events at a given x together
+ var groupedEvents = events.GroupBy (e => e.x).OrderBy (g => g.Key);
+ int lastX = groupedEvents.First ().Key; // Initialize with the first event's x
+
+ foreach (var group in groupedEvents)
+ {
+ int currentX = group.Key;
+ // Get active intervals based on state *before* processing events at currentX
+ var currentActiveIntervals = GetActiveIntervals ();
+
+ // 1. Output rectangles for the segment ending *before* this x coordinate
+ if (currentX > lastX && currentActiveIntervals.Count > 0)
+ {
+ merged.AddRange (MergeVerticalIntervals (currentActiveIntervals, lastX, currentX));
}
- // Process the event
- if (evt.isStart)
+ // 2. Process all events *at* this x coordinate to update counts
+ foreach (var evt in group)
{
- active.Add ((evt.yTop, evt.yBottom));
- }
- else
- {
- active.Remove ((evt.yTop, evt.yBottom));
+ var interval = (evt.yTop, evt.yBottom);
+ if (evt.isStart)
+ {
+ activeCounts.TryGetValue (interval, out int count);
+ activeCounts [interval] = count + 1;
+ }
+ else
+ {
+ // Only decrement/remove if the interval exists
+ if (activeCounts.TryGetValue (interval, out int count))
+ {
+ if (count - 1 <= 0)
+ {
+ activeCounts.Remove (interval);
+ }
+ else
+ {
+ activeCounts [interval] = count - 1;
+ }
+ }
+ }
}
- lastX = evt.x;
+ // 3. Update lastX for the next segment
+ lastX = currentX;
}
return minimize ? MinimizeRectangles (merged) : merged;
diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json
index 230533b29..df672d65c 100644
--- a/Terminal.Gui/Resources/config.json
+++ b/Terminal.Gui/Resources/config.json
@@ -52,6 +52,9 @@
"MessageBox.DefaultButtonAlignment": "Center",
"MessageBox.DefaultBorderStyle": "Heavy",
"Button.DefaultShadow": "Opaque",
+ "Menuv2.DefaultBorderStyle": "Single",
+ "MenuBarv2.DefaultBorderStyle": "None",
+ "StatusBar.DefaultSeparatorLineStyle": "Single",
"ColorSchemes": [
{
"TopLevel": {
@@ -132,16 +135,16 @@
"Background": "DarkBlue"
},
"Focus": {
- "Foreground": "White",
- "Background": "Blue"
+ "Foreground": "DarkBlue",
+ "Background": "White"
},
"HotNormal": {
"Foreground": "Yellow",
"Background": "DarkBlue"
},
"HotFocus": {
- "Foreground": "Yellow",
- "Background": "Blue"
+ "Foreground": "Blue",
+ "Background": "White"
},
"Disabled": {
"Foreground": "Gray",
@@ -853,6 +856,18 @@
}
]
}
+ },
+ {
+ "Minimal": {
+ "Dialog.DefaultShadow": "None",
+ "FrameView.DefaultBorderStyle": "None",
+ "Window.DefaultBorderStyle": "None",
+ "MessageBox.DefaultBorderStyle": "None",
+ "Button.DefaultShadow": "None",
+ "Menuv2.DefaultBorderStyle": "None",
+ "Glyphs.LeftBracket": "[",
+ "Glyphs.RightBracket": "]"
+ }
}
]
}
\ No newline at end of file
diff --git a/Terminal.Gui/Terminal.Gui.sln b/Terminal.Gui/Terminal.Gui.sln
new file mode 100644
index 000000000..7724d3f2e
--- /dev/null
+++ b/Terminal.Gui/Terminal.Gui.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.2.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui", "Terminal.Gui.csproj", "{79692A4F-7704-552C-0EF5-40B81C4F2E81}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {79692A4F-7704-552C-0EF5-40B81C4F2E81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {79692A4F-7704-552C-0EF5-40B81C4F2E81}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {79692A4F-7704-552C-0EF5-40B81C4F2E81}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {79692A4F-7704-552C-0EF5-40B81C4F2E81}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {4B162456-436F-4899-B765-26C9DBD2991D}
+ EndGlobalSection
+EndGlobal
diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs
index 0cb68c94f..9700e7e14 100644
--- a/Terminal.Gui/View/Adornment/Border.cs
+++ b/Terminal.Gui/View/Adornment/Border.cs
@@ -201,6 +201,7 @@ public class Border : Adornment
);
}
+ // TODO: Make LineStyle nullable https://github.com/gui-cs/Terminal.Gui/issues/4021
///
/// Sets the style of the border by changing the . This is a helper API for setting the
/// to (1,1,1,1) and setting the line style of the views that comprise the border. If
diff --git a/Terminal.Gui/View/Adornment/Margin.cs b/Terminal.Gui/View/Adornment/Margin.cs
index 9d5dc44e8..25ad7bf86 100644
--- a/Terminal.Gui/View/Adornment/Margin.cs
+++ b/Terminal.Gui/View/Adornment/Margin.cs
@@ -104,25 +104,86 @@ public class Margin : Adornment
ShadowStyle = base.ShadowStyle;
}
- ///
- /// The color scheme for the Margin. If set to (the default), the margin will be transparent.
- ///
- public override ColorScheme? ColorScheme
- {
- get
- {
- if (base.ColorScheme is { })
- {
- return base.ColorScheme;
- }
+ // TODO: We may actualy need this. Not clear what broke, if anything by commenting it out. See https://github.com/gui-cs/Terminal.Gui/issues/4016
+ ///////
+ /////// The color scheme for the Margin. If set to (the default), the margin will be transparent.
+ ///////
+ //public override ColorScheme? ColorScheme
+ //{
+ // get
+ // {
+ // //if (base.ColorScheme is { })
+ // {
+ // return base.ColorScheme;
+ // }
- return (Parent?.SuperView?.ColorScheme ?? Colors.ColorSchemes ["TopLevel"])!;
- }
- set
+ // //return (Parent?.SuperView?.ColorScheme ?? Colors.ColorSchemes ["TopLevel"])!;
+ // }
+ // set
+ // {
+ // base.ColorScheme = value;
+ // Parent?.SetNeedsDraw ();
+ // }
+ //}
+
+ ///
+ public override Attribute GetNormalColor ()
+ {
+ if (_colorScheme is { })
{
- base.ColorScheme = value;
- Parent?.SetNeedsDraw ();
+ return _colorScheme.Normal;
}
+ if (Parent is { })
+ {
+ return Parent.GetNormalColor ();
+ }
+
+ return base.GetNormalColor ();
+ }
+
+ ///
+ public override Attribute GetHotNormalColor ()
+ {
+ if (Parent is { })
+ {
+ return Parent.GetHotNormalColor ();
+ }
+ return base.GetHotNormalColor ();
+ }
+
+ ///
+ public override Attribute GetFocusColor ()
+ {
+ if (Parent is { })
+ {
+ return Parent.GetFocusColor ();
+ }
+ return base.GetFocusColor ();
+ }
+
+ ///
+ public override Attribute GetHotFocusColor ()
+ {
+ if (Parent is { })
+ {
+ return Parent.GetHotFocusColor ();
+ }
+
+ return base.GetHotFocusColor ();
+ }
+
+ ///
+ protected override bool OnSettingNormalAttribute ()
+ {
+ if (Parent is { })
+ {
+ SetAttribute (Parent.GetNormalColor ());
+
+ return true;
+ }
+
+ return false;
+
}
///
@@ -138,6 +199,8 @@ public class Margin : Adornment
// This just draws/clears the thickness, not the insides.
if (Diagnostics.HasFlag (ViewDiagnosticFlags.Thickness) || base.ColorScheme is { })
{
+ // TODO: This is a hack. See https://github.com/gui-cs/Terminal.Gui/issues/4016
+ SetAttribute (GetNormalColor ());
Thickness.Draw (screen, Diagnostics, ToString ());
}
diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs
index fa158cbdf..284d6da8e 100644
--- a/Terminal.Gui/View/Adornment/ShadowView.cs
+++ b/Terminal.Gui/View/Adornment/ShadowView.cs
@@ -54,6 +54,7 @@ internal class ShadowView : View
///
protected override bool OnDrawingContent ()
{
+ SetAttribute (GetNormalColor ());
switch (ShadowStyle)
{
case ShadowStyle.Opaque:
diff --git a/Terminal.Gui/View/IDesignable.cs b/Terminal.Gui/View/IDesignable.cs
index febaaf45e..13d8cbae6 100644
--- a/Terminal.Gui/View/IDesignable.cs
+++ b/Terminal.Gui/View/IDesignable.cs
@@ -12,7 +12,7 @@ public interface IDesignable
/// Optional arbitrary, View-specific, context.
/// A non-null type for .
/// if the view successfully loaded demo data.
- public bool EnableForDesign (ref readonly TContext context) where TContext : notnull => EnableForDesign ();
+ public bool EnableForDesign (ref TContext context) where TContext : notnull => EnableForDesign ();
///
/// Causes the View to enable design-time mode. This typically means that the view will load demo data and
diff --git a/Terminal.Gui/View/View.Adornments.cs b/Terminal.Gui/View/View.Adornments.cs
index 9e0549839..c2d728058 100644
--- a/Terminal.Gui/View/View.Adornments.cs
+++ b/Terminal.Gui/View/View.Adornments.cs
@@ -121,6 +121,7 @@ public partial class View // Adornments
///
public Border? Border { get; private set; }
+ // TODO: Make BorderStyle nullable https://github.com/gui-cs/Terminal.Gui/issues/4021
/// Gets or sets whether the view has a one row/col thick border.
///
///
@@ -133,7 +134,7 @@ public partial class View // Adornments
/// to `0` and to .
///
///
- /// Calls and raises , which allows change
+ /// Raises and raises , which allows change
/// to be cancelled.
///
/// For more advanced customization of the view's border, manipulate see directly.
@@ -148,44 +149,21 @@ public partial class View // Adornments
return;
}
- LineStyle old = Border?.LineStyle ?? LineStyle.None;
-
- // It's tempting to try to optimize this by checking that old != value and returning.
- // Do not.
-
- CancelEventArgs e = new (ref old, ref value);
-
- if (OnBorderStyleChanging (e) || e.Cancel)
- {
- return;
- }
-
- BorderStyleChanging?.Invoke (this, e);
-
- if (e.Cancel)
- {
- return;
- }
-
- SetBorderStyle (e.NewValue);
- SetAdornmentFrames ();
- SetNeedsLayout ();
+ SetBorderStyle (value);
+ OnBorderStyleChanged ();
+ BorderStyleChanged?.Invoke (this, EventArgs.Empty);
}
}
///
- /// Called when the is changing.
+ /// Called when the has changed.
///
- ///
- /// Set e.Cancel to true to prevent the from changing.
- ///
- ///
- protected virtual bool OnBorderStyleChanging (CancelEventArgs e) { return false; }
+ protected virtual bool OnBorderStyleChanged () { return false; }
///
- /// Fired when the is changing. Allows the event to be cancelled.
+ /// Fired when the has changed.
///
- public event EventHandler>? BorderStyleChanging;
+ public event EventHandler? BorderStyleChanged;
///
/// Sets the of the view to the specified value.
@@ -204,7 +182,7 @@ public partial class View // Adornments
/// For more advanced customization of the view's border, manipulate see directly.
///
///
- public virtual void SetBorderStyle (LineStyle style)
+ internal void SetBorderStyle (LineStyle style)
{
if (style != LineStyle.None)
{
@@ -219,6 +197,9 @@ public partial class View // Adornments
}
Border.LineStyle = style;
+
+ SetAdornmentFrames ();
+ SetNeedsLayout ();
}
///
diff --git a/Terminal.Gui/View/View.Attribute.cs b/Terminal.Gui/View/View.Attribute.cs
index 20e201b66..02ac0f3e5 100644
--- a/Terminal.Gui/View/View.Attribute.cs
+++ b/Terminal.Gui/View/View.Attribute.cs
@@ -1,121 +1,21 @@
#nullable enable
+using System.ComponentModel;
+
namespace Terminal.Gui;
public partial class View
{
- // TODO: Rename "Color"->"Attribute" given we'll soon have non-color information in Attributes?
- // TODO: See https://github.com/gui-cs/Terminal.Gui/issues/457
-
- #region ColorScheme
-
- private ColorScheme? _colorScheme;
-
- /// The color scheme for this view, if it is not defined, it returns the 's color scheme.
- public virtual ColorScheme? ColorScheme
- {
- get => _colorScheme ?? SuperView?.ColorScheme;
- set
- {
- if (_colorScheme == value)
- {
- return;
- }
-
- _colorScheme = value;
-
- // BUGBUG: This should be in Border.cs somehow
- if (Border is { } && Border.LineStyle != LineStyle.None && Border.ColorScheme is { })
- {
- Border.ColorScheme = _colorScheme;
- }
-
- SetNeedsDraw ();
- }
- }
-
- /// Determines the current based on the value.
- ///
- /// if is or
- /// if is . If it's
- /// overridden can return other values.
- ///
- public virtual Attribute GetFocusColor ()
- {
- ColorScheme? cs = ColorScheme ?? new ();
-
- return Enabled ? GetColor (cs.Focus) : cs.Disabled;
- }
-
- /// Determines the current based on the value.
- ///
- /// if is or
- /// if is . If it's
- /// overridden can return other values.
- ///
- public virtual Attribute GetHotFocusColor ()
- {
- ColorScheme? cs = ColorScheme ?? new ();
-
- return Enabled ? GetColor (cs.HotFocus) : cs.Disabled;
- }
-
- /// Determines the current based on the value.
- ///
- /// if is or
- /// if is . If it's
- /// overridden can return other values.
- ///
- public virtual Attribute GetHotNormalColor ()
- {
- ColorScheme? cs = ColorScheme ?? new ();
-
- return Enabled ? GetColor (cs.HotNormal) : cs.Disabled;
- }
-
- /// Determines the current based on the value.
- ///
- /// if is or
- /// if is . If it's
- /// overridden can return other values.
- ///
- public virtual Attribute GetNormalColor ()
- {
- ColorScheme? cs = ColorScheme ?? new ();
-
- Attribute disabled = new (cs.Disabled.Foreground, cs.Disabled.Background);
-
- if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering)
- {
- disabled = new (disabled.Foreground.GetDarkerColor (), disabled.Background.GetDarkerColor ());
- }
-
- return Enabled ? GetColor (cs.Normal) : disabled;
- }
-
- private Attribute GetColor (Attribute inputAttribute)
- {
- Attribute attr = inputAttribute;
-
- if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering)
- {
- attr = new (attr.Foreground.GetDarkerColor (), attr.Background.GetDarkerColor ());
- }
-
- return attr;
- }
-
- #endregion ColorScheme
-
- #region Attribute
-
/// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.
///
/// THe Attribute to set.
- public Attribute SetAttribute (Attribute attribute) { return Driver?.SetAttribute (attribute) ?? Attribute.Default; }
+ public Attribute SetAttribute (Attribute attribute)
+ {
+ return Driver?.SetAttribute (attribute) ?? Attribute.Default;
+ }
/// Gets the current .
/// The current attribute.
public Attribute GetAttribute () { return Driver?.GetAttribute () ?? Attribute.Default; }
- #endregion Attribute
+
}
diff --git a/Terminal.Gui/View/View.ColorScheme.cs b/Terminal.Gui/View/View.ColorScheme.cs
new file mode 100644
index 000000000..14abe6fbd
--- /dev/null
+++ b/Terminal.Gui/View/View.ColorScheme.cs
@@ -0,0 +1,211 @@
+#nullable enable
+using System.ComponentModel;
+
+namespace Terminal.Gui;
+
+public partial class View
+{
+ // TODO: See https://github.com/gui-cs/Terminal.Gui/issues/4014
+ // TODO: See https://github.com/gui-cs/Terminal.Gui/issues/4016
+ // TODO: Enable ability to tell if ColorScheme was explicitly set; ColorScheme, as is, hides this.
+ internal ColorScheme? _colorScheme;
+
+ /// The color scheme for this view, if it is not defined, it returns the 's color scheme.
+ public virtual ColorScheme? ColorScheme
+ {
+ // BUGBUG: This prevents the ability to know if ColorScheme was explicitly set or not.
+ get => _colorScheme ?? SuperView?.ColorScheme;
+ set
+ {
+ if (_colorScheme == value)
+ {
+ return;
+ }
+
+ _colorScheme = value;
+
+ // BUGBUG: This should be in Border.cs somehow
+ if (Border is { } && Border.LineStyle != LineStyle.None && Border.ColorScheme is { })
+ {
+ Border.ColorScheme = _colorScheme;
+ }
+
+ SetNeedsDraw ();
+ }
+ }
+
+ /// Determines the current based on the value.
+ ///
+ /// if is or
+ /// if is . If it's
+ /// overridden can return other values.
+ ///
+ public virtual Attribute GetFocusColor ()
+ {
+ Attribute currAttribute = ColorScheme?.Normal ?? Attribute.Default;
+ var newAttribute = new Attribute ();
+ CancelEventArgs args = new (in currAttribute, ref newAttribute);
+ GettingFocusColor?.Invoke (this, args);
+
+ if (args.Cancel)
+ {
+ return args.NewValue;
+ }
+
+ ColorScheme? cs = ColorScheme ?? new ();
+
+ return Enabled ? GetColor (cs.Focus) : cs.Disabled;
+ }
+
+ ///
+ /// Raised the Focus Color is being retrieved, from . Cancel the event and set the new
+ /// attribute in the event args to
+ /// a different value to change the focus color.
+ ///
+ public event EventHandler>? GettingFocusColor;
+
+ /// Determines the current based on the value.
+ ///
+ /// if is or
+ /// if is . If it's
+ /// overridden can return other values.
+ ///
+ public virtual Attribute GetHotFocusColor ()
+ {
+ Attribute currAttribute = ColorScheme?.Normal ?? Attribute.Default;
+ var newAttribute = new Attribute ();
+ CancelEventArgs args = new (in currAttribute, ref newAttribute);
+ GettingHotFocusColor?.Invoke (this, args);
+
+ if (args.Cancel)
+ {
+ return args.NewValue;
+ }
+
+ ColorScheme? cs = ColorScheme ?? new ();
+
+ return Enabled ? GetColor (cs.HotFocus) : cs.Disabled;
+ }
+
+ ///
+ /// Raised the HotFocus Color is being retrieved, from . Cancel the event and set the new
+ /// attribute in the event args to
+ /// a different value to change the focus color.
+ ///
+ public event EventHandler>? GettingHotFocusColor;
+
+ /// Determines the current based on the value.
+ ///
+ /// if is or
+ /// if is . If it's
+ /// overridden can return other values.
+ ///
+ public virtual Attribute GetHotNormalColor ()
+ {
+ Attribute currAttribute = ColorScheme?.Normal ?? Attribute.Default;
+ var newAttribute = new Attribute ();
+ CancelEventArgs args = new (in currAttribute, ref newAttribute);
+ GettingHotNormalColor?.Invoke (this, args);
+
+ if (args.Cancel)
+ {
+ return args.NewValue;
+ }
+
+ ColorScheme? cs = ColorScheme ?? new ();
+
+ return Enabled ? GetColor (cs.HotNormal) : cs.Disabled;
+ }
+
+ ///
+ /// Raised the HotNormal Color is being retrieved, from . Cancel the event and set the
+ /// new attribute in the event args to
+ /// a different value to change the focus color.
+ ///
+ public event EventHandler>? GettingHotNormalColor;
+
+ /// Determines the current based on the value.
+ ///
+ /// if is or
+ /// if is . If it's
+ /// overridden can return other values.
+ ///
+ public virtual Attribute GetNormalColor ()
+ {
+ Attribute currAttribute = ColorScheme?.Normal ?? Attribute.Default;
+ var newAttribute = new Attribute ();
+ CancelEventArgs args = new (in currAttribute, ref newAttribute);
+ GettingNormalColor?.Invoke (this, args);
+
+ if (args.Cancel)
+ {
+ return args.NewValue;
+ }
+
+ ColorScheme? cs = ColorScheme ?? new ();
+ Attribute disabled = new (cs.Disabled.Foreground, cs.Disabled.Background);
+
+ if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering)
+ {
+ disabled = new (disabled.Foreground.GetDarkerColor (), disabled.Background.GetDarkerColor ());
+ }
+
+ return Enabled ? GetColor (cs.Normal) : disabled;
+ }
+
+ ///
+ /// Raised the Normal Color is being retrieved, from . Cancel the event and set the new
+ /// attribute in the event args to
+ /// a different value to change the focus color.
+ ///
+ public event EventHandler>? GettingNormalColor;
+
+ ///
+ /// Sets the Normal attribute if the setting process is not canceled. It triggers an event and checks for
+ /// cancellation before proceeding.
+ ///
+ public void SetNormalAttribute ()
+ {
+ if (OnSettingNormalAttribute ())
+ {
+ return;
+ }
+
+ var args = new CancelEventArgs ();
+ SettingNormalAttribute?.Invoke (this, args);
+
+ if (args.Cancel)
+ {
+ return;
+ }
+
+ if (ColorScheme is { })
+ {
+ SetAttribute (GetNormalColor ());
+ }
+ }
+
+ ///
+ /// Called when the normal attribute for the View is to be set. This is called before the View is drawn.
+ ///
+ /// to stop default behavior.
+ protected virtual bool OnSettingNormalAttribute () { return false; }
+
+ /// Raised when the normal attribute for the View is to be set. This is raised before the View is drawn.
+ ///
+ /// Set to to stop default behavior.
+ ///
+ public event EventHandler? SettingNormalAttribute;
+
+ private Attribute GetColor (Attribute inputAttribute)
+ {
+ Attribute attr = inputAttribute;
+
+ if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering)
+ {
+ attr = new (attr.Foreground.GetDarkerColor (), attr.Background.GetDarkerColor ());
+ }
+
+ return attr;
+ }
+}
diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs
index 8446c9f97..366d7e09f 100644
--- a/Terminal.Gui/View/View.Command.cs
+++ b/Terminal.Gui/View/View.Command.cs
@@ -1,5 +1,6 @@
#nullable enable
using System.ComponentModel;
+using System.Dynamic;
namespace Terminal.Gui;
@@ -115,18 +116,18 @@ public partial class View // Command APIs
///
protected bool? RaiseAccepting (ICommandContext? ctx)
{
- Logging.Trace($"{ctx?.Source?.Title}");
+ Logging.Debug ($"{Title} ({ctx?.Source?.Title})");
CommandEventArgs args = new () { Context = ctx };
// Best practice is to invoke the virtual method first.
// This allows derived classes to handle the event and potentially cancel it.
- Logging.Trace ($"Calling OnAccepting...");
+ Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Calling OnAccepting...");
args.Cancel = OnAccepting (args) || args.Cancel;
- if (!args.Cancel)
+ if (!args.Cancel && Accepting is {})
{
// If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
- Logging.Trace ($"Raising Accepting...");
+ Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Raising Accepting...");
Accepting?.Invoke (this, args);
}
@@ -142,7 +143,9 @@ public partial class View // Command APIs
{
// TODO: It's a bit of a hack that this uses KeyBinding. There should be an InvokeCommmand that
// TODO: is generic?
- bool? handled = isDefaultView.InvokeCommand (Command.Accept, ctx);
+
+ Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - InvokeCommand on Default View ({isDefaultView.Title})");
+ bool ? handled = isDefaultView.InvokeCommand (Command.Accept, ctx);
if (handled == true)
{
return true;
@@ -151,7 +154,7 @@ public partial class View // Command APIs
if (SuperView is { })
{
- Logging.Trace ($"Invoking Accept on SuperView: {SuperView.Title}...");
+ Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Invoking Accept on SuperView ({SuperView.Title}/{SuperView.Id})...");
return SuperView?.InvokeCommand (Command.Accept, ctx);
}
}
@@ -197,6 +200,7 @@ public partial class View // Command APIs
///
protected bool? RaiseSelecting (ICommandContext? ctx)
{
+ Logging.Debug ($"{Title} ({ctx?.Source?.Title})");
CommandEventArgs args = new () { Context = ctx };
// Best practice is to invoke the virtual method first.
@@ -239,6 +243,7 @@ public partial class View // Command APIs
protected bool? RaiseHandlingHotKey ()
{
CommandEventArgs args = new () { Context = new CommandContext () { Command = Command.HotKey } };
+ Logging.Debug ($"{Title} ({args.Context?.Source?.Title})");
// Best practice is to invoke the virtual method first.
// This allows derived classes to handle the event and potentially cancel it.
@@ -421,7 +426,12 @@ public partial class View // Command APIs
_commandImplementations.TryGetValue (Command.NotBound, out implementation);
}
- return implementation! (null);
+ return implementation! (new CommandContext
+[Obsolete ("Use MenuBarv2 instead.", false)]
public class MenuBar : View, IDesignable
{
// Spaces before the Title
@@ -1680,7 +1681,7 @@ public class MenuBar : View, IDesignable
///
- public bool EnableForDesign (ref readonly TContext context) where TContext : notnull
+ public bool EnableForDesign (ref TContext context) where TContext : notnull
{
if (context is not Func actionFn)
{
diff --git a/Terminal.Gui/Views/Menuv1/MenuBarItem.cs b/Terminal.Gui/Views/Menuv1/MenuBarItem.cs
index e68b1f87b..0a284bed3 100644
--- a/Terminal.Gui/Views/Menuv1/MenuBarItem.cs
+++ b/Terminal.Gui/Views/Menuv1/MenuBarItem.cs
@@ -6,6 +6,7 @@ namespace Terminal.Gui;
/// is a menu item on . MenuBarItems do not support
/// .
///
+[Obsolete ("Use MenuBarItemv2 instead.", false)]
public class MenuBarItem : MenuItem
{
/// Initializes a new as a .
diff --git a/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs
index b578e755c..c6f005eee 100644
--- a/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs
+++ b/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs
@@ -1,5 +1,7 @@
namespace Terminal.Gui;
+#pragma warning disable CS0618 // Type or member is obsolete
+
/// An which allows passing a cancelable menu closing event.
public class MenuClosingEventArgs : EventArgs
{
diff --git a/Terminal.Gui/Views/Menuv1/MenuItem.cs b/Terminal.Gui/Views/Menuv1/MenuItem.cs
index d5dd714bc..7f5742f45 100644
--- a/Terminal.Gui/Views/Menuv1/MenuItem.cs
+++ b/Terminal.Gui/Views/Menuv1/MenuItem.cs
@@ -6,6 +6,8 @@ namespace Terminal.Gui;
/// A has title, an associated help text, and an action to execute on activation. MenuItems
/// can also have a checked indicator (see ).
///
+[Obsolete ("Use MenuItemv2 instead.", false)]
+
public class MenuItem
{
internal MenuBar _menuBar;
diff --git a/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs
index 7bdc9df43..4e9879847 100644
--- a/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs
+++ b/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs
@@ -1,4 +1,5 @@
namespace Terminal.Gui;
+#pragma warning disable CS0618 // Type or member is obsolete
/// Defines arguments for the event
public class MenuOpenedEventArgs : EventArgs
diff --git a/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs
index d7f23d36f..8956e0190 100644
--- a/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs
+++ b/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs
@@ -1,5 +1,7 @@
namespace Terminal.Gui;
+#pragma warning disable CS0618 // Type or member is obsolete
+
///
/// An which allows passing a cancelable menu opening event or replacing with a new
/// .
diff --git a/Terminal.Gui/Views/OptionSelector.cs b/Terminal.Gui/Views/OptionSelector.cs
new file mode 100644
index 000000000..02e1067d9
--- /dev/null
+++ b/Terminal.Gui/Views/OptionSelector.cs
@@ -0,0 +1,318 @@
+#nullable enable
+namespace Terminal.Gui;
+
+///
+/// Provides a user interface for displaying and selecting a single item from a list of options.
+/// Each option is represented by a checkbox, but only one can be selected at a time.
+///
+public class OptionSelector : View, IOrientation, IDesignable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OptionSelector ()
+ {
+ CanFocus = true;
+
+ Width = Dim.Auto (DimAutoStyle.Content);
+ Height = Dim.Auto (DimAutoStyle.Content);
+
+ _orientationHelper = new (this);
+ _orientationHelper.Orientation = Orientation.Vertical;
+
+ // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state
+ AddCommand (Command.Accept, HandleAcceptCommand);
+
+ CreateCheckBoxes ();
+ }
+
+ private bool? HandleAcceptCommand (ICommandContext? ctx) { return RaiseAccepting (ctx); }
+
+ private int? _selectedItem;
+
+ ///
+ /// Gets or sets the index of the selected item.
+ ///
+ public int? SelectedItem
+ {
+ get => _selectedItem;
+ set
+ {
+ if (_selectedItem == value)
+ {
+ return;
+ }
+
+ int? previousSelectedItem = _selectedItem;
+ _selectedItem = value;
+
+ UpdateChecked ();
+
+ RaiseSelectedItemChanged (previousSelectedItem);
+ }
+ }
+
+ private void RaiseSelectedItemChanged (int? previousSelectedItem)
+ {
+ OnSelectedItemChanged (SelectedItem, previousSelectedItem);
+ if (SelectedItem.HasValue)
+ {
+ SelectedItemChanged?.Invoke (this, new (SelectedItem, previousSelectedItem));
+ }
+ }
+
+ ///
+ /// Called when has changed.
+ ///
+ protected virtual void OnSelectedItemChanged (int? selectedItem, int? previousSelectedItem) { }
+
+ ///
+ /// Raised when has changed.
+ ///
+ public event EventHandler? SelectedItemChanged;
+
+ private IReadOnlyList? _options;
+
+ ///
+ /// Gets or sets the list of options.
+ ///
+ public IReadOnlyList? Options
+ {
+ get => _options;
+ set
+ {
+ _options = value;
+ CreateCheckBoxes ();
+ }
+ }
+
+ private bool _assignHotKeysToCheckBoxes;
+
+ ///
+ /// If the CheckBoxes will each be automatically assigned a hotkey.
+ /// will be used to ensure unique keys are assigned. Set
+ /// before setting with any hotkeys that may conflict with other Views.
+ ///
+ public bool AssignHotKeysToCheckBoxes
+ {
+ get => _assignHotKeysToCheckBoxes;
+ set
+ {
+ if (_assignHotKeysToCheckBoxes == value)
+ {
+ return;
+ }
+ _assignHotKeysToCheckBoxes = value;
+ CreateCheckBoxes ();
+ UpdateChecked ();
+ }
+ }
+
+ ///
+ /// Gets the list of hotkeys already used by the CheckBoxes or that should not be used if
+ ///
+ /// is enabled.
+ ///
+ public List UsedHotKeys { get; } = new ();
+
+ private void CreateCheckBoxes ()
+ {
+ if (Options is null)
+ {
+ return;
+ }
+
+ foreach (CheckBox cb in RemoveAll ())
+ {
+ cb.Dispose ();
+ }
+
+ for (var index = 0; index < Options.Count; index++)
+ {
+ Add (CreateCheckBox (Options [index], index));
+ }
+
+ SetLayout ();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected virtual CheckBox CreateCheckBox (string name, int index)
+ {
+ string nameWithHotKey = name;
+ if (AssignHotKeysToCheckBoxes)
+ {
+ // Find the first char in label that is [a-z], [A-Z], or [0-9]
+ for (var i = 0; i < name.Length; i++)
+ {
+ char c = char.ToLowerInvariant (name [i]);
+ if (UsedHotKeys.Contains (new (c)) || !char.IsAsciiLetterOrDigit (c))
+ {
+ continue;
+ }
+
+ if (char.IsAsciiLetterOrDigit (c))
+ {
+ char? hotChar = c;
+ nameWithHotKey = name.Insert (i, HotKeySpecifier.ToString ());
+ UsedHotKeys.Add (new (hotChar));
+
+ break;
+ }
+ }
+ }
+
+ var checkbox = new CheckBox
+ {
+ CanFocus = true,
+ Title = nameWithHotKey,
+ Id = name,
+ Data = index,
+ HighlightStyle = HighlightStyle.Hover,
+ RadioStyle = true
+ };
+
+ checkbox.GettingNormalColor += (_, e) =>
+ {
+ if (SuperView is { HasFocus: true })
+ {
+ e.Cancel = true;
+
+ if (!HasFocus)
+ {
+ e.NewValue = GetFocusColor ();
+ }
+ else
+ {
+ // If _colorScheme was set, it's because of Hover
+ if (checkbox._colorScheme is { })
+ {
+ e.NewValue = checkbox._colorScheme.Normal;
+ }
+ else
+ {
+ e.NewValue = GetNormalColor ();
+ }
+ }
+ }
+ };
+
+ checkbox.GettingHotNormalColor += (_, e) =>
+ {
+ if (SuperView is { HasFocus: true })
+ {
+ e.Cancel = true;
+ if (!HasFocus)
+ {
+ e.NewValue = GetHotFocusColor ();
+ }
+ else
+ {
+ // If _colorScheme was set, it's because of Hover
+ if (checkbox._colorScheme is { })
+ {
+ e.NewValue = checkbox._colorScheme.Normal;
+ }
+ else
+ {
+ e.NewValue = GetNormalColor ();
+ }
+ }
+ }
+ };
+ checkbox.Selecting += (sender, args) =>
+ {
+ if (RaiseSelecting (args.Context) is true)
+ {
+ args.Cancel = true;
+
+ return;
+ }
+ ;
+
+ if (RaiseAccepting (args.Context) is true)
+ {
+ args.Cancel = true;
+ }
+ };
+
+ checkbox.CheckedStateChanged += (sender, args) =>
+ {
+ if (checkbox.CheckedState == CheckState.Checked)
+ {
+ SelectedItem = index;
+ }
+ };
+
+ return checkbox;
+ }
+
+ private void SetLayout ()
+ {
+ foreach (View sv in SubViews)
+ {
+ if (Orientation == Orientation.Vertical)
+ {
+ sv.X = 0;
+ sv.Y = Pos.Align (Alignment.Start);
+ }
+ else
+ {
+ sv.X = Pos.Align (Alignment.Start);
+ sv.Y = 0;
+ sv.Margin!.Thickness = new (0, 0, 1, 0);
+ }
+ }
+ }
+
+ private void UpdateChecked ()
+ {
+ foreach (CheckBox cb in SubViews.OfType ())
+ {
+ var index = (int)(cb.Data ?? throw new InvalidOperationException ("CheckBox.Data must be set"));
+
+ cb.CheckedState = index == SelectedItem ? CheckState.Checked : CheckState.UnChecked;
+ }
+ }
+
+ #region IOrientation
+
+ ///
+ /// Gets or sets the for this . The default is
+ /// .
+ ///
+ public Orientation Orientation
+ {
+ get => _orientationHelper.Orientation;
+ set => _orientationHelper.Orientation = value;
+ }
+
+ private readonly OrientationHelper _orientationHelper;
+
+#pragma warning disable CS0067 // The event is never used
+ ///
+ public event EventHandler>? OrientationChanging;
+
+ ///
+ public event EventHandler>? OrientationChanged;
+#pragma warning restore CS0067 // The event is never used
+
+ /// Called when has changed.
+ ///
+ public void OnOrientationChanged (Orientation newOrientation) { SetLayout (); }
+
+ #endregion IOrientation
+
+ ///
+ public bool EnableForDesign ()
+ {
+ AssignHotKeysToCheckBoxes = true;
+ Options = new [] { "Option 1", "Option 2", "Option 3" };
+
+ return true;
+ }
+}
diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs
index daefd8b44..9ff442ec0 100644
--- a/Terminal.Gui/Views/RadioGroup.cs
+++ b/Terminal.Gui/Views/RadioGroup.cs
@@ -41,8 +41,6 @@ public class RadioGroup : View, IDesignable, IOrientation
MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept);
SubViewLayout += RadioGroup_LayoutStarted;
-
- HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
}
private bool? HandleHotKeyCommand (ICommandContext? ctx)
@@ -281,23 +279,24 @@ public class RadioGroup : View, IDesignable, IOrientation
// Pick a unique hotkey for each radio label
for (var labelIndex = 0; labelIndex < value.Length; labelIndex++)
{
- string label = value [labelIndex];
- string? newLabel = label;
+ string name = value [labelIndex];
+ string? nameWithHotKey = name;
if (AssignHotKeysToRadioLabels)
{
// Find the first char in label that is [a-z], [A-Z], or [0-9]
- for (var i = 0; i < label.Length; i++)
+ for (var i = 0; i < name.Length; i++)
{
- if (UsedHotKeys.Contains (new (label [i])) || !char.IsAsciiLetterOrDigit (label [i]))
+ char c = char.ToLowerInvariant (name [i]);
+ if (UsedHotKeys.Contains (new (c)) || !char.IsAsciiLetterOrDigit (c))
{
continue;
}
- if (char.IsAsciiLetterOrDigit (label [i]))
+ if (char.IsAsciiLetterOrDigit (c))
{
- char? hotChar = label [i];
- newLabel = label.Insert (i, HotKeySpecifier.ToString ());
+ char? hotChar = c;
+ nameWithHotKey = name.Insert (i, HotKeySpecifier.ToString ());
UsedHotKeys.Add (new (hotChar));
break;
@@ -305,9 +304,9 @@ public class RadioGroup : View, IDesignable, IOrientation
}
}
- _radioLabels.Add (newLabel);
+ _radioLabels.Add (nameWithHotKey);
- if (TextFormatter.FindHotKey (newLabel, HotKeySpecifier, out _, out Key hotKey))
+ if (TextFormatter.FindHotKey (nameWithHotKey, HotKeySpecifier, out _, out Key hotKey))
{
AddKeyBindingsForHotKey (Key.Empty, hotKey, labelIndex);
}
diff --git a/Terminal.Gui/Views/SelectedItemChangedArgs.cs b/Terminal.Gui/Views/SelectedItemChangedArgs.cs
index a2f5eb47c..dca578b2d 100644
--- a/Terminal.Gui/Views/SelectedItemChangedArgs.cs
+++ b/Terminal.Gui/Views/SelectedItemChangedArgs.cs
@@ -1,4 +1,5 @@
-namespace Terminal.Gui;
+#nullable enable
+namespace Terminal.Gui;
/// Event arguments for the SelectedItemChanged event.
public class SelectedItemChangedArgs : EventArgs
@@ -6,15 +7,15 @@ public class SelectedItemChangedArgs : EventArgs
/// Initializes a new class.
///
///
- public SelectedItemChangedArgs (int selectedItem, int previousSelectedItem)
+ public SelectedItemChangedArgs (int? selectedItem, int? previousSelectedItem)
{
PreviousSelectedItem = previousSelectedItem;
SelectedItem = selectedItem;
}
- /// Gets the index of the item that was previously selected. -1 if there was no previous selection.
- public int PreviousSelectedItem { get; }
+ /// Gets the index of the item that was previously selected. null if there was no previous selection.
+ public int? PreviousSelectedItem { get; }
- /// Gets the index of the item that is now selected. -1 if there is no selection.
- public int SelectedItem { get; }
+ /// Gets the index of the item that is now selected. null if there is no selection.
+ public int? SelectedItem { get; }
}
diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs
index 7f98a9400..ed1464ff7 100644
--- a/Terminal.Gui/Views/Shortcut.cs
+++ b/Terminal.Gui/Views/Shortcut.cs
@@ -60,8 +60,6 @@ public class Shortcut : View, IOrientation, IDesignable
/// The help text to display.
public Shortcut (Key key, string? commandText, Action? action, string? helpText = null)
{
- Id = $"shortcut:{commandText}";
-
HighlightStyle = HighlightStyle.None;
CanFocus = true;
@@ -90,11 +88,11 @@ public class Shortcut : View, IOrientation, IDesignable
Title = commandText ?? string.Empty;
HelpView.Id = "_helpView";
- HelpView.CanFocus = false;
+ //HelpView.CanFocus = false;
HelpView.Text = helpText ?? string.Empty;
KeyView.Id = "_keyView";
- KeyView.CanFocus = false;
+ //KeyView.CanFocus = false;
key ??= Key.Empty;
Key = key;
@@ -119,18 +117,6 @@ public class Shortcut : View, IOrientation, IDesignable
// Once Frame.Width gets below this value, LayoutStarted makes HelpView an KeyView smaller.
private int? _minimumNaturalWidth;
- ///
- protected override bool OnHighlight (CancelEventArgs args)
- {
- if (args.NewValue.HasFlag (HighlightStyle.Hover))
- {
- SetFocus ();
- return true;
- }
-
- return false;
- }
-
///
/// Gets or sets the for this .
///
@@ -176,9 +162,6 @@ public class Shortcut : View, IOrientation, IDesignable
Add (KeyView);
SetKeyViewDefaultLayout ();
}
-
- // BUGBUG: Causes ever other layout to lose focus colors
- //SetColors ();
}
// Force Width to DimAuto to calculate natural width and then set it back
@@ -260,44 +243,54 @@ public class Shortcut : View, IOrientation, IDesignable
}
///
- /// Called when a Command has been invoked on this Shortcut.
+ /// Dispatches the Command in the (Raises Selected, then Accepting, then invoke the Action, if any).
+ /// Called when Command.Select, Accept, or HotKey has been invoked on this Shortcut.
///
///
- ///
+ ///
+ /// if no event was raised; input processing should continue.
+ /// if the event was raised and was not handled (or cancelled); input processing should continue.
+ /// if the event was raised and handled (or cancelled); input processing should stop.
+ ///
internal virtual bool? DispatchCommand (ICommandContext? commandContext)
{
- Logging.Trace($"{commandContext?.Source?.Title}");
CommandContext? keyCommandContext = commandContext as CommandContext? ?? default (CommandContext);
+ Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) Command: {commandContext?.Command}");
+
if (keyCommandContext?.Binding.Data != this)
{
+ // TODO: Optimize this to only do this if CommandView is custom (non View)
// Invoke Select on the CommandView to cause it to change state if it wants to
// If this causes CommandView to raise Accept, we eat it
keyCommandContext = keyCommandContext!.Value with { Binding = keyCommandContext.Value.Binding with { Data = this } };
- Logging.Trace ($"Invoking Select on CommandView.");
+ Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Invoking Select on CommandView ({CommandView.GetType ().Name}).");
CommandView.InvokeCommand (Command.Select, keyCommandContext);
}
- // BUGBUG: Why does this use keyCommandContext and not commandContext?
- Logging.Trace ($"RaiseSelecting ...");
- if (RaiseSelecting (keyCommandContext) is true)
+ Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - RaiseSelecting ...");
+
+ if (RaiseSelecting (commandContext) is true)
{
return true;
}
- // The default HotKey handler sets Focus
- Logging.Trace ($"SetFocus...");
- SetFocus ();
+ if (CanFocus && SuperView is { CanFocus: true })
+ {
+ // The default HotKey handler sets Focus
+ Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - SetFocus...");
+ SetFocus ();
+ }
var cancel = false;
- if (commandContext is { })
+ if (commandContext is { Source: null })
{
commandContext.Source = this;
}
- Logging.Trace ($"RaiseAccepting...");
+ Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Calling RaiseAccepting...");
cancel = RaiseAccepting (commandContext) is true;
if (cancel)
@@ -305,21 +298,15 @@ public class Shortcut : View, IOrientation, IDesignable
return true;
}
- if (commandContext?.Command != Command.Accept)
- {
- // return false;
- }
-
if (Action is { })
{
- Logging.Trace ($"Invoke Action...");
+ Logging.Debug ($"{Title} ({commandContext?.Source?.Title}) - Invoke Action...");
Action.Invoke ();
// Assume if there's a subscriber to Action, it's handled.
cancel = true;
}
-
return cancel;
}
@@ -437,7 +424,7 @@ public class Shortcut : View, IOrientation, IDesignable
// The default behavior is for CommandView to not get focus. I
// If you want it to get focus, you need to set it.
- _commandView.CanFocus = false;
+ // _commandView.CanFocus = false;
_commandView.HotKeyChanged += (s, e) =>
{
@@ -492,9 +479,31 @@ public class Shortcut : View, IOrientation, IDesignable
CommandView.VerticalTextAlignment = Alignment.Center;
CommandView.TextAlignment = Alignment.Start;
CommandView.TextFormatter.WordWrap = false;
- CommandView.HighlightStyle = HighlightStyle.None;
+ //CommandView.HighlightStyle = HighlightStyle.None;
+ CommandView.GettingNormalColor += CommandViewOnGettingNormalColor;
+ CommandView.GettingHotNormalColor += CommandViewOnGettingHotNormalColor;
+
}
+ private void CommandViewOnGettingNormalColor (object? sender, CancelEventArgs e)
+ {
+ if (HasFocus)
+ {
+ e.Cancel = true;
+ e.NewValue = GetFocusColor ();
+ }
+ }
+
+ private void CommandViewOnGettingHotNormalColor (object? sender, CancelEventArgs e)
+ {
+ if (HasFocus && e is { })
+ {
+ e.Cancel = true;
+ e.NewValue = GetHotFocusColor ();
+ }
+ }
+
+
private void Shortcut_TitleChanged (object? sender, EventArgs e)
{
// If the Title changes, update the CommandView text.
@@ -533,6 +542,9 @@ public class Shortcut : View, IOrientation, IDesignable
HelpView.TextAlignment = Alignment.Start;
HelpView.TextFormatter.WordWrap = false;
HelpView.HighlightStyle = HighlightStyle.None;
+
+ HelpView.GettingNormalColor += CommandViewOnGettingNormalColor;
+ HelpView.GettingHotNormalColor += CommandViewOnGettingHotNormalColor;
}
///
@@ -619,10 +631,10 @@ public class Shortcut : View, IOrientation, IDesignable
}
///
- /// Gets the subview that displays the key. Internal for unit testing.
+ /// Gets the subview that displays the key. Is drawn with Normal and HotNormal colors reversed.
///
- public View KeyView { get; } = new ();
+ public ShortcutKeyView KeyView { get; } = new ();
private int _minimumKeyTextSize;
@@ -698,17 +710,6 @@ public class Shortcut : View, IOrientation, IDesignable
#region Focus
- ///
- public override ColorScheme? ColorScheme
- {
- get => base.ColorScheme;
- set
- {
- base.ColorScheme = _nonFocusColorScheme = value;
- SetColors ();
- }
- }
-
private bool _forceFocusColors;
///
@@ -720,78 +721,31 @@ public class Shortcut : View, IOrientation, IDesignable
set
{
_forceFocusColors = value;
- SetColors (value);
- //SetNeedsDraw();
+ SetNeedsDraw ();
}
}
- private ColorScheme? _nonFocusColorScheme;
-
- ///
- ///
- internal void SetColors (bool highlight = false)
+ ///
+ public override Attribute GetNormalColor ()
{
- if (HasFocus || highlight || ForceFocusColors)
+ if (HasFocus)
{
- if (_nonFocusColorScheme is null)
- {
- _nonFocusColorScheme = base.ColorScheme;
- }
-
- base.ColorScheme ??= new (Attribute.Default);
-
- // When we have focus, we invert the colors
- base.ColorScheme = new (base.ColorScheme)
- {
- Normal = GetFocusColor (),
- HotNormal = GetHotFocusColor (),
- HotFocus = GetHotNormalColor (),
- Focus = GetNormalColor (),
- };
- }
- else
- {
- if (_nonFocusColorScheme is { })
- {
- base.ColorScheme = _nonFocusColorScheme;
- //_nonFocusColorScheme = null;
- }
- else
- {
- base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme;
- }
+ return base.GetFocusColor ();
}
- // Set KeyView's colors to show "hot"
- if (IsInitialized && base.ColorScheme is { })
- {
- var cs = new ColorScheme (base.ColorScheme)
- {
- Normal = GetHotNormalColor (),
- HotNormal = GetNormalColor ()
- };
- KeyView.ColorScheme = cs;
- }
-
- if (CommandView.Margin is { })
- {
- CommandView.Margin.ColorScheme = base.ColorScheme;
- }
- if (HelpView.Margin is { })
- {
- HelpView.Margin.ColorScheme = base.ColorScheme;
- }
-
- if (KeyView.Margin is { })
- {
- KeyView.Margin.ColorScheme = base.ColorScheme;
- }
+ return base.GetNormalColor ();
}
- ///
- protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view)
+ ///
+ public override Attribute GetHotNormalColor ()
{
- SetColors ();
+ if (HasFocus)
+
+ {
+ return base.GetHotFocusColor ();
+ }
+
+ return base.GetHotNormalColor ();
}
#endregion Focus
@@ -832,3 +786,21 @@ public class Shortcut : View, IOrientation, IDesignable
base.Dispose (disposing);
}
}
+
+///
+/// A helper class used by to display the key. Reverses the Normal and HotNormal colors.
+///
+public class ShortcutKeyView : View
+{
+ ///
+ public override Attribute GetNormalColor ()
+ {
+ if (SuperView is { HasFocus: true })
+
+ {
+ return base.GetHotFocusColor ();
+ }
+
+ return base.GetHotNormalColor ();
+ }
+}
diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs
index 57743989f..38238f744 100644
--- a/Terminal.Gui/Views/StatusBar.cs
+++ b/Terminal.Gui/Views/StatusBar.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Reflection;
+#nullable enable
namespace Terminal.Gui;
@@ -23,16 +22,45 @@ public class StatusBar : Bar, IDesignable
Y = Pos.AnchorEnd ();
Width = Dim.Fill ();
Height = Dim.Auto (DimAutoStyle.Content, 1);
- BorderStyle = LineStyle.Dashed;
- ColorScheme = Colors.ColorSchemes ["Menu"];
- SubViewLayout += StatusBar_LayoutStarted;
+ if (Border is { })
+ {
+ Border.LineStyle = DefaultSeparatorLineStyle;
+ }
+
+ base.ColorScheme = Colors.ColorSchemes ["Menu"];
+
+ Applied += OnConfigurationManagerApplied;
+ SuperViewChanged += OnSuperViewChanged;
}
- // StatusBar arranges the items horizontally.
- // The first item has no left border, the last item has no right border.
- // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart).
- private void StatusBar_LayoutStarted (object sender, LayoutEventArgs e)
+ private void OnSuperViewChanged (object? sender, SuperViewChangedEventArgs e)
+ {
+ if (SuperView is null)
+ {
+ // BUGBUG: This is a hack for avoiding a race condition in ConfigurationManager.Apply
+ // BUGBUG: For some reason in some unit tests, when Top is disposed, MenuBar.Dispose does not get called.
+ // BUGBUG: Yet, the MenuBar does get Removed from Top (and it's SuperView set to null).
+ // BUGBUG: Related: https://github.com/gui-cs/Terminal.Gui/issues/4021
+ Applied -= OnConfigurationManagerApplied;
+ }
+ }
+ private void OnConfigurationManagerApplied (object? sender, ConfigurationManagerEventArgs e)
+ {
+ if (Border is { })
+ {
+ Border.LineStyle = DefaultSeparatorLineStyle;
+ }
+ }
+
+ ///
+ /// Gets or sets the default Line Style for the separators between the shortcuts of the StatusBar.
+ ///
+ [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
+ public static LineStyle DefaultSeparatorLineStyle { get; set; } = LineStyle.Dashed;
+
+ ///
+ protected override void OnSubViewLayout (LayoutEventArgs args)
{
for (int index = 0; index < SubViews.Count; index++)
{
@@ -40,13 +68,9 @@ public class StatusBar : Bar, IDesignable
barItem.BorderStyle = BorderStyle;
- if (index == SubViews.Count - 1)
+ if (barItem.Border is { })
{
- barItem.Border.Thickness = new Thickness (0, 0, 0, 0);
- }
- else
- {
- barItem.Border.Thickness = new Thickness (0, 0, 1, 0);
+ barItem.Border.Thickness = index == SubViews.Count - 1 ? new Thickness (0, 0, 0, 0) : new Thickness (0, 0, 1, 0);
}
if (barItem is Shortcut shortcut)
@@ -54,6 +78,7 @@ public class StatusBar : Bar, IDesignable
shortcut.Orientation = Orientation.Horizontal;
}
}
+ base.OnSubViewLayout (args);
}
///
@@ -108,7 +133,7 @@ public class StatusBar : Bar, IDesignable
Text = "I'll Hide",
// Visible = false
};
- button1.Accepting += Button_Clicked;
+ button1.Accepting += OnButtonClicked;
Add (button1);
shortcut.Accepting += (s, e) =>
@@ -135,7 +160,15 @@ public class StatusBar : Bar, IDesignable
return true;
- void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ("Hi", $"You clicked {sender}"); }
+ void OnButtonClicked (object? sender, EventArgs? e) { MessageBox.Query ("Hi", $"You clicked {sender}"); }
}
+ ///
+ protected override void Dispose (bool disposing)
+ {
+ base.Dispose (disposing);
+
+ SuperViewChanged -= OnSuperViewChanged;
+ Applied -= OnConfigurationManagerApplied;
+ }
}
diff --git a/Terminal.Gui/Views/Wizard/WizardStep.cs b/Terminal.Gui/Views/Wizard/WizardStep.cs
index cf1b781da..617a47c19 100644
--- a/Terminal.Gui/Views/Wizard/WizardStep.cs
+++ b/Terminal.Gui/Views/Wizard/WizardStep.cs
@@ -163,10 +163,12 @@ public class WizardStep : View
/// Removes all s from the .
///
- public override void RemoveAll ()
+ public override IReadOnlyCollection RemoveAll ()
{
- _contentView.RemoveAll ();
+ IReadOnlyCollection removed = _contentView.RemoveAll ();
ShowHide ();
+
+ return removed;
}
/// Does the work to show and hide the contentView and helpView as appropriate
diff --git a/TerminalGuiFluentTesting/FakeInput.cs b/TerminalGuiFluentTesting/FakeInput.cs
index e90844382..fa57c164d 100644
--- a/TerminalGuiFluentTesting/FakeInput.cs
+++ b/TerminalGuiFluentTesting/FakeInput.cs
@@ -23,12 +23,12 @@ internal class FakeInput : IConsoleInput
///
public void Initialize (ConcurrentQueue inputBuffer) { InputBuffer = inputBuffer; }
- public ConcurrentQueue InputBuffer { get; set; }
+ public ConcurrentQueue? InputBuffer { get; set; }
///
public void Run (CancellationToken token)
{
// Blocks until either the token or the hardStopToken is cancelled.
- WaitHandle.WaitAny (new [] { token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle });
+ WaitHandle.WaitAny ([token.WaitHandle, _hardStopToken.WaitHandle, _timeoutCts.Token.WaitHandle]);
}
}
diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs
index 9b9fe7af4..5efc590e7 100644
--- a/TerminalGuiFluentTesting/GuiTestContext.cs
+++ b/TerminalGuiFluentTesting/GuiTestContext.cs
@@ -3,13 +3,12 @@ using System.Text;
using Microsoft.Extensions.Logging;
using Terminal.Gui;
using Terminal.Gui.ConsoleDrivers;
-using static Unix.Terminal.Curses;
namespace TerminalGuiFluentTesting;
///
-/// Fluent API context for testing a Terminal.Gui application. Create
-/// an instance using static class.
+/// Fluent API context for testing a Terminal.Gui application. Create
+/// an instance using static class.
///
public class GuiTestContext : IDisposable
{
@@ -23,7 +22,7 @@ public class GuiTestContext : IDisposable
private View? _lastView;
private readonly StringBuilder _logsSb;
private readonly V2TestDriver _driver;
- private bool _finished=false;
+ private bool _finished;
internal GuiTestContext (Func topLevelBuilder, int width, int height, V2TestDriver driver)
{
@@ -68,6 +67,7 @@ public class GuiTestContext : IDisposable
t.Closed += (s, e) => { _finished = true; };
Application.Run (t); // This will block, but it's on a background thread now
+ t.Dispose ();
Application.Shutdown ();
}
catch (OperationCanceledException)
@@ -97,12 +97,12 @@ public class GuiTestContext : IDisposable
private string GetDriverName ()
{
return _driver switch
- {
- V2TestDriver.V2Win => "v2win",
- V2TestDriver.V2Net => "v2net",
- _ =>
- throw new ArgumentOutOfRangeException ()
- };
+ {
+ V2TestDriver.V2Win => "v2win",
+ V2TestDriver.V2Net => "v2net",
+ _ =>
+ throw new ArgumentOutOfRangeException ()
+ };
}
///
@@ -115,7 +115,7 @@ public class GuiTestContext : IDisposable
return this;
}
- Application.Invoke (() => {Application.RequestStop ();});
+ Application.Invoke (() => { Application.RequestStop (); });
// Wait for the application to stop, but give it a 1-second timeout
if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
@@ -148,7 +148,7 @@ public class GuiTestContext : IDisposable
}
///
- /// Cleanup to avoid state bleed between tests
+ /// Cleanup to avoid state bleed between tests
///
public void Dispose ()
{
@@ -184,7 +184,7 @@ public class GuiTestContext : IDisposable
}
///
- /// Simulates changing the console size e.g. by resizing window in your operating system
+ /// Simulates changing the console size e.g. by resizing window in your operating system
///
/// new Width for the console.
/// new Height for the console.
@@ -203,11 +203,11 @@ public class GuiTestContext : IDisposable
writer.WriteLine (text);
- return WaitIteration ();
+ return this; //WaitIteration();
}
///
- /// Writes all Terminal.Gui engine logs collected so far to the
+ /// Writes all Terminal.Gui engine logs collected so far to the
///
///
///
@@ -215,12 +215,12 @@ public class GuiTestContext : IDisposable
{
writer.WriteLine (_logsSb.ToString ());
- return WaitIteration ();
+ return this; //WaitIteration();
}
///
- /// Waits until the end of the current iteration of the main loop. Optionally
- /// running a given action on the UI thread at that time.
+ /// Waits until the end of the current iteration of the main loop. Optionally
+ /// running a given action on the UI thread at that time.
///
///
///
@@ -255,8 +255,8 @@ public class GuiTestContext : IDisposable
}
///
- /// Performs the supplied immediately.
- /// Enables running commands without breaking the Fluent API calls.
+ /// Performs the supplied immediately.
+ /// Enables running commands without breaking the Fluent API calls.
///
///
///
@@ -266,22 +266,20 @@ public class GuiTestContext : IDisposable
{
doAction ();
}
- catch(Exception)
+ catch (Exception)
{
HardStop ();
throw;
-
}
return this;
}
-
///
- /// Simulates a right click at the given screen coordinates on the current driver.
- /// This is a raw input event that goes through entire processing pipeline as though
- /// user had pressed the mouse button physically.
+ /// Simulates a right click at the given screen coordinates on the current driver.
+ /// This is a raw input event that goes through entire processing pipeline as though
+ /// user had pressed the mouse button physically.
///
/// 0 indexed screen coordinates
/// 0 indexed screen coordinates
@@ -289,29 +287,25 @@ public class GuiTestContext : IDisposable
public GuiTestContext RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); }
///
- /// Simulates a left click at the given screen coordinates on the current driver.
- /// This is a raw input event that goes through entire processing pipeline as though
- /// user had pressed the mouse button physically.
+ /// Simulates a left click at the given screen coordinates on the current driver.
+ /// This is a raw input event that goes through entire processing pipeline as though
+ /// user had pressed the mouse button physically.
///
/// 0 indexed screen coordinates
/// 0 indexed screen coordinates
///
- public GuiTestContext LeftClick (int screenX, int screenY)
- {
- return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY);
- }
+ public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); }
- public GuiTestContext LeftClick (Func evaluator) where T : View
- {
- return Click (WindowsConsole.ButtonState.Button1Pressed,evaluator);
- }
+ public GuiTestContext LeftClick (Func evaluator) where T : View { return Click (WindowsConsole.ButtonState.Button1Pressed, evaluator); }
- private GuiTestContext Click (WindowsConsole.ButtonState btn, Func evaluator) where T:View
+ private GuiTestContext Click (WindowsConsole.ButtonState btn, Func evaluator) where T : View
{
- var v = Find (evaluator);
- var screen = v.ViewportToScreen (new Point (0, 0));
+ T v = Find (evaluator);
+ Point screen = v.ViewportToScreen (new Point (0, 0));
+
return Click (btn, screen.X, screen.Y);
}
+
private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
{
switch (_driver)
@@ -339,29 +333,32 @@ public class GuiTestContext : IDisposable
MousePosition = new ((short)screenX, (short)screenY)
}
});
+
break;
case V2TestDriver.V2Net:
int netButton = btn switch
- {
- WindowsConsole.ButtonState.Button1Pressed => 0,
- WindowsConsole.ButtonState.Button2Pressed => 1,
- WindowsConsole.ButtonState.Button3Pressed => 2,
- WindowsConsole.ButtonState.RightmostButtonPressed => 2,
- _ => throw new ArgumentOutOfRangeException (nameof (btn))
- };
- foreach (var k in NetSequences.Click (netButton, screenX, screenY))
+ {
+ WindowsConsole.ButtonState.Button1Pressed => 0,
+ WindowsConsole.ButtonState.Button2Pressed => 1,
+ WindowsConsole.ButtonState.Button3Pressed => 2,
+ WindowsConsole.ButtonState.RightmostButtonPressed => 2,
+ _ => throw new ArgumentOutOfRangeException (nameof (btn))
+ };
+
+ foreach (ConsoleKeyInfo k in NetSequences.Click (netButton, screenX, screenY))
{
SendNetKey (k);
}
+
break;
default:
throw new ArgumentOutOfRangeException ();
}
- WaitIteration ();
+ return WaitIteration ();
- return this;
+ ;
}
public GuiTestContext Down ()
@@ -370,24 +367,26 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.DOWN);
- WaitIteration ();
+
break;
case V2TestDriver.V2Net:
- foreach (var k in NetSequences.Down)
+ foreach (ConsoleKeyInfo k in NetSequences.Down)
{
SendNetKey (k);
}
+
break;
default:
throw new ArgumentOutOfRangeException ();
}
+ return WaitIteration ();
- return this;
+ ;
}
///
- /// Simulates the Right cursor key
+ /// Simulates the Right cursor key
///
///
///
@@ -397,24 +396,26 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.RIGHT);
- WaitIteration ();
+
break;
case V2TestDriver.V2Net:
- foreach (var k in NetSequences.Right)
+ foreach (ConsoleKeyInfo k in NetSequences.Right)
{
SendNetKey (k);
}
+
WaitIteration ();
+
break;
default:
throw new ArgumentOutOfRangeException ();
}
- return this;
+ return WaitIteration ();
}
///
- /// Simulates the Left cursor key
+ /// Simulates the Left cursor key
///
///
///
@@ -424,23 +425,24 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.LEFT);
- WaitIteration ();
+
break;
case V2TestDriver.V2Net:
- foreach (var k in NetSequences.Left)
+ foreach (ConsoleKeyInfo k in NetSequences.Left)
{
SendNetKey (k);
}
+
break;
default:
throw new ArgumentOutOfRangeException ();
}
- return this;
+ return WaitIteration ();
}
///
- /// Simulates the up cursor key
+ /// Simulates the up cursor key
///
///
///
@@ -450,23 +452,24 @@ public class GuiTestContext : IDisposable
{
case V2TestDriver.V2Win:
SendWindowsKey (ConsoleKeyMapping.VK.UP);
- WaitIteration ();
+
break;
case V2TestDriver.V2Net:
- foreach (var k in NetSequences.Up)
+ foreach (ConsoleKeyInfo k in NetSequences.Up)
{
SendNetKey (k);
}
+
break;
default:
throw new ArgumentOutOfRangeException ();
}
- return this;
+ return WaitIteration ();
}
///
- /// Simulates pressing the Return/Enter (newline) key.
+ /// Simulates pressing the Return/Enter (newline) key.
///
///
///
@@ -484,20 +487,21 @@ public class GuiTestContext : IDisposable
wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
wVirtualScanCode = 28
});
+
break;
case V2TestDriver.V2Net:
SendNetKey (new ('\r', ConsoleKey.Enter, false, false, false));
+
break;
default:
throw new ArgumentOutOfRangeException ();
}
- return this;
+ return WaitIteration ();
}
-
///
- /// Simulates pressing the Esc (Escape) key.
+ /// Simulates pressing the Esc (Escape) key.
///
///
///
@@ -515,12 +519,14 @@ public class GuiTestContext : IDisposable
wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE,
wVirtualScanCode = 1
});
+
break;
case V2TestDriver.V2Net:
// Note that this accurately describes how Esc comes in. Typically, ConsoleKey is None
// even though you would think it would be Escape - it isn't
SendNetKey (new ('\u001b', ConsoleKey.None, false, false, false));
+
break;
default:
throw new ArgumentOutOfRangeException ();
@@ -529,10 +535,8 @@ public class GuiTestContext : IDisposable
return this;
}
-
-
///
- /// Simulates pressing the Tab key.
+ /// Simulates pressing the Tab key.
///
///
///
@@ -550,12 +554,14 @@ public class GuiTestContext : IDisposable
wVirtualKeyCode = 0,
wVirtualScanCode = 0
});
+
break;
case V2TestDriver.V2Net:
// Note that this accurately describes how Tab comes in. Typically, ConsoleKey is None
// even though you would think it would be Tab - it isn't
SendNetKey (new ('\t', ConsoleKey.None, false, false, false));
+
break;
default:
throw new ArgumentOutOfRangeException ();
@@ -565,8 +571,8 @@ public class GuiTestContext : IDisposable
}
///
- /// Registers a right click handler on the added view (or root view) that
- /// will open the supplied .
+ /// Registers a right click handler on the added view (or root view) that
+ /// will open the supplied .
///
///
///
@@ -587,7 +593,7 @@ public class GuiTestContext : IDisposable
}
///
- /// The last view added (e.g. with ) or the root/current top.
+ /// The last view added (e.g. with ) or the root/current top.
///
public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to");
@@ -620,11 +626,7 @@ public class GuiTestContext : IDisposable
WaitIteration ();
}
-
- private void SendNetKey (ConsoleKeyInfo consoleKeyInfo)
- {
- _netInput.InputBuffer.Enqueue (consoleKeyInfo);
- }
+ private void SendNetKey (ConsoleKeyInfo consoleKeyInfo) { _netInput.InputBuffer.Enqueue (consoleKeyInfo); }
///
/// Sends a special key e.g. cursor key that does not map to a specific character
@@ -666,10 +668,23 @@ public class GuiTestContext : IDisposable
}
///
- /// Sets the input focus to the given .
- /// Throws if focus did not change due to system
- /// constraints e.g.
- /// is
+ /// Sends a key to the application. This goes directly to Application and does not go through
+ /// a driver.
+ ///
+ ///
+ ///
+ public GuiTestContext RaiseKeyDownEvent (Key key)
+ {
+ Application.RaiseKeyDownEvent (key);
+
+ return this; //WaitIteration();
+ }
+
+ ///
+ /// Sets the input focus to the given .
+ /// Throws if focus did not change due to system
+ /// constraints e.g.
+ /// is
///
///
///
@@ -687,27 +702,28 @@ public class GuiTestContext : IDisposable
}
///
- /// Tabs through the UI until a View matching the
- /// is found (of Type T) or all views are looped through (back to the beginning)
- /// in which case triggers hard stop and Exception
+ /// Tabs through the UI until a View matching the
+ /// is found (of Type T) or all views are looped through (back to the beginning)
+ /// in which case triggers hard stop and Exception
///
///
///
- public GuiTestContext Focus (Func evaluator) where T:View
+ public GuiTestContext Focus (Func evaluator) where T : View
{
- var t = Application.Top;
+ Toplevel? t = Application.Top;
HashSet seen = new ();
if (t == null)
{
Fail ("Application.Top was null when trying to set focus");
+
return this;
}
do
{
- var next = t.MostFocused;
+ View? next = t.MostFocused;
// Is view found?
if (next is T v && evaluator (v))
@@ -716,13 +732,14 @@ public class GuiTestContext : IDisposable
}
// No, try tab to the next (or first)
- this.Tab ();
+ Tab ();
WaitIteration ();
next = t.MostFocused;
if (next is null)
{
Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null");
+
return this;
}
@@ -734,22 +751,20 @@ public class GuiTestContext : IDisposable
return this;
}
-
}
while (true);
}
-
-
private T Find (Func evaluator) where T : View
{
- var t = Application.Top;
+ Toplevel? t = Application.Top;
if (t == null)
{
Fail ("Application.Top was null when attempting to find view");
}
- var f = FindRecursive(t!, evaluator);
+
+ T? f = FindRecursive (t!, evaluator);
if (f == null)
{
@@ -761,7 +776,7 @@ public class GuiTestContext : IDisposable
private T? FindRecursive (View current, Func evaluator) where T : View
{
- foreach (var subview in current.SubViews)
+ foreach (View subview in current.SubViews)
{
if (subview is T match && evaluator (match))
{
@@ -769,7 +784,8 @@ public class GuiTestContext : IDisposable
}
// Recursive call
- var result = FindRecursive (subview, evaluator);
+ T? result = FindRecursive (subview, evaluator);
+
if (result != null)
{
return result;
@@ -783,8 +799,7 @@ public class GuiTestContext : IDisposable
{
Stop ();
- throw new Exception (reason);
-
+ throw new (reason);
}
public GuiTestContext Send (Key key)
@@ -798,6 +813,7 @@ public class GuiTestContext : IDisposable
{
Fail ("Expected Application.Driver to be IConsoleDriverFacade");
}
+
return this;
}
}
diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs
index f79325319..d47ca128e 100644
--- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs
+++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs
@@ -8,13 +8,47 @@ public class BasicFluentAssertionTests
{
private readonly TextWriter _out;
- public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
+ public BasicFluentAssertionTests (ITestOutputHelper outputHelper)
+ {
+ _out = new TestOutputWriter (outputHelper);
+ }
+
+
+ [Theory]
+ [ClassData (typeof (V2TestDrivers))]
+ public void GuiTestContext_NewInstance_Runs (V2TestDriver d)
+ {
+ using GuiTestContext context = With.A (40, 10, d);
+ Assert.True (Application.Top!.Running);
+
+ context.WriteOutLogs (_out);
+ context.Stop ();
+ }
+
+
+ [Theory]
+ [ClassData (typeof (V2TestDrivers))]
+ public void GuiTestContext_QuitKey_Stops (V2TestDriver d)
+ {
+ using GuiTestContext context = With.A (40, 10, d);
+ Assert.True (Application.Top!.Running);
+
+ Toplevel top = Application.Top;
+ context.RaiseKeyDownEvent (Application.QuitKey);
+ Assert.False (top!.Running);
+
+ Application.Top?.Dispose ();
+ Application.Shutdown();
+
+ context.WriteOutLogs (_out);
+ context.Stop ();
+ }
[Theory]
[ClassData (typeof (V2TestDrivers))]
public void GuiTestContext_StartsAndStopsWithoutError (V2TestDriver d)
{
- using GuiTestContext context = With.A (40, 10,d);
+ using GuiTestContext context = With.A (40, 10, d);
// No actual assertions are needed — if no exceptions are thrown, it's working
context.Stop ();
@@ -51,10 +85,10 @@ public class BasicFluentAssertionTests
{
var clicked = false;
- MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })];
+ MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })];
using GuiTestContext c = With.A (40, 10, d)
- .WithContextMenu (new PopoverMenu(menuItems))
+ .WithContextMenu (new PopoverMenu (menuItems))
.ScreenShot ("Before open menu", _out)
// Click in main area inside border
@@ -90,7 +124,7 @@ public class BasicFluentAssertionTests
new ("Six", "", null)
];
- using GuiTestContext c = With.A (40, 10,d)
+ using GuiTestContext c = With.A (40, 10, d)
.WithContextMenu (new PopoverMenu (menuItems))
.ScreenShot ("Before open menu", _out)
@@ -100,7 +134,7 @@ public class BasicFluentAssertionTests
.Down ()
.Down ()
.Down ()
- .Right()
+ .Right ()
.ScreenShot ("After open submenu", _out)
.Down ()
.Enter ()
diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs
index 2eeb59484..b1a3ced79 100644
--- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs
+++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs
@@ -11,7 +11,10 @@ public class FileDialogFluentTests
{
private readonly TextWriter _out;
- public FileDialogFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); }
+ public FileDialogFluentTests (ITestOutputHelper outputHelper)
+ {
+ _out = new TestOutputWriter (outputHelper);
+ }
private MockFileSystem CreateExampleFileSystem ()
{
@@ -41,27 +44,25 @@ public class FileDialogFluentTests
[ClassData (typeof (V2TestDrivers))]
public void CancelFileDialog_UsingEscape (V2TestDriver d)
{
- var sd = new SaveDialog ( CreateExampleFileSystem ());
+ var sd = new SaveDialog (CreateExampleFileSystem ());
using var c = With.A (sd, 100, 20, d)
- .ScreenShot ("Save dialog",_out)
- .Escape()
+ .ScreenShot ("Save dialog", _out)
+ .Escape ()
+ .Then (() => Assert.True (sd.Canceled))
.Stop ();
-
- Assert.True (sd.Canceled);
}
[Theory]
[ClassData (typeof (V2TestDrivers))]
public void CancelFileDialog_UsingCancelButton_TabThenEnter (V2TestDriver d)
{
- var sd = new SaveDialog (CreateExampleFileSystem ());
+ var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false };
using var c = With.A (sd, 100, 20, d)
.ScreenShot ("Save dialog", _out)
- .Focus