diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 24c2a1b88..0c40f67c5 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -571,7 +571,6 @@ namespace Terminal.Gui { if (autoSize && value.Anchor (0) != TextFormatter.Size.Width - (TextFormatter.IsHorizontalDirection (TextDirection) && TextFormatter.Text.Contains (HotKeySpecifier) ? 1 : 0)) { - autoSize = false; } SetMinWidthHeight (); @@ -600,7 +599,6 @@ namespace Terminal.Gui { if (autoSize && value.Anchor (0) != TextFormatter.Size.Height - (TextFormatter.IsVerticalDirection (TextDirection) && TextFormatter.Text.Contains (HotKeySpecifier) ? 1 : 0)) { - autoSize = false; } SetMinWidthHeight (); diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index cde610256..66bca7fb4 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1659,8 +1659,10 @@ namespace Terminal.Gui { } /// - /// Gets or sets a value indicating whether pressing the TAB key in a - /// types a TAB character in the view instead of moving the focus to the next view in the tab order. + /// Gets or sets whether the inserts a tab character into the text or ignores + /// tab input. If set to `false` and the user presses the tab key (or shift-tab) the focus will move to the + /// next view (or previous with shift-tab). The default is `true`; if the user presses the tab key, a tab + /// character will be inserted into the text. /// public bool AllowsTab { get => allowsTab; diff --git a/Terminal.Gui/Windows/Wizard.cs b/Terminal.Gui/Windows/Wizard.cs index 926cd4b86..cd8b3dc20 100644 --- a/Terminal.Gui/Windows/Wizard.cs +++ b/Terminal.Gui/Windows/Wizard.cs @@ -30,6 +30,8 @@ namespace Terminal.Gui { // 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 (); /// @@ -74,8 +76,8 @@ namespace Terminal.Gui { 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; - + this.ColorScheme = Colors.Dialog; + Y = 0; Height = Dim.Fill (1); // for button frame Width = Dim.Fill (); @@ -178,7 +180,7 @@ namespace Terminal.Gui { if (showControls) { if (showHelp) { Controls.Width = Dim.Percent (70); - helpTextView.X = Pos.Right (Controls) ; + helpTextView.X = Pos.Right (Controls); helpTextView.Width = Dim.Fill (); } else { @@ -195,29 +197,7 @@ namespace Terminal.Gui { 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; + } // WizardStep /// /// Initializes a new instance of the class using positioning. @@ -259,46 +239,90 @@ namespace Terminal.Gui { 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; - } - }; + backBtn.Clicked += BackBtn_Clicked; + nextfinishBtn.Clicked += NextfinishBtn_Clicked; + Loaded += Wizard_Loaded; + Closing += Wizard_Closing; } - private List steps = new List (); - private int currentStep = 0; + 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; + } + if (steps.Count > 0) { + CurrentStep = steps.First.Value; + } + } + + 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) { + var current = steps.Find (CurrentStep); + if (current != null && current.Next != null) { + GotoStep (current.Next.Value); + } + } + } + } + + private void BackBtn_Clicked () + { + var args = new WizardButtonEventArgs (); + MovingBack?.Invoke (args); + if (!args.Cancel) { + var current = steps.Find (CurrentStep); + if (current != null && current.Previous != null) { + GotoStep (current.Previous.Value); + } + } + } + + 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 @@ -308,8 +332,9 @@ namespace Terminal.Gui { /// The "Next..." button of the last step added will read "Finish" (unless changed from default). public void AddStep (WizardStep newStep) { - steps.Add (newStep); + steps.AddLast (newStep); this.Add (newStep); + SetNeedsLayout (); } /// @@ -322,7 +347,7 @@ namespace Terminal.Gui { } set { wizardTitle = value; - base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + steps [currentStep].Title : string.Empty)}"; + base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + currentStep.Title : string.Empty)}"; } } private ustring wizardTitle = ustring.Empty; @@ -330,16 +355,16 @@ namespace Terminal.Gui { /// /// for transition events. /// - public class WizardStepEventArgs : EventArgs { + 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 + /// Initializes a new instance of /// - public WizardStepEventArgs () + public WizardButtonEventArgs () { Cancel = false; } @@ -349,7 +374,7 @@ namespace Terminal.Gui { /// 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; + public event Action MovingBack; /// /// This event is raised when the Next/Finish button in the is clicked. The Next/Finish button is always @@ -357,7 +382,7 @@ namespace Terminal.Gui { /// raised if the is the last Step in the Wizard flow /// (otherwise the event is raised). /// - public event Action MovingNext; + public event Action MovingNext; /// /// This event is raised when the Next/Finish button in the is clicked. The Next/Finish button is always @@ -365,69 +390,143 @@ namespace Terminal.Gui { /// raised if the is the last Step in the Wizard flow /// (otherwise the event is raised). /// - public event Action Finished; + public event Action Finished; + /// - /// This event is raised when the current step )) in the changes. + /// This event is raised when the user has cancelled the (with Ctrl-Q or ESC). /// - public event Action CurrentStepChanged; + public event Action Cancelled; /// /// for events. /// - public class CurrentStepChangedEventArgs : EventArgs { + public class StepChangeEventArgs : EventArgs { /// - /// The new current . + /// The current (or previous) . /// - public int CurrentStepIndex { get; } + public WizardStep OldStep { get; } /// - /// Initializes a new instance of + /// The the is changing to or has changed to. /// - /// The new current . - public CurrentStepChangedEventArgs (int currentStepIndex) + 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) { - CurrentStepIndex = currentStepIndex; + 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 int CurrentStep { + public WizardStep CurrentStep { get => currentStep; set { - currentStep = value; - OnCurrentStepChanged (); + GotoStep (value); } } /// - /// Called when the current has changed (). + /// Called when the is about to transition to another . Fires the event. /// - public virtual void OnCurrentStepChanged () + /// 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) { - CurrentStepChanged?.Invoke (new CurrentStepChangedEventArgs (currentStep)); - // Hide all but the first step + 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)) { + return false; + } + + // Hide all but the new step foreach (WizardStep step in steps) { - step.Visible = (steps [currentStep] == step); + step.Visible = (step == newStep); } - // TODO: Add support for "Wizard Title - Step Title" - base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + steps [currentStep].Title : string.Empty)}"; + base.Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + newStep.Title : string.Empty)}"; - backBtn.Text = steps [currentStep].BackButtonText != ustring.Empty ? steps [currentStep].BackButtonText : "_Back"; - if (currentStep == 0) { - backBtn.Visible = false; + // TODO: Move strings to loc + + // Configure the Back button + backBtn.Text = newStep.BackButtonText != ustring.Empty ? newStep.BackButtonText : "_Back"; + backBtn.Visible = (newStep != steps.First.Value); + + // Configure the Next/Finished button + if (newStep == steps.Last.Value) { + nextfinishBtn.Text = newStep.NextButtonText != ustring.Empty ? newStep.NextButtonText : "Fi_nish"; } else { - backBtn.Visible = true; + nextfinishBtn.Text = newStep.NextButtonText != ustring.Empty ? newStep.NextButtonText : "_Next..."; } - if (currentStep == steps.Count - 1) { - nextfinishBtn.Text = steps [currentStep].NextButtonText != ustring.Empty ? steps [currentStep].NextButtonText : "Fi_nish"; + // Set focus to the nav buttons + if (backBtn.HasFocus) { + backBtn.SetFocus (); } else { - nextfinishBtn.Text = steps [currentStep].NextButtonText != ustring.Empty ? steps [currentStep].NextButtonText : "_Next..."; + nextfinishBtn.SetFocus (); } + + var oldStep = currentStep; + currentStep = newStep; + + LayoutSubviews (); + Redraw (this.Bounds); + + if (OnStepChanged (oldStep, currentStep)) { + // For correctness we do this, but it's meaningless because there's nothing to cancel + return false; + } + + return true; } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/Dialogs.cs b/UICatalog/Scenarios/Dialogs.cs index 4f99d4568..766c1e719 100644 --- a/UICatalog/Scenarios/Dialogs.cs +++ b/UICatalog/Scenarios/Dialogs.cs @@ -215,6 +215,18 @@ namespace UICatalog.Scenarios { }; dialog.Add (add); + var addChar = new Button ($"Add a {Char.ConvertFromUtf32(CODE_POINT)} to each button") { + X = Pos.Center (), + Y = Pos.Center () + 1 + }; + addChar.Clicked += () => { + foreach (var button in buttons) { + button.Text += Char.ConvertFromUtf32 (CODE_POINT); + } + dialog.LayoutSubviews (); + }; + dialog.Add (addChar); + Application.Run (dialog); buttonPressedLabel.Text = $"{clicked}"; diff --git a/UICatalog/Scenarios/Wizards.cs b/UICatalog/Scenarios/Wizards.cs index 8ec9ca9cc..80a178f78 100644 --- a/UICatalog/Scenarios/Wizards.cs +++ b/UICatalog/Scenarios/Wizards.cs @@ -16,7 +16,6 @@ namespace UICatalog.Scenarios { X = Pos.Center (), Y = 0, Width = Dim.Percent (75), - Height = 10, ColorScheme = Colors.Base, }; Win.Add (frame); @@ -69,9 +68,18 @@ namespace UICatalog.Scenarios { }; frame.Add (titleEdit); + var useStepView = new CheckBox () { + Text = "Add 3rd step controls to WizardStep instead of WizardStep.Controls", + Checked = false, + X = Pos.Left (titleEdit), + Y = Pos.Bottom (titleEdit) + }; + frame.Add (useStepView); + + void Top_Loaded () { - frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + 2; + frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit) + Dim.Height (useStepView) + 2; Top.Loaded -= Top_Loaded; } Top.Loaded += Top_Loaded; @@ -127,6 +135,11 @@ namespace UICatalog.Scenarios { actionLabel.Text = "Finished"; }; + wizard.Cancelled += (args) => { + //args.Cancel = true; + actionLabel.Text = "Cancelled"; + }; + // Add 1st step var firstStep = new Wizard.WizardStep ("End User License Agreement"); wizard.AddStep (firstStep); @@ -138,6 +151,15 @@ namespace UICatalog.Scenarios { 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."; + + + View viewForControls = secondStep.Controls; + ustring frameMsg = "Added to WizardStep.Controls"; + if (useStepView.Checked) { + viewForControls = secondStep; + frameMsg = "Added to WizardStep directly"; + } + var buttonLbl = new Label () { Text = "Second Step Button: ", AutoSize = true, X = 1, Y = 1 }; var button = new Button () { Text = "Press Me", @@ -147,13 +169,28 @@ namespace UICatalog.Scenarios { button.Clicked += () => { MessageBox.Query ("Wizard Scenario", "The Second Step Button was pressed."); }; - secondStep.Controls.Add (buttonLbl, button); + viewForControls.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); + viewForControls.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); + viewForControls.Add (lbl, lastNameField); + var checkBox = new CheckBox () { Text = "Un-check me!", Checked = true, X = Pos.Left (lastNameField), Y = Pos.Bottom (lastNameField) }; + viewForControls.Add (checkBox); + + // Add a frame to demonstrate difference between adding controls to + // WizardStep.Controls vs. WizardStep directly. This is here to demonstrate why + // adding to .Controls is preferred. + var frame = new FrameView ($"A Broken Frame - {frameMsg}") { + X = 0, + Y = Pos.Bottom (checkBox) + 2, + Width = Dim.Fill (), + Height = 4, + //ColorScheme = Colors.Error, + }; + frame.Add (new TextField ("This is a TextField inside of the frame.")); + viewForControls.Add (frame); // Add 3rd step var thirdStep = new Wizard.WizardStep ("Third Step"); @@ -169,20 +206,35 @@ namespace UICatalog.Scenarios { thirdStep.Controls.Add (progLbl, progressBar); // Add 4th step - var fourthStep = new Wizard.WizardStep ("Hidden Help pane"); + var fourthStep = new Wizard.WizardStep ("Step Four"); 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.", + Text = "This step (Step Four) shows how to hide the Help pane. The control pane contains this TextView (but it's hard to tell it's a TextView because of Issue #1800).", X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), WordWrap = true, + AllowsTab = false }; fourthStep.Controls.Add (someText); + //fourthStep.NextButtonText = "4"; var scrollBar = new ScrollBarView (someText, true); + wizard.StepChanging += (args) => { + if (args.NewStep == fourthStep) { + var btn = MessageBox.ErrorQuery ("Wizards", "Move to Step Four?", "Yes", "No"); + args.Cancel = btn == 1; + } + }; + + wizard.StepChanged += (args) => { + if (args.NewStep == fourthStep) { + var btn = MessageBox.ErrorQuery ("Wizards", "Yay. Moved to Step Four", "Ok"); + } + }; + scrollBar.ChangedPosition += () => { someText.TopRow = scrollBar.Position; if (someText.TopRow != scrollBar.Position) { @@ -216,13 +268,8 @@ namespace UICatalog.Scenarios { 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) { diff --git a/UnitTests/WizardTests.cs b/UnitTests/WizardTests.cs index 43aec149f..70b4d2bea 100644 --- a/UnitTests/WizardTests.cs +++ b/UnitTests/WizardTests.cs @@ -93,6 +93,37 @@ namespace Terminal.Gui.Views { // and that the title is correct public void OneStepWizard_Shows () { + var d = ((FakeDriver)Application.Driver); + + var title = "1234"; + var stepTitle = "ABCD"; + + int width = 30; + int height = 7; + d.SetBufferSize (width, height); + + var btnBackText = "Back"; + var btnBack = string.Empty; // $"{d.LeftBracket} {btnBackText} {d.RightBracket}"; + var btnNextText = "Finish"; // "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 - 7)}{d.URDCorner}"; + var row2 = $"{d.VDLine}{new String (' ', width - 2)}{d.VDLine}"; + var row3 = row2; + var row4 = row3; + 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 }; + wizard.AddStep (new Wizard.WizardStep (stepTitle)); + //wizard.LayoutSubviews (); + var firstIteration = false; + var runstate = Application.Begin (wizard); + Application.RunMainLoopIteration (ref runstate, true, ref firstIteration); + + GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{row2}\n{row3}\n{row4}\n{separatorRow}\n{buttonRow}\n{bottomRow}", output); + Application.End (runstate); } [Fact, AutoInitShutdown] @@ -150,7 +181,7 @@ namespace Terminal.Gui.Views { 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 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}";