Wizard.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. using System;
  2. using System.Collections.Generic;
  3. using NStack;
  4. using Terminal.Gui.Resources;
  5. namespace Terminal.Gui {
  6. /// <summary>
  7. /// Provides a step-based "wizard" UI. The Wizard supports multiple steps. Each step (<see cref="WizardStep"/>) can host
  8. /// arbitrary <see cref="View"/>s, much like a <see cref="Dialog"/>. Each step also has a pane for help text. Along the
  9. /// bottom of the Wizard view are customizable buttons enabling the user to navigate forward and backward through the Wizard.
  10. /// </summary>
  11. /// <remarks>
  12. /// </remarks>
  13. public class Wizard : Dialog {
  14. /// <summary>
  15. /// One step for the Wizard. The <see cref="WizardStep"/> view hosts two sub-views: 1) add <see cref="View"/>s to <see cref="WizardStep.Controls"/>,
  16. /// 2) use <see cref="WizardStep.HelpText"/> to set the contents of the <see cref="TextView"/> that shows on the
  17. /// right side. Use <see cref="WizardStep.showControls"/> and <see cref="WizardStep.showHelp"/> to
  18. /// control wether the control or help pane are shown.
  19. /// </summary>
  20. /// <remarks>
  21. /// If <see cref="Button"/>s are added, do not set <see cref="Button.IsDefault"/> to true as this will conflict
  22. /// with the Next button of the Wizard.
  23. /// </remarks>
  24. public class WizardStep : View {
  25. /// <summary>
  26. /// The title of the <see cref="WizardStep"/>.
  27. /// </summary>
  28. public ustring Title { get => title; set => title = value; }
  29. // TODO: Update Wizard title when step title is changed if step is current - this will require step to slueth it's parent
  30. private ustring title;
  31. // The controlPane is a separate view, so when devs add controls to the Step and help is visible, Y = Pos.AnchorEnd()
  32. // will work as expected.
  33. private View controlPane = new FrameView ();
  34. /// <summary>
  35. /// THe pane that holds the controls for the <see cref="WizardStep"/>. Use <see cref="WizardStep.Controls"/> `Add(View`) to add
  36. /// controls. Note that the Controls view is sized to take 70% of the Wizard's width and the <see cref="WizardStep.HelpText"/>
  37. /// takes the other 30%. This can be adjusted by setting `Width` from `Dim.Percent(70)` to
  38. /// another value. If <see cref="WizardStep.ShowHelp"/> is set to `false` the control pane will fill the entire
  39. /// Wizard.
  40. /// </summary>
  41. public View Controls { get => controlPane; }
  42. /// <summary>
  43. /// Sets or gets help text for the <see cref="WizardStep"/>.If <see cref="WizardStep.ShowHelp"/> is set to
  44. /// `false` the control pane will fill the entire wizard.
  45. /// </summary>
  46. /// <remarks>The help text is displayed using a read-only <see cref="TextView"/>.</remarks>
  47. public ustring HelpText { get => helpTextView.Text; set => helpTextView.Text = value; }
  48. private TextView helpTextView = new TextView ();
  49. /// <summary>
  50. /// Sets or gets the text for the back button. The back button will only be visible on
  51. /// steps after the first step.
  52. /// </summary>
  53. /// <remarks>The default text is "Back"</remarks>
  54. public ustring BackButtonText { get; set; } = ustring.Empty;
  55. // TODO: Update button text of Wizard button when step's button text is changed if step is current - this will require step to slueth it's parent
  56. /// <summary>
  57. /// Sets or gets the text for the next/finish button.
  58. /// </summary>
  59. /// <remarks>The default text is "Next..." if the Pane is not the last pane. Otherwise it is "Finish"</remarks>
  60. public ustring NextButtonText { get; set; } = ustring.Empty;
  61. // TODO: Update button text of Wizard button when step's button text is changed if step is current - this will require step to slueth it's parent
  62. /// <summary>
  63. /// Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/> positioning.
  64. /// </summary>
  65. /// <param name="title">Title for the Step. Will be appended to the containing Wizard's title as
  66. /// "Wizard Title - Wizard Step Title" when this step is active.</param>
  67. /// <remarks>
  68. /// </remarks>
  69. public WizardStep (ustring title)
  70. {
  71. this.Title = title; // this.Title holds just the "Wizard Title"; base.Title holds "Wizard Title - Step Title"
  72. this.ColorScheme = Colors.Dialog;
  73. Y = 0;
  74. Height = Dim.Fill (1); // for button frame
  75. Width = Dim.Fill ();
  76. Controls.ColorScheme = Colors.Dialog;
  77. Controls.Border.BorderStyle = BorderStyle.None;
  78. Controls.Border.Padding = new Thickness (0);
  79. Controls.Border.BorderThickness = new Thickness (0);
  80. this.Add (Controls);
  81. helpTextView.ColorScheme = Colors.Menu;
  82. helpTextView.Y = 0;
  83. helpTextView.ReadOnly = true;
  84. helpTextView.WordWrap = true;
  85. this.Add (helpTextView);
  86. ShowHide ();
  87. var scrollBar = new ScrollBarView (helpTextView, true);
  88. scrollBar.ChangedPosition += () => {
  89. helpTextView.TopRow = scrollBar.Position;
  90. if (helpTextView.TopRow != scrollBar.Position) {
  91. scrollBar.Position = helpTextView.TopRow;
  92. }
  93. helpTextView.SetNeedsDisplay ();
  94. };
  95. scrollBar.OtherScrollBarView.ChangedPosition += () => {
  96. helpTextView.LeftColumn = scrollBar.OtherScrollBarView.Position;
  97. if (helpTextView.LeftColumn != scrollBar.OtherScrollBarView.Position) {
  98. scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn;
  99. }
  100. helpTextView.SetNeedsDisplay ();
  101. };
  102. scrollBar.VisibleChanged += () => {
  103. if (scrollBar.Visible && helpTextView.RightOffset == 0) {
  104. helpTextView.RightOffset = 1;
  105. } else if (!scrollBar.Visible && helpTextView.RightOffset == 1) {
  106. helpTextView.RightOffset = 0;
  107. }
  108. };
  109. scrollBar.OtherScrollBarView.VisibleChanged += () => {
  110. if (scrollBar.OtherScrollBarView.Visible && helpTextView.BottomOffset == 0) {
  111. helpTextView.BottomOffset = 1;
  112. } else if (!scrollBar.OtherScrollBarView.Visible && helpTextView.BottomOffset == 1) {
  113. helpTextView.BottomOffset = 0;
  114. }
  115. };
  116. helpTextView.DrawContent += (e) => {
  117. scrollBar.Size = helpTextView.Lines;
  118. scrollBar.Position = helpTextView.TopRow;
  119. if (scrollBar.OtherScrollBarView != null) {
  120. scrollBar.OtherScrollBarView.Size = helpTextView.Maxlength;
  121. scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn;
  122. }
  123. scrollBar.LayoutSubviews ();
  124. scrollBar.Refresh ();
  125. };
  126. this.Add (scrollBar);
  127. }
  128. /// <summary>
  129. /// If true (the default) the help will be visible. If false, the help will not be shown and the control pane will
  130. /// fill the wizard step.
  131. /// </summary>
  132. public bool ShowHelp {
  133. get => showHelp;
  134. set {
  135. showHelp = value;
  136. ShowHide ();
  137. }
  138. }
  139. private bool showHelp = true;
  140. /// <summary>
  141. /// If true (the default) the <see cref="Controls"/> View will be visible. If false, the controls will not be shown and the help will
  142. /// fill the wizard step.
  143. /// </summary>
  144. public bool ShowControls {
  145. get => showControls;
  146. set {
  147. showControls = value;
  148. ShowHide ();
  149. }
  150. }
  151. private bool showControls = true;
  152. /// <summary>
  153. /// Does the work to show and hide the controls, help, and buttons as appropriate
  154. /// </summary>
  155. private void ShowHide ()
  156. {
  157. Controls.Height = Dim.Fill (1);
  158. helpTextView.Height = Dim.Fill (1);
  159. helpTextView.Width = Dim.Fill ();
  160. if (showControls) {
  161. if (showHelp) {
  162. Controls.Width = Dim.Percent (70);
  163. helpTextView.X = Pos.Right (Controls);
  164. helpTextView.Width = Dim.Fill ();
  165. } else {
  166. Controls.Width = Dim.Percent (100);
  167. }
  168. } else {
  169. if (showHelp) {
  170. helpTextView.X = 0;
  171. } else {
  172. // Error - no pane shown
  173. }
  174. }
  175. Controls.Visible = showControls;
  176. helpTextView.Visible = showHelp;
  177. }
  178. } // WizardStep
  179. /// <summary>
  180. /// Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/> positioning.
  181. /// </summary>
  182. /// <remarks>
  183. /// The Wizard will be vertically and horizontally centered in the container.
  184. /// After initialization use <c>X</c>, <c>Y</c>, <c>Width</c>, and <c>Height</c> change size and position.
  185. /// </remarks>
  186. public Wizard () : this (ustring.Empty)
  187. {
  188. }
  189. /// <summary>
  190. /// Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/> positioning.
  191. /// </summary>
  192. /// <param name="title">Title for the Wizard.</param>
  193. /// <remarks>
  194. /// The Wizard will be vertically and horizontally centered in the container.
  195. /// After initialization use <c>X</c>, <c>Y</c>, <c>Width</c>, and <c>Height</c> change size and position.
  196. /// </remarks>
  197. public Wizard (ustring title) : base (title)
  198. {
  199. wizardTitle = title;
  200. // Using Justify causes the Back and Next buttons to be hard justified against
  201. // the left and right edge
  202. ButtonAlignment = ButtonAlignments.Justify;
  203. this.Border.BorderStyle = BorderStyle.Double;
  204. // Add a horiz separator
  205. var separator = new LineView (Graphs.Orientation.Horizontal) {
  206. Y = Pos.AnchorEnd (2)
  207. };
  208. Add (separator);
  209. // BUGBUG: Space is to work around https://github.com/migueldeicaza/gui.cs/issues/1812
  210. backBtn = new Button (Strings.wzBack) { AutoSize = true };
  211. AddButton (backBtn);
  212. nextfinishBtn = new Button (Strings.wzFinish) { AutoSize = true };
  213. nextfinishBtn.IsDefault = true;
  214. AddButton (nextfinishBtn);
  215. backBtn.Clicked += BackBtn_Clicked;
  216. nextfinishBtn.Clicked += NextfinishBtn_Clicked;
  217. Loaded += Wizard_Loaded;
  218. Closing += Wizard_Closing;
  219. }
  220. private bool finishedPressed = false;
  221. private void Wizard_Closing (ToplevelClosingEventArgs obj)
  222. {
  223. if (!finishedPressed) {
  224. var args = new WizardButtonEventArgs ();
  225. Cancelled?.Invoke (args);
  226. }
  227. }
  228. private void Wizard_Loaded ()
  229. {
  230. foreach (var step in steps) {
  231. step.Y = 0;
  232. }
  233. if (steps.Count > 0) {
  234. CurrentStep = steps.First.Value;
  235. }
  236. }
  237. private void NextfinishBtn_Clicked ()
  238. {
  239. if (CurrentStep == steps.Last.Value) {
  240. var args = new WizardButtonEventArgs ();
  241. Finished?.Invoke (args);
  242. if (!args.Cancel) {
  243. finishedPressed = true;
  244. Application.RequestStop (this);
  245. }
  246. } else {
  247. var args = new WizardButtonEventArgs ();
  248. MovingNext?.Invoke (args);
  249. if (!args.Cancel) {
  250. var current = steps.Find (CurrentStep);
  251. if (current != null && current.Next != null) {
  252. GotoStep (current.Next.Value);
  253. }
  254. }
  255. }
  256. }
  257. private void BackBtn_Clicked ()
  258. {
  259. var args = new WizardButtonEventArgs ();
  260. MovingBack?.Invoke (args);
  261. if (!args.Cancel) {
  262. var current = steps.Find (CurrentStep);
  263. if (current != null && current.Previous != null) {
  264. GotoStep (current.Previous.Value);
  265. }
  266. }
  267. }
  268. private LinkedList<WizardStep> steps = new LinkedList<WizardStep> ();
  269. private WizardStep currentStep = null;
  270. /// <summary>
  271. /// If the <see cref="CurrentStep"/> is not the first step in the wizard, this button causes
  272. /// the <see cref="MovingBack"/> event to be fired and the wizard moves to the previous step.
  273. /// </summary>
  274. /// <remarks>
  275. /// Use the <see cref="MovingBack"></see> event to be notified when the user attempts to go back.
  276. /// </remarks>
  277. public Button BackButton { get => backBtn; }
  278. private Button backBtn;
  279. /// <summary>
  280. /// If the <see cref="CurrentStep"/> is the last step in the wizard, this button causes
  281. /// the <see cref="Finished"/> event to be fired and the wizard to close. If the step is not the last step,
  282. /// the <see cref="MovingNext"/> event will be fired and the wizard will move next step.
  283. /// </summary>
  284. /// <remarks>
  285. /// Use the <see cref="MovingNext"></see> and <see cref="Finished"></see> events to be notified
  286. /// when the user attempts go to the next step or finish the wizard.
  287. /// </remarks>
  288. public Button NextFinishButton { get => nextfinishBtn; }
  289. private Button nextfinishBtn;
  290. /// <summary>
  291. /// Adds a step to the wizard. The Next and Back buttons navigate through the added steps in the
  292. /// order they were added.
  293. /// </summary>
  294. /// <param name="newStep"></param>
  295. /// <remarks>The "Next..." button of the last step added will read "Finish" (unless changed from default).</remarks>
  296. public void AddStep (WizardStep newStep)
  297. {
  298. steps.AddLast (newStep);
  299. this.Add (newStep);
  300. SetNeedsLayout ();
  301. }
  302. /// <summary>
  303. /// The title of the Wizard, shown at the top of the Wizard with " - currentStep.Title" appended.
  304. /// </summary>
  305. public new ustring Title {
  306. get {
  307. // The base (Dialog) Title holds the full title ("Wizard Title - Step Title")
  308. return base.Title;
  309. }
  310. set {
  311. wizardTitle = value;
  312. base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + currentStep.Title : string.Empty)}";
  313. }
  314. }
  315. private ustring wizardTitle = ustring.Empty;
  316. /// <summary>
  317. /// <see cref="EventArgs"/> for <see cref="WizardStep"/> transition events.
  318. /// </summary>
  319. public class WizardButtonEventArgs : EventArgs {
  320. /// <summary>
  321. /// Set to true to cancel the transition to the next step.
  322. /// </summary>
  323. public bool Cancel { get; set; }
  324. /// <summary>
  325. /// Initializes a new instance of <see cref="WizardButtonEventArgs"/>
  326. /// </summary>
  327. public WizardButtonEventArgs ()
  328. {
  329. Cancel = false;
  330. }
  331. }
  332. /// <summary>
  333. /// This event is raised when the Back button in the <see cref="Wizard"/> is clicked. The Back button is always
  334. /// the first button in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any.
  335. /// </summary>
  336. public event Action<WizardButtonEventArgs> MovingBack;
  337. /// <summary>
  338. /// This event is raised when the Next/Finish button in the <see cref="Wizard"/> is clicked. The Next/Finish button is always
  339. /// the last button in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any. This event is only
  340. /// raised if the <see cref="CurrentStep"/> is the last Step in the Wizard flow
  341. /// (otherwise the <see cref="Finished"/> event is raised).
  342. /// </summary>
  343. public event Action<WizardButtonEventArgs> MovingNext;
  344. /// <summary>
  345. /// This event is raised when the Next/Finish button in the <see cref="Wizard"/> is clicked. The Next/Finish button is always
  346. /// the last button in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any. This event is only
  347. /// raised if the <see cref="CurrentStep"/> is the last Step in the Wizard flow
  348. /// (otherwise the <see cref="Finished"/> event is raised).
  349. /// </summary>
  350. public event Action<WizardButtonEventArgs> Finished;
  351. /// <summary>
  352. /// This event is raised when the user has cancelled the <see cref="Wizard"/> (with Ctrl-Q or ESC).
  353. /// </summary>
  354. public event Action<WizardButtonEventArgs> Cancelled;
  355. /// <summary>
  356. /// <see cref="EventArgs"/> for <see cref="WizardStep"/> events.
  357. /// </summary>
  358. public class StepChangeEventArgs : EventArgs {
  359. /// <summary>
  360. /// The current (or previous) <see cref="WizardStep"/>.
  361. /// </summary>
  362. public WizardStep OldStep { get; }
  363. /// <summary>
  364. /// The <see cref="WizardStep"/> the <see cref="Wizard"/> is changing to or has changed to.
  365. /// </summary>
  366. public WizardStep NewStep { get; }
  367. /// <summary>
  368. /// Event handlers can set to true before returning to cancel the step transition.
  369. /// </summary>
  370. public bool Cancel { get; set; }
  371. /// <summary>
  372. /// Initializes a new instance of <see cref="StepChangeEventArgs"/>
  373. /// </summary>
  374. /// <param name="oldStep">The current <see cref="WizardStep"/>.</param>
  375. /// <param name="newStep">The new <see cref="WizardStep"/>.</param>
  376. public StepChangeEventArgs (WizardStep oldStep, WizardStep newStep)
  377. {
  378. OldStep = oldStep;
  379. NewStep = newStep;
  380. Cancel = false;
  381. }
  382. }
  383. /// <summary>
  384. /// This event is raised when the current <see cref="CurrentStep"/>) is about to change. Use <see cref="StepChangeEventArgs.Cancel"/>
  385. /// to abort the transition.
  386. /// </summary>
  387. public event Action<StepChangeEventArgs> StepChanging;
  388. /// <summary>
  389. /// This event is raised after the <see cref="Wizard"/> has changed the <see cref="CurrentStep"/>.
  390. /// </summary>
  391. public event Action<StepChangeEventArgs> StepChanged;
  392. /// <summary>
  393. /// Gets or sets the currently active <see cref="WizardStep"/>.
  394. /// </summary>
  395. public WizardStep CurrentStep {
  396. get => currentStep;
  397. set {
  398. GotoStep (value);
  399. }
  400. }
  401. /// <summary>
  402. /// Called when the <see cref="Wizard"/> is about to transition to another <see cref="WizardStep"/>. Fires the <see cref="StepChanging"/> event.
  403. /// </summary>
  404. /// <param name="oldStep">The step the Wizard is about to change from</param>
  405. /// <param name="newStep">The step the Wizard is about to change to</param>
  406. /// <returns>True if the change is to be cancelled.</returns>
  407. public virtual bool OnStepChanging (WizardStep oldStep, WizardStep newStep)
  408. {
  409. var args = new StepChangeEventArgs (oldStep, newStep);
  410. StepChanging?.Invoke (args);
  411. return args.Cancel;
  412. }
  413. /// <summary>
  414. /// Called when the <see cref="Wizard"/> has completed transition to a new <see cref="WizardStep"/>. Fires the <see cref="StepChanged"/> event.
  415. /// </summary>
  416. /// <param name="oldStep">The step the Wizard changed from</param>
  417. /// <param name="newStep">The step the Wizard has changed to</param>
  418. /// <returns>True if the change is to be cancelled.</returns>
  419. public virtual bool OnStepChanged (WizardStep oldStep, WizardStep newStep)
  420. {
  421. var args = new StepChangeEventArgs (oldStep, newStep);
  422. StepChanged?.Invoke (args);
  423. return args.Cancel;
  424. }
  425. /// <summary>
  426. /// Changes to the specified <see cref="WizardStep"/>.
  427. /// </summary>
  428. /// <param name="newStep">The step to go to.</param>
  429. /// <returns>True if the transition to the step succeeded. False if the step was not found or the operation was cancelled.</returns>
  430. public bool GotoStep (WizardStep newStep)
  431. {
  432. if (OnStepChanging (currentStep, newStep)) {
  433. return false;
  434. }
  435. // Hide all but the new step
  436. foreach (WizardStep step in steps) {
  437. step.Visible = (step == newStep);
  438. }
  439. base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + newStep.Title : string.Empty)}";
  440. // Configure the Back button
  441. backBtn.Text = newStep.BackButtonText != ustring.Empty ? newStep.BackButtonText : Strings.wzBack; // "_Back";
  442. backBtn.Visible = (newStep != steps.First.Value);
  443. // Configure the Next/Finished button
  444. if (newStep == steps.Last.Value) {
  445. nextfinishBtn.Text = newStep.NextButtonText != ustring.Empty ? newStep.NextButtonText : Strings.wzFinish; // "Fi_nish";
  446. } else {
  447. nextfinishBtn.Text = newStep.NextButtonText != ustring.Empty ? newStep.NextButtonText : Strings.wzNext; // "_Next...";
  448. }
  449. // Set focus to the nav buttons
  450. if (backBtn.HasFocus) {
  451. backBtn.SetFocus ();
  452. } else {
  453. nextfinishBtn.SetFocus ();
  454. }
  455. var oldStep = currentStep;
  456. currentStep = newStep;
  457. LayoutSubviews ();
  458. Redraw (this.Bounds);
  459. if (OnStepChanged (oldStep, currentStep)) {
  460. // For correctness we do this, but it's meaningless because there's nothing to cancel
  461. return false;
  462. }
  463. return true;
  464. }
  465. }
  466. }