Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3258ab0
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 14, 2025
f586e22
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 19, 2025
0454f9c
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 20, 2025
ee4db26
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 20, 2025
d11f7be
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 20, 2025
14617fc
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 20, 2025
1deabf2
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 21, 2025
529f4ba
Initial plan
Copilot Nov 21, 2025
d8cc1e3
Implement step 1: Make Toplevel implement IRunnable interface
Copilot Nov 22, 2025
6df84b6
Implement step 2: Update Dialog to implement IRunnable<int?>
Copilot Nov 22, 2025
78f4a08
Implement step 3: Update MessageBox to use Dialog.Result
Copilot Nov 22, 2025
e43409f
Implement steps 4-6: Update Wizard and enable FluentExample POST_4148
Copilot Nov 22, 2025
89c09aa
Implement step 7: Add comprehensive Phase 2 unit tests and fix ambigu…
Copilot Nov 22, 2025
96c8720
Address code review feedback: Clarify Wizard.WasFinished documentation
Copilot Nov 22, 2025
c68fcee
Fix Phase 2 tests: Use fake driver and IDisposable pattern
Copilot Nov 22, 2025
973ff4c
Fix all failing tests: Simplify Phase2 tests and fix MessageBox butto…
Copilot Nov 22, 2025
96eafdd
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 22, 2025
edeae9f
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Nov 25, 2025
d9899dc
merged
tig Nov 25, 2025
b9f66d4
fixed
tig Nov 25, 2025
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
1 change: 1 addition & 0 deletions Examples/FluentExample/FluentExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<DefineConstants>$(DefineConstants);POST_4148</DefineConstants>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
Expand Down
106 changes: 99 additions & 7 deletions Terminal.Gui/Views/Dialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ namespace Terminal.Gui.Views;
/// scheme.
/// </summary>
/// <remarks>
/// To run the <see cref="Dialog"/> modally, create the <see cref="Dialog"/>, and pass it to
/// <see cref="IApplication.Run(Toplevel, Func{Exception, bool})"/>. This will execute the dialog until
/// it terminates via the <see cref="Application.QuitKey"/> (`Esc` by default),
/// or when one of the views or buttons added to the dialog calls
/// <see cref="IApplication.RequestStop()"/>.
/// <para>
/// To run the <see cref="Dialog"/> modally, create the <see cref="Dialog"/>, and pass it to
/// <see cref="IApplication.Run(Toplevel, Func{Exception, bool})"/>. This will execute the dialog until
/// it terminates via the <see cref="Application.QuitKey"/> (`Esc` by default),
/// or when one of the views or buttons added to the dialog calls
/// <see cref="Application.RequestStop"/>.
/// </para>
/// <para>
/// <b>Phase 2:</b> <see cref="Dialog"/> now implements <see cref="IRunnable{TResult}"/> with
/// <c>int?</c> as the result type, returning the index of the clicked button. The <see cref="Result"/>
/// property replaces the need for manual result tracking. A result of <see langword="null"/> indicates
/// the dialog was canceled (ESC pressed, window closed without clicking a button).
/// </para>
/// </remarks>
public class Dialog : Window
public class Dialog : Window, IRunnable<int?>
{
private static LineStyle _defaultBorderStyle = LineStyle.Heavy; // Resources/config.json overrides
private static Alignment _defaultButtonAlignment = Alignment.End; // Resources/config.json overrides
Expand Down Expand Up @@ -91,7 +99,13 @@ public Button [] Buttons
}

/// <summary>Gets a value indicating whether the <see cref="Dialog"/> was canceled.</summary>
/// <remarks>The default value is <see langword="true"/>.</remarks>
/// <remarks>
/// <para>The default value is <see langword="true"/>.</para>
/// <para>
/// <b>Deprecated:</b> Use <see cref="Result"/> instead. This property is maintained for backward
/// compatibility. A <see langword="null"/> <see cref="Result"/> indicates the dialog was canceled.
/// </para>
/// </remarks>
public bool Canceled
{
get { return _canceled; }
Expand All @@ -107,6 +121,21 @@ public bool Canceled
}
}

/// <summary>
/// Gets or sets the result data extracted when the dialog was accepted, or <see langword="null"/> if not accepted.
/// </summary>
/// <remarks>
/// <para>
/// Returns the zero-based index of the button that was clicked, or <see langword="null"/> if the
/// dialog was canceled (ESC pressed, window closed without clicking a button).
/// </para>
/// <para>
/// This property is automatically set in <see cref="OnIsRunningChanging"/> when the dialog is
/// closing. The result is extracted by finding which button has focus when the dialog stops.
/// </para>
/// </remarks>
public int? Result { get; set; }

