MessageBox.cs 19 KB

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