MessageBox.cs 21 KB

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