Bläddra i källkod

Adds multi-step Wizard view (#1783)

* Initial commit for Wizard

* Fixes #1777 - Dialog button justification. Adds unit tests

* Added missing API doc

* Work in progress

* Added tests for wide chars

* Tests

* more tests

* wip

* fixed test that broke by adjusting dialog button alignment

* fixed test that broke by adjusting dialog button alignment

* Fixed Dialogs scenario crash re: Parse v TryParse

* broke tests. can't figure out how.

* Revert "broke tests. can't figure out how."

This reverts commit f3c53928ac9db2606384943dae307de76efd2f3a.

* Fixed unit tests

* Reverted workaround that doesn't work

* Refactor and cleanup
Tig Kindel 3 år sedan
förälder
incheckning
48dbbb05a7

+ 1 - 1
Terminal.Gui/Windows/Dialog.cs

@@ -152,7 +152,7 @@ namespace Terminal.Gui {
 
 		void LayoutStartedHandler ()
 		{
-			if (buttons.Count == 0) return;
+			if (buttons.Count == 0 || !IsInitialized) return;
 
 			int shiftLeft = 0;
 

+ 417 - 0
Terminal.Gui/Windows/Wizard.cs

@@ -0,0 +1,417 @@
+using System;
+using System.Collections.Generic;
+using NStack;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Provides a step-based "wizard" UI. The Wizard supports multiple steps. Each step (<see cref="WizardStep"/>) can host 
+	/// arbitrary <see cref="View"/>s, much like a <see cref="Dialog"/>. Each step also has a pane for help text. Along the
+	/// bottom of the Wizard view are customizable buttons enabling the user to navigate forward and backward through the Wizard. 
+	/// </summary>
+	/// <remarks>
+	/// </remarks>
+	public class Wizard : Dialog {
+
+		/// <summary>
+		/// 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"/>, 
+		/// 2) use <see cref="WizardStep.HelpText"/> to set the contents of the <see cref="TextView"/> that shows on the
+		/// right side. Use <see cref="WizardStep.showControls"/> and <see cref="WizardStep.showHelp"/> to 
+		/// control wether the control or help pane are shown. 
+		/// </summary>
+		/// <remarks>
+		/// If <see cref="Button"/>s are added, do not set <see cref="Button.IsDefault"/> to true as this will conflict
+		/// with the Next button of the Wizard.
+		/// </remarks>
+		public class WizardStep : View {
+			/// <summary>
+			/// The title of the <see cref="WizardStep"/>.
+			/// </summary>
+			public ustring Title { get => title; set => title = value; }
+			// TODO: Update Wizard title when step title is changed if step is current - this will require step to slueth it's parent 
+			private ustring title;
+
+			private View controlPane = new FrameView ();
+
+			/// <summary>
+			/// THe pane that holds the controls for the <see cref="WizardStep"/>. Use <see cref="WizardStep.Controls"/> `Add(View`) to add 
+			/// controls. Note that the Controls view is sized to take 70% of the Wizard's width and the <see cref="WizardStep.HelpText"/> 
+			/// takes the other 30%. This can be adjusted by setting `Width` from `Dim.Percent(70)` to 
+			/// another value. If <see cref="WizardStep.ShowHelp"/> is set to `false` the control pane will fill the entire 
+			/// Wizard.
+			/// </summary>
+			public View Controls { get => controlPane; }
+
+			/// <summary>
+			/// Sets or gets help text for the <see cref="WizardStep"/>.If <see cref="WizardStep.ShowHelp"/> is set to 
+			/// `false` the control pane will fill the entire wizard.
+			/// </summary>
+			/// <remarks>The help text is displayed using a read-only <see cref="TextView"/>.</remarks>
+			public ustring HelpText { get => helpTextView.Text; set => helpTextView.Text = value; }
+			private TextView helpTextView = new TextView ();
+
+			/// <summary>
+			/// Sets or gets the text for the back button. The back button will only be visible on 
+			/// steps after the first step.
+			/// </summary>
+			/// <remarks>The default text is "Back"</remarks>
+			public ustring BackButtonText { get; set; } = ustring.Empty;
+			// 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 
+
+			/// <summary>
+			/// Sets or gets the text for the next/finish button.
+			/// </summary>
+			/// <remarks>The default text is "Next..." if the Pane is not the last pane. Otherwise it is "Finish"</remarks>
+			public ustring NextButtonText { get; set; } = ustring.Empty;
+			// 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 
+
+			/// <summary>
+			/// Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/> positioning.
+			/// </summary>
+			/// <param name="title">Title for the Step. Will be appended to the containing Wizard's title as 
+			/// "Wizard Title - Wizard Step Title" when this step is active.</param>
+			/// <remarks>
+			/// </remarks>
+			public WizardStep (ustring title)
+			{
+				this.Title = title; // this.Title holds just the "Wizard Title"; base.Title holds "Wizard Title - Step Title"
+				this.ColorScheme = Colors.Menu;
+		
+				Y = 0;
+				Height = Dim.Fill (1); // for button frame
+				Width = Dim.Fill ();
+
+				Controls.ColorScheme = Colors.Dialog;
+				Controls.Border.BorderStyle = BorderStyle.None;
+				Controls.Border.Padding = new Thickness (0);
+				Controls.Border.BorderThickness = new Thickness (0);
+				this.Add (Controls);
+
+				helpTextView.ColorScheme = Colors.Menu;
+				helpTextView.Y = 0;
+				helpTextView.ReadOnly = true;
+				helpTextView.WordWrap = true;
+				this.Add (helpTextView);
+				ShowHide ();
+
+				var scrollBar = new ScrollBarView (helpTextView, true);
+
+				scrollBar.ChangedPosition += () => {
+					helpTextView.TopRow = scrollBar.Position;
+					if (helpTextView.TopRow != scrollBar.Position) {
+						scrollBar.Position = helpTextView.TopRow;
+					}
+					helpTextView.SetNeedsDisplay ();
+				};
+
+				scrollBar.VisibleChanged += () => {
+					if (scrollBar.Visible && helpTextView.RightOffset == 0) {
+						helpTextView.RightOffset = 1;
+					} else if (!scrollBar.Visible && helpTextView.RightOffset == 1) {
+						helpTextView.RightOffset = 0;
+					}
+				};
+
+				helpTextView.DrawContent += (e) => {
+					scrollBar.Size = helpTextView.Lines;
+					scrollBar.Position = helpTextView.TopRow;
+					if (scrollBar.OtherScrollBarView != null) {
+						scrollBar.OtherScrollBarView.Size = helpTextView.Maxlength;
+						scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn;
+					}
+					scrollBar.LayoutSubviews ();
+					scrollBar.Refresh ();
+				};
+				this.Add (scrollBar);
+			}
+
+			/// <summary>
+			/// If true (the default) the help will be visible. If false, the help will not be shown and the control pane will
+			/// fill the wizard step.
+			/// </summary>
+			public bool ShowHelp {
+				get => showHelp;
+				set {
+					showHelp = value;
+					ShowHide ();
+				}
+			}
+			private bool showHelp = true;
+
+			/// <summary>
+			/// If true (the default) the <see cref="Controls"/> View will be visible. If false, the controls will not be shown and the help will
+			/// fill the wizard step.
+			/// </summary>
+			public bool ShowControls {
+				get => showControls;
+				set {
+					showControls = value;
+					ShowHide ();
+				}
+			}
+			private bool showControls = true;
+
+			/// <summary>
+			/// Does the work to show and hide the controls, help, and buttons as appropriate
+			/// </summary>
+			private void ShowHide ()
+			{
+				Controls.Height = Dim.Fill (1);
+				helpTextView.Height = Dim.Fill (1);
+				helpTextView.Width = Dim.Fill ();
+
+				if (showControls) {
+					if (showHelp) {
+						Controls.Width = Dim.Percent (70);
+						helpTextView.X = Pos.Right (Controls) ;
+						helpTextView.Width = Dim.Fill ();
+
+					} else {
+						Controls.Width = Dim.Percent (100);
+					}
+				} else {
+					if (showHelp) {
+						helpTextView.X = 0;
+					} else {
+						// Error - no pane shown
+					}
+
+				}
+				Controls.Visible = showControls;
+				helpTextView.Visible = showHelp;
+			}
+		}
+
+		/// <summary>
+		/// If the <see cref="CurrentStep"/> is not the first step in the wizard, this button causes
+		/// the <see cref="MovingBack"/> event to be fired and the wizard moves to the previous step. 
+		/// </summary>
+		/// <remarks>
+		/// Use the <see cref="MovingBack"></see> event to be notified when the user attempts to go back.
+		/// </remarks>
+		public Button BackButton { get => backBtn; }
+		private Button backBtn;
+
+		/// <summary>
+		/// If the <see cref="CurrentStep"/> is the last step in the wizard, this button causes
+		/// the <see cref="Finished"/> event to be fired and the wizard to close. If the step is not the last step,
+		/// the <see cref="MovingNext"/> event will be fired and the wizard will move next step. 
+		/// </summary>
+		/// <remarks>
+		/// Use the <see cref="MovingNext"></see> and <see cref="Finished"></see> events to be notified 
+		/// when the user attempts go to the next step or finish the wizard.
+		/// </remarks>
+		public Button NextFinishButton { get => nextfinishBtn; }
+		private Button nextfinishBtn;
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/> positioning.
+		/// </summary>
+		/// <remarks>
+		/// The Wizard will be vertically and horizontally centered in the container.
+		/// After initialization use <c>X</c>, <c>Y</c>, <c>Width</c>, and <c>Height</c> change size and position.
+		/// </remarks>
+		public Wizard () : this (ustring.Empty)
+		{
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="Wizard"/> class using <see cref="LayoutStyle.Computed"/> positioning.
+		/// </summary>
+		/// <param name="title">Title for the Wizard.</param>
+		/// <remarks>
+		/// The Wizard will be vertically and horizontally centered in the container.
+		/// After initialization use <c>X</c>, <c>Y</c>, <c>Width</c>, and <c>Height</c> change size and position.
+		/// </remarks>
+		public Wizard (ustring title) : base (title)
+		{
+			wizardTitle = title;
+			// Using Justify causes the Back and Next buttons to be hard justified against
+			// the left and right edge
+			ButtonAlignment = ButtonAlignments.Justify;
+			this.Border.BorderStyle = BorderStyle.Double;
+
+			// Add a horiz separator
+			var separator = new LineView (Graphs.Orientation.Horizontal) {
+				Y = Pos.AnchorEnd (2)
+			};
+			Add (separator);
+
+			backBtn = new Button ("_Back") { AutoSize = true };
+			AddButton (backBtn);
+
+			nextfinishBtn = new Button ("_Next...") { AutoSize = true };
+			nextfinishBtn.IsDefault = true;
+			AddButton (nextfinishBtn);
+
+			backBtn.Clicked += () => {
+				var args = new WizardStepEventArgs ();
+				MovingBack?.Invoke (args);
+				if (!args.Cancel) {
+					if (currentStep > 0) {
+						CurrentStep--;
+					}
+				}
+			};
+
+			nextfinishBtn.Clicked += () => {
+				if (currentStep == steps.Count - 1) {
+					var args = new WizardStepEventArgs ();
+					Finished?.Invoke (args);
+					if (!args.Cancel) {
+						Application.RequestStop (this);
+					}
+				} else {
+					var args = new WizardStepEventArgs ();
+					MovingNext?.Invoke (args);
+					if (!args.Cancel) {
+						CurrentStep++;
+					}
+				}
+			};
+
+			Loaded += () => {
+				foreach (var step in steps) {
+					step.Y = 0;
+				}
+				if (steps.Count > 0) {
+
+					CurrentStep = 0;
+				}
+			};
+
+		}
+
+		private List<WizardStep> steps = new List<WizardStep> ();
+		private int currentStep = 0;
+
+		/// <summary>
+		/// Adds a step to the wizard. The Next and Back buttons navigate through the added steps in the
+		/// order they were added.
+		/// </summary>
+		/// <param name="newStep"></param>
+		/// <remarks>The "Next..." button of the last step added will read "Finish" (unless changed from default).</remarks>
+		public void AddStep (WizardStep newStep)
+		{
+			steps.Add (newStep);
+			this.Add (newStep);
+		}
+
+		/// <summary>
+		/// The title of the Wizard, shown at the top of the Wizard with " - currentStep.Title" appended.
+		/// </summary>
+		public new ustring Title {
+			get {
+				// The base (Dialog) Title holds the full title ("Wizard Title - Step Title")
+				return base.Title;
+			}
+			set {
+				wizardTitle = value;
+				base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + steps [currentStep].Title : string.Empty)}";
+			}
+		}
+		private ustring wizardTitle = ustring.Empty;
+
+		/// <summary>	
+		/// <see cref="EventArgs"/> for <see cref="WizardStep"/> transition events.
+		/// </summary>
+		public class WizardStepEventArgs : EventArgs {
+			/// <summary>
+			/// Set to true to cancel the transition to the next step.
+			/// </summary>
+			public bool Cancel { get; set; }
+
+			/// <summary>
+			/// Initializes a new instance of <see cref="WizardStepEventArgs"/>
+			/// </summary>
+			public WizardStepEventArgs ()
+			{
+				Cancel = false;
+			}
+		}
+
+		/// <summary>
+		/// This event is raised when the Back button in the <see cref="Wizard"/> is clicked. The Back button is always
+		/// the first button in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any.
+		/// </summary>
+		public event Action<WizardStepEventArgs> MovingBack;
+
+		/// <summary>
+		/// This event is raised when the Next/Finish button in the <see cref="Wizard"/> is clicked. The Next/Finish button is always
+		/// the last button in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any. This event is only
+		/// raised if the <see cref="CurrentStep"/> is the last Step in the Wizard flow 
+		/// (otherwise the <see cref="Finished"/> event is raised).
+		/// </summary>
+		public event Action<WizardStepEventArgs> MovingNext;
+
+		/// <summary>
+		/// This event is raised when the Next/Finish button in the <see cref="Wizard"/> is clicked. The Next/Finish button is always
+		/// the last button in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any. This event is only
+		/// raised if the <see cref="CurrentStep"/> is the last Step in the Wizard flow 
+		/// (otherwise the <see cref="Finished"/> event is raised).
+		/// </summary>
+		public event Action<WizardStepEventArgs> Finished;
+
+		/// <summary>
+		/// This event is raised when the current step )<see cref="CurrentStep"/>) in the <see cref="Wizard"/> changes.
+		/// </summary>
+		public event Action<CurrentStepChangedEventArgs> CurrentStepChanged;
+
+		/// <summary>
+		/// <see cref="EventArgs"/> for <see cref="WizardStep"/> events.
+		/// </summary>
+		public class CurrentStepChangedEventArgs : EventArgs {
+			/// <summary>
+			/// The new current <see cref="WizardStep"/>.
+			/// </summary>
+			public int CurrentStepIndex { get; }
+
+			/// <summary>
+			/// Initializes a new instance of <see cref="CurrentStepChangedEventArgs"/>
+			/// </summary>
+			/// <param name="currentStepIndex">The new current <see cref="WizardStep"/>.</param>
+			public CurrentStepChangedEventArgs (int currentStepIndex)
+			{
+				CurrentStepIndex = currentStepIndex;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the currently active <see cref="WizardStep"/>.
+		/// </summary>
+		public int CurrentStep {
+			get => currentStep;
+			set {
+				currentStep = value;
+				OnCurrentStepChanged ();
+			}
+		}
+
+		/// <summary>
+		/// Called when the current <see cref="WizardStep"/> has changed (<see cref="CurrentStep"/>).
+		/// </summary>
+		public virtual void OnCurrentStepChanged ()
+		{
+			CurrentStepChanged?.Invoke (new CurrentStepChangedEventArgs (currentStep));
+			// Hide all but the first step
+			foreach (WizardStep step in steps) {
+				step.Visible = (steps [currentStep] == step);
+			}
+
+			// TODO: Add support for "Wizard Title - Step Title"
+			base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + steps [currentStep].Title : string.Empty)}";
+
+			backBtn.Text = steps [currentStep].BackButtonText != ustring.Empty ? steps [currentStep].BackButtonText : "_Back";
+			if (currentStep == 0) {
+				backBtn.Visible = false;
+			} else {
+				backBtn.Visible = true;
+			}
+
+			if (currentStep == steps.Count - 1) {
+				nextfinishBtn.Text = steps [currentStep].NextButtonText != ustring.Empty ? steps [currentStep].NextButtonText : "Fi_nish";
+			} else {
+				nextfinishBtn.Text = steps [currentStep].NextButtonText != ustring.Empty ? steps [currentStep].NextButtonText : "_Next...";
+			}
+		}
+	}
+}

+ 12 - 0
UICatalog/Properties/launchSettings.json

@@ -6,6 +6,18 @@
     "UICatalog : -usc": {
       "commandName": "Project",
       "commandLineArgs": "-usc"
+    },
+    "Wizards": {
+      "commandName": "Project",
+      "commandLineArgs": "Wizards"
+    },
+    "Dialogs": {
+      "commandName": "Project",
+      "commandLineArgs": "Dialogs"
+    },
+    "Buttons": {
+      "commandName": "Project",
+      "commandLineArgs": "Buttons"
     }
   }
 }

