Files
Terminal.Gui/Terminal.Gui/Views/MessageBox.cs
2024-04-03 09:41:59 -06:00

475 lines
19 KiB
C#

namespace Terminal.Gui;
/// <summary>
/// MessageBox displays a modal message to the user, with a title, a message and a series of options that the user
/// can choose from.
/// </summary>
/// <para>
/// The difference between the <see cref="Query(string, string, string[])"/> and
/// <see cref="ErrorQuery(string, string, string[])"/> method is the default set of colors used for the message box.
/// </para>
/// <para>
/// The following example pops up a <see cref="MessageBox"/> with the specified title and text, plus two
/// <see cref="Button"/>s. The value -1 is returned when the user cancels the <see cref="MessageBox"/> by pressing the
/// ESC key.
/// </para>
/// <example>
/// <code lang="c#">
/// var n = MessageBox.Query ("Quit Demo", "Are you sure you want to quit this demo?", "Yes", "No");
/// if (n == 0)
/// quit = true;
/// else
/// quit = false;
/// </code>
/// </example>
public static class MessageBox
{
/// <summary>
/// The index of the selected button, or -1 if the user pressed ESC to close the dialog. This is useful for web
/// based console where by default there is no SynchronizationContext or TaskScheduler.
/// </summary>
public static int Clicked { get; private set; } = -1;
/// <summary>
/// Defines the default border styling for <see cref="Dialog"/>. Can be configured via
/// <see cref="ConfigurationManager"/>.
/// </summary>
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="width">Width for the window.</param>
/// <param name="height">Height for the window.</param>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
/// the contents.
/// </remarks>
public static int ErrorQuery (int width, int height, string title, string message, params string [] buttons)
{
return QueryFull (true, width, height, title, message, 0, true, buttons);
}
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// The message box will be vertically and horizontally centered in the container and the size will be
/// automatically determined from the size of the title, message. and buttons.
/// </remarks>
public static int ErrorQuery (string title, string message, params string [] buttons) { return QueryFull (true, 0, 0, title, message, 0, true, buttons); }
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="width">Width for the window.</param>
/// <param name="height">Height for the window.</param>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
/// the contents.
/// </remarks>
public static int ErrorQuery (
int width,
int height,
string title,
string message,
int defaultButton = 0,
params string [] buttons
)
{
return QueryFull (true, width, height, title, message, defaultButton, true, buttons);
}
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// The message box will be vertically and horizontally centered in the container and the size will be
/// automatically determined from the size of the title, message. and buttons.
/// </remarks>
public static int ErrorQuery (string title, string message, int defaultButton = 0, params string [] buttons)
{
return QueryFull (true, 0, 0, title, message, defaultButton, true, buttons);
}
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="width">Width for the window.</param>
/// <param name="height">Height for the window.</param>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="wrapMessagge">If wrap the message or not.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
/// the contents.
/// </remarks>
public static int ErrorQuery (
int width,
int height,
string title,
string message,
int defaultButton = 0,
bool wrapMessagge = true,
params string [] buttons
)
{
return QueryFull (true, width, height, title, message, defaultButton, wrapMessagge, buttons);
}
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="wrapMessagge">If wrap the message or not.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// The message box will be vertically and horizontally centered in the container and the size will be
/// automatically determined from the size of the title, message. and buttons.
/// </remarks>
public static int ErrorQuery (
string title,
string message,
int defaultButton = 0,
bool wrapMessagge = true,
params string [] buttons
)
{
return QueryFull (true, 0, 0, title, message, defaultButton, wrapMessagge, buttons);
}
/// <summary>
/// Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="width">Width for the window.</param>
/// <param name="height">Height for the window.</param>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
/// contents.
/// </remarks>
public static int Query (int width, int height, string title, string message, params string [] buttons)
{
return QueryFull (false, width, height, title, message, 0, true, buttons);
}
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// The message box will be vertically and horizontally centered in the container and the size will be
/// automatically determined from the size of the message and buttons.
/// </remarks>
public static int Query (string title, string message, params string [] buttons) { return QueryFull (false, 0, 0, title, message, 0, true, buttons); }
/// <summary>
/// Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="width">Width for the window.</param>
/// <param name="height">Height for the window.</param>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
/// contents.
/// </remarks>
public static int Query (
int width,
int height,
string title,
string message,
int defaultButton = 0,
params string [] buttons
)
{
return QueryFull (false, width, height, title, message, defaultButton, true, buttons);
}
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// The message box will be vertically and horizontally centered in the container and the size will be
/// automatically determined from the size of the message and buttons.
/// </remarks>
public static int Query (string title, string message, int defaultButton = 0, params string [] buttons)
{
return QueryFull (false, 0, 0, title, message, defaultButton, true, buttons);
}
/// <summary>
/// Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="width">Width for the window.</param>
/// <param name="height">Height for the window.</param>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="wrapMessagge">If wrap the message or not.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
/// contents.
/// </remarks>
public static int Query (
int width,
int height,
string title,
string message,
int defaultButton = 0,
bool wrapMessagge = true,
params string [] buttons
)
{
return QueryFull (false, width, height, title, message, defaultButton, wrapMessagge, buttons);
}
/// <summary>
/// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
/// to the user.
/// </summary>
/// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
/// <param name="title">Title for the query.</param>
/// <param name="message">Message to display, might contain multiple lines.</param>
/// <param name="defaultButton">Index of the default button.</param>
/// <param name="wrapMessage">If wrap the message or not.</param>
/// <param name="buttons">Array of buttons to add.</param>
/// <remarks>
/// The message box will be vertically and horizontally centered in the container and the size will be
/// automatically determined from the size of the message and buttons.
/// </remarks>
public static int Query (
string title,
string message,
int defaultButton = 0,
bool wrapMessage = true,
params string [] buttons
)
{
return QueryFull (false, 0, 0, title, message, defaultButton, wrapMessage, buttons);
}
private static int QueryFull (
bool useErrorColors,
int width,
int height,
string title,
string message,
int defaultButton = 0,
bool wrapMessage = true,
params string [] buttons
)
{
// Create button array for Dialog
var count = 0;
List<Button> buttonList = new ();
if (buttons is { })
{
if (defaultButton > buttons.Length - 1)
{
defaultButton = buttons.Length - 1;
}
foreach (string s in buttons)
{
var b = new Button { Text = s };
if (count == defaultButton)
{
b.IsDefault = true;
}
buttonList.Add (b);
count++;
}
}
Dialog d;
d = new Dialog
{
Buttons = buttonList.ToArray (),
Title = title,
BorderStyle = DefaultBorderStyle,
Width = Dim.Percent (60),
Height = 5 // Border + one line of text + vspace + buttons
};
if (width != 0)
{
d.Width = width;
}
if (height != 0)
{
d.Height = height;
}
if (useErrorColors)
{
d.ColorScheme = Colors.ColorSchemes ["Error"];
}
else
{
d.ColorScheme = Colors.ColorSchemes ["Dialog"];
}
var messageLabel = new Label
{
AutoSize = !wrapMessage,
Text = message,
TextAlignment = TextAlignment.Centered,
X = Pos.Center (),
Y = 0
};
if (!messageLabel.AutoSize)
{
messageLabel.Width = Dim.Fill ();
messageLabel.Height = Dim.Fill (1);
}
messageLabel.TextFormatter.WordWrap = wrapMessage;
messageLabel.TextFormatter.MultiLine = !wrapMessage;
d.Add (messageLabel);
// Setup actions
Clicked = -1;
for (var n = 0; n < buttonList.Count; n++)
{
int buttonId = n;
Button b = buttonList [n];
b.Accept += (s, e) =>
{
Clicked = buttonId;
Application.RequestStop ();
};
if (b.IsDefault)
{
b.SetFocus ();
}
}
d.Loaded += Dialog_Loaded;
// Run the modal; do not shutdown the mainloop driver when done
Application.Run (d);
d.Dispose ();
return Clicked;
void Dialog_Loaded (object s, EventArgs e)
{
if (width != 0 || height != 0)
{
return;
}
// TODO: replace with Dim.Fit when implemented
Rectangle maxBounds = d.SuperView?.Bounds ?? Application.Top.Bounds;
Thickness adornmentsThickness = d.GetAdornmentsThickness ();
if (wrapMessage)
{
messageLabel.TextFormatter.Size = new (
maxBounds.Size.Width
- adornmentsThickness.Horizontal,
maxBounds.Size.Height
- adornmentsThickness.Vertical);
}
string msg = messageLabel.TextFormatter.Format ();
Size messageSize = messageLabel.TextFormatter.FormatAndGetSize ();
// Ensure the width fits the text + buttons
int newWidth = Math.Max (
width,
Math.Max (
messageSize.Width + adornmentsThickness.Horizontal,
d.GetButtonsWidth () + d.Buttons.Length + adornmentsThickness.Horizontal));
if (newWidth > d.Frame.Width)
{
d.Width = newWidth;
}
// Ensure height fits the text + vspace + buttons
if (messageSize.Height == 0)
{
d.Height = Math.Max (height, 3 + adornmentsThickness.Vertical);
}
else
{
string lastLine = messageLabel.TextFormatter.GetLines () [^1];
// INTENT: Instead of the check against \n or \r\n, how about just Environment.NewLine?
d.Height = Math.Max (
height,
messageSize.Height
+ (lastLine.EndsWith ("\r\n") || lastLine.EndsWith ('\n') ? 1 : 2)
+ adornmentsThickness.Vertical);
}
d.SetRelativeLayout (d.SuperView?.Frame ?? Application.Top.Frame);
d.LayoutSubviews ();
}
}
}