using System;
using System.Collections.Generic;
using System.Linq;
using NStack;
using Terminal.Gui.Resources;
namespace Terminal.Gui {
///
/// Provides navigation and a user interface (UI) to collect related data across multiple steps. Each step () can host
/// arbitrary s, much like a . 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.
///
///
/// The Wizard can be displayed either as a modal (pop-up) (like ) or as an embedded .
///
/// By default, is true. In this case launch the Wizard with Application.Run(wizard).
///
/// See for more details.
///
///
///
/// using Terminal.Gui;
/// using NStack;
///
/// Application.Init();
///
/// var wizard = new Wizard ($"Setup Wizard");
///
/// // Add 1st step
/// var firstStep = new Wizard.WizardStep ("End User License Agreement");
/// wizard.AddStep(firstStep);
/// firstStep.NextButtonText = "Accept!";
/// firstStep.HelpText = "This is the End User License Agreement.";
///
/// // Add 2nd step
/// var secondStep = new Wizard.WizardStep ("Second Step");
/// wizard.AddStep(secondStep);
/// secondStep.HelpText = "This is the help text for the Second Step.";
/// var lbl = new Label ("Name:") { AutoSize = true };
/// secondStep.Add(lbl);
///
/// var name = new TextField () { X = Pos.Right (lbl) + 1, Width = Dim.Fill () - 1 };
/// secondStep.Add(name);
///
/// wizard.Finished += (args) =>
/// {
/// MessageBox.Query("Wizard", $"Finished. The Name entered is '{name.Text}'", "Ok");
/// Application.RequestStop();
/// };
///
/// Application.Top.Add (wizard);
/// Application.Run ();
/// Application.Shutdown ();
///
///
public class Wizard : Dialog {
///
/// Represents a basic step that is displayed in a . The view is divided horizontally in two. On the left is the
/// content view where s can be added, On the right is the help for the step.
/// Set to set the help text. If the help text is empty the help pane will not
/// be shown.
///
/// If there are no Views added to the WizardStep the (if not empty) will fill the wizard step.
///
///
/// If s are added, do not set to true as this will conflict
/// with the Next button of the Wizard.
///
/// Subscribe to the event to be notified when the step is active; see also: .
///
/// To enable or disable a step from being shown to the user, set .
///
///
public class WizardStep : FrameView {
///
/// The title of the .
///
/// The Title is only displayed when the is used as a modal pop-up (see .
public new ustring Title {
// BUGBUG: v2 - No need for this as View now has Title w/ notifications.
get => title;
set {
if (!OnTitleChanging (title, value)) {
var old = title;
title = value;
OnTitleChanged (old, title);
}
base.Title = value;
SetNeedsDisplay ();
}
}
private ustring title = ustring.Empty;
// The contentView works like the ContentView in FrameView.
private View contentView = new View () { Data = "WizardContentView" };
///
/// Sets or gets help text for the .If is empty
/// the help pane will not be visible and the content will fill the entire WizardStep.
///
/// The help text is displayed using a read-only .
public ustring HelpText {
get => helpTextView.Text;
set {
helpTextView.Text = value;
ShowHide ();
SetNeedsDisplay ();
}
}
private TextView helpTextView = new TextView ();
///
/// Sets or gets the text for the back button. The back button will only be visible on
/// steps after the first step.
///
/// The default text is "Back"
public ustring BackButtonText { get; set; } = ustring.Empty;
///
/// Sets or gets the text for the next/finish button.
///
/// The default text is "Next..." if the Pane is not the last pane. Otherwise it is "Finish"
public ustring NextButtonText { get; set; } = ustring.Empty;
///
/// Initializes a new instance of the class using positioning.
///
/// Title for the Step. Will be appended to the containing Wizard's title as
/// "Wizard Title - Wizard Step Title" when this step is active.
///
///
public WizardStep (ustring title)
{
this.Title = title; // this.Title holds just the "Wizard Title"; base.Title holds "Wizard Title - Step Title"
this.Border.BorderStyle = BorderStyle.Rounded;
base.Add (contentView);
helpTextView.ReadOnly = true;
helpTextView.WordWrap = true;
base.Add (helpTextView);
// BUGBUG: v2 - Disabling scrolling for now
//var scrollBar = new ScrollBarView (helpTextView, true);
//scrollBar.ChangedPosition += (s,e) => {
// helpTextView.TopRow = scrollBar.Position;
// if (helpTextView.TopRow != scrollBar.Position) {
// scrollBar.Position = helpTextView.TopRow;
// }
// helpTextView.SetNeedsDisplay ();
//};
//scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
// helpTextView.LeftColumn = scrollBar.OtherScrollBarView.Position;
// if (helpTextView.LeftColumn != scrollBar.OtherScrollBarView.Position) {
// scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn;
// }
// helpTextView.SetNeedsDisplay ();
//};
//scrollBar.VisibleChanged += (s,e) => {
// if (scrollBar.Visible && helpTextView.RightOffset == 0) {
// helpTextView.RightOffset = 1;
// } else if (!scrollBar.Visible && helpTextView.RightOffset == 1) {
// helpTextView.RightOffset = 0;
// }
//};
//scrollBar.OtherScrollBarView.VisibleChanged += (s,e) => {
// if (scrollBar.OtherScrollBarView.Visible && helpTextView.BottomOffset == 0) {
// helpTextView.BottomOffset = 1;
// } else if (!scrollBar.OtherScrollBarView.Visible && helpTextView.BottomOffset == 1) {
// helpTextView.BottomOffset = 0;
// }
//};
//helpTextView.DrawContent += (s,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 ();
//};
//base.Add (scrollBar);
ShowHide ();
}
///
/// Does the work to show and hide the contentView and helpView as appropriate
///
internal void ShowHide ()
{
contentView.Height = Dim.Fill ();
helpTextView.Height = Dim.Fill ();
helpTextView.Width = Dim.Fill ();
if (contentView.InternalSubviews?.Count > 0) {
if (helpTextView.Text.Length > 0) {
contentView.Width = Dim.Percent (70);
helpTextView.X = Pos.Right (contentView);
helpTextView.Width = Dim.Fill ();
} else {
contentView.Width = Dim.Percent (100);
}
} else {
if (helpTextView.Text.Length > 0) {
helpTextView.X = 0;
} else {
// Error - no pane shown
}
}
contentView.Visible = contentView.InternalSubviews?.Count > 0;
helpTextView.Visible = helpTextView.Text.Length > 0;
}
///
/// Add the specified to the .
///
/// to add to this container
public override void Add (View view)
{
contentView.Add (view);
if (view.CanFocus) {
CanFocus = true;
}
ShowHide ();
}
///
/// Removes a from .
///
///
///
public override void Remove (View view)
{
if (view == null) {
return;
}
SetNeedsDisplay ();
var touched = view.Frame;
contentView.Remove (view);
if (contentView.InternalSubviews.Count < 1) {
this.CanFocus = false;
}
ShowHide ();
}
///
/// Removes all s from the .
///
///
///
public override void RemoveAll ()
{
contentView.RemoveAll ();
ShowHide ();
}
} // end of WizardStep class
///
/// Initializes a new instance of the class using positioning.
///
///
/// The Wizard will be vertically and horizontally centered in the container.
/// After initialization use X, Y, Width, and Height change size and position.
///
public Wizard () : this (ustring.Empty)
{
}
///
/// Initializes a new instance of the class using positioning.
///
/// Sets the for the Wizard.
///
/// The Wizard will be vertically and horizontally centered in the container.
/// After initialization use X, Y, Width, and Height change size and position.
///
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;
this.Border.PaddingThickness = new Thickness (0);
//// Add a horiz separator
//var separator = new LineView (Graphs.Orientation.Horizontal) {
// Y = Pos.AnchorEnd (2)
//};
//Add (separator);
// BUGBUG: Space is to work around https://github.com/gui-cs/Terminal.Gui/issues/1812
backBtn = new Button (Strings.wzBack) { AutoSize = true };
AddButton (backBtn);
nextfinishBtn = new Button (Strings.wzFinish) { AutoSize = true };
nextfinishBtn.IsDefault = true;
AddButton (nextfinishBtn);
backBtn.Clicked += BackBtn_Clicked;
nextfinishBtn.Clicked += NextfinishBtn_Clicked;
Loaded += Wizard_Loaded;
Closing += Wizard_Closing;
if (Modal) {
ClearKeybinding (Command.QuitToplevel);
AddKeyBinding (Key.Esc, Command.QuitToplevel);
}
SetNeedsLayout ();
}
private void Wizard_Loaded (object sender, EventArgs args)
{
CurrentStep = GetFirstStep (); // gets the first step if CurrentStep == null
}
private bool finishedPressed = false;
private void Wizard_Closing (object sender, ToplevelClosingEventArgs obj)
{
if (!finishedPressed) {
var args = new WizardButtonEventArgs ();
Cancelled?.Invoke (this, args);
}
}
private void NextfinishBtn_Clicked (object sender, EventArgs e)
{
if (CurrentStep == GetLastStep ()) {
var args = new WizardButtonEventArgs ();
Finished?.Invoke (this, args);
if (!args.Cancel) {
finishedPressed = true;
if (IsCurrentTop) {
Application.RequestStop (this);
} else {
// Wizard was created as a non-modal (just added to another View).
// Do nothing
}
}
} else {
var args = new WizardButtonEventArgs ();
MovingNext?.Invoke (this, args);
if (!args.Cancel) {
GoNext ();
}
}
}
///
/// is derived from and Dialog causes Esc to call
/// , closing the Dialog. Wizard overrides
/// to instead fire the event when Wizard is being used as a non-modal (see .
/// See for more.
///
///
///
public override bool ProcessKey (KeyEvent kb)
{
if (!Modal) {
switch (kb.Key) {
case Key.Esc:
var args = new WizardButtonEventArgs ();
Cancelled?.Invoke (this, args);
return false;
}
}
return base.ProcessKey (kb);
}
///
/// Causes the wizad to move to the next enabled step (or last step if is not set).
/// If there is no previous step, does nothing.
///
public void GoNext ()
{
var nextStep = GetNextStep ();
if (nextStep != null) {
GoToStep (nextStep);
}
}
///
/// Returns the next enabled after the current step. Takes into account steps which
/// are disabled. If is null returns the first enabled step.
///
/// The next step after the current step, if there is one; otherwise returns null, which
/// indicates either there are no enabled steps or the current step is the last enabled step.
public WizardStep GetNextStep ()
{
LinkedListNode step = null;
if (CurrentStep == null) {
// Get first step, assume it is next
step = steps.First;
} else {
// Get the step after current
step = steps.Find (CurrentStep);
if (step != null) {
step = step.Next;
}
}
// step now points to the potential next step
while (step != null) {
if (step.Value.Enabled) {
return step.Value;
}
step = step.Next;
}
return null;
}
private void BackBtn_Clicked (object sender, EventArgs e)
{
var args = new WizardButtonEventArgs ();
MovingBack?.Invoke (this, args);
if (!args.Cancel) {
GoBack ();
}
}
///
/// Causes the wizad to move to the previous enabled step (or first step if is not set).
/// If there is no previous step, does nothing.
///
public void GoBack ()
{
var previous = GetPreviousStep ();
if (previous != null) {
GoToStep (previous);
}
}
///
/// Returns the first enabled before the current step. Takes into account steps which
/// are disabled. If is null returns the last enabled step.
///
/// The first step ahead of the current step, if there is one; otherwise returns null, which
/// indicates either there are no enabled steps or the current step is the first enabled step.
public WizardStep GetPreviousStep ()
{
LinkedListNode step = null;
if (CurrentStep == null) {
// Get last step, assume it is previous
step = steps.Last;
} else {
// Get the step before current
step = steps.Find (CurrentStep);
if (step != null) {
step = step.Previous;
}
}
// step now points to the potential previous step
while (step != null) {
if (step.Value.Enabled) {
return step.Value;
}
step = step.Previous;
}
return null;
}
///
/// Returns the first enabled step in the Wizard
///
/// The last enabled step
public WizardStep GetFirstStep ()
{
return steps.FirstOrDefault (s => s.Enabled);
}
///
/// Returns the last enabled step in the Wizard
///
/// The last enabled step
public WizardStep GetLastStep ()
{
return steps.LastOrDefault (s => s.Enabled);
}
private LinkedList steps = new LinkedList ();
private WizardStep currentStep = null;
///
/// If the is not the first step in the wizard, this button causes
/// the event to be fired and the wizard moves to the previous step.
///
///
/// Use the event to be notified when the user attempts to go back.
///
public Button BackButton { get => backBtn; }
private Button backBtn;
///
/// If the is the last step in the wizard, this button causes
/// the event to be fired and the wizard to close. If the step is not the last step,
/// the event will be fired and the wizard will move next step.
///
///
/// Use the and events to be notified
/// when the user attempts go to the next step or finish the wizard.
///
public Button NextFinishButton { get => nextfinishBtn; }
private Button nextfinishBtn;
///
/// Adds a step to the wizard. The Next and Back buttons navigate through the added steps in the
/// order they were added.
///
///
/// The "Next..." button of the last step added will read "Finish" (unless changed from default).
public void AddStep (WizardStep newStep)
{
SizeStep (newStep);
newStep.EnabledChanged += (s,e)=> UpdateButtonsAndTitle();
newStep.TitleChanged += (s,e) => UpdateButtonsAndTitle ();
steps.AddLast (newStep);
this.Add (newStep);
UpdateButtonsAndTitle ();
}
///
/// The title of the Wizard, shown at the top of the Wizard with " - currentStep.Title" appended.
///
///
/// The Title is only displayed when the is set to false.
///
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 && currentStep != null ? " - " + currentStep.Title : string.Empty)}";
}
}
private ustring wizardTitle = ustring.Empty;
///
/// Raised when the Back button in the is clicked. The Back button is always
/// the first button in the array of Buttons passed to the constructor, if any.
///
public event EventHandler MovingBack;
///
/// Raised when the Next/Finish button in the is clicked (or the user presses Enter).
/// The Next/Finish button is always the last button in the array of Buttons passed to the constructor,
/// if any. This event is only raised if the is the last Step in the Wizard flow
/// (otherwise the event is raised).
///
public event EventHandler MovingNext;
///
/// Raised when the Next/Finish button in the is clicked. The Next/Finish button is always
/// the last button in the array of Buttons passed to the constructor, if any. This event is only
/// raised if the is the last Step in the Wizard flow
/// (otherwise the event is raised).
///
public event EventHandler Finished;
///
/// Raised when the user has cancelled the by pressin the Esc key.
/// To prevent a modal ( is true) Wizard from
/// closing, cancel the event by setting to
/// true before returning from the event handler.
///
public event EventHandler Cancelled;
///
/// This event is raised when the current ) is about to change. Use
/// to abort the transition.
///
public event EventHandler StepChanging;
///
/// This event is raised after the has changed the .
///
public event EventHandler StepChanged;
///
/// Gets or sets the currently active .
///
public WizardStep CurrentStep {
get => currentStep;
set {
GoToStep (value);
}
}
///
/// Called when the is about to transition to another . Fires the event.
///
/// The step the Wizard is about to change from
/// The step the Wizard is about to change to
/// True if the change is to be cancelled.
public virtual bool OnStepChanging (WizardStep oldStep, WizardStep newStep)
{
var args = new StepChangeEventArgs (oldStep, newStep);
StepChanging?.Invoke (this, args);
return args.Cancel;
}
///
/// Called when the has completed transition to a new . Fires the event.
///
/// The step the Wizard changed from
/// The step the Wizard has changed to
/// True if the change is to be cancelled.
public virtual bool OnStepChanged (WizardStep oldStep, WizardStep newStep)
{
var args = new StepChangeEventArgs (oldStep, newStep);
StepChanged?.Invoke (this, args);
return args.Cancel;
}
///
/// Changes to the specified .
///
/// The step to go to.
/// True if the transition to the step succeeded. False if the step was not found or the operation was cancelled.
public bool GoToStep (WizardStep newStep)
{
if (OnStepChanging (currentStep, newStep) || (newStep != null && !newStep.Enabled)) {
return false;
}
// Hide all but the new step
foreach (WizardStep step in steps) {
step.Visible = (step == newStep);
step.ShowHide ();
}
var oldStep = currentStep;
currentStep = newStep;
UpdateButtonsAndTitle ();
// Set focus to the nav buttons
if (backBtn.HasFocus) {
backBtn.SetFocus ();
} else {
nextfinishBtn.SetFocus ();
}
if (OnStepChanged (oldStep, currentStep)) {
// For correctness we do this, but it's meaningless because there's nothing to cancel
return false;
}
return true;
}
private void UpdateButtonsAndTitle ()
{
if (CurrentStep == null) return;
base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + CurrentStep.Title : string.Empty)}";
// Configure the Back button
backBtn.Text = CurrentStep.BackButtonText != ustring.Empty ? CurrentStep.BackButtonText : Strings.wzBack; // "_Back";
backBtn.Visible = (CurrentStep != GetFirstStep ());
// Configure the Next/Finished button
if (CurrentStep == GetLastStep ()) {
nextfinishBtn.Text = CurrentStep.NextButtonText != ustring.Empty ? CurrentStep.NextButtonText : Strings.wzFinish; // "Fi_nish";
} else {
nextfinishBtn.Text = CurrentStep.NextButtonText != ustring.Empty ? CurrentStep.NextButtonText : Strings.wzNext; // "_Next...";
}
SizeStep (CurrentStep);
SetNeedsLayout ();
LayoutSubviews ();
Redraw (Bounds);
}
private void SizeStep (WizardStep step)
{
if (Modal) {
// If we're modal, then we expand the WizardStep so that the top and side
// borders and not visible. The bottom border is the separator above the buttons.
step.X = step.Y = -1;
step.Height = Dim.Fill (1); // for button frame
step.Width = Dim.Fill (-1);
} else {
// If we're not a modal, then we show the border around the WizardStep
step.X = step.Y = 0;
step.Height = Dim.Fill (1); // for button frame
step.Width = Dim.Fill (0);
}
}
///
/// Determines whether the is displayed as modal pop-up or not.
///
/// The default is true. The Wizard will be shown with a frame with and will behave like
/// any window.
///
/// If set to false the Wizard will have no frame and will behave like any embedded .
///
/// To use Wizard as an embedded View
///
/// - Set to false.
/// - Add the Wizard to a containing view with .
///
///
/// If a non-Modal Wizard is added to the application after has been called
/// the first step must be explicitly set by setting to :
///
/// wizard.CurrentStep = wizard.GetNextStep();
///
///
public new bool Modal {
get => base.Modal;
set {
base.Modal = value;
foreach (var step in steps) {
SizeStep (step);
}
if (base.Modal) {
ColorScheme = Colors.Dialog;
Border.BorderStyle = BorderStyle.Rounded;
// Border.Effect3D = true;
// Border.DrawMarginFrame = true;
} else {
if (SuperView != null) {
ColorScheme = SuperView.ColorScheme;
} else {
ColorScheme = Colors.Base;
}
CanFocus = true;
// Border.Effect3D = false;
Border.BorderStyle = BorderStyle.None;
// Border.DrawMarginFrame = false;
}
}
}
}
}