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 { get => title; set { if (!OnTitleChanging (title, value)) { var old = title; title = value; OnTitleChanged (old, title); } base.Title = value; SetNeedsDisplay (); } } private ustring title = ustring.Empty; /// /// An which allows passing a cancelable new value event. /// public class TitleEventArgs : EventArgs { /// /// The new Window Title. /// public ustring NewTitle { get; set; } /// /// The old Window Title. /// public ustring OldTitle { get; set; } /// /// Flag which allows cancelling the Title change. /// public bool Cancel { get; set; } /// /// Initializes a new instance of /// /// The that is/has been replaced. /// The new to be replaced. public TitleEventArgs (ustring oldTitle, ustring newTitle) { OldTitle = oldTitle; NewTitle = newTitle; } } /// /// Called before the changes. Invokes the event, which can be cancelled. /// /// The that is/has been replaced. /// The new to be replaced. /// true if an event handler cancelled the Title change. public virtual bool OnTitleChanging (ustring oldTitle, ustring newTitle) { var args = new TitleEventArgs (oldTitle, newTitle); TitleChanging?.Invoke (args); return args.Cancel; } /// /// Event fired when the is changing. Set to /// true to cancel the Title change. /// public event Action TitleChanging; /// /// Called when the has been changed. Invokes the event. /// /// The that is/has been replaced. /// The new to be replaced. public virtual void OnTitleChanged (ustring oldTitle, ustring newTitle) { var args = new TitleEventArgs (oldTitle, newTitle); TitleChanged?.Invoke (args); } /// /// Event fired after the has been changed. /// public event Action TitleChanged; // The contentView works like the ContentView in FrameView. private View contentView = new View (); /// /// 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); 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.OtherScrollBarView.ChangedPosition += () => { helpTextView.LeftColumn = scrollBar.OtherScrollBarView.Position; if (helpTextView.LeftColumn != scrollBar.OtherScrollBarView.Position) { scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn; } helpTextView.SetNeedsDisplay (); }; scrollBar.VisibleChanged += () => { if (scrollBar.Visible && helpTextView.RightOffset == 0) { helpTextView.RightOffset = 1; } else if (!scrollBar.Visible && helpTextView.RightOffset == 1) { helpTextView.RightOffset = 0; } }; scrollBar.OtherScrollBarView.VisibleChanged += () => { if (scrollBar.OtherScrollBarView.Visible && helpTextView.BottomOffset == 0) { helpTextView.BottomOffset = 1; } else if (!scrollBar.OtherScrollBarView.Visible && helpTextView.BottomOffset == 1) { helpTextView.BottomOffset = 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 (); }; base.Add (scrollBar); } /// /// 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.Padding = 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); } } private void Wizard_Loaded () { CurrentStep = GetFirstStep (); // gets the first step if CurrentStep == null } private bool finishedPressed = false; private void Wizard_Closing (ToplevelClosingEventArgs obj) { if (!finishedPressed) { var args = new WizardButtonEventArgs (); Cancelled?.Invoke (args); } } private void NextfinishBtn_Clicked () { if (CurrentStep == GetLastStep ()) { var args = new WizardButtonEventArgs (); Finished?.Invoke (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 (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 (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 () { var args = new WizardButtonEventArgs (); MovingBack?.Invoke (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 += UpdateButtonsAndTitle; newStep.TitleChanged += (args) => 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; /// /// for transition events. /// public class WizardButtonEventArgs : EventArgs { /// /// Set to true to cancel the transition to the next step. /// public bool Cancel { get; set; } /// /// Initializes a new instance of /// public WizardButtonEventArgs () { Cancel = false; } } /// /// 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 Action 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 Action 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 Action 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 Action Cancelled; /// /// for events. /// public class StepChangeEventArgs : EventArgs { /// /// The current (or previous) . /// public WizardStep OldStep { get; } /// /// The the is changing to or has changed to. /// public WizardStep NewStep { get; } /// /// Event handlers can set to true before returning to cancel the step transition. /// public bool Cancel { get; set; } /// /// Initializes a new instance of /// /// The current . /// The new . public StepChangeEventArgs (WizardStep oldStep, WizardStep newStep) { OldStep = oldStep; NewStep = newStep; Cancel = false; } } /// /// This event is raised when the current ) is about to change. Use /// to abort the transition. /// public event Action StepChanging; /// /// This event is raised after the has changed the . /// public event Action 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 (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 (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; } } } } }