MessageBox.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. namespace Terminal.Gui;
  2. /// <summary>
  3. /// MessageBox displays a modal message to the user, with a title, a message and a series of options that the user
  4. /// can choose from.
  5. /// </summary>
  6. /// <para>
  7. /// The difference between the <see cref="Query(string, string, string[])"/> and
  8. /// <see cref="ErrorQuery(string, string, string[])"/> method is the default set of colors used for the message box.
  9. /// </para>
  10. /// <para>
  11. /// The following example pops up a <see cref="MessageBox"/> with the specified title and text, plus two
  12. /// <see cref="Button"/>s. The value -1 is returned when the user cancels the <see cref="MessageBox"/> by pressing the
  13. /// ESC key.
  14. /// </para>
  15. /// <example>
  16. /// <code lang="c#">
  17. /// var n = MessageBox.Query ("Quit Demo", "Are you sure you want to quit this demo?", "Yes", "No");
  18. /// if (n == 0)
  19. /// quit = true;
  20. /// else
  21. /// quit = false;
  22. /// </code>
  23. /// </example>
  24. public static class MessageBox
  25. {
  26. /// <summary>
  27. /// The index of the selected button, or -1 if the user pressed ESC to close the dialog. This is useful for web
  28. /// based console where by default there is no SynchronizationContext or TaskScheduler.
  29. /// </summary>
  30. public static int Clicked { get; private set; } = -1;
  31. /// <summary>
  32. /// Defines the default border styling for <see cref="Dialog"/>. Can be configured via
  33. /// <see cref="ConfigurationManager"/>.
  34. /// </summary>
  35. [SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
  36. public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
  37. /// <summary>
  38. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  39. /// to the user.
  40. /// </summary>
  41. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  42. /// <param name="width">Width for the window.</param>
  43. /// <param name="height">Height for the window.</param>
  44. /// <param name="title">Title for the query.</param>
  45. /// <param name="message">Message to display, might contain multiple lines.</param>
  46. /// <param name="buttons">Array of buttons to add.</param>
  47. /// <remarks>
  48. /// Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
  49. /// the contents.
  50. /// </remarks>
  51. public static int ErrorQuery (int width, int height, string title, string message, params string [] buttons)
  52. {
  53. return QueryFull (true, width, height, title, message, 0, true, buttons);
  54. }
  55. /// <summary>
  56. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  57. /// to the user.
  58. /// </summary>
  59. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  60. /// <param name="title">Title for the query.</param>
  61. /// <param name="message">Message to display, might contain multiple lines.</param>
  62. /// <param name="buttons">Array of buttons to add.</param>
  63. /// <remarks>
  64. /// The message box will be vertically and horizontally centered in the container and the size will be
  65. /// automatically determined from the size of the title, message. and buttons.
  66. /// </remarks>
  67. public static int ErrorQuery (string title, string message, params string [] buttons) { return QueryFull (true, 0, 0, title, message, 0, true, buttons); }
  68. /// <summary>
  69. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  70. /// to the user.
  71. /// </summary>
  72. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  73. /// <param name="width">Width for the window.</param>
  74. /// <param name="height">Height for the window.</param>
  75. /// <param name="title">Title for the query.</param>
  76. /// <param name="message">Message to display, might contain multiple lines.</param>
  77. /// <param name="defaultButton">Index of the default button.</param>
  78. /// <param name="buttons">Array of buttons to add.</param>
  79. /// <remarks>
  80. /// Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
  81. /// the contents.
  82. /// </remarks>
  83. public static int ErrorQuery (
  84. int width,
  85. int height,
  86. string title,
  87. string message,
  88. int defaultButton = 0,
  89. params string [] buttons
  90. )
  91. {
  92. return QueryFull (true, width, height, title, message, defaultButton, true, buttons);
  93. }
  94. /// <summary>
  95. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  96. /// to the user.
  97. /// </summary>
  98. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  99. /// <param name="title">Title for the query.</param>
  100. /// <param name="message">Message to display, might contain multiple lines.</param>
  101. /// <param name="defaultButton">Index of the default button.</param>
  102. /// <param name="buttons">Array of buttons to add.</param>
  103. /// <remarks>
  104. /// The message box will be vertically and horizontally centered in the container and the size will be
  105. /// automatically determined from the size of the title, message. and buttons.
  106. /// </remarks>
  107. public static int ErrorQuery (string title, string message, int defaultButton = 0, params string [] buttons)
  108. {
  109. return QueryFull (true, 0, 0, title, message, defaultButton, true, buttons);
  110. }
  111. /// <summary>
  112. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  113. /// to the user.
  114. /// </summary>
  115. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  116. /// <param name="width">Width for the window.</param>
  117. /// <param name="height">Height for the window.</param>
  118. /// <param name="title">Title for the query.</param>
  119. /// <param name="message">Message to display, might contain multiple lines.</param>
  120. /// <param name="defaultButton">Index of the default button.</param>
  121. /// <param name="wrapMessagge">If wrap the message or not.</param>
  122. /// <param name="buttons">Array of buttons to add.</param>
  123. /// <remarks>
  124. /// Use <see cref="ErrorQuery(string, string, string[])"/> instead; it automatically sizes the MessageBox based on
  125. /// the contents.
  126. /// </remarks>
  127. public static int ErrorQuery (
  128. int width,
  129. int height,
  130. string title,
  131. string message,
  132. int defaultButton = 0,
  133. bool wrapMessagge = true,
  134. params string [] buttons
  135. )
  136. {
  137. return QueryFull (true, width, height, title, message, defaultButton, wrapMessagge, buttons);
  138. }
  139. /// <summary>
  140. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  141. /// to the user.
  142. /// </summary>
  143. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  144. /// <param name="title">Title for the query.</param>
  145. /// <param name="message">Message to display, might contain multiple lines.</param>
  146. /// <param name="defaultButton">Index of the default button.</param>
  147. /// <param name="wrapMessagge">If wrap the message or not.</param>
  148. /// <param name="buttons">Array of buttons to add.</param>
  149. /// <remarks>
  150. /// The message box will be vertically and horizontally centered in the container and the size will be
  151. /// automatically determined from the size of the title, message. and buttons.
  152. /// </remarks>
  153. public static int ErrorQuery (
  154. string title,
  155. string message,
  156. int defaultButton = 0,
  157. bool wrapMessagge = true,
  158. params string [] buttons
  159. )
  160. {
  161. return QueryFull (true, 0, 0, title, message, defaultButton, wrapMessagge, buttons);
  162. }
  163. /// <summary>
  164. /// Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  165. /// to the user.
  166. /// </summary>
  167. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  168. /// <param name="width">Width for the window.</param>
  169. /// <param name="height">Height for the window.</param>
  170. /// <param name="title">Title for the query.</param>
  171. /// <param name="message">Message to display, might contain multiple lines.</param>
  172. /// <param name="buttons">Array of buttons to add.</param>
  173. /// <remarks>
  174. /// Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
  175. /// contents.
  176. /// </remarks>
  177. public static int Query (int width, int height, string title, string message, params string [] buttons)
  178. {
  179. return QueryFull (false, width, height, title, message, 0, true, buttons);
  180. }
  181. /// <summary>
  182. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  183. /// to the user.
  184. /// </summary>
  185. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  186. /// <param name="title">Title for the query.</param>
  187. /// <param name="message">Message to display, might contain multiple lines.</param>
  188. /// <param name="buttons">Array of buttons to add.</param>
  189. /// <remarks>
  190. /// The message box will be vertically and horizontally centered in the container and the size will be
  191. /// automatically determined from the size of the message and buttons.
  192. /// </remarks>
  193. public static int Query (string title, string message, params string [] buttons) { return QueryFull (false, 0, 0, title, message, 0, true, buttons); }
  194. /// <summary>
  195. /// Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  196. /// to the user.
  197. /// </summary>
  198. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  199. /// <param name="width">Width for the window.</param>
  200. /// <param name="height">Height for the window.</param>
  201. /// <param name="title">Title for the query.</param>
  202. /// <param name="message">Message to display, might contain multiple lines.</param>
  203. /// <param name="defaultButton">Index of the default button.</param>
  204. /// <param name="buttons">Array of buttons to add.</param>
  205. /// <remarks>
  206. /// Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
  207. /// contents.
  208. /// </remarks>
  209. public static int Query (
  210. int width,
  211. int height,
  212. string title,
  213. string message,
  214. int defaultButton = 0,
  215. params string [] buttons
  216. )
  217. {
  218. return QueryFull (false, width, height, title, message, defaultButton, true, buttons);
  219. }
  220. /// <summary>
  221. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  222. /// to the user.
  223. /// </summary>
  224. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  225. /// <param name="title">Title for the query.</param>
  226. /// <param name="message">Message to display, might contain multiple lines.</param>
  227. /// <param name="defaultButton">Index of the default button.</param>
  228. /// <param name="buttons">Array of buttons to add.</param>
  229. /// <remarks>
  230. /// The message box will be vertically and horizontally centered in the container and the size will be
  231. /// automatically determined from the size of the message and buttons.
  232. /// </remarks>
  233. public static int Query (string title, string message, int defaultButton = 0, params string [] buttons)
  234. {
  235. return QueryFull (false, 0, 0, title, message, defaultButton, true, buttons);
  236. }
  237. /// <summary>
  238. /// Presents a normal <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  239. /// to the user.
  240. /// </summary>
  241. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  242. /// <param name="width">Width for the window.</param>
  243. /// <param name="height">Height for the window.</param>
  244. /// <param name="title">Title for the query.</param>
  245. /// <param name="message">Message to display, might contain multiple lines.</param>
  246. /// <param name="defaultButton">Index of the default button.</param>
  247. /// <param name="wrapMessagge">If wrap the message or not.</param>
  248. /// <param name="buttons">Array of buttons to add.</param>
  249. /// <remarks>
  250. /// Use <see cref="Query(string, string, string[])"/> instead; it automatically sizes the MessageBox based on the
  251. /// contents.
  252. /// </remarks>
  253. public static int Query (
  254. int width,
  255. int height,
  256. string title,
  257. string message,
  258. int defaultButton = 0,
  259. bool wrapMessagge = true,
  260. params string [] buttons
  261. )
  262. {
  263. return QueryFull (false, width, height, title, message, defaultButton, wrapMessagge, buttons);
  264. }
  265. /// <summary>
  266. /// Presents an error <see cref="MessageBox"/> with the specified title and message and a list of buttons to show
  267. /// to the user.
  268. /// </summary>
  269. /// <returns>The index of the selected button, or -1 if the user pressed ESC to close the dialog.</returns>
  270. /// <param name="title">Title for the query.</param>
  271. /// <param name="message">Message to display, might contain multiple lines.</param>
  272. /// <param name="defaultButton">Index of the default button.</param>
  273. /// <param name="wrapMessage">If wrap the message or not.</param>
  274. /// <param name="buttons">Array of buttons to add.</param>
  275. /// <remarks>
  276. /// The message box will be vertically and horizontally centered in the container and the size will be
  277. /// automatically determined from the size of the message and buttons.
  278. /// </remarks>
  279. public static int Query (
  280. string title,
  281. string message,
  282. int defaultButton = 0,
  283. bool wrapMessage = true,
  284. params string [] buttons
  285. )
  286. {
  287. return QueryFull (false, 0, 0, title, message, defaultButton, wrapMessage, buttons);
  288. }
  289. private static int QueryFull (
  290. bool useErrorColors,
  291. int width,
  292. int height,
  293. string title,
  294. string message,
  295. int defaultButton = 0,
  296. bool wrapMessage = true,
  297. params string [] buttons
  298. )
  299. {
  300. // Create button array for Dialog
  301. var count = 0;
  302. List<Button> buttonList = new ();
  303. if (buttons is { })
  304. {
  305. if (defaultButton > buttons.Length - 1)
  306. {
  307. defaultButton = buttons.Length - 1;
  308. }
  309. foreach (string s in buttons)
  310. {
  311. var b = new Button
  312. {
  313. Text = s,
  314. };
  315. if (count == defaultButton)
  316. {
  317. b.IsDefault = true;
  318. }
  319. buttonList.Add (b);
  320. count++;
  321. }
  322. }
  323. Alignment buttonJust = Dialog.DefaultButtonJustification;
  324. Dialog.DefaultButtonJustification = Alignment.Centered;
  325. var d = new Dialog
  326. {
  327. Buttons = buttonList.ToArray (),
  328. Title = title,
  329. BorderStyle = DefaultBorderStyle,
  330. Width = Dim.Percent (60),
  331. Height = 5 // Border + one line of text + vspace + buttons
  332. };
  333. Dialog.DefaultButtonJustification = buttonJust;
  334. if (width != 0)
  335. {
  336. d.Width = width;
  337. }
  338. if (height != 0)
  339. {
  340. d.Height = height;
  341. }
  342. if (useErrorColors)
  343. {
  344. d.ColorScheme = Colors.ColorSchemes ["Error"];
  345. }
  346. else
  347. {
  348. d.ColorScheme = Colors.ColorSchemes ["Dialog"];
  349. }
  350. var messageLabel = new Label
  351. {
  352. Text = message,
  353. TextJustification = Alignment.Centered,
  354. X = Pos.Center (),
  355. Y = 0
  356. };
  357. if (wrapMessage)
  358. {
  359. messageLabel.Width = Dim.Fill ();
  360. messageLabel.Height = Dim.Fill (1);
  361. }
  362. messageLabel.TextFormatter.WordWrap = wrapMessage;
  363. messageLabel.TextFormatter.MultiLine = !wrapMessage;
  364. d.Add (messageLabel);
  365. // Setup actions
  366. Clicked = -1;
  367. for (var n = 0; n < buttonList.Count; n++)
  368. {
  369. int buttonId = n;
  370. Button b = buttonList [n];
  371. b.Accept += (s, e) =>
  372. {
  373. Clicked = buttonId;
  374. Application.RequestStop ();
  375. };
  376. if (b.IsDefault)
  377. {
  378. b.SetFocus ();
  379. }
  380. }
  381. d.Loaded += Dialog_Loaded;
  382. // Run the modal; do not shutdown the mainloop driver when done
  383. Application.Run (d);
  384. d.Dispose ();
  385. return Clicked;
  386. void Dialog_Loaded (object s, EventArgs e)
  387. {
  388. if (width != 0 || height != 0)
  389. {
  390. return;
  391. }
  392. // TODO: replace with Dim.Fit when implemented
  393. Rectangle maxBounds = d.SuperView?.Viewport ?? Application.Top.Viewport;
  394. Thickness adornmentsThickness = d.GetAdornmentsThickness ();
  395. if (wrapMessage)
  396. {
  397. messageLabel.TextFormatter.Size = new (
  398. maxBounds.Size.Width
  399. - adornmentsThickness.Horizontal,
  400. maxBounds.Size.Height
  401. - adornmentsThickness.Vertical);
  402. }
  403. string msg = messageLabel.TextFormatter.Format ();
  404. Size messageSize = messageLabel.TextFormatter.FormatAndGetSize ();
  405. // Ensure the width fits the text + buttons
  406. int newWidth = Math.Max (
  407. width,
  408. Math.Max (
  409. messageSize.Width + adornmentsThickness.Horizontal,
  410. d.GetButtonsWidth () + d.Buttons.Length + adornmentsThickness.Horizontal));
  411. if (newWidth > d.Frame.Width)
  412. {
  413. d.Width = newWidth;
  414. }
  415. // Ensure height fits the text + vspace + buttons
  416. if (messageSize.Height == 0)
  417. {
  418. d.Height = Math.Max (height, 3 + adornmentsThickness.Vertical);
  419. }
  420. else
  421. {
  422. string lastLine = messageLabel.TextFormatter.GetLines () [^1];
  423. // INTENT: Instead of the check against \n or \r\n, how about just Environment.NewLine?
  424. d.Height = Math.Max (
  425. height,
  426. messageSize.Height
  427. + (lastLine.EndsWith ("\r\n") || lastLine.EndsWith ('\n') ? 1 : 2)
  428. + adornmentsThickness.Vertical);
  429. }
  430. d.SetRelativeLayout (d.SuperView?.ContentSize.GetValueOrDefault () ?? Application.Top.ContentSize.GetValueOrDefault ());
  431. d.LayoutSubviews ();
  432. }
  433. }
  434. }