/// <summary>
/// Defines the default border styling for <see cref="Dialog"/>. Can be configured via
/// <see cref="ConfigurationManager"/>.
Expand Down Expand Up @@ -198,4 +227,67 @@ protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attri

return false;
}

#region IRunnable<int> Implementation

/// <summary>
/// Called when the dialog is about to stop running. Extracts the button result before the dialog is removed
/// from the runnable stack.
/// </summary>
/// <param name="oldIsRunning">The current value of IsRunning.</param>
/// <param name="newIsRunning">The new value of IsRunning (true = starting, false = stopping).</param>
/// <returns><see langword="true"/> to cancel; <see langword="false"/> to proceed.</returns>
/// <remarks>
/// This method is called by the IRunnable infrastructure when the dialog is stopping. It extracts
/// which button was clicked (if any) before views are disposed.
/// </remarks>
protected virtual bool OnIsRunningChanging (bool oldIsRunning, bool newIsRunning)
{
if (!newIsRunning && oldIsRunning) // Stopping
{
// Extract result BEFORE disposal - find which button has focus or was last clicked
Result = null; // Default: canceled (null = no button clicked)

for (var i = 0; i < _buttons.Count; i++)
{
if (_buttons [i].HasFocus)
{
Result = i;
_canceled = false;
break;
}
}

// If no button has focus, check if any button was the last focused view
if (Result is null && MostFocused is Button btn && _buttons.Contains (btn))
{
Result = _buttons.IndexOf (btn);
_canceled = false;
}

// Update legacy Canceled property for backward compatibility
if (Result is null)
{
_canceled = true;
}
}
else if (newIsRunning) // Starting
{
// Clear result when starting
Result = null;
_canceled = true; // Default to canceled until a button is clicked
}

// Call base implementation (Toplevel.IRunnable.RaiseIsRunningChanging)
return ((IRunnable)this).RaiseIsRunningChanging (oldIsRunning, newIsRunning);
}

// Explicitly implement IRunnable<int> to override the behavior from Toplevel's IRunnable
bool IRunnable.RaiseIsRunningChanging (bool oldIsRunning, bool newIsRunning)
{
// Call our virtual method so subclasses can override
return OnIsRunningChanging (oldIsRunning, newIsRunning);
}

#endregion
}
62 changes: 37 additions & 25 deletions Terminal.Gui/Views/MessageBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,14 @@ public static int DefaultMinimumHeight
/// The index of the selected button, or <see langword="null"/> if the user pressed <see cref="Application.QuitKey"/>.
/// </summary>
/// <remarks>
/// This global variable is useful for web-based consoles without a SynchronizationContext or TaskScheduler.
/// Warning: Not thread-safe.
/// <para>
/// Warning: This is a global variable and should be used with caution. It is not thread safe.
/// </para>
/// <para>
/// <b>Deprecated:</b> This property is maintained for backward compatibility. The MessageBox methods
/// now return the button index directly, and <see cref="Dialog.Result"/> provides a cleaner,
/// non-global alternative for custom dialog implementations.
/// </para>
/// </remarks>
public static int? Clicked { get; private set; }

Expand Down Expand Up @@ -573,7 +579,6 @@ params string [] buttons
// Create button array for Dialog
var count = 0;
List<Button> buttonList = new ();
Clicked = null;

if (buttons is { })
{
Expand All @@ -584,36 +589,34 @@ params string [] buttons

foreach (string s in buttons)
{
int buttonIndex = count; // Capture index for closure
var b = new Button
{
Text = s,
Data = count
IsDefault = count == defaultButton,
Data = buttonIndex
};

if (count == defaultButton)
{
b.IsDefault = true;

b.Accepting += (s, e) =>
// Set up Accepting handler to store result in Dialog before RequestStop
b.Accepting += (s, e) =>
{
// Store the button index in the dialog before stopping
// This ensures Dialog.Result is set correctly
if (e?.Context?.Source is Button { Data: int index } button)
{
if (e?.Context?.Source is Button button)
{
Clicked = (int)button.Data!;
}
else
{
Clicked = defaultButton;
}

if (e is { })
if (button.SuperView is Dialog dialog)
{
e.Handled = true;
dialog.Result = index;
dialog.Canceled = false;
}
}

if (e is { })
{
(s as View)?.App?.RequestStop ();
};
}

e.Handled = true;
}
};
buttonList.Add (b);
count++;
}
Expand All @@ -631,7 +634,7 @@ params string [] buttons
d.Width = Dim.Auto (
DimAutoStyle.Auto,
Dim.Func (_ => (int)((app.Screen.Width - d.GetAdornmentsThickness ().Horizontal) * (DefaultMinimumWidth / 100f))),
Dim.Func (_ => (int)((app.Screen.Width - d.GetAdornmentsThickness ().Horizontal) * 0.9f)));
Dim.Func (_ => (int)((app.Screen.Width - d.GetAdornmentsThickness ().Horizontal) * 0.9f)));

