using System; using System.Collections.Generic; using System.Linq; using NStack; using Terminal.Gui.Resources; 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. /// /// 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 : 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; // The controlPane is a separate view, so when devs add controls to the Step and help is visible, Y = Pos.AnchorEnd() // will work as expected. 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.Dialog; 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.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 (); }; this.Add (scrollBar); } //public override void OnEnabledChanged() //{ // if (Enabled) { } // base.OnEnabledChanged (); //} /// /// 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; } } // WizardStep /// /// 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); // BUGBUG: Space is to work around https://github.com/migueldeicaza/gui.cs/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; } private bool finishedPressed = false; private void Wizard_Closing (ToplevelClosingEventArgs obj) { if (!finishedPressed) { var args = new WizardButtonEventArgs (); Cancelled?.Invoke (args); } } private void Wizard_Loaded () { foreach (var step in steps) { step.Y = 0; } CurrentStep = GetNextStep (); // gets the first step if CurrentStep == null } private void NextfinishBtn_Clicked () { if (CurrentStep == steps.Last.Value) { var args = new WizardButtonEventArgs (); Finished?.Invoke (args); if (!args.Cancel) { finishedPressed = true; Application.RequestStop (this); } } else { var args = new WizardButtonEventArgs (); MovingNext?.Invoke (args); if (!args.Cancel) { GoNext (); } } } /// /// 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 last 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) { steps.AddLast (newStep); this.Add (newStep); newStep.EnabledChanged += UpdateButtonsAndTitle; //newStep.TitleChanged += UpdateButtonsAndTitle; UpdateButtonsAndTitle (); } /// /// 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 && 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; } } /// /// 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 user has cancelled the (with Ctrl-Q or ESC). /// 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); } 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..."; } SetNeedsLayout (); LayoutSubviews (); Redraw (Bounds); } } }