MessageBox.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  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 { Text = s };
  312. if (count == defaultButton)
  313. {
  314. b.IsDefault = true;
  315. }
  316. buttonList.Add (b);
  317. count++;
  318. }
  319. }
  320. Dialog d;
  321. d = new Dialog
  322. {
  323. Buttons = buttonList.ToArray (),
  324. Title = title,
  325. BorderStyle = DefaultBorderStyle,
  326. Width = Dim.Percent (60),
  327. Height = 5 // Border + one line of text + vspace + buttons
  328. };
  329. if (width != 0)
  330. {
  331. d.Width = width;
  332. }
  333. if (height != 0)
  334. {
  335. d.Height = height;
  336. }
  337. if (useErrorColors)
  338. {
  339. d.ColorScheme = Colors.ColorSchemes ["Error"];
  340. }
  341. else
  342. {
  343. d.ColorScheme = Colors.ColorSchemes ["Dialog"];
  344. }
  345. var messageLabel = new Label
  346. {
  347. AutoSize = !wrapMessage,
  348. Text = message,
  349. TextAlignment = TextAlignment.Centered,
  350. X = Pos.Center (),
  351. Y = 0
  352. };
  353. if (!messageLabel.AutoSize)
  354. {
  355. messageLabel.Width = Dim.Fill ();
  356. messageLabel.Height = Dim.Fill (1);
  357. }
  358. messageLabel.TextFormatter.WordWrap = wrapMessage;
  359. messageLabel.TextFormatter.MultiLine = !wrapMessage;
  360. d.Add (messageLabel);
  361. d.Loaded += (s, e) =>
  362. {
  363. if (width != 0 || height != 0)
  364. {
  365. return;
  366. }
  367. // TODO: replace with Dim.Fit when implemented
  368. Rectangle maxBounds = d.SuperView?.Viewport ?? Application.Top.Viewport;
  369. Thickness adornmentsThickness = d.GetAdornmentsThickness ();
  370. if (wrapMessage)
  371. {
  372. messageLabel.TextFormatter.Size = new (
  373. maxBounds.Size.Width
  374. - adornmentsThickness.Horizontal,
  375. maxBounds.Size.Height
  376. - adornmentsThickness.Vertical
  377. );
  378. }
  379. string msg = messageLabel.TextFormatter.Format ();
  380. Size messageSize = messageLabel.TextFormatter.FormatAndGetSize ();
  381. // Ensure the width fits the text + buttons
  382. int newWidth = Math.Max (
  383. width,
  384. Math.Max (
  385. messageSize.Width + adornmentsThickness.Horizontal,
  386. d.GetButtonsWidth () + d.Buttons.Length + adornmentsThickness.Horizontal
  387. )
  388. );
  389. if (newWidth > d.Frame.Width)
  390. {
  391. d.Width = newWidth;
  392. }
  393. // Ensure height fits the text + vspace + buttons
  394. if (messageSize.Height == 0)
  395. {
  396. d.Height = Math.Max (height, 3 + adornmentsThickness.Vertical);
  397. }
  398. else
  399. {
  400. string lastLine = messageLabel.TextFormatter.GetLines () [^1];
  401. // INTENT: Instead of the check against \n or \r\n, how about just Environment.NewLine?
  402. d.Height = Math.Max (
  403. height,
  404. messageSize.Height
  405. + (lastLine.EndsWith ("\r\n") || lastLine.EndsWith ('\n') ? 1 : 2)
  406. + adornmentsThickness.Vertical
  407. );
  408. }
  409. d.SetRelativeLayout (d.SuperView?.Frame ?? Application.Top.Frame);
  410. };
  411. // Setup actions
  412. Clicked = -1;
  413. for (var n = 0; n < buttonList.Count; n++)
  414. {
  415. int buttonId = n;
  416. Button b = buttonList [n];
  417. b.Accept += (s, e) =>
  418. {
  419. Clicked = buttonId;
  420. Application.RequestStop ();
  421. };
  422. if (b.IsDefault)
  423. {
  424. b.SetFocus ();
  425. }
  426. }
  427. // Run the modal; do not shutdown the mainloop driver when done
  428. Application.Run (d);
  429. return Clicked;
  430. }
  431. }