d.Height = Dim.Auto (
DimAutoStyle.Auto,
Expand Down Expand Up @@ -659,8 +662,17 @@ params string [] buttons

// Run the modal; do not shut down the mainloop driver when done
app.Run (d);

// Use Dialog.Result instead of manually tracking with Clicked
// Dialog automatically extracts which button was clicked in OnIsRunningChanging
int result = d.Result ?? -1;

// Update legacy Clicked property for backward compatibility
Clicked = result;

d.Dispose ();

return Clicked;
return result;

}
}
121 changes: 120 additions & 1 deletion Terminal.Gui/Views/Toplevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,15 @@ namespace Terminal.Gui.Views;
/// and run (e.g. <see cref="Dialog"/>s). To run a Toplevel, create the <see cref="Toplevel"/> and call
/// <see cref="IApplication.Run(Toplevel, Func{Exception, bool})"/>.
/// </para>
/// <para>
/// <b>Phase 2:</b> <see cref="Toplevel"/> now implements <see cref="IRunnable"/> as an adapter pattern for
/// backward compatibility. The lifecycle events (<see cref="Activate"/>, <see cref="Deactivate"/>,
/// <see cref="Closing"/>, <see cref="Closed"/>) are bridged to the new IRunnable events
/// (<see cref="IRunnable.IsModalChanging"/>, <see cref="IRunnable.IsModalChanged"/>,
/// <see cref="IRunnable.IsRunningChanging"/>, <see cref="IRunnable.IsRunningChanged"/>).
/// </para>
/// </remarks>
public partial class Toplevel : View
public partial class Toplevel : View, IRunnable
{
/// <summary>
/// Initializes a new instance of the <see cref="Toplevel"/> class,
Expand Down Expand Up @@ -199,6 +206,118 @@ internal virtual void OnUnloaded ()

#endregion

#region IRunnable Implementation - Adapter Pattern for Backward Compatibility

/// <inheritdoc/>
bool IRunnable.IsRunning => App?.RunnableSessionStack?.Any (token => token.Runnable == this) ?? false;

/// <inheritdoc/>
bool IRunnable.RaiseIsRunningChanging (bool oldIsRunning, bool newIsRunning)
{
// Bridge to legacy Closing event when stopping
if (!newIsRunning && oldIsRunning)
{
ToplevelClosingEventArgs args = new (this);

if (OnClosing (args))
{
return true; // Canceled
}
}

return false;
}

/// <inheritdoc/>
event EventHandler<CancelEventArgs<bool>>? IRunnable.IsRunningChanging
{
add { }
remove { }
}

/// <inheritdoc/>
void IRunnable.RaiseIsRunningChangedEvent (bool newIsRunning)
{
// Update Running property to maintain backward compatibility
Running = newIsRunning;

// Bridge to legacy events
if (newIsRunning)
{
OnLoaded ();
}
else
{
OnClosed (this);
OnUnloaded ();
}
}

/// <inheritdoc/>
event EventHandler<EventArgs<bool>>? IRunnable.IsRunningChanged
{
add { }
remove { }
}

/// <inheritdoc/>
bool IRunnable.IsModal
{
get
{
if (App is null)
{
return false;
}

// Check if this toplevel is at the top of the RunnableSessionStack
if (App.RunnableSessionStack is { } && App.RunnableSessionStack.TryPeek (out RunnableSessionToken? topToken))
{
return topToken?.Runnable == this;
}

// Fallback: Check if this is the TopRunnable
return App.TopRunnable == this;
}
}

/// <inheritdoc/>
bool IRunnable.RaiseIsModalChanging (bool oldIsModal, bool newIsModal)
{
// No cancellation for modal changes in legacy Toplevel
return false;
}

/// <inheritdoc/>
event EventHandler<CancelEventArgs<bool>>? IRunnable.IsModalChanging
{
add { }
remove { }
}

/// <inheritdoc/>
void IRunnable.RaiseIsModalChangedEvent (bool newIsModal)
{
// Bridge to legacy Activate/Deactivate events
if (newIsModal)
{
OnActivate (App?.TopRunnable as Toplevel ?? this);
}
else
{
OnDeactivate (App?.TopRunnable as Toplevel ?? this);
}
}

/// <inheritdoc/>
event EventHandler<EventArgs<bool>>? IRunnable.IsModalChanged
{
add { }
remove { }
}

#endregion

#region Size / Position Management

// TODO: Make cancelable?
Expand Down
Loading
Loading