namespace Terminal.Gui.Views;
///
/// A . Supports a simple API for adding s
/// across the bottom. By default, the is centered and used the
/// scheme.
///
///
///
/// To run the modally, create the , and pass it to
/// . This will execute the dialog until
/// it terminates via the (`Esc` by default),
/// or when one of the views or buttons added to the dialog calls
/// .
///
///
/// Phase 2: now implements with
/// int? as the result type, returning the index of the clicked button. The
/// property replaces the need for manual result tracking. A result of indicates
/// the dialog was canceled (ESC pressed, window closed without clicking a button).
///
///
public class Dialog : Window, IRunnable
{
///
/// Initializes a new instance of the class with no s.
///
///
/// By default, , , , and are
/// set
/// such that the will be centered in, and no larger than 90% of , if
/// there is one. Otherwise,
/// it will be bound by the screen dimensions.
///
public Dialog ()
{
Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped;
base.ShadowStyle = DefaultShadow;
BorderStyle = DefaultBorderStyle;
X = Pos.Center ();
Y = Pos.Center ();
Width = Dim.Auto (DimAutoStyle.Auto, Dim.Percent (DefaultMinimumWidth), Dim.Percent (90));
Height = Dim.Auto (DimAutoStyle.Auto, Dim.Percent (DefaultMinimumHeight), Dim.Percent (90));
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog);
Modal = true;
ButtonAlignment = DefaultButtonAlignment;
ButtonAlignmentModes = DefaultButtonAlignmentModes;
}
private readonly List _buttons = [];
private bool _canceled;
///
/// Adds a to the , its layout will be controlled by the
///
///
/// Button to add.
public void AddButton (Button button)
{
// Use a distinct GroupId so users can use Pos.Align for other views in the Dialog
button.X = Pos.Align (ButtonAlignment, ButtonAlignmentModes, GetHashCode ());
button.Y = Pos.AnchorEnd ();
_buttons.Add (button);
Add (button);
}
// TODO: Update button.X = Pos.Justify when alignment changes
/// Determines how the s are aligned along the bottom of the dialog.
public Alignment ButtonAlignment { get; set; }
///
/// Gets or sets the alignment modes for the dialog's buttons.
///
public AlignmentModes ButtonAlignmentModes { get; set; }
/// Optional buttons to lay out at the bottom of the dialog.
public Button [] Buttons
{
get => _buttons.ToArray ();
init
{
foreach (Button b in value)
{
AddButton (b);
}
}
}
/// Gets a value indicating whether the was canceled.
///
/// The default value is .
///
/// Deprecated: Use instead. This property is maintained for backward
/// compatibility. A indicates the dialog was canceled.
///
///
public bool Canceled
{
get { return _canceled; }
set
{
#if DEBUG_IDISPOSABLE
if (EnableDebugIDisposableAsserts && WasDisposed)
{
throw new ObjectDisposedException (GetType ().FullName);
}
#endif
_canceled = value;
}
}
///
/// Gets or sets the result data extracted when the dialog was accepted, or if not accepted.
///
///
///
/// Returns the zero-based index of the button that was clicked, or if the
/// dialog was canceled (ESC pressed, window closed without clicking a button).
///
///
/// This property is automatically set in when the dialog is
/// closing. The result is extracted by finding which button has focus when the dialog stops.
///
///
public int? Result { get; set; }
///
/// Defines the default border styling for . Can be configured via
/// .
///
[ConfigurationProperty (Scope = typeof (ThemeScope))]
public new static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Heavy;
/// The default for .
/// This property can be set in a Theme.
[ConfigurationProperty (Scope = typeof (ThemeScope))]
public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End;
/// The default for .
/// This property can be set in a Theme.
[ConfigurationProperty (Scope = typeof (ThemeScope))]
public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems;
///
/// Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via
/// .
///
[ConfigurationProperty (Scope = typeof (ThemeScope))]
public static int DefaultMinimumHeight { get; set; } = 80;
///
/// Defines the default minimum Dialog width, as a percentage of the container width. Can be configured via
/// .
///
[ConfigurationProperty (Scope = typeof (ThemeScope))]
public static int DefaultMinimumWidth { get; set; } = 80;
///
/// Gets or sets whether all s are shown with a shadow effect by default.
///
[ConfigurationProperty (Scope = typeof (ThemeScope))]
public new static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.Transparent;
// Dialogs are Modal and Focus is indicated by their Border. The following code ensures the
// Text of the dialog (e.g. for a MessageBox) is always drawn using the Normal Attribute.
private bool _drawingText;
///
protected override bool OnDrawingText ()
{
_drawingText = true;
return false;
}
///
protected override void OnDrewText ()
{
_drawingText = false;
}
///
protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute)
{
if (_drawingText && role is VisualRole.Focus && Border?.Thickness != Thickness.Empty)
{
currentAttribute = GetScheme ().Normal;
return true;
}
return false;
}
#region IRunnable Implementation
///
/// Called when the dialog is about to stop running. Extracts the button result before the dialog is removed
/// from the runnable stack.
///
/// The current value of IsRunning.
/// The new value of IsRunning (true = starting, false = stopping).
/// to cancel; to proceed.
///
/// 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.
///
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 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
}