MessageBox.cs 20 KB

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