using System; using System.Collections.Generic; using NStack; namespace Terminal.Gui { /// /// Provides a step-based "wizard" UI. The Wizard supports 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. /// /// /// public class Wizard : Dialog { /// /// One step for the Wizard. The view hosts two sub-views: 1) add s to , /// 2) use to set the contents of the that shows on the /// right side. Use and to /// control wether the control or help pane are shown. /// /// /// If s are added, do not set to true as this will conflict /// with the Next button of the Wizard. /// public class WizardStep : View { /// /// The title of the . /// 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 (); /// /// THe pane that holds the controls for the . Use `Add(View`) to add /// controls. Note that the Controls view is sized to take 70% of the Wizard's width and the /// takes the other 30%. This can be adjusted by setting `Width` from `Dim.Percent(70)` to /// another value. If is set to `false` the control pane will fill the entire /// Wizard. /// public View Controls { get => controlPane; } /// /// Sets or gets help text for the .If is set to /// `false` the control pane will fill the entire wizard. /// /// The help text is displayed using a read-only . public ustring HelpText { get => helpTextView.Text; set => helpTextView.Text = value; } 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; // 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 /// /// 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; // 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 /// /// 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.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); } /// /// 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. /// public bool ShowHelp { get => showHelp; set { showHelp = value; ShowHide (); } } private bool showHelp = true; /// /// If true (the default) the View will be visible. If false, the controls will not be shown and the help will /// fill the wizard step. /// public bool ShowControls { get => showControls; set { showControls = value; ShowHide (); } } private bool showControls = true; /// /// Does the work to show and hide the controls, help, and buttons as appropriate /// 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; } } /// /// 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; /// /// 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. /// /// Title 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; // 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 steps = new List (); private int currentStep = 0; /// /// 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) { steps.Add (newStep); this.Add (newStep); } /// /// The title of the Wizard, shown at the top of the Wizard with " - currentStep.Title" appended. /// 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; /// /// for transition events. /// public class WizardStepEventArgs : EventArgs { /// /// Set to true to cancel the transition to the next step. /// public bool Cancel { get; set; } /// /// Initializes a new instance of /// public WizardStepEventArgs () { Cancel = false; } } /// /// This event is 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; /// /// This event is 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 MovingNext; /// /// This event is 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; /// /// This event is raised when the current step )) in the changes. /// public event Action CurrentStepChanged; /// /// for events. /// public class CurrentStepChangedEventArgs : EventArgs { /// /// The new current . /// public int CurrentStepIndex { get; } /// /// Initializes a new instance of /// /// The new current . public CurrentStepChangedEventArgs (int currentStepIndex) { CurrentStepIndex = currentStepIndex; } } /// /// Gets or sets the currently active . /// public int CurrentStep { get => currentStep; set { currentStep = value; OnCurrentStepChanged (); } } /// /// Called when the current has changed (). /// 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..."; } } } }