+ 10 - 1
UICatalog/Scenarios/Dialogs.cs

@@ -174,6 +174,13 @@ namespace UICatalog.Scenarios {
 						};
 						buttons.Add (button);
 					}
+					if (buttons.Count > 1) {
+						buttons [1].Text = "Accept";
+						buttons [1].IsDefault = true;
+						buttons [0].Visible = false;
+						buttons [0].Text = "_Back";
+						buttons [0].IsDefault = false;
+					}
 
 					// This tests dynamically adding buttons; ensuring the dialog resizes if needed and 
 					// the buttons are laid out correctly
@@ -202,7 +209,9 @@ namespace UICatalog.Scenarios {
 						};
 						buttons.Add (button);
 						dialog.AddButton (button);
-						button.TabIndex = buttons [buttons.Count - 2].TabIndex + 1;
+						if (buttons.Count > 1) {
+							button.TabIndex = buttons [buttons.Count - 2].TabIndex + 1;
+						}
 					};
 					dialog.Add (add);
 

+ 237 - 0
UICatalog/Scenarios/Wizards.cs

@@ -0,0 +1,237 @@
+using NStack;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "Wizards", Description: "Demonstrates how to the Wizard class")]
+	[ScenarioCategory ("Dialogs")]
+	public class Wizards : Scenario {
+		public override void Setup ()
+		{
+			Win.ColorScheme = Colors.Base;
+			var frame = new FrameView ("Wizard Options") {
+				X = Pos.Center (),
+				Y = 0,
+				Width = Dim.Percent (75),
+				Height = 10,
+				ColorScheme = Colors.Base,
+			};
+			Win.Add (frame);
+
+			var label = new Label ("Width:") {
+				X = 0,
+				Y = 0,
+				Width = 15,
+				Height = 1,
+				TextAlignment = Terminal.Gui.TextAlignment.Right,
+			};
+			frame.Add (label);
+			var widthEdit = new TextField ("80") {
+				X = Pos.Right (label) + 1,
+				Y = Pos.Top (label),
+				Width = 5,
+				Height = 1
+			};
+			frame.Add (widthEdit);
+
+			label = new Label ("Height:") {
+				X = 0,
+				Y = Pos.Bottom (label),
+				Width = Dim.Width (label),
+				Height = 1,
+				TextAlignment = Terminal.Gui.TextAlignment.Right,
+			};
+			frame.Add (label);
+			var heightEdit = new TextField ("20") {
+				X = Pos.Right (label) + 1,
+				Y = Pos.Top (label),
+				Width = 5,
+				Height = 1
+			};
+			frame.Add (heightEdit);
+
+			label = new Label ("Title:") {
+				X = 0,
+				Y = Pos.Bottom (label),
+				Width = Dim.Width (label),
+				Height = 1,
+				TextAlignment = Terminal.Gui.TextAlignment.Right,
+			};
+			frame.Add (label);
+			var titleEdit = new TextField ("Title") {
+				X = Pos.Right (label) + 1,
+				Y = Pos.Top (label),
+				Width = Dim.Fill (),
+				Height = 1
+			};
+			frame.Add (titleEdit);
+
+			void Top_Loaded ()
+			{
+				frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + 2;
+				Top.Loaded -= Top_Loaded;
+			}
+			Top.Loaded += Top_Loaded;
+
+			label = new Label ("Action:") {
+				X = Pos.Center (),
+				Y = Pos.AnchorEnd (1),
+				AutoSize = true,
+				TextAlignment = Terminal.Gui.TextAlignment.Right,
+			};
+			Win.Add (label);
+			var actionLabel = new Label (" ") {
+				X = Pos.Right (label),
+				Y = Pos.AnchorEnd (1),
+				AutoSize = true,
+				ColorScheme = Colors.Error,
+			};
+
+			var showWizardButton = new Button ("Show Wizard") {
+				X = Pos.Center (),
+				Y = Pos.Bottom (frame) + 2,
+				IsDefault = true,
+			};
+			showWizardButton.Clicked += () => {
+				try {
+					int width = 0;
+					int.TryParse (widthEdit.Text.ToString (), out width);
+					int height = 0;
+					int.TryParse (heightEdit.Text.ToString (), out height);
+
+					if (width < 1 || height < 1) {
+						MessageBox.ErrorQuery ("Nope", "Height and width must be greater than 0 (much bigger)", "Ok");
+						return;
+					}
+
+					var wizard = new Wizard (titleEdit.Text) {
+						Width = width,
+						Height = height
+					};
+
+					wizard.MovingBack += (args) => {
+						//args.Cancel = true;
+						actionLabel.Text = "Moving Back";
+					};
+
+					wizard.MovingNext += (args) => {
+						//args.Cancel = true;
+						actionLabel.Text = "Moving Next";
+					};
+
+					wizard.Finished += (args) => {
+						//args.Cancel = true;
+						actionLabel.Text = "Finished";
+					};
+
+					// Add 1st step
+					var firstStep = new Wizard.WizardStep ("End User License Agreement");
+					wizard.AddStep (firstStep);
+					firstStep.ShowControls = false;
+					firstStep.NextButtonText = "Accept!";
+					firstStep.HelpText = "This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA.";
+
+					// Add 2nd step
+					var secondStep = new Wizard.WizardStep ("Second Step");
+					wizard.AddStep (secondStep);
+					secondStep.HelpText = "This is the help text for the Second Step.\n\nPress the button to see a message box.\n\nEnter name too.";
+					var buttonLbl = new Label () { Text = "Second Step Button: ", AutoSize = true, X = 1, Y = 1 };
+					var button = new Button () {
+						Text = "Press Me",
+						X = Pos.Right (buttonLbl),
+						Y = Pos.Top (buttonLbl)
+					};
+					button.Clicked += () => {
+						MessageBox.Query ("Wizard Scenario", "The Second Step Button was pressed.");
+					};
+					secondStep.Controls.Add (buttonLbl, button);
+					var lbl = new Label () { Text = "First Name: ", AutoSize = true, X = 1, Y = Pos.Bottom (buttonLbl) };
+					var firstNameField = new TextField () { Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) };
+					secondStep.Controls.Add (lbl, firstNameField);
+					lbl = new Label () { Text = "Last Name:  ", AutoSize = true, X = 1, Y = Pos.Bottom (lbl) };
+					var lastNameField = new TextField () { Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) };
+					secondStep.Controls.Add (lbl, lastNameField);
+
+					// Add 3rd step
+					var thirdStep = new Wizard.WizardStep ("Third Step");
+					wizard.AddStep (thirdStep);
+					thirdStep.HelpText = "This is the help text for the Third Step.";
+					var progLbl = new Label () { Text = "Third Step ProgressBar: ", AutoSize = true, X = 1, Y = 10 };
+					var progressBar = new ProgressBar () {
+						X = Pos.Right (progLbl),
+						Y = Pos.Top (progLbl),
+						Width = 40,
+						Fraction = 0.42F
+					};
+					thirdStep.Controls.Add (progLbl, progressBar);
+
+					// Add 4th step
+					var fourthStep = new Wizard.WizardStep ("Hidden Help pane");
+					wizard.AddStep (fourthStep);
+					fourthStep.ShowHelp = false;
+					var someText = new TextView () {
+						Text = "This step shows how to hide the Help pane. The control pane contains this TextView.",
+						X = 0,
+						Y = 0,
+						Width = Dim.Fill (),
+						Height = Dim.Fill (),
+						WordWrap = true,
+					};
+					fourthStep.Controls.Add (someText);
+					var scrollBar = new ScrollBarView (someText, true);
+
+					scrollBar.ChangedPosition += () => {
+						someText.TopRow = scrollBar.Position;
+						if (someText.TopRow != scrollBar.Position) {
+							scrollBar.Position = someText.TopRow;
+						}
+						someText.SetNeedsDisplay ();
+					};
+
+					scrollBar.VisibleChanged += () => {
+						if (scrollBar.Visible && someText.RightOffset == 0) {
+							someText.RightOffset = 1;
+						} else if (!scrollBar.Visible && someText.RightOffset == 1) {
+							someText.RightOffset = 0;
+						}
+					};
+
+					someText.DrawContent += (e) => {
+						scrollBar.Size = someText.Lines;
+						scrollBar.Position = someText.TopRow;
+						if (scrollBar.OtherScrollBarView != null) {
+							scrollBar.OtherScrollBarView.Size = someText.Maxlength;
+							scrollBar.OtherScrollBarView.Position = someText.LeftColumn;
+						}
+						scrollBar.LayoutSubviews ();
+						scrollBar.Refresh ();
+					};
+					fourthStep.Controls.Add (scrollBar);
+
+					// Add last step
+					var lastStep = new Wizard.WizardStep ("The last step");
+					wizard.AddStep (lastStep);
+					lastStep.HelpText = "The wizard is complete! Press the Finish button to continue. Pressing ESC will cancel the wizard.";
+
+
+					// TODO: Demo setting initial Pane
+
+					wizard.Finished += (args) => {
+						Application.RequestStop (wizard);
+					};
+
+					Application.Run (wizard);
+
+				} catch (FormatException) {
+					actionLabel.Text = "Invalid Options";
+				}
+			};
+			Win.Add (showWizardButton);
+
+			Win.Add (actionLabel);
+		}
+	}
+}

+ 108 - 29
UnitTests/DialogTests.cs

@@ -19,10 +19,10 @@ namespace Terminal.Gui.Views {
 			this.output = output;
 		}
 
-		private Application.RunState RunButtonTestDialog (string title, int width, Dialog.ButtonAlignments align, params Button [] btns)
+		private (Application.RunState, Dialog) RunButtonTestDialog (string title, int width, Dialog.ButtonAlignments align, params Button [] btns)
 		{
 			var dlg = new Dialog (title, width, 3, btns) { ButtonAlignment = align };
-			return Application.Begin (dlg);
+			return (Application.Begin (dlg), dlg);
 		}
 
 		[Fact]
@@ -30,6 +30,7 @@ namespace Terminal.Gui.Views {
 		public void ButtonAlignment_One ()
 		{
 			var d = ((FakeDriver)Application.Driver);
+			Application.RunState runstate = null;
 
 			var title = "1234";
 			// E.g "|[ ok ]|"
@@ -41,28 +42,28 @@ namespace Terminal.Gui.Views {
 
 			d.SetBufferSize (width, 3);
 
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}      {d.LeftBracket} {btnText} {d.RightBracket}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btnText));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btnText));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}      {d.LeftBracket} {btnText} {d.RightBracket}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btnText));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btnText));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{d.LeftBracket} {btnText} {d.RightBracket}      {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btnText));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btnText));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
@@ -71,6 +72,8 @@ namespace Terminal.Gui.Views {
 		[AutoInitShutdown]
 		public void ButtonAlignment_Two ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -87,36 +90,100 @@ namespace Terminal.Gui.Views {
 
 			d.SetBufferSize (buttonRow.Length, 3);
 
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $@"{d.VLine}{btn1}   {btn2}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $@"{d.VLine}  {btn1} {btn2}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $@"{d.VLine}{btn1} {btn2}  {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_Two_Hidden ()
+		{
+			Application.RunState runstate = null;
+			bool firstIteration = false;
+
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			// E.g "|[ yes ][ no ]|"
+			var btn1Text = "yes";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "no";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+
+			var buttonRow = $@"{d.VLine} {btn1} {btn2} {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+
+			d.SetBufferSize (buttonRow.Length, 3);
+
+			Dialog dlg = null;
+			Button button1, button2;
+
+			//// Default (Center)
+			//button1 = new Button (btn1Text);
+			//button2 = new Button (btn2Text);
+			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, button1, button2);
+			//button1.Visible = false;
+			//Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
+			//buttonRow = $@"{d.VLine}         {btn2} {d.VLine}";
+			//GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			//Application.End (runstate);
+
+			// Justify
+			Assert.Equal (width, buttonRow.Length);
+			button1 = new Button (btn1Text);
+			button2 = new Button (btn2Text);
+			(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, button1, button2);
+			button1.Visible = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref firstIteration);
+			buttonRow = $@"{d.VLine}          {btn2}{d.VLine}";
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
+
+			//// Right
+			//buttonRow = $@"{d.VLine}  {btn1} {btn2}{d.VLine}";
+			//Assert.Equal (width, buttonRow.Length);
+			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
+			//GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			//Application.End (runstate);
+
+			//// Left
+			//buttonRow = $@"{d.VLine}{btn1} {btn2}  {d.VLine}";
+			//Assert.Equal (width, buttonRow.Length);
+			//(runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text));
+			//GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			//Application.End (runstate);
 		}
 
 		[Fact]
 		[AutoInitShutdown]
 		public void ButtonAlignment_Three ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -135,28 +202,28 @@ namespace Terminal.Gui.Views {
 
 			d.SetBufferSize (buttonRow.Length, 3);
 
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $@"{d.VLine}{btn1}  {btn2}  {btn3}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $@"{d.VLine}  {btn1} {btn2} {btn3}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $@"{d.VLine}{btn1} {btn2} {btn3}  {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
@@ -165,6 +232,8 @@ namespace Terminal.Gui.Views {
 		[AutoInitShutdown]
 		public void ButtonAlignment_Four ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -186,28 +255,28 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (buttonRow.Length, 3);
 
 			// Default - Center
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}{btn1} {btn2}  {btn3}  {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}  {btn1} {btn2} {btn3} {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}  {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
@@ -216,6 +285,8 @@ namespace Terminal.Gui.Views {
 		[AutoInitShutdown]
 		public void ButtonAlignment_Four_Wider ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -240,28 +311,28 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (width, 3);
 
 			// Default - Center
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}{btn1}    {btn2}     {btn3}     {btn4}{d.VLine}";
 			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}           {btn1} {btn2} {btn3} {btn4}{d.VLine}";
 			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}           {d.VLine}";
 			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
@@ -270,6 +341,8 @@ namespace Terminal.Gui.Views {
 		[AutoInitShutdown]
 		public void ButtonAlignment_Four_WideOdd ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -293,28 +366,28 @@ namespace Terminal.Gui.Views {
 			d.SetBufferSize (buttonRow.Length, 3);
 
 			// Default - Center
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Justify
 			buttonRow = $"{d.VLine}{btn1}    {btn2}     {btn3}     {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Right
 			buttonRow = $"{d.VLine}           {btn1} {btn2} {btn3} {btn4}{d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 
 			// Left
 			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}           {d.VLine}";
 			Assert.Equal (width, buttonRow.Length);
-			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
@@ -323,6 +396,8 @@ namespace Terminal.Gui.Views {
 		[AutoInitShutdown]
 		public void Zero_Buttons_Works ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -333,7 +408,7 @@ namespace Terminal.Gui.Views {
 			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
 			d.SetBufferSize (buttonRow.Length, 3);
 
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, null);
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, null);
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 
 			Application.End (runstate);
@@ -343,6 +418,8 @@ namespace Terminal.Gui.Views {
 		[AutoInitShutdown]
 		public void One_Button_Works ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -354,7 +431,7 @@ namespace Terminal.Gui.Views {
 			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
 			d.SetBufferSize (buttonRow.Length, 3);
 
-			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
+			(runstate, var _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 			Application.End (runstate);
 		}
@@ -363,6 +440,8 @@ namespace Terminal.Gui.Views {
 		[AutoInitShutdown]
 		public void Add_Button_Works ()
 		{
+			Application.RunState runstate = null;
+
 			var d = ((FakeDriver)Application.Driver);
 
 			var title = "1234";
@@ -380,7 +459,7 @@ namespace Terminal.Gui.Views {
 
 			// Default (center)
 			var dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Center };
-			var runstate = Application.Begin (dlg);
+			runstate = Application.Begin (dlg);
 			var buttonRow = $"{d.VLine}    {btn1}     {d.VLine}";
 			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
 

+ 164 - 0
UnitTests/WizardTests.cs

@@ -0,0 +1,164 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using Terminal.Gui;
+using Xunit;
+using System.Globalization;
+using Xunit.Abstractions;
+using NStack;
+
+namespace Terminal.Gui.Views {
+
+	public class WizardTests {
+		readonly ITestOutputHelper output;
+
+		public WizardTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		private void RunButtonTestWizard (string title, int width, int height)
+		{
+			var wizard = new Wizard (title) { Width = width, Height = height };
+			Application.End (Application.Begin (wizard));
+		}
+
+		// =========== WizardStep Tests
+		[Fact, AutoInitShutdown]
+		public void WizardStep_Title ()
+		{
+			// Verify default title
+
+			// Verify set actually changes property
+
+			// Verify set changes Wizard title (TODO: NOT YET IMPLEMENTED)
+		}
+
+		[Fact, AutoInitShutdown]
+		public void WizardStep_ButtonText ()
+		{
+			// Verify default button text
+
+			// Verify set actually changes property
+
+			// Verify set actually changes buttons for the current step
+		}
+
+		// =========== Wizard Tests
+		[Fact, AutoInitShutdown]
+		public void DefaultConstructor_SizedProperly ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var wizard = new Wizard ();
+			Assert.NotEqual (0, wizard.Width);
+			Assert.NotEqual (0, wizard.Height);
+		}
+
+		[Fact, AutoInitShutdown]
+		// Verify a zero-step wizard doesn't crash and shows a blank wizard
+		// and that the title is correct
+		public void ZeroStepWizard_Shows ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			var stepTitle = "";
+
+			int width = 30;
+			int height = 6;
+			d.SetBufferSize (width, height);
+
+			var btnBackText = "Back";
+			var btnBack = $"{d.LeftBracket} {btnBackText} {d.RightBracket}";
+			var btnNextText = "Next...";
+			var btnNext = $"{d.LeftBracket}{d.LeftDefaultIndicator} {btnNextText} {d.RightDefaultIndicator}{d.RightBracket}";
+
+			var topRow = $"{d.ULDCorner} {title}{stepTitle} {new String (d.HDLine.ToString () [0], width - title.Length - stepTitle.Length - 4)}{d.URDCorner}";
+			var row2 = $"{d.VDLine}{new String (' ', width - 2)}{d.VDLine}";
+			var row3 = row2;
+			var separatorRow = $"{d.VDLine}{new String (d.HLine.ToString () [0], width - 2)}{d.VDLine}";
+			var buttonRow = $"{d.VDLine}{btnBack}{new String (' ', width - btnBack.Length - btnNext.Length - 2)}{btnNext}{d.VDLine}";
+			var bottomRow = $"{d.LLDCorner}{new String (d.HDLine.ToString () [0], width - 2)}{d.LRDCorner}";
+
+			var wizard = new Wizard (title) { Width = width, Height = height };
+			Application.End (Application.Begin (wizard));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{row2}\n{row3}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		// This test verifies that a single step wizard shows the correct buttons
+		// and that the title is correct
+		public void OneStepWizard_Shows ()
+		{
+		}
+
+		[Fact, AutoInitShutdown]
+		// This test verifies that the 2nd step in a wizard with 2 steps 
+		// shows the correct buttons on both steps
+		// and that the title is correct
+		public void TwoStepWizard_Next_Shows_SecondStep ()
+		{
+			// verify step one
+
+			// Next
+
+			// verify step two
+
+			// Back
+
+			// verify step one again
+		}
+
+		[Fact, AutoInitShutdown]
+		// This test verifies that the 2nd step in a wizard with more than 2 steps 
+		// shows the correct buttons on all steps
+		// and that the title is correct
+		public void ThreeStepWizard_Next_Shows_Steps ()
+		{
+
+			// verify step one
+
+			// Next
+
+			// verify step two
+
+			// Back
+
+			// verify step one again
+		}
+
+		[Fact, AutoInitShutdown]
+		// this test is needed because Wizard overrides Dialog's title behavior ("Title - StepTitle")
+		public void Setting_Title_Works ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			var stepTitle = " - ABCD";
+
+			int width = 40;
+			int height = 4;
+			d.SetBufferSize (width, height);
+
+			var btnNextText = "Finish";
+			var btnNext = $"{d.LeftBracket}{d.LeftDefaultIndicator} {btnNextText} {d.RightDefaultIndicator}{d.RightBracket}";
+
+			var topRow = $"{d.ULDCorner} {title}{stepTitle} {new String (d.HDLine.ToString () [0], width - title.Length - stepTitle.Length - 4)}{d.URDCorner}";
+			var separatorRow = $"{d.VDLine}{new String (d.HLine.ToString () [0], width - 2)}{d.VDLine}";
+
+			// Once this is fixed, revert to commented out line: https://github.com/migueldeicaza/gui.cs/issues/1791
+			var buttonRow = $"{d.VDLine}{new String (' ', width - btnNext.Length - 3)}{btnNext} {d.VDLine}";
+			//var buttonRow = $"{d.VDLine}{new String (' ', width - btnNext.Length - 2)}{btnNext}{d.VDLine}";
+			var bottomRow = $"{d.LLDCorner}{new String (d.HDLine.ToString () [0], width - 2)}{d.LRDCorner}";
+
+			var wizard = new Wizard (title) { Width = width, Height = height };
+			wizard.AddStep (new Wizard.WizardStep ("ABCD"));
+
+			Application.End (Application.Begin (wizard));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output);
+		}
+	}
+}