Fixes #2485 ++ - Wizard v2 architecture modernization with Padding-based layout (#4510)

* Initial plan

* Fix Wizard v2 architecture issues - ScrollBar API, event handlers, key bindings

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Implement issue #4155 - Put nav buttons in bottom Padding, Help in right Padding

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Address code review feedback - Extract helper method, improve null checks

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix disposal issue - Ensure _helpTextView is always disposed

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Refactor & improvements. WIP

* Tweaking layout

* Wizard tweaks

* Added View.GetSubViews that optinoally gets subviews of adornments

* Refactor Wizard API: modern events, layout, and design

- Replaced custom event args with standard .NET event args (CancelEventArgs, ValueChangingEventArgs, etc.)
- Removed Finished event; use Accepting for wizard completion
- Updated Cancelled, MovingBack, MovingNext to use CancelEventArgs
- Refactored UICatalog scenarios and tests to new event model
- Improved WizardStep sizing and wizard auto-resizing to content
- Enhanced IDesignable for Wizard and WizardStep with richer design-time UI
- Simplified help text padding logic in WizardStep
- Removed obsolete code and modernized code style throughout
- Improves API consistency, usability, and .NET idiomatic usage

* Fixes #4515 - Navigating into and out of Adornments does not work

* WIP. QUite broken.

* All fixed?

* Tweaks.

* Exclude Margin subviews from drawing; add shadow tests

Update Margin adornment to skip drawing subviews that are themselves Margin views, preventing unsupported nested Margin rendering. Add unit tests to verify that opaque-shadowed buttons in Margin are not drawn, while Border and Padding still support shadow rendering. Update test class to use output helper and assert driver output.

* Final code cleanup and test improvements.

* Update Margin.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update View.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update View.Hierarchy.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update View.Hierarchy.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor: code style, formatting, and minor logic cleanup

- Standardized spacing and formatting for method signatures and object initializations.
- Converted simple methods and properties to expression-bodied members for conciseness.
- Replaced named arguments with positional arguments for consistency.
- Improved XML documentation formatting for readability.
- Simplified logic in event handlers (e.g., Wizard Back button).
- Removed redundant checks where properties are guaranteed to exist.
- Fixed minor bugs related to padding, height calculation, and event handling.
- Adopted consistent use of `var` for local variables.
- Corrected namespace declarations.
- Refactored methods returning constants to use expression-bodied syntax.
- General code cleanup for clarity and maintainability; no breaking changes.

* api docs

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot
2025-12-21 07:42:04 -07:00
committed by GitHub
parent af0efb3c64
commit 4145b984ba
29 changed files with 3507 additions and 1380 deletions

View File

@@ -1,4 +1,7 @@
using System; #nullable enable
// ReSharper disable AccessToDisposedClosure
// ReSharper disable AssignNullToNotNullAttribute
namespace UICatalog.Scenarios; namespace UICatalog.Scenarios;
@@ -10,12 +13,11 @@ public class Adornments : Scenario
public override void Main () public override void Main ()
{ {
Application.Init (); Application.Init ();
using IApplication app = Application.Instance;
Window appWindow = new () using Window appWindow = new ();
{ appWindow.Title = GetQuitKeyAndName ();
Title = GetQuitKeyAndName (), appWindow.BorderStyle = LineStyle.None;
BorderStyle = LineStyle.None
};
var editor = new AdornmentsEditor var editor = new AdornmentsEditor
{ {
@@ -31,7 +33,7 @@ public class Adornments : Scenario
appWindow.Add (editor); appWindow.Add (editor);
var window = new Window Window window = new ()
{ {
Title = "The _Window", Title = "The _Window",
Arrangement = ViewArrangement.Overlapped | ViewArrangement.Movable, Arrangement = ViewArrangement.Overlapped | ViewArrangement.Movable,
@@ -41,11 +43,11 @@ public class Adornments : Scenario
}; };
appWindow.Add (window); appWindow.Add (window);
var tf1 = new TextField { Width = 10, Text = "TextField" }; TextField tf1 = new () { Width = 10, Text = "TextField" };
var color = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () }; ColorPicker16 color = new () { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () };
color.BorderStyle = LineStyle.RoundedDotted; color.BorderStyle = LineStyle.RoundedDotted;
color.ColorChanged += (s, e) => color.ColorChanged += (_, e) =>
{ {
color.SuperView!.SetScheme ( color.SuperView!.SetScheme (
new (color.SuperView.GetScheme ()) new (color.SuperView.GetScheme ())
@@ -58,12 +60,12 @@ public class Adornments : Scenario
}); });
}; };
var button = new Button { X = Pos.Center (), Y = Pos.Center (), Text = "Press me!" }; Button button = new () { X = Pos.Center (), Y = Pos.Center (), Text = "Press me!" };
button.Accepting += (s, e) => button.Accepting += (_, _) =>
MessageBox.Query (appWindow.App, 20, 7, "Hi", $"Am I a {window.GetType ().Name}?", "Yes", "No"); MessageBox.Query (appWindow.App!, 20, 7, "Hi", $"Am I a {window.GetType ().Name}?", "Yes", "No");
var label = new TextView TextView label = new ()
{ {
X = Pos.Center (), X = Pos.Center (),
Y = Pos.Bottom (button), Y = Pos.Bottom (button),
@@ -74,9 +76,9 @@ public class Adornments : Scenario
}; };
label.Border!.Thickness = new (1, 3, 1, 1); label.Border!.Thickness = new (1, 3, 1, 1);
var btnButtonInWindow = new Button { X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Text = "Button" }; Button btnButtonInWindow = new () { X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Text = "Button" };
var labelAnchorEnd = new Label Label labelAnchorEnd = new ()
{ {
Y = Pos.AnchorEnd (), Y = Pos.AnchorEnd (),
Width = 40, Width = 40,
@@ -87,68 +89,76 @@ public class Adornments : Scenario
window.Margin!.Data = "Margin"; window.Margin!.Data = "Margin";
window.Margin!.Text = "Margin Text"; window.Margin!.Text = "Margin Text";
window.Margin!.Thickness = new (0); window.Margin!.Thickness = new (3);
window.Border!.Data = "Border"; window.Border!.Data = "Border";
window.Border!.Text = "Border Text"; window.Border!.Text = "Border Text";
window.Border!.Thickness = new (0); window.Border!.Thickness = new (5);
window.Border!.SetScheme (SchemeManager.GetScheme (Schemes.Dialog));
window.Padding.Data = "Padding"; window.Padding!.Data = "Padding";
window.Padding.Text = "Padding Text line 1\nPadding Text line 3\nPadding Text line 3\nPadding Text line 4\nPadding Text line 5"; window.Padding.Text = "Padding Text line 1\nPadding Text line 3\nPadding Text line 3\nPadding Text line 4\nPadding Text line 5";
window.Padding.Thickness = new (3); window.Padding.Thickness = new (4);
window.Padding.SchemeName = "Error"; window.Padding!.SetScheme (SchemeManager.GetScheme (Schemes.Menu));
window.Padding.CanFocus = true; window.Padding.CanFocus = true;
var longLabel = new Label Label longLabel = new ()
{ {
X = 40, Y = 5, Title = "This is long text (in a label) that should clip." X = 40, Y = 5, Title = "This is long text (in a label) that should clip."
}; };
longLabel.TextFormatter.WordWrap = true; longLabel.TextFormatter.WordWrap = true;
window.Add (tf1, color, button, label, btnButtonInWindow, labelAnchorEnd, longLabel); window.Add (tf1, color, button, label, btnButtonInWindow, labelAnchorEnd, longLabel);
window.Initialized += (s, e) => window.Initialized += (_, _) =>
{ {
editor.ViewToEdit = window; editor.ViewToEdit = window;
editor.ShowViewIdentifier = true; editor.ShowViewIdentifier = true;
var labelInPadding = new Label { X = 0, Y = 1, Title = "_Text:" }; // NOTE: Adding SubViews to Margin is not supported
window.Padding.Add (labelInPadding);
var textFieldInPadding = new TextField Button btnButtonInBorder = new ()
{
X = Pos.Right (labelInPadding) + 1,
Y = Pos.Top (labelInPadding), Width = 10,
Text = "text (Y = 1)",
CanFocus = true
};
textFieldInPadding.Accepting += (s, e) => MessageBox.Query (appWindow.App, 20, 7, "TextField", textFieldInPadding.Text, "Ok");
window.Padding.Add (textFieldInPadding);
var btnButtonInPadding = new Button
{ {
X = Pos.Center (), X = Pos.Center (),
Y = 1, Y = 1,
Text = "_Button in Padding Y = 1", Text = "_Button in Border Y = 1"
CanFocus = true,
HighlightStates = MouseState.None,
}; };
btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (appWindow.App, 20, 7, "Hi", "Button in Padding Pressed!", "Ok"); btnButtonInBorder.Accepting += (_, _) => MessageBox.Query (appWindow.App!, 20, 7, "Hi", "Button in Border Pressed!", "Ok");
btnButtonInPadding.BorderStyle = LineStyle.Dashed; window.Border.Add (btnButtonInBorder);
btnButtonInPadding.Border!.Thickness = new (1, 1, 1, 1);
Label labelInPadding = new () { X = 0, Y = 1, Title = "_Text:" };
window.Padding.Add (labelInPadding);
TextField textFieldInPadding = new ()
{
X = Pos.Right (labelInPadding) + 1,
Y = Pos.Top (labelInPadding), Width = 10,
Text = "text (Y = 1)"
};
textFieldInPadding.Accepting += (_, _) => MessageBox.Query (appWindow.App!, 20, 7, "TextField", textFieldInPadding.Text, "Ok");
window.Padding.Add (textFieldInPadding);
Button btnButtonInPadding = new ()
{
X = Pos.Center (),
Y = 1,
Text = "_Button in Padding Y = 1"
};
btnButtonInPadding.Accepting += (_, _) => MessageBox.Query (appWindow.App!, 20, 7, "Hi", "Button in Padding Pressed!", "Ok");
window.Padding.Add (btnButtonInPadding); window.Padding.Add (btnButtonInPadding);
#if SUBVIEW_BASED_BORDER #if SUBVIEW_BASED_BORDER
btnButtonInPadding.Border!.CloseButton.Visible = true; btnButtonInPadding.Border!.CloseButton.Visible = true;
view.Border!.CloseButton.Visible = true; view.Border!.CloseButton.Visible = true;
view.Border!.CloseButton.Accept += (s, e) => view.Border!.CloseButton.Accept += (_, _) =>
{ {
MessageBox.Query (20, 7, "Hi", "Window Close Button Pressed!", "Ok"); MessageBox.Query (20, 7, "Hi", "Window Close Button Pressed!", "Ok");
e.Handled = true; e.Handled = true;
}; };
view.Accept += (s, e) => MessageBox.Query (20, 7, "Hi", "Window Close Button Pressed!", "Ok"); view.Accept += (_, _) => MessageBox.Query (20, 7, "Hi", "Window Close Button Pressed!", "Ok");
#endif #endif
}; };
@@ -156,9 +166,6 @@ public class Adornments : Scenario
editor.AutoSelectSuperView = window; editor.AutoSelectSuperView = window;
editor.AutoSelectAdornments = true; editor.AutoSelectAdornments = true;
Application.Run (appWindow); app.Run (appWindow);
appWindow.Dispose ();
Application.Shutdown ();
} }
} }

View File

@@ -1,158 +0,0 @@
#nullable enable
namespace UICatalog.Scenarios;
[ScenarioMetadata ("WizardAsView", "Shows using the Wizard class in an non-modal way")]
[ScenarioCategory ("Wizards")]
public class WizardAsView : Scenario
{
public override void Main ()
{
Application.Init ();
// MenuBar
MenuBar menu = new ();
menu.Add (
new MenuBarItem (
"_File",
[
new MenuItem
{
Title = "_Restart Configuration...",
Action = () => MessageBox.Query (
Application.Instance,
"Wizard",
"Are you sure you want to reset the Wizard and start over?",
"Ok",
"Cancel"
)
},
new MenuItem
{
Title = "Re_boot Server...",
Action = () => MessageBox.Query (
Application.Instance,
"Wizard",
"Are you sure you want to reboot the server start over?",
"Ok",
"Cancel"
)
},
new MenuItem
{
Title = "_Shutdown Server...",
Action = () => MessageBox.Query (
Application.Instance,
"Wizard",
"Are you sure you want to cancel setup and shutdown?",
"Ok",
"Cancel"
)
}
]
)
);
// No need for a Title because the border is disabled
Wizard wizard = new ()
{
X = 0,
Y = Pos.Bottom (menu),
Width = Dim.Fill (),
Height = Dim.Fill (),
ShadowStyle = ShadowStyle.None
};
// Set Modal to false to cause the Wizard class to render without a frame and
// behave like an non-modal View (vs. a modal/pop-up Window).
// wizard.Modal = false;
wizard.MovingBack += (s, args) =>
{
//args.Cancel = true;
//actionLabel.Text = "Moving Back";
};
wizard.MovingNext += (s, args) =>
{
//args.Cancel = true;
//actionLabel.Text = "Moving Next";
};
wizard.Finished += (s, args) =>
{
//args.Cancel = true;
MessageBox.Query ((s as View)?.App!, "Setup Wizard", "Finished", "Ok");
Application.RequestStop ();
};
wizard.Cancelled += (s, args) =>
{
int? btn = MessageBox.Query ((s as View)?.App!, "Setup Wizard", "Are you sure you want to cancel?", "Yes", "No");
args.Cancel = btn == 1;
if (btn == 0)
{
Application.RequestStop ();
}
};
// Add 1st step
WizardStep firstStep = new () { Title = "End User License Agreement" };
wizard.AddStep (firstStep);
firstStep.NextButtonText = "Accept!";
firstStep.HelpText =
"This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA.";
// Add 2nd step
WizardStep secondStep = new () { Title = "Second Step" };
wizard.AddStep (secondStep);
secondStep.HelpText =
"This is the help text for the Second Step.\n\nPress the button to change the Title.\n\nIf First Name is empty the step will prevent moving to the next step.";
Label buttonLbl = new () { Text = "Second Step Button: ", X = 0, Y = 0 };
Button button = new ()
{
Text = "Press Me to Rename Step",
X = Pos.Right (buttonLbl),
Y = Pos.Top (buttonLbl)
};
button.Accepting += (s, e) =>
{
secondStep.Title = "2nd Step";
MessageBox.Query ((s as View)?.App!,
"Wizard Scenario",
"This Wizard Step's title was changed to '2nd Step'",
"Ok"
);
};
secondStep.Add (buttonLbl, button);
Label lbl = new () { Text = "First Name: ", X = Pos.Left (buttonLbl), Y = Pos.Bottom (buttonLbl) };
TextField firstNameField = new () { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) };
secondStep.Add (lbl, firstNameField);
lbl = new () { Text = "Last Name: ", X = Pos.Left (buttonLbl), Y = Pos.Bottom (lbl) };
TextField lastNameField = new () { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) };
secondStep.Add (lbl, lastNameField);
// Add last step
WizardStep lastStep = new () { Title = "The last step" };
wizard.AddStep (lastStep);
lastStep.HelpText =
"The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing Esc will cancel.";
Window window = new ();
window.Add (menu, wizard);
Application.Run (window);
window.Dispose ();
Application.Shutdown ();
}
}

View File

@@ -1,4 +1,9 @@
namespace UICatalog.Scenarios; #nullable enable
// ReSharper disable AccessToDisposedClosure
using Terminal.Gui.Views;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("Wizards", "Demonstrates the Wizard class")] [ScenarioMetadata ("Wizards", "Demonstrates the Wizard class")]
[ScenarioCategory ("Dialogs")] [ScenarioCategory ("Dialogs")]
@@ -6,69 +11,37 @@
[ScenarioCategory ("Runnable")] [ScenarioCategory ("Runnable")]
public class Wizards : Scenario public class Wizards : Scenario
{ {
private Wizard? _wizard;
private View? _actionLabel;
private TextField? _titleEdit;
public override void Main () public override void Main ()
{ {
Application.Init (); Application.Init ();
var win = new Window { Title = GetQuitKeyAndName () }; using IApplication app = Application.Instance;
var frame = new FrameView using Window win = new ();
win.Title = GetQuitKeyAndName ();
FrameView settingsFrame = new ()
{ {
X = Pos.Center (), X = Pos.Center (),
Y = 0, Y = 0,
Width = Dim.Percent (75), Width = Dim.Percent (75),
SchemeName = "Base", Height = Dim.Auto (),
Title = "Wizard Options" Title = "Wizard Options"
}; };
win.Add (frame); win.Add (settingsFrame);
var label = new Label { X = 0, Y = 0, TextAlignment = Alignment.End, Text = "_Width:", Width = 10 }; Label label = new ()
frame.Add (label);
var widthEdit = new TextField
{
X = Pos.Right (label) + 1,
Y = Pos.Top (label),
Width = 5,
Height = 1,
Text = "80"
};
frame.Add (widthEdit);
label = new ()
{ {
X = 0, X = 0,
Y = Pos.Bottom (label),
Width = Dim.Width (label),
Height = 1,
TextAlignment = Alignment.End,
Text = "_Height:"
};
frame.Add (label);
var heightEdit = new TextField
{
X = Pos.Right (label) + 1,
Y = Pos.Top (label),
Width = 5,
Height = 1,
Text = "20"
};
frame.Add (heightEdit);
label = new ()
{
X = 0,
Y = Pos.Bottom (label),
Width = Dim.Width (label),
Height = 1,
TextAlignment = Alignment.End, TextAlignment = Alignment.End,
Text = "_Title:" Text = "_Title:"
}; };
frame.Add (label); settingsFrame.Add (label);
var titleEdit = new TextField _titleEdit = new ()
{ {
X = Pos.Right (label) + 1, X = Pos.Right (label) + 1,
Y = Pos.Top (label), Y = Pos.Top (label),
@@ -76,15 +49,26 @@ public class Wizards : Scenario
Height = 1, Height = 1,
Text = "Gandolf" Text = "Gandolf"
}; };
frame.Add (titleEdit); settingsFrame.Add (_titleEdit);
void Win_Loaded (object sender, EventArgs args) CheckBox cbRun = new ()
{ {
frame.Height = widthEdit.Frame.Height + heightEdit.Frame.Height + titleEdit.Frame.Height + 2; Title = "_Run Wizard as a modal",
win.IsModalChanged -= Win_Loaded; X = 0,
} Y = Pos.Bottom (label),
CheckedState = CheckState.Checked
};
settingsFrame.Add (cbRun);
win.IsModalChanged += Win_Loaded; Button showWizardButton = new ()
{
X = Pos.Center (),
Y = Pos.Bottom (cbRun),
IsDefault = true,
Text = "_Show Wizard"
};
settingsFrame.Add (showWizardButton);
label = new () label = new ()
{ {
@@ -92,295 +76,104 @@ public class Wizards : Scenario
}; };
win.Add (label); win.Add (label);
var actionLabel = new Label _actionLabel = new ()
{ {
X = Pos.Right (label), Y = Pos.AnchorEnd (1), SchemeName = "Error" X = Pos.Right (label),
Y = Pos.AnchorEnd (1),
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Error),
Width = Dim.Auto (),
Height = Dim.Auto ()
}; };
win.Add (actionLabel); win.Add (_actionLabel);
var showWizardButton = new Button if (cbRun.CheckedState != CheckState.Checked)
{ {
X = Pos.Center (), Y = Pos.Bottom (frame) + 2, IsDefault = true, Text = "_Show Wizard" showWizardButton.Enabled = false;
}; _wizard = CreateWizard ();
win.Add (_wizard);
showWizardButton.Accepting += (s, e) =>
{
try
{
var width = 0;
int.TryParse (widthEdit.Text, out width);
var height = 0;
int.TryParse (heightEdit.Text, out height);
if (width < 1 || height < 1)
{
MessageBox.ErrorQuery (
(s as View)?.App,
"Nope",
"Height and width must be greater than 0 (much bigger)",
"Ok"
);
return;
} }
actionLabel.Text = string.Empty; cbRun.CheckedStateChanged += (_, a) =>
var wizard = new Wizard { Title = titleEdit.Text, Width = width, Height = height };
wizard.MovingBack += (s, args) =>
{ {
//args.Cancel = true; if (a.Value == CheckState.Checked)
actionLabel.Text = "Moving Back"; {
showWizardButton.Enabled = true;
_wizard!.X = Pos.Center ();
_wizard.Y = Pos.Center ();
win.Remove (_wizard);
_wizard.Dispose ();
_wizard = null;
}
else
{
showWizardButton.Enabled = false;
_wizard = CreateWizard ();
_wizard.Y = Pos.Bottom (settingsFrame) + 1;
win.Add (_wizard);
}
}; };
wizard.MovingNext += (s, args) => showWizardButton.Accepting += (_, _) =>
{ {
//args.Cancel = true; _wizard = CreateWizard ();
actionLabel.Text = "Moving Next"; app.Run (_wizard);
_wizard.Dispose ();
}; };
wizard.Finished += (s, args) => app.Run (win);
}
private Wizard CreateWizard ()
{ {
//args.Cancel = true; Wizard wizard = new ();
actionLabel.Text = "Finished";
if (_titleEdit is { })
{
wizard.Title = _titleEdit.Text;
}
wizard.MovingBack += (_, args) =>
{
// Set Cancel to true to prevent moving back
args.Cancel = false;
_actionLabel!.Text = "Moving Back";
};
wizard.MovingNext += (_, args) =>
{
// Set Cancel to true to prevent moving next
args.Cancel = false;
_actionLabel!.Text = "Moving Next";
};
wizard.Accepting += (s, args) =>
{
_actionLabel!.Text = "Finished";
MessageBox.Query ((s as View)?.App!, "Wizard", "The Wizard has been completed and accepted!", "_Ok");
if (wizard.IsRunning)
{
// Don't set args.Handled to true to allow the wizard to close
args.Handled = false;
}
else
{
wizard.App!.RequestStop();
args.Handled = true;
}
}; };
wizard.Cancelled += (s, args) => wizard.Cancelled += (s, args) =>
{ {
//args.Cancel = true; _actionLabel!.Text = "Cancelled";
actionLabel.Text = "Cancelled";
int? btn = MessageBox.Query ((s as View)?.App!, "Wizard", "Are you sure you want to cancel?", "_Yes", "_No");
args.Cancel = btn is not 0;
}; };
// Add 1st step ((IDesignable)wizard).EnableForDesign ();
var firstStep = new WizardStep { Title = "End User License Agreement" };
firstStep.NextButtonText = "Accept!";
firstStep.HelpText = return wizard;
"This is the End User License Agreement.\n\n\n\n\n\nThis is a test of the emergency broadcast system. This is a test of the emergency broadcast system.\nThis is a test of the emergency broadcast system.\n\n\nThis is a test of the emergency broadcast system.\n\nThis is a test of the emergency broadcast system.\n\n\n\nThe end of the EULA.";
OptionSelector optionSelector = new ()
{
Labels = ["_One", "_Two", "_3"]
};
firstStep.Add (optionSelector);
wizard.AddStep (firstStep);
// Add 2nd step
var secondStep = new WizardStep { Title = "Second Step" };
wizard.AddStep (secondStep);
secondStep.HelpText =
"This is the help text for the Second Step.\n\nPress the button to change the Title.\n\nIf First Name is empty the step will prevent moving to the next step.";
var buttonLbl = new Label { Text = "Second Step Button: ", X = 1, Y = 1 };
var button = new Button
{
Text = "Press Me to Rename Step", X = Pos.Right (buttonLbl), Y = Pos.Top (buttonLbl)
};
OptionSelector optionSelecor2 = new ()
{
Labels = ["_A", "_B", "_C"],
Orientation = Orientation.Horizontal
};
secondStep.Add (optionSelecor2);
button.Accepting += (s, e) =>
{
secondStep.Title = "2nd Step";
MessageBox.Query (
(s as View)?.App,
"Wizard Scenario",
"This Wizard Step's title was changed to '2nd Step'"
);
};
secondStep.Add (buttonLbl, button);
var lbl = new Label { Text = "First Name: ", X = 1, Y = Pos.Bottom (buttonLbl) };
var firstNameField =
new TextField { Text = "Number", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) };
secondStep.Add (lbl, firstNameField);
lbl = new () { Text = "Last Name: ", X = 1, Y = Pos.Bottom (lbl) };
var lastNameField = new TextField { Text = "Six", Width = 30, X = Pos.Right (lbl), Y = Pos.Top (lbl) };
secondStep.Add (lbl, lastNameField);
var thirdStepEnabledCeckBox = new CheckBox
{
Text = "Enable Step _3",
CheckedState = CheckState.UnChecked,
X = Pos.Left (lastNameField),
Y = Pos.Bottom (lastNameField)
};
secondStep.Add (thirdStepEnabledCeckBox);
// Add a frame
var frame = new FrameView
{
X = 0,
Y = Pos.Bottom (thirdStepEnabledCeckBox) + 2,
Width = Dim.Fill (),
Height = 4,
Title = "A Broken Frame (by Depeche Mode)",
TabStop = TabBehavior.NoStop
};
frame.Add (new TextField { Text = "This is a TextField inside of the frame." });
secondStep.Add (frame);
wizard.StepChanging += (s, args) =>
{
if (args.OldStep == secondStep && string.IsNullOrEmpty (firstNameField.Text))
{
args.Cancel = true;
int? btn = MessageBox.ErrorQuery (
(s as View)?.App,
"Second Step",
"You must enter a First Name to continue",
"Ok"
);
} }
};
// Add 3rd (optional) step
var thirdStep = new WizardStep { Title = "Third Step (Optional)" };
wizard.AddStep (thirdStep);
thirdStep.HelpText =
"This is step is optional (WizardStep.Enabled = false). Enable it with the checkbox in Step 2.";
var step3Label = new Label { Text = "This step is optional.", X = 0, Y = 0 };
thirdStep.Add (step3Label);
var progLbl = new Label { Text = "Third Step ProgressBar: ", X = 1, Y = 10 };
var progressBar = new ProgressBar
{
X = Pos.Right (progLbl), Y = Pos.Top (progLbl), Width = 40, Fraction = 0.42F
};
thirdStep.Add (progLbl, progressBar);
thirdStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked;
thirdStepEnabledCeckBox.CheckedStateChanged += (s, e) =>
{
thirdStep.Enabled =
thirdStepEnabledCeckBox.CheckedState == CheckState.Checked;
};
// Add 4th step
var fourthStep = new WizardStep { Title = "Step Four" };
wizard.AddStep (fourthStep);
var someText = new TextView
{
Text =
"This step (Step Four) shows how to show/hide the Help pane. The step contains this TextView (but it's hard to tell it's a TextView because of Issue #1800).",
X = 0,
Y = 0,
Width = Dim.Fill (),
WordWrap = true,
AllowsTab = false,
SchemeName = "Base"
};
someText.Height = Dim.Fill (
Dim.Func (v => someText.SuperView is { IsInitialized: true }
? someText.SuperView.SubViews
.First (view => view.Y.Has<PosAnchorEnd> (out _))
.Frame.Height
: 1));
var help = "This is helpful.";
fourthStep.Add (someText);
var hideHelpBtn = new Button
{
Text = "Press me to show/hide help",
X = Pos.Center (),
Y = Pos.AnchorEnd ()
};
hideHelpBtn.Accepting += (s, e) =>
{
if (fourthStep.HelpText.Length > 0)
{
fourthStep.HelpText = string.Empty;
}
else
{
fourthStep.HelpText = help;
}
};
fourthStep.Add (hideHelpBtn);
fourthStep.NextButtonText = "_Go To Last Step";
//var scrollBar = new ScrollBarView (someText, true);
//scrollBar.ChangedPosition += (s, e) =>
// {
// someText.TopRow = scrollBar.Position;
// if (someText.TopRow != scrollBar.Position)
// {
// scrollBar.Position = someText.TopRow;
// }
// someText.SetNeedsDraw ();
// };
//someText.DrawingContent += (s, e) =>
// {
// scrollBar.Size = someText.Lines;
// scrollBar.Position = someText.TopRow;
// if (scrollBar.OtherScrollBarView != null)
// {
// scrollBar.OtherScrollBarView.Size = someText.Maxlength;
// scrollBar.OtherScrollBarView.Position = someText.LeftColumn;
// }
// };
//fourthStep.Add (scrollBar);
// Add last step
var lastStep = new WizardStep { Title = "The last step" };
wizard.AddStep (lastStep);
lastStep.HelpText =
"The wizard is complete!\n\nPress the Finish button to continue.\n\nPressing ESC will cancel the wizard.";
var finalFinalStepEnabledCeckBox =
new CheckBox { Text = "Enable _Final Final Step", CheckedState = CheckState.UnChecked, X = 0, Y = 1 };
lastStep.Add (finalFinalStepEnabledCeckBox);
// Add an optional FINAL last step
var finalFinalStep = new WizardStep { Title = "The VERY last step" };
wizard.AddStep (finalFinalStep);
finalFinalStep.HelpText =
"This step only shows if it was enabled on the other last step.";
finalFinalStep.Enabled = thirdStepEnabledCeckBox.CheckedState == CheckState.Checked;
finalFinalStepEnabledCeckBox.CheckedStateChanged += (s, e) =>
{
finalFinalStep.Enabled =
finalFinalStepEnabledCeckBox.CheckedState
== CheckState.Checked;
};
Application.Run (wizard);
wizard.Dispose ();
}
catch (FormatException)
{
actionLabel.Text = "Invalid Options";
}
};
win.Add (showWizardButton);
Application.Run (win);
win.Dispose ();
Application.Shutdown ();
}
private void Wizard_StepChanged (object sender, StepChangeEventArgs e) { throw new NotImplementedException (); }
} }

View File

@@ -663,7 +663,8 @@ public class UICatalog
// 'app' closed cleanly. // 'app' closed cleanly.
foreach (View? inst in View.Instances) foreach (View? inst in View.Instances)
{ {
Debug.Assert (inst.WasDisposed); //Debug.Assert (inst.WasDisposed);
Logging.Error ($"View instance not disposed: {inst}");
} }
View.Instances.Clear (); View.Instances.Clear ();

View File

@@ -31,6 +31,10 @@ public class Adornment : View, IDesignable
CanFocus = false; CanFocus = false;
TabStop = TabBehavior.NoStop; TabStop = TabBehavior.NoStop;
Parent = parent; Parent = parent;
// By default, Adornments have no key bindings.
KeyBindings.Clear ();
} }
/// <summary>The Parent of this Adornment (the View this Adornment surrounds).</summary> /// <summary>The Parent of this Adornment (the View this Adornment surrounds).</summary>
@@ -246,37 +250,6 @@ public class Adornment : View, IDesignable
return Thickness.Contains (outside, location); return Thickness.Contains (outside, location);
} }
/// <summary>
/// INTERNAL: Gets all Views (Subviews and Adornments) in the of <see cref="Adornment"/> hierarchcy that are at <paramref name="screenLocation"/>,
/// regardless of whether they will be drawn or see mouse events or not. Views with <see cref="View.Visible"/> set to <see langword="false"/> will not be included.
/// The list is ordered by depth. The deepest View is at the end of the list (the topmost View is at element 0).
/// </summary>
/// <param name="adornment">The root Adornment from which the search for subviews begins.</param>
/// <param name="screenLocation">The screen-relative location where the search for views is focused.</param>
/// <returns>A list of views that are located under the specified point.</returns>
internal static List<View?> GetViewsAtLocation (Adornment? adornment, in Point screenLocation)
{
List<View?> result = [];
if (adornment is null || adornment.Thickness == Thickness.Empty)
{
return result;
}
Point superViewRelativeLocation = adornment.Parent!.SuperView?.ScreenToViewport (screenLocation) ?? screenLocation;
if (adornment.Contains (superViewRelativeLocation))
{
List<View?> adornmentResult = GetViewsAtLocation (adornment as View, screenLocation);
if (adornmentResult.Count > 0)
{
result.AddRange (adornmentResult);
}
}
return result;
}
#endregion View Overrides #endregion View Overrides
/// <inheritdoc/> /// <inheritdoc/>

View File

@@ -371,7 +371,7 @@ public partial class Border
}); });
AddCommand ( AddCommand (
Command.Tab, Command.NextTabStop,
() => () =>
{ {
// BUGBUG: If an arrangeable view has only arrangeable subviews, it's not possible to activate // BUGBUG: If an arrangeable view has only arrangeable subviews, it's not possible to activate
@@ -386,7 +386,7 @@ public partial class Border
}); });
AddCommand ( AddCommand (
Command.BackTab, Command.PreviousTabStop,
() => () =>
{ {
AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop); AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop);
@@ -396,14 +396,17 @@ public partial class Border
}); });
HotKeyBindings.Add (Key.Esc, Command.Quit); HotKeyBindings.Add (Key.Esc, Command.Quit);
HotKeyBindings.Add (Application.ArrangeKey, Command.Quit); HotKeyBindings.Add (App!.Keyboard.ArrangeKey, Command.Quit);
HotKeyBindings.Add (Key.CursorUp, Command.Up); HotKeyBindings.Add (Key.CursorUp, Command.Up);
HotKeyBindings.Add (Key.CursorDown, Command.Down); HotKeyBindings.Add (Key.CursorDown, Command.Down);
HotKeyBindings.Add (Key.CursorLeft, Command.Left); HotKeyBindings.Add (Key.CursorLeft, Command.Left);
HotKeyBindings.Add (Key.CursorRight, Command.Right); HotKeyBindings.Add (Key.CursorRight, Command.Right);
HotKeyBindings.Add (Key.Tab, Command.Tab); KeyBindings.Remove (App!.Keyboard.NextTabKey);
HotKeyBindings.Add (Key.Tab.WithShift, Command.BackTab); KeyBindings.Remove (App!.Keyboard.PrevTabKey);
HotKeyBindings.Add (App!.Keyboard.NextTabKey, Command.NextTabStop);
HotKeyBindings.Add (App!.Keyboard.PrevTabKey, Command.PreviousTabStop);
} }
private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e) private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e)

View File

@@ -53,7 +53,7 @@ public class Margin : Adornment
// QUESTION: Why can't this just be the NeedsDisplay region? // QUESTION: Why can't this just be the NeedsDisplay region?
private Region? _cachedClip; private Region? _cachedClip;
internal Region? GetCachedClip () { return _cachedClip; } internal Region? GetCachedClip () => _cachedClip;
internal void ClearCachedClip () { _cachedClip = null; } internal void ClearCachedClip () { _cachedClip = null; }
@@ -67,15 +67,18 @@ public class Margin : Adornment
} }
/// <summary> /// <summary>
/// INTERNAL API - Draws the transparent margins for the specified views. This is called from <see cref="View.Draw(DrawContext)"/> on each /// INTERNAL API - Draws the transparent margins for the specified views. This is called from
/// <see cref="View.Draw(DrawContext)"/> on each
/// iteration of the main loop after all Views have been drawn. /// iteration of the main loop after all Views have been drawn.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Non-transparent margins are drawn as-normal in <see cref="View.DrawAdornments"/>. /// Non-transparent margins are drawn as-normal in <see cref="View.DrawAdornments"/>.
/// </remarks> /// </remarks>
/// <param name="views"></param> /// <param name="views"></param>
/// <returns><see langword="true"/></returns> /// <returns>
internal static bool DrawTransparentMargins (IEnumerable<View> views) /// <see langword="true"/>
/// </returns>
internal static bool DrawMargins (IEnumerable<View> views)
{ {
Stack<View> stack = new (views); Stack<View> stack = new (views);
@@ -96,7 +99,10 @@ public class Margin : Adornment
margin.ClearCachedClip (); margin.ClearCachedClip ();
} }
foreach (View subview in view.SubViews.OrderBy (v => v.HasFocus && v.ShadowStyle != ShadowStyle.None).Reverse ()) // Do not include Margin views of subviews; not supported
foreach (View subview in view.GetSubViews (false, includePadding: true, includeBorder: true)
.OrderBy (v => v.ShadowStyle != ShadowStyle.None)
.Reverse ())
{ {
stack.Push (subview); stack.Push (subview);
} }
@@ -141,17 +147,13 @@ public class Margin : Adornment
if (ShadowStyle != ShadowStyle.None) if (ShadowStyle != ShadowStyle.None)
{ {
// Don't clear where the shadow goes // Don't clear where the shadow goes
screen = Rectangle.Inflate (screen, -ShadowSize.Width, -ShadowSize.Height);
} }
return true; return true;
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override bool OnDrawingText () protected override bool OnDrawingText () => ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent);
{
return ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent);
}
#region Shadow #region Shadow
@@ -186,14 +188,23 @@ public class Margin : Adornment
if (ShadowStyle != ShadowStyle.None) if (ShadowStyle != ShadowStyle.None)
{ {
// Turn off shadow // Turn off shadow
_originalThickness = new (Thickness.Left, Thickness.Top, Math.Max (Thickness.Right - ShadowSize.Width, 0), Math.Max (Thickness.Bottom - ShadowSize.Height, 0)); _originalThickness = new (
Thickness.Left,
Thickness.Top,
Math.Max (Thickness.Right - ShadowSize.Width, 0),
Math.Max (Thickness.Bottom - ShadowSize.Height, 0));
} }
if (style != ShadowStyle.None) if (style != ShadowStyle.None)
{ {
// Turn on shadow // Turn on shadow
_isThicknessChanging = true; _isThicknessChanging = true;
Thickness = new (_originalThickness.Value.Left, _originalThickness.Value.Top, _originalThickness.Value.Right + ShadowSize.Width, _originalThickness.Value.Bottom + ShadowSize.Height);
Thickness = new (
_originalThickness.Value.Left,
_originalThickness.Value.Top,
_originalThickness.Value.Right + ShadowSize.Width,
_originalThickness.Value.Bottom + ShadowSize.Height);
_isThicknessChanging = false; _isThicknessChanging = false;
} }
@@ -279,7 +290,7 @@ public class Margin : Adornment
{ {
result = newValue; result = newValue;
bool wasValid = true; var wasValid = true;
if (newValue.Width < 0) if (newValue.Width < 0)
{ {
@@ -288,7 +299,6 @@ public class Margin : Adornment
wasValid = false; wasValid = false;
} }
if (newValue.Height < 0) if (newValue.Height < 0)
{ {
result = ShadowStyle is ShadowStyle.Opaque or ShadowStyle.Transparent ? result with { Height = 1 } : originalValue; result = ShadowStyle is ShadowStyle.Opaque or ShadowStyle.Transparent ? result with { Height = 1 } : originalValue;
@@ -301,7 +311,7 @@ public class Margin : Adornment
return false; return false;
} }
bool wasUpdated = false; var wasUpdated = false;
if ((ShadowStyle == ShadowStyle.Opaque && newValue.Width != 1) || (ShadowStyle == ShadowStyle.Transparent && newValue.Width < 1)) if ((ShadowStyle == ShadowStyle.Opaque && newValue.Width != 1) || (ShadowStyle == ShadowStyle.Transparent && newValue.Width < 1))
{ {
@@ -341,6 +351,7 @@ public class Margin : Adornment
// Note, for visual effects reasons, we only move horizontally. // Note, for visual effects reasons, we only move horizontally.
// TODO: Add a setting or flag that lets the view move vertically as well. // TODO: Add a setting or flag that lets the view move vertically as well.
_isThicknessChanging = true; _isThicknessChanging = true;
Thickness = new ( Thickness = new (
Thickness.Left - PRESS_MOVE_HORIZONTAL, Thickness.Left - PRESS_MOVE_HORIZONTAL,
Thickness.Top - PRESS_MOVE_VERTICAL, Thickness.Top - PRESS_MOVE_VERTICAL,
@@ -369,6 +380,7 @@ public class Margin : Adornment
// Note, for visual effects reasons, we only move horizontally. // Note, for visual effects reasons, we only move horizontally.
// TODO: Add a setting or flag that lets the view move vertically as well. // TODO: Add a setting or flag that lets the view move vertically as well.
_isThicknessChanging = true; _isThicknessChanging = true;
Thickness = new ( Thickness = new (
Thickness.Left + PRESS_MOVE_HORIZONTAL, Thickness.Left + PRESS_MOVE_HORIZONTAL,
Thickness.Top + PRESS_MOVE_VERTICAL, Thickness.Top + PRESS_MOVE_VERTICAL,
@@ -421,5 +433,4 @@ public class Margin : Adornment
} }
#endregion Shadow #endregion Shadow
} }

View File

@@ -1,5 +1,3 @@
namespace Terminal.Gui.ViewBase; namespace Terminal.Gui.ViewBase;
/// <summary>The Padding for a <see cref="View"/>. Accessed via <see cref="View.Padding"/></summary> /// <summary>The Padding for a <see cref="View"/>. Accessed via <see cref="View.Padding"/></summary>
@@ -10,16 +8,17 @@ public class Padding : Adornment
{ {
/// <inheritdoc/> /// <inheritdoc/>
public Padding () public Padding ()
{ /* Do nothing; A parameter-less constructor is required to support all views unit tests. */ {
/* Do nothing; A parameter-less constructor is required to support all views unit tests. */
} }
/// <inheritdoc/> /// <inheritdoc/>
public Padding (View parent) : base (parent) public Padding (View parent) : base (parent)
{ {
/* Do nothing; View.CreateAdornment requires a constructor that takes a parent */ CanFocus = true;
TabStop = TabBehavior.NoStop;
} }
/// <summary>Called when a mouse event occurs within the Padding.</summary> /// <summary>Called when a mouse event occurs within the Padding.</summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
@@ -44,6 +43,7 @@ public class Padding : Adornment
{ {
Parent.SetFocus (); Parent.SetFocus ();
Parent.SetNeedsDraw (); Parent.SetNeedsDraw ();
return mouseEvent.Handled = true; return mouseEvent.Handled = true;
} }
} }
@@ -51,4 +51,49 @@ public class Padding : Adornment
return false; return false;
} }
/// <summary>
/// Gets all SubViews of this Padding, optionally including SubViews of the Padding's Parent.
/// </summary>
/// <param name="includeMargin">
/// Ignored.
/// </param>
/// <param name="includeBorder">
/// Ignored.
/// </param>
/// <param name="includePadding">
/// If <see langword="true"/>, includes SubViews from <see cref="Padding"/>. If <see langword="false"/> (default),
/// returns only the direct SubViews
/// of this Padding.
/// </param>
/// <returns>
/// A read-only collection containing all SubViews. If <paramref name="includePadding"/> is
/// <see langword="true"/>, the collection includes SubViews from this Padding's direct SubViews as well
/// as SubViews from the Padding's Parent.
/// </returns>
/// <remarks>
/// <para>
/// This method returns a snapshot of the SubViews at the time of the call. The collection is
/// safe to iterate even if SubViews are added or removed during iteration.
/// </para>
/// <para>
/// The order of SubViews in the returned collection is:
/// <list type="number">
/// <item>Direct SubViews of this Padding</item>
/// <item>SubViews of Parent (if <paramref name="includePadding"/> is <see langword="true"/>)</item>
/// </list>
/// </para>
/// </remarks>
public override IReadOnlyCollection<View> GetSubViews (bool includeMargin = false, bool includeBorder = false, bool includePadding = false)
{
List<View> subViewsOfThisAdornment = new (base.GetSubViews (false, false, includePadding));
if (includePadding && Parent is { })
{
// Include SubViews from Parent. Since we are a Padding of Parent do not
// request Adornments again to avoid infinite recursion.
subViewsOfThisAdornment.AddRange (Parent.GetSubViews (false, false, false));
}
return subViewsOfThisAdornment;
}
} }

View File

@@ -1,5 +1,4 @@
 namespace Terminal.Gui.ViewBase;
namespace Terminal.Gui.ViewBase;
public partial class View // Adornments public partial class View // Adornments
{ {
@@ -158,7 +157,7 @@ public partial class View // Adornments
/// <summary> /// <summary>
/// Called when the <see cref="BorderStyle"/> has changed. /// Called when the <see cref="BorderStyle"/> has changed.
/// </summary> /// </summary>
protected virtual bool OnBorderStyleChanged () { return false; } protected virtual bool OnBorderStyleChanged () => false;
/// <summary> /// <summary>
/// Fired when the <see cref="BorderStyle"/> has changed. /// Fired when the <see cref="BorderStyle"/> has changed.
@@ -230,7 +229,7 @@ public partial class View // Adornments
/// <returns>A thickness that describes the sum of the Adornments' thicknesses.</returns> /// <returns>A thickness that describes the sum of the Adornments' thicknesses.</returns>
public Thickness GetAdornmentsThickness () public Thickness GetAdornmentsThickness ()
{ {
Thickness result = Thickness.Empty; var result = Thickness.Empty;
if (Margin is { }) if (Margin is { })
{ {

View File

@@ -1,5 +1,4 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
namespace Terminal.Gui.ViewBase; namespace Terminal.Gui.ViewBase;
@@ -9,14 +8,17 @@ public partial class View // Drawing APIs
/// Draws a set of peer views (views that share the same SuperView). /// Draws a set of peer views (views that share the same SuperView).
/// </summary> /// </summary>
/// <param name="views">The peer views to draw.</param> /// <param name="views">The peer views to draw.</param>
/// <param name="force">If <see langword="true"/>, <see cref="View.SetNeedsDraw()"/> will be called on each view to force it to be drawn.</param> /// <param name="force">
/// If <see langword="true"/>, <see cref="View.SetNeedsDraw()"/> will be called on each view to force
/// it to be drawn.
/// </param>
internal static void Draw (IEnumerable<View> views, bool force) internal static void Draw (IEnumerable<View> views, bool force)
{ {
// **Snapshot once** — every recursion level gets its own frozen array // **Snapshot once** — every recursion level gets its own frozen array
View [] viewsArray = views.Snapshot (); View [] viewsArray = views.Snapshot ();
// The draw context is used to track the region drawn by each view. // The draw context is used to track the region drawn by each view.
DrawContext context = new DrawContext (); var context = new DrawContext ();
foreach (View view in viewsArray) foreach (View view in viewsArray)
{ {
@@ -29,7 +31,7 @@ public partial class View // Drawing APIs
} }
// Draw Transparent margins last to ensure they are drawn on top of the content. // Draw Transparent margins last to ensure they are drawn on top of the content.
Margin.DrawTransparentMargins (viewsArray); Margin.DrawMargins (viewsArray);
// DrawMargins may have caused some views have NeedsDraw/NeedsSubViewDraw set; clear them all. // DrawMargins may have caused some views have NeedsDraw/NeedsSubViewDraw set; clear them all.
foreach (View view in viewsArray) foreach (View view in viewsArray)
@@ -42,6 +44,7 @@ public partial class View // Drawing APIs
// when peer subviews still need drawing), so we must do it here after ALL peers are processed. // when peer subviews still need drawing), so we must do it here after ALL peers are processed.
// We only clear the flag if ALL the SuperView's SubViews no longer need drawing. // We only clear the flag if ALL the SuperView's SubViews no longer need drawing.
View? lastSuperView = null; View? lastSuperView = null;
foreach (View view in viewsArray) foreach (View view in viewsArray)
{ {
if (view is not Adornment && view.SuperView is { } && view.SuperView != lastSuperView) if (view is not Adornment && view.SuperView is { } && view.SuperView != lastSuperView)
@@ -69,7 +72,8 @@ public partial class View // Drawing APIs
/// or <see cref="NeedsLayout"/> set. /// or <see cref="NeedsLayout"/> set.
/// </para> /// </para>
/// <para> /// <para>
/// See the View Drawing Deep Dive for more information: <see href="https://gui-cs.github.io/Terminal.Gui/docs/drawing.html"/>. /// See the View Drawing Deep Dive for more information:
/// <see href="https://gui-cs.github.io/Terminal.Gui/docs/drawing.html"/>.
/// </para> /// </para>
/// </remarks> /// </remarks>
public void Draw (DrawContext? context = null) public void Draw (DrawContext? context = null)
@@ -78,6 +82,7 @@ public partial class View // Drawing APIs
{ {
return; return;
} }
Region? originalClip = GetClip (); Region? originalClip = GetClip ();
// TODO: This can be further optimized by checking NeedsDraw below and only // TODO: This can be further optimized by checking NeedsDraw below and only
@@ -327,7 +332,7 @@ public partial class View // Drawing APIs
/// false (the default), this method will cause the <see cref="LineCanvas"/> be prepared to be rendered. /// false (the default), this method will cause the <see cref="LineCanvas"/> be prepared to be rendered.
/// </summary> /// </summary>
/// <returns><see langword="true"/> to stop further drawing of the Adornments.</returns> /// <returns><see langword="true"/> to stop further drawing of the Adornments.</returns>
protected virtual bool OnDrawingAdornments () { return false; } protected virtual bool OnDrawingAdornments () => false;
#endregion DrawAdornments #endregion DrawAdornments
@@ -347,6 +352,7 @@ public partial class View // Drawing APIs
{ {
// BUGBUG: We should add the Viewport to context.DrawRegion here? // BUGBUG: We should add the Viewport to context.DrawRegion here?
SetNeedsDraw (); SetNeedsDraw ();
return; return;
} }
@@ -362,7 +368,7 @@ public partial class View // Drawing APIs
/// Called when the <see cref="Viewport"/> is to be cleared. /// Called when the <see cref="Viewport"/> is to be cleared.
/// </summary> /// </summary>
/// <returns><see langword="true"/> to stop further clearing.</returns> /// <returns><see langword="true"/> to stop further clearing.</returns>
protected virtual bool OnClearingViewport () { return false; } protected virtual bool OnClearingViewport () => false;
/// <summary>Event invoked when the <see cref="Viewport"/> is to be cleared.</summary> /// <summary>Event invoked when the <see cref="Viewport"/> is to be cleared.</summary>
/// <remarks> /// <remarks>
@@ -463,13 +469,13 @@ public partial class View // Drawing APIs
/// </summary> /// </summary>
/// <param name="context">The draw context to report drawn areas to.</param> /// <param name="context">The draw context to report drawn areas to.</param>
/// <returns><see langword="true"/> to stop further drawing of <see cref="Text"/>.</returns> /// <returns><see langword="true"/> to stop further drawing of <see cref="Text"/>.</returns>
protected virtual bool OnDrawingText (DrawContext? context) { return false; } protected virtual bool OnDrawingText (DrawContext? context) => false;
/// <summary> /// <summary>
/// Called when the <see cref="Text"/> of the View is to be drawn. /// Called when the <see cref="Text"/> of the View is to be drawn.
/// </summary> /// </summary>
/// <returns><see langword="true"/> to stop further drawing of <see cref="Text"/>.</returns> /// <returns><see langword="true"/> to stop further drawing of <see cref="Text"/>.</returns>
protected virtual bool OnDrawingText () { return false; } protected virtual bool OnDrawingText () => false;
/// <summary>Raised when the <see cref="Text"/> of the View is to be drawn.</summary> /// <summary>Raised when the <see cref="Text"/> of the View is to be drawn.</summary>
/// <returns> /// <returns>
@@ -529,9 +535,7 @@ public partial class View // Drawing APIs
DrawingContent?.Invoke (this, dev); DrawingContent?.Invoke (this, dev);
if (dev.Cancel) if (dev.Cancel)
{ { }
return;
}
// No default drawing; let event handlers or overrides handle it // No default drawing; let event handlers or overrides handle it
} }
@@ -546,15 +550,20 @@ public partial class View // Drawing APIs
/// Override this method to draw custom content for your View. /// Override this method to draw custom content for your View.
/// </para> /// </para>
/// <para> /// <para>
/// <b>Transparency Support:</b> If your View has <see cref="ViewportSettings"/> with <see cref="ViewportSettingsFlags.Transparent"/> /// <b>Transparency Support:</b> If your View has <see cref="ViewportSettings"/> with
/// <see cref="ViewportSettingsFlags.Transparent"/>
/// set, you should report the exact regions you draw to via the <paramref name="context"/> parameter. This allows /// set, you should report the exact regions you draw to via the <paramref name="context"/> parameter. This allows
/// the transparency system to exclude only the drawn areas from the clip region, letting views beneath show through /// the transparency system to exclude only the drawn areas from the clip region, letting views beneath show
/// through
/// in the areas you didn't draw. /// in the areas you didn't draw.
/// </para> /// </para>
/// <para> /// <para>
/// Use <see cref="DrawContext.AddDrawnRectangle"/> for simple rectangular areas, or <see cref="DrawContext.AddDrawnRegion"/> /// Use <see cref="DrawContext.AddDrawnRectangle"/> for simple rectangular areas, or
/// for complex, non-rectangular shapes. All coordinates passed to these methods must be in <b>screen-relative coordinates</b>. /// <see cref="DrawContext.AddDrawnRegion"/>
/// Use <see cref="View.ViewportToScreen(in Rectangle)"/> or <see cref="View.ContentToScreen(in Point)"/> to convert from /// for complex, non-rectangular shapes. All coordinates passed to these methods must be in
/// <b>screen-relative coordinates</b>.
/// Use <see cref="View.ViewportToScreen(in Rectangle)"/> or <see cref="View.ContentToScreen(in Point)"/> to
/// convert from
/// viewport-relative or content-relative coordinates. /// viewport-relative or content-relative coordinates.
/// </para> /// </para>
/// <para> /// <para>
@@ -583,24 +592,31 @@ public partial class View // Drawing APIs
/// } /// }
/// </code> /// </code>
/// </remarks> /// </remarks>
protected virtual bool OnDrawingContent (DrawContext? context) { return false; } protected virtual bool OnDrawingContent (DrawContext? context) => false;
/// <summary>Raised when the View's content is to be drawn.</summary> /// <summary>Raised when the View's content is to be drawn.</summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
/// Subscribe to this event to draw custom content for the View. Use the drawing methods available on <see cref="View"/> /// Subscribe to this event to draw custom content for the View. Use the drawing methods available on
/// such as <see cref="View.AddRune(int, int, Rune)"/>, <see cref="View.AddStr(string)"/>, and <see cref="View.FillRect(Rectangle, Rune)"/>. /// <see cref="View"/>
/// such as <see cref="View.AddRune(int, int, Rune)"/>, <see cref="View.AddStr(string)"/>, and
/// <see cref="View.FillRect(Rectangle, Rune)"/>.
/// </para> /// </para>
/// <para> /// <para>
/// The event is invoked after <see cref="ClearingViewport"/> and <see cref="Text"/> have been drawn, but after <see cref="SubViews"/> have been drawn. /// The event is invoked after <see cref="ClearingViewport"/> and <see cref="Text"/> have been drawn, but after
/// <see cref="SubViews"/> have been drawn.
/// </para> /// </para>
/// <para> /// <para>
/// <b>Transparency Support:</b> If the View has <see cref="ViewportSettings"/> with <see cref="ViewportSettingsFlags.Transparent"/> /// <b>Transparency Support:</b> If the View has <see cref="ViewportSettings"/> with
/// set, use the <see cref="DrawEventArgs.DrawContext"/> to report which areas were actually drawn. This enables proper transparency /// <see cref="ViewportSettingsFlags.Transparent"/>
/// by excluding only the drawn areas from the clip region. See <see cref="DrawContext"/> for details on reporting drawn regions. /// set, use the <see cref="DrawEventArgs.DrawContext"/> to report which areas were actually drawn. This enables
/// proper transparency
/// by excluding only the drawn areas from the clip region. See <see cref="DrawContext"/> for details on reporting
/// drawn regions.
/// </para> /// </para>
/// <para> /// <para>
/// The <see cref="DrawEventArgs.NewViewport"/> property provides the view-relative rectangle describing the currently visible viewport into the View. /// The <see cref="DrawEventArgs.NewViewport"/> property provides the view-relative rectangle describing the
/// currently visible viewport into the View.
/// </para> /// </para>
/// </remarks> /// </remarks>
public event EventHandler<DrawEventArgs>? DrawingContent; public event EventHandler<DrawEventArgs>? DrawingContent;
@@ -643,13 +659,13 @@ public partial class View // Drawing APIs
/// </summary> /// </summary>
/// <param name="context">The draw context to report drawn areas to, or null if not tracking.</param> /// <param name="context">The draw context to report drawn areas to, or null if not tracking.</param>
/// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns> /// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns>
protected virtual bool OnDrawingSubViews (DrawContext? context) { return false; } protected virtual bool OnDrawingSubViews (DrawContext? context) => false;
/// <summary> /// <summary>
/// Called when the <see cref="SubViews"/> are to be drawn. /// Called when the <see cref="SubViews"/> are to be drawn.
/// </summary> /// </summary>
/// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns> /// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns>
protected virtual bool OnDrawingSubViews () { return false; } protected virtual bool OnDrawingSubViews () => false;
/// <summary>Raised when the <see cref="SubViews"/> are to be drawn.</summary> /// <summary>Raised when the <see cref="SubViews"/> are to be drawn.</summary>
/// <remarks> /// <remarks>
@@ -680,6 +696,7 @@ public partial class View // Drawing APIs
{ {
//view.SetNeedsDraw (); //view.SetNeedsDraw ();
} }
view.Draw (context); view.Draw (context);
if (view.SuperViewRendersLineCanvas) if (view.SuperViewRendersLineCanvas)
@@ -711,7 +728,7 @@ public partial class View // Drawing APIs
/// Called when the <see cref="View.LineCanvas"/> is to be rendered. See <see cref="RenderLineCanvas"/>. /// Called when the <see cref="View.LineCanvas"/> is to be rendered. See <see cref="RenderLineCanvas"/>.
/// </summary> /// </summary>
/// <returns><see langword="true"/> to stop further drawing of <see cref="LineCanvas"/>.</returns> /// <returns><see langword="true"/> to stop further drawing of <see cref="LineCanvas"/>.</returns>
protected virtual bool OnRenderingLineCanvas () { return false; } protected virtual bool OnRenderingLineCanvas () => false;
/// <summary>The canvas that any line drawing that is to be shared by subviews of this view should add lines to.</summary> /// <summary>The canvas that any line drawing that is to be shared by subviews of this view should add lines to.</summary>
/// <remarks><see cref="Border"/> adds lines to this LineCanvas.</remarks> /// <remarks><see cref="Border"/> adds lines to this LineCanvas.</remarks>
@@ -922,5 +939,4 @@ public partial class View // Drawing APIs
public event EventHandler<DrawEventArgs>? DrawComplete; public event EventHandler<DrawEventArgs>? DrawComplete;
#endregion DrawComplete #endregion DrawComplete
} }

View File

@@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace Terminal.Gui.ViewBase; namespace Terminal.Gui.ViewBase;
@@ -19,6 +18,73 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// </remarks> /// </remarks>
public IReadOnlyCollection<View> SubViews => InternalSubViews?.AsReadOnly () ?? _empty; public IReadOnlyCollection<View> SubViews => InternalSubViews?.AsReadOnly () ?? _empty;
/// <summary>
/// Gets all SubViews of this View, optionally including SubViews of the View's Adornments
/// (Margin, Border, and Padding).
/// </summary>
/// <param name="includeMargin">
/// If <see langword="true"/>, includes SubViews from <see cref="Margin"/>. If <see langword="false"/> (default),
/// returns only the direct SubViews
/// of this View.
/// </param>
/// <param name="includeBorder">
/// If <see langword="true"/>, includes SubViews from <see cref="Border"/>. If <see langword="false"/> (default),
/// returns only the direct SubViews
/// of this View.
/// </param>
/// <param name="includePadding">
/// If <see langword="true"/>, includes SubViews from <see cref="Padding"/>. If <see langword="false"/> (default),
/// returns only the direct SubViews
/// of this View.
/// </param>
/// <returns>
/// A read-only collection containing all SubViews. If <paramref name="includeMargin"/> is
/// <see langword="true"/>, the collection includes SubViews from this View's direct SubViews as well
/// as SubViews from the Margin, Border, and Padding adornments.
/// </returns>
/// <remarks>
/// <para>
/// This method returns a snapshot of the SubViews at the time of the call. The collection is
/// safe to iterate even if SubViews are added or removed during iteration.
/// </para>
/// <para>
/// The order of SubViews in the returned collection is:
/// <list type="number">
/// <item>Direct SubViews of this View</item>
/// <item>SubViews of Margin (if <paramref name="includeMargin"/> is <see langword="true"/>)</item>
/// <item>SubViews of Border (if <paramref name="includeBorder"/> is <see langword="true"/>)</item>
/// <item>SubViews of Padding (if <paramref name="includePadding"/> is <see langword="true"/>)</item>
/// </list>
/// </para>
/// </remarks>
public virtual IReadOnlyCollection<View> GetSubViews (bool includeMargin = false, bool includeBorder = false, bool includePadding = false)
{
List<View> result = [];
// Add direct SubViews
result.AddRange (InternalSubViews);
if (includeMargin && Margin is { SubViews: { Count: > 0 } } && Margin.Thickness != Thickness.Empty)
{
// Add Margin SubViews
result.AddRange (Margin.SubViews);
}
if (includeBorder && Border is { SubViews: { Count: > 0 } } && Border.Thickness != Thickness.Empty)
{
// Add Border SubViews
result.AddRange (Border.SubViews);
}
if (includePadding && Padding is { SubViews: { Count: > 0 } } && Padding.Thickness != Thickness.Empty)
{
// Add Padding SubViews
result.AddRange (Padding.SubViews);
}
return result.AsReadOnly ();
}
private View? _superView; private View? _superView;
/// <summary> /// <summary>
@@ -87,7 +153,6 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
#region AddRemove #region AddRemove
// TODO: Make this non-virtual once WizardStep is refactored to use events
/// <summary>Adds a SubView (child) to this view.</summary> /// <summary>Adds a SubView (child) to this view.</summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
@@ -117,7 +182,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// <seealso cref="SuperViewChanging"/> /// <seealso cref="SuperViewChanging"/>
/// <seealso cref="OnSuperViewChanged"/> /// <seealso cref="OnSuperViewChanged"/>
/// <seealso cref="SuperViewChanged"/> /// <seealso cref="SuperViewChanged"/>
public virtual View? Add (View? view) public View? Add (View? view)
{ {
if (view is null) if (view is null)
{ {
@@ -136,6 +201,15 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
Logging.Warning ($"{view} has already been Added to {this}."); Logging.Warning ($"{view} has already been Added to {this}.");
} }
// TODO: Add AddingSubView event
if (this is Margin)
{
if (view is not ShadowView)
{
throw new InvalidOperationException ("SubViews of Margin are not supported.");
}
}
// TODO: Make this thread safe // TODO: Make this thread safe
InternalSubViews.Add (view); InternalSubViews.Add (view);
@@ -143,6 +217,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
if (!view.SetSuperView (this)) if (!view.SetSuperView (this))
{ {
InternalSubViews.Remove (view); InternalSubViews.Remove (view);
// The change was cancelled // The change was cancelled
return null; return null;
} }
@@ -226,7 +301,6 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// </remarks> /// </remarks>
public event EventHandler<SuperViewChangedEventArgs>? SubViewAdded; public event EventHandler<SuperViewChangedEventArgs>? SubViewAdded;
// TODO: Make this non-virtual once WizardStep is refactored to use events
/// <summary>Removes a SubView added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary> /// <summary>Removes a SubView added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
/// <remarks> /// <remarks>
/// <para> /// <para>
@@ -253,7 +327,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// <seealso cref="SuperViewChanging"/> /// <seealso cref="SuperViewChanging"/>
/// <seealso cref="OnSuperViewChanged"/> /// <seealso cref="OnSuperViewChanged"/>
/// <seealso cref="SuperViewChanged"/> /// <seealso cref="SuperViewChanged"/>
public virtual View? Remove (View? view) public View? Remove (View? view)
{ {
if (view is null) if (view is null)
{ {

View File

@@ -1,6 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
namespace Terminal.Gui.ViewBase; namespace Terminal.Gui.ViewBase;
public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...) public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...)
@@ -100,9 +99,9 @@ public partial class View // Focus and cross-view navigation management (TabStop
// TabGroup is special-cased. // TabGroup is special-cased.
if (focused?.TabStop == TabBehavior.TabGroup) if (focused?.TabStop == TabBehavior.TabGroup)
{ {
if (SuperView?.GetFocusChain (direction, TabBehavior.TabGroup)?.Length > 0) if (SuperView?.GetFocusChain (direction, TabBehavior.TabGroup).Length > 0)
{ {
// Our superview has a TabGroup subview; signal we couldn't move so we nav out to it // Our superview has a TabGroup subview; signal we couldn't move so nav out to it
return false; return false;
} }
} }
@@ -244,7 +243,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// <see langword="true"/>, if the focus advance is to be cancelled, <see langword="false"/> /// <see langword="true"/>, if the focus advance is to be cancelled, <see langword="false"/>
/// otherwise. /// otherwise.
/// </returns> /// </returns>
protected virtual bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) { return false; } protected virtual bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) => false;
/// <summary> /// <summary>
/// Raised when <see cref="View.AdvanceFocus"/> is about to advance focus. /// Raised when <see cref="View.AdvanceFocus"/> is about to advance focus.
@@ -348,7 +347,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
{ {
get get
{ {
View? focused = SubViews.FirstOrDefault (v => v.HasFocus); View? focused = GetSubViews (includePadding: true).FirstOrDefault (v => v.HasFocus);
if (focused is { }) if (focused is { })
{ {
@@ -755,7 +754,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// <see langword="true"/>, if the change to <see cref="View.HasFocus"/> is to be cancelled, <see langword="false"/> /// <see langword="true"/>, if the change to <see cref="View.HasFocus"/> is to be cancelled, <see langword="false"/>
/// otherwise. /// otherwise.
/// </returns> /// </returns>
protected virtual bool OnHasFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) { return false; } protected virtual bool OnHasFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) => false;
/// <summary> /// <summary>
/// Raised when <see cref="View.HasFocus"/> is about to change. /// Raised when <see cref="View.HasFocus"/> is about to change.
@@ -841,7 +840,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
// Temporarily ensure this view can't get focus // Temporarily ensure this view can't get focus
bool prevCanFocus = _canFocus; bool prevCanFocus = _canFocus;
_canFocus = false; _canFocus = false;
bool restoredFocus = applicationFocused!.RestoreFocus (); bool restoredFocus = applicationFocused.RestoreFocus ();
_canFocus = prevCanFocus; _canFocus = prevCanFocus;
if (restoredFocus) if (restoredFocus)
@@ -922,9 +921,6 @@ public partial class View // Focus and cross-view navigation management (TabStop
return; return;
} }
// Get whatever peer has focus, if any so we can update our superview's _previouslyMostFocused
View? focusedPeer = superViewOrParent?.Focused;
// Set HasFocus false // Set HasFocus false
_hasFocus = false; _hasFocus = false;
@@ -987,7 +983,9 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// This event cannot be cancelled. /// This event cannot be cancelled.
/// </para> /// </para>
/// </remarks> /// </remarks>
#pragma warning disable CS0067 // Event is never used
public event EventHandler<HasFocusEventArgs>? HasFocusChanged; public event EventHandler<HasFocusEventArgs>? HasFocusChanged;
#pragma warning restore CS0067 // Event is never used
#endregion HasFocus #endregion HasFocus
@@ -1007,35 +1005,28 @@ public partial class View // Focus and cross-view navigation management (TabStop
if (behavior.HasValue) if (behavior.HasValue)
{ {
filteredSubViews = InternalSubViews?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true }); filteredSubViews = GetSubViews (includePadding: true)
.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true });
} }
else else
{ {
filteredSubViews = InternalSubViews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); filteredSubViews = GetSubViews (includePadding: true)
.Where (v => v is { CanFocus: true, Visible: true, Enabled: true });
} }
// How about in Adornments? if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior && Padding.Thickness != Thickness.Empty)
if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior)
{ {
filteredSubViews = filteredSubViews?.Append (Padding); filteredSubViews = filteredSubViews.Append (Padding);
} }
if (Border is { CanFocus: true, Visible: true, Enabled: true } && Border.TabStop == behavior) // Border and Margin do not participate in focus chain navigation.
{
filteredSubViews = filteredSubViews?.Append (Border);
}
if (Margin is { CanFocus: true, Visible: true, Enabled: true } && Margin.TabStop == behavior)
{
filteredSubViews = filteredSubViews?.Append (Margin);
}
if (direction == NavigationDirection.Backward) if (direction == NavigationDirection.Backward)
{ {
filteredSubViews = filteredSubViews?.Reverse (); filteredSubViews = filteredSubViews?.Reverse ();
} }
return filteredSubViews?.ToArray () ?? Array.Empty<View> (); return filteredSubViews?.ToArray () ?? [];
} }
private TabBehavior? _tabStop; private TabBehavior? _tabStop;

View File

@@ -132,7 +132,7 @@ public partial class View
private void ConfigureVerticalScrollBarEvents (ScrollBar scrollBar) private void ConfigureVerticalScrollBarEvents (ScrollBar scrollBar)
{ {
Padding!.Thickness = Padding.Thickness with { Right = scrollBar.Visible ? Padding.Thickness.Right + 1 : 0 }; Padding!.Thickness = Padding.Thickness with { Right = scrollBar.Visible ? Padding.Thickness.Right + 1 : Padding.Thickness.Right };
scrollBar.PositionChanged += (_, args) => scrollBar.PositionChanged += (_, args) =>
{ {
@@ -153,7 +153,7 @@ public partial class View
private void ConfigureHorizontalScrollBarEvents (ScrollBar scrollBar) private void ConfigureHorizontalScrollBarEvents (ScrollBar scrollBar)
{ {
Padding!.Thickness = Padding.Thickness with { Bottom = scrollBar.Visible ? Padding.Thickness.Bottom + 1 : 0 }; Padding!.Thickness = Padding.Thickness with { Bottom = scrollBar.Visible ? Padding.Thickness.Bottom + 1 : Padding.Thickness.Bottom };
scrollBar.PositionChanged += (_, args) => scrollBar.PositionChanged += (_, args) =>
{ {

View File

@@ -500,17 +500,27 @@ public partial class View : IDisposable, ISupportInitializeNotification
return; return;
} }
if (!OnTitleChanging (ref value)) if (OnTitleChanging (ref value))
{ {
string old = _title; return;
}
CancelEventArgs<string> args = new (ref _title, ref value);
TitleChanging?.Invoke (this, args);
if (args.Cancel)
{
return;
}
_title = value; _title = value;
TitleTextFormatter.Text = _title; TitleTextFormatter.Text = _title;
SetTitleTextFormatterSize (); SetTitleTextFormatterSize ();
SetHotKeyFromTitle (); SetHotKeyFromTitle ();
SetNeedsDraw (); SetNeedsDraw ();
OnTitleChanged (); OnTitleChanged ();
} TitleChanged?.Invoke (this, new (in _title));
} }
} }
@@ -524,26 +534,13 @@ public partial class View : IDisposable, ISupportInitializeNotification
1); 1);
} }
// TODO: Change this event to match the standard TG event model.
/// <summary>Called when the <see cref="View.Title"/> has been changed. Invokes the <see cref="TitleChanged"/> event.</summary>
protected void OnTitleChanged () { TitleChanged?.Invoke (this, new (in _title)); }
/// <summary> /// <summary>
/// Called before the <see cref="View.Title"/> changes. Invokes the <see cref="TitleChanging"/> event, which can /// Called before the <see cref="View.Title"/> changes. Invokes the <see cref="TitleChanging"/> event, which can
/// be cancelled. /// be cancelled.
/// </summary> /// </summary>
/// <param name="newTitle">The new <see cref="View.Title"/> to be replaced.</param> /// <param name="newTitle">The new <see cref="View.Title"/> to be replaced.</param>
/// <returns>`true` if an event handler canceled the Title change.</returns> /// <returns>`true` if an event handler canceled the Title change.</returns>
protected bool OnTitleChanging (ref string newTitle) protected virtual bool OnTitleChanging (ref string newTitle) => false;
{
CancelEventArgs<string> args = new (ref _title, ref newTitle);
TitleChanging?.Invoke (this, args);
return args.Cancel;
}
/// <summary>Raised after the <see cref="View.Title"/> has been changed.</summary>
public event EventHandler<EventArgs<string>>? TitleChanged;
/// <summary> /// <summary>
/// Raised when the <see cref="View.Title"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to `true` /// Raised when the <see cref="View.Title"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to `true`
@@ -551,6 +548,13 @@ public partial class View : IDisposable, ISupportInitializeNotification
/// </summary> /// </summary>
public event EventHandler<CancelEventArgs<string>>? TitleChanging; public event EventHandler<CancelEventArgs<string>>? TitleChanging;
/// <summary>Called when the <see cref="View.Title"/> has been changed. Invokes the <see cref="TitleChanged"/> event.</summary>
protected virtual void OnTitleChanged () { }
/// <summary>Raised after the <see cref="View.Title"/> has been changed.</summary>
public event EventHandler<EventArgs<string>>? TitleChanged;
#endregion #endregion
#if DEBUG_IDISPOSABLE #if DEBUG_IDISPOSABLE

View File

@@ -4766,6 +4766,10 @@ public class TextView : View, IDesignable
It supports word wrap and history for undo. It supports word wrap and history for undo.
"""; """;
// This enables AllViews_HasFocus_Changed_Event to pass since it requires
// tab navigation to work
AllowsTab = false;
return true; return true;
} }

View File

@@ -1,104 +1,159 @@
using System.ComponentModel;
namespace Terminal.Gui.Views; namespace Terminal.Gui.Views;
/// <summary> /// <summary>
/// Provides navigation and a user interface (UI) to collect related data across multiple steps. Each step ( /// A multistep user interface for collecting related data. Each <see cref="WizardStep"/> can host arbitrary
/// <see cref="WizardStep"/>) can host arbitrary <see cref="View"/>s, much like a <see cref="Dialog"/>. Each step also /// <see cref="View"/>s and display help text. Navigation buttons enable moving between steps.
/// 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.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The Wizard can be displayed either as a modal (pop-up) <see cref="Window"/> (like <see cref="Dialog"/>) or as /// Can be displayed as a modal (pop-up) or embedded view.
/// an embedded <see cref="View"/>.
/// </remarks> /// </remarks>
/// <example> /// <example>
/// <code> /// <code>
/// using Terminal.Gui; /// using Terminal.Gui;
/// using System.Text;
/// ///
/// Application.Init(); /// using IApplication app = Application.Create ();
/// app.Init ();
/// ///
/// var wizard = new Wizard ($"Setup Wizard"); /// using Wizard wiz = new () { Title = "Setup Wizard" };
/// ///
/// // Add 1st step /// // Add first step
/// var firstStep = new WizardStep ("End User License Agreement"); /// WizardStep firstStep = new () { Title = "License Agreement" };
/// wizard.AddStep(firstStep);
/// firstStep.NextButtonText = "Accept!"; /// firstStep.NextButtonText = "Accept!";
/// firstStep.HelpText = "This is the End User License Agreement."; /// firstStep.HelpText = "End User License Agreement text.";
/// wizard.AddStep(firstStep);
/// ///
/// // Add 2nd step /// // Add second step
/// var secondStep = new WizardStep ("Second Step"); /// WizardStep secondStep = new () { Title = "User Info" };
/// secondStep.HelpText = "Enter your information.";
/// TextField name = new () { X = 0, Width = 20 };
/// secondStep.Add (new Label { Text = "Name:" }, name);
/// wizard.AddStep (secondStep); /// wizard.AddStep (secondStep);
/// secondStep.HelpText = "This is the help text for the Second Step.";
/// var lbl = new Label () { Text = "Name:" };
/// secondStep.Add(lbl);
/// ///
/// var name = new TextField { X = Pos.Right (lbl) + 1, Width = Dim.Fill () - 1 }; /// wizard.Accepting += (_, e) =>
/// secondStep.Add(name);
///
/// wizard.Finished += (args) =>
/// { /// {
/// MessageBox.Query("Wizard", $"Finished. The Name entered is '{name.Text}'", "Ok"); /// MessageBox.Query ("Complete", $"Name: {name.Text}", "Ok");
/// Application.RequestStop(); /// e.Handled = true;
/// }; /// };
/// ///
/// Application.TopRunnable.Add (wizard); /// app.Run (wizard);
/// Application.Run ();
/// Application.Shutdown ();
/// </code> /// </code>
/// </example> /// </example>
public class Wizard : Dialog public class Wizard : Runnable, IDesignable
{ {
private readonly LinkedList<WizardStep> _steps = new ();
private WizardStep? _currentStep;
private bool _finishedPressed;
private string _wizardTitle = string.Empty; private string _wizardTitle = string.Empty;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Wizard"/> class. /// Initializes a new instance of the <see cref="Wizard"/> class, centered with automatic sizing.
/// </summary> /// </summary>
/// <remarks>
/// The Wizard will be vertically and horizontally centered in the container. After initialization use <c>X</c>,
/// <c>Y</c>, <c>Width</c>, and <c>Height</c> change size and position.
/// </remarks>
public Wizard () public Wizard ()
{ {
// TODO: LastEndRestStart will enable a "Quit" button to always appear at the far left TabStop = TabBehavior.TabGroup;
ButtonAlignment = Alignment.Start; X = Pos.Center ();
ButtonAlignmentModes |= AlignmentModes.IgnoreFirstOrLast; Y = Pos.Center ();
BorderStyle = LineStyle.Double; Width = Dim.Auto (minimumContentDim: Dim.Percent (80), maximumContentDim: Dim.Percent (90));
Height = Dim.Auto (minimumContentDim: Dim.Percent (10), maximumContentDim: Dim.Percent (90));
BackButton = new () { Text = Strings.wzBack }; SetStyle ();
BackButton = new ()
{
Text = Strings.wzBack,
X = 0,
Y = Pos.AnchorEnd ()
};
NextFinishButton = new () NextFinishButton = new ()
{ {
Text = Strings.wzFinish, Text = Strings.wzFinish,
IsDefault = true IsDefault = true,
X = Pos.AnchorEnd (),
Y = Pos.AnchorEnd ()
}; };
NextFinishButton.FrameChanged += (_, _) => { Padding!.Thickness = Padding.Thickness with { Bottom = NextFinishButton.Frame.Height }; };
// Add a horiz separator AddCommand (Command.Quit, QuitHandler);
var separator = new Line { Orientation = Orientation.Horizontal, X = -1, Y = Pos.Top (BackButton) - 1, Length = Dim.Fill (-1) }; KeyBindings.Add (Application.QuitKey, Command.Quit);
base.Add (separator); return;
AddButton (BackButton);
AddButton (NextFinishButton);
BackButton.Accepting += BackBtn_Accepting; // Add key binding for Esc when not modal - fires Cancelled event
NextFinishButton.Accepting += NextFinishBtn_Accepting; bool? QuitHandler (ICommandContext? ctx)
{
CancelEventArgs args = new ();
Cancelled?.Invoke (this, args);
IsModalChanged += Wizard_IsModalChanged; return args.Cancel;
IsRunningChanged += Wizard_IsRunningChanged; }
TitleChanged += Wizard_TitleChanged; }
SetNeedsLayout (); private void SetStyle ()
{
if (IsRunning)
{
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog);
Padding?.SetScheme (SchemeManager.GetScheme (Schemes.Base));
BorderStyle = Dialog.DefaultBorderStyle;
Arrangement |= ViewArrangement.Movable | ViewArrangement.Resizable;
base.ShadowStyle = Dialog.DefaultShadow;
}
else
{
SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Base);
Padding?.SetScheme (SchemeManager.GetScheme (Schemes.Dialog));
BorderStyle = LineStyle.Dotted;
// strip out movable and resizable
Arrangement &= ~(ViewArrangement.Movable | ViewArrangement.Resizable);
base.ShadowStyle = ShadowStyle.None;
}
}
/// <inheritdoc/>
protected override void OnTitleChanged ()
{
if (string.IsNullOrEmpty (_wizardTitle))
{
_wizardTitle = Title;
}
}
/// <inheritdoc/>
public override void EndInit ()
{
// Configure Padding
if (Padding is { })
{
// Add buttons to bottom Padding instead of using AddButton
Padding?.Add (BackButton);
Padding?.Add (NextFinishButton);
}
BackButton.Accepting += BackBtnOnAccepting;
NextFinishButton.Accepting += NextFinishBtnOnAccepting;
CurrentStep = GetFirstStep ();
base.EndInit ();
}
/// <inheritdoc/>
protected override void OnIsModalChanged (bool newIsModal)
{
SetStyle ();
base.OnIsModalChanged (newIsModal);
} }
/// <summary> /// <summary>
/// If the <see cref="CurrentStep"/> is not the first step in the wizard, this button causes the /// The Back button. Navigates to the previous step and raises <see cref="MovingBack"/>.
/// <see cref="MovingBack"/> event to be fired and the wizard moves to the previous step. /// Hidden on the first step.
/// </summary> /// </summary>
/// <remarks>Use the <see cref="MovingBack"></see> event to be notified when the user attempts to go back.</remarks>
public Button BackButton { get; } public Button BackButton { get; }
private readonly LinkedList<WizardStep> _steps = [];
private WizardStep? _currentStep;
/// <summary>Gets or sets the currently active <see cref="WizardStep"/>.</summary> /// <summary>Gets or sets the currently active <see cref="WizardStep"/>.</summary>
public WizardStep? CurrentStep public WizardStep? CurrentStep
{ {
@@ -106,113 +161,62 @@ public class Wizard : Dialog
set => GoToStep (value); set => GoToStep (value);
} }
///// <summary>
///// Determines whether the <see cref="Wizard"/> is displayed as modal pop-up or not. The default is
///// <see langword="true"/>. The Wizard will be shown with a frame and title and will behave like any
///// <see cref="Runnable"/> window. If set to <c>false</c> the Wizard will have no frame and will behave like any
///// embedded <see cref="View"/>. To use Wizard as an embedded View
///// <list type="number">
///// <item>
///// <description>Set <see cref="Modal"/> to <c>false</c>.</description>
///// </item>
///// <item>
///// <description>Add the Wizard to a containing view with <see cref="View.Add(View)"/>.</description>
///// </item>
///// </list>
///// If a non-Modal Wizard is added to the application after
///// <see cref="IApplication.Run(IRunnable, Func{Exception, bool})"/> has
///// been called the first step must be explicitly set by setting <see cref="CurrentStep"/> to
///// <see cref="GetNextStep()"/>:
///// <code>
///// wizard.CurrentStep = wizard.GetNextStep();
///// </code>
///// </summary>
//public new bool Modal
//{
// get => base.Modal;
// set
// {
// base.Modal = value;
// foreach (WizardStep step in _steps)
// {
// SizeStep (step);
// }
// if (base.Modal)
// {
// SchemeName = "Dialog";
// BorderStyle = LineStyle.Rounded;
// }
// else
// {
// CanFocus = true;
// BorderStyle = LineStyle.None;
// }
// }
//}
/// <summary> /// <summary>
/// If the <see cref="CurrentStep"/> is the last step in the wizard, this button causes the <see cref="Finished"/> /// The Next/Finish button. On the last step, raises <see cref="View.Accepting"/>.
/// event to be fired and the wizard to close. If the step is not the last step, the <see cref="MovingNext"/> event /// On other steps, raises <see cref="MovingNext"/> and navigates forward.
/// will be fired and the wizard will move next step.
/// </summary> /// </summary>
/// <remarks>
/// Use the <see cref="MovingNext"></see> and <see cref="Finished"></see> events to be notified when the user
/// attempts go to the next step or finish the wizard.
/// </remarks>
public Button NextFinishButton { get; } public Button NextFinishButton { get; }
private Size _maxStepSize = Size.Empty;
/// <summary> /// <summary>
/// Adds a step to the wizard. The Next and Back buttons navigate through the added steps in the order they were /// Adds a step to the wizard. Steps are navigated in the order added.
/// added.
/// </summary> /// </summary>
/// <param name="newStep"></param> /// <param name="newStep">The step to add.</param>
/// <remarks>The "Next..." button of the last step added will read "Finish" (unless changed from default).</remarks>
public void AddStep (WizardStep newStep) public void AddStep (WizardStep newStep)
{ {
SizeStep (newStep); newStep.EnabledChanged += (_, _) => UpdateButtonsAndTitle ();
newStep.TitleChanged += (_, _) => UpdateButtonsAndTitle ();
newStep.EnabledChanged += (s, e) => UpdateButtonsAndTitle ();
newStep.TitleChanged += (s, e) => UpdateButtonsAndTitle ();
_steps.AddLast (newStep); _steps.AddLast (newStep);
Add (newStep); Add (newStep);
// Find the step's natural size
//newStep.SuperViewRendersLineCanvas = true;
newStep.Width = Dim.Auto ();
newStep.Height = Dim.Auto ();
newStep.SetRelativeLayout (App?.Screen.Size ?? new Size (2048, 2048));
newStep.LayoutSubViews ();
_maxStepSize = new (
Math.Max (_maxStepSize.Width, newStep.Frame.Width),
Math.Max (_maxStepSize.Height, newStep.Frame.Height));
newStep.Width = Dim.Fill ();
newStep.Height = Dim.Fill ();
newStep.SetRelativeLayout (App?.Screen.Size ?? new Size (2048, 2048));
newStep.LayoutSubViews ();
Width = Dim.Auto (minimumContentDim: _maxStepSize.Width + 2);
Height = Dim.Auto (minimumContentDim: _maxStepSize.Height + NextFinishButton.Frame.Height);
UpdateButtonsAndTitle (); UpdateButtonsAndTitle ();
} }
/// <summary> /// <summary>Raised when the user cancels the wizard by pressing the Esc key.</summary>
/// Raised when the user has cancelled the <see cref="Wizard"/> by pressing the Esc key. To prevent a modal ( public event EventHandler<CancelEventArgs>? Cancelled;
/// <see cref="WizardButtonEventArgs.Cancel"/> to <c>true</c> before returning from the event handler.
/// </summary>
public event EventHandler<WizardButtonEventArgs>? Cancelled;
/// <summary> /// <summary>Returns the first enabled step.</summary>
/// Raised when the Next/Finish button in the <see cref="Wizard"/> is clicked. The Next/Finish button is always
/// the last button in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any. This event is only
/// raised if the <see cref="CurrentStep"/> is the last Step in the Wizard flow (otherwise the <see cref="Finished"/>
/// event is raised).
/// </summary>
public event EventHandler<WizardButtonEventArgs>? Finished;
/// <summary>Returns the first enabled step in the Wizard</summary>
/// <returns>The last enabled step</returns>
public WizardStep? GetFirstStep () { return _steps.FirstOrDefault (s => s.Enabled); } public WizardStep? GetFirstStep () { return _steps.FirstOrDefault (s => s.Enabled); }
/// <summary>Returns the last enabled step in the Wizard</summary> /// <summary>Returns the last enabled step.</summary>
/// <returns>The last enabled step</returns>
public WizardStep? GetLastStep () { return _steps.LastOrDefault (s => s.Enabled); } public WizardStep? GetLastStep () { return _steps.LastOrDefault (s => s.Enabled); }
/// <summary> /// <summary>
/// Returns the next enabled <see cref="WizardStep"/> after the current step. Takes into account steps which are /// Returns the next enabled step after <see cref="CurrentStep"/>, skipping disabled steps.
/// disabled. If <see cref="CurrentStep"/> is <c>null</c> returns the first enabled step.
/// </summary> /// </summary>
/// <returns> /// <returns>The next enabled step, or <c>null</c> if none exists.</returns>
/// The next step after the current step, if there is one; otherwise returns <c>null</c>, which indicates either
/// there are no enabled steps or the current step is the last enabled step.
/// </returns>
public WizardStep? GetNextStep () public WizardStep? GetNextStep ()
{ {
LinkedListNode<WizardStep>? step = null; LinkedListNode<WizardStep>? step;
if (CurrentStep is null) if (CurrentStep is null)
{ {
@@ -223,11 +227,7 @@ public class Wizard : Dialog
{ {
// Get the step after current // Get the step after current
step = _steps.Find (CurrentStep); step = _steps.Find (CurrentStep);
step = step?.Next;
if (step is { })
{
step = step.Next;
}
} }
// step now points to the potential next step // step now points to the potential next step
@@ -245,16 +245,12 @@ public class Wizard : Dialog
} }
/// <summary> /// <summary>
/// Returns the first enabled <see cref="WizardStep"/> before the current step. Takes into account steps which are /// Returns the previous enabled step before <see cref="CurrentStep"/>, skipping disabled steps.
/// disabled. If <see cref="CurrentStep"/> is <c>null</c> returns the last enabled step.
/// </summary> /// </summary>
/// <returns> /// <returns>The previous enabled step, or <c>null</c> if none exists.</returns>
/// The first step ahead of the current step, if there is one; otherwise returns <c>null</c>, which indicates
/// either there are no enabled steps or the current step is the first enabled step.
/// </returns>
public WizardStep? GetPreviousStep () public WizardStep? GetPreviousStep ()
{ {
LinkedListNode<WizardStep>? step = null; LinkedListNode<WizardStep>? step;
if (CurrentStep is null) if (CurrentStep is null)
{ {
@@ -265,11 +261,7 @@ public class Wizard : Dialog
{ {
// Get the step before current // Get the step before current
step = _steps.Find (CurrentStep); step = _steps.Find (CurrentStep);
step = step?.Previous;
if (step is { })
{
step = step.Previous;
}
} }
// step now points to the potential previous step // step now points to the potential previous step
@@ -286,188 +278,112 @@ public class Wizard : Dialog
return null; return null;
} }
/// <summary> /// <summary>Navigates to the previous enabled step.</summary>
/// Causes the wizard to move to the previous enabled step (or first step if <see cref="CurrentStep"/> is not set). /// <returns><see langword="true"/> if the transition succeeded; otherwise <see langword="false"/>.</returns>
/// If there is no previous step, does nothing.
/// </summary>
/// <returns><see langword="true"/> if the transition to the step succeeded. <see langword="false"/> if the step was not found or the operation was cancelled.</returns>
public bool GoBack () public bool GoBack ()
{ {
WizardStep? previous = GetPreviousStep (); WizardStep? previous = GetPreviousStep ();
if (previous is { }) return previous is { } && GoToStep (previous);
{
return GoToStep (previous);
} }
return false; /// <summary>Navigates to the next enabled step.</summary>
} /// <returns><see langword="true"/> if the transition succeeded; otherwise <see langword="false"/>.</returns>
/// <summary>
/// Causes the wizard to move to the next enabled step (or last step if <see cref="CurrentStep"/> is not set). If
/// there is no previous step, does nothing.
/// </summary>
/// <returns><see langword="true"/> if the transition to the step succeeded. <see langword="false"/> if the step was not found or the operation was cancelled.</returns>
public bool GoNext () public bool GoNext ()
{ {
WizardStep? nextStep = GetNextStep (); WizardStep? nextStep = GetNextStep ();
if (nextStep is { }) return nextStep is { } && GoToStep (nextStep);
{
return GoToStep (nextStep);
} }
return false; /// <summary>
} /// Raised when the user clicks the Back button. Cancel to prevent navigation.
/// </summary>
public event EventHandler<CancelEventArgs>? MovingBack;
/// <summary>Changes to the specified <see cref="WizardStep"/>.</summary> /// <summary>
/// <param name="newStep">The step to go to.</param> /// Raised when the user clicks Next on a non-final step. Cancel to prevent navigation.
/// <returns><see langword="true"/> if the transition to the step succeeded. <see langword="false"/> if the step was not found or the operation was cancelled.</returns> /// </summary>
public event EventHandler<CancelEventArgs>? MovingNext;
/// <summary>Navigates to the specified step.</summary>
/// <param name="newStep">The step to navigate to.</param>
/// <returns><see langword="true"/> if the transition succeeded; otherwise <see langword="false"/>.</returns>
public bool GoToStep (WizardStep? newStep) public bool GoToStep (WizardStep? newStep)
{ {
if (OnStepChanging (_currentStep, newStep) || newStep is { Enabled: false }) return CWPPropertyHelper.ChangeProperty (
this,
ref _currentStep,
newStep,
OnStepChanging,
StepChanging,
newValue =>
{ {
return false; ValueChangingEventArgs<WizardStep?> args = new (_currentStep, newValue);
StepChanging?.Invoke (this, args);
if (args.Handled)
{
return;
} }
// Hide all but the new step // Hide all but the new step
foreach (WizardStep step in _steps) foreach (WizardStep step in _steps)
{ {
step.Visible = step == newStep; step.Visible = step == newValue;
step.ShowHide (); step.ShowHide ();
} }
WizardStep? oldStep = _currentStep; _currentStep = newValue;
_currentStep = newStep;
UpdateButtonsAndTitle (); UpdateButtonsAndTitle ();
},
// Set focus on the contentview OnStepChanged,
newStep?.SubViews.ToArray () [0].SetFocus (); StepChanged,
out _);
if (OnStepChanged (oldStep, _currentStep))
{
// For correctness, we do this, but it's meaningless because there's nothing to cancel
return false;
} }
return true; /// <summary>Called before changing steps. Raises <see cref="StepChanging"/>.</summary>
} /// <returns><see langword="true"/> to cancel the change.</returns>
protected virtual bool OnStepChanging (ValueChangingEventArgs<WizardStep?> args) => false;
/// <summary> /// <summary>Raised before <see cref="CurrentStep"/> changes. Set <c>Handled</c> to cancel.</summary>
/// Raised when the Back button in the <see cref="Wizard"/> is clicked. The Back button is always the first button public event EventHandler<ValueChangingEventArgs<WizardStep?>>? StepChanging;
/// in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any.
/// </summary>
public event EventHandler<WizardButtonEventArgs>? MovingBack;
/// <summary> /// <summary>Called after changing steps. Raises <see cref="StepChanged"/>.</summary>
/// Raised when the Next/Finish button in the <see cref="Wizard"/> is clicked (or the user presses Enter). The protected virtual void OnStepChanged (ValueChangedEventArgs<WizardStep?> args) { }
/// Next/Finish button is always the last button in the array of Buttons passed to the <see cref="Wizard"/>
/// constructor, if any. This event is only raised if the <see cref="CurrentStep"/> is the last Step in the Wizard flow
/// (otherwise the <see cref="Finished"/> event is raised).
/// </summary>
public event EventHandler<WizardButtonEventArgs>? MovingNext;
/// <summary> /// <summary>Raised after <see cref="CurrentStep"/> changes.</summary>
/// <see cref="Wizard"/> is derived from <see cref="Dialog"/> and Dialog causes <c>Esc</c> to call public event EventHandler<ValueChangedEventArgs<WizardStep?>>? StepChanged;
/// <see cref="IApplication.RequestStop(IRunnable)"/>, closing the Dialog. Wizard overrides
/// <see cref="OnKeyDownNotHandled"/> to instead fire the <see cref="Cancelled"/> event when Wizard is being used as a private void BackBtnOnAccepting (object? sender, CommandEventArgs e)
/// non-modal.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
protected override bool OnKeyDownNotHandled (Key key)
{ {
// BUGBUG: Why is this not handled by a key binding??? CancelEventArgs args = new ();
if (!IsModal)
{
if (key == Key.Esc)
{
var args = new WizardButtonEventArgs ();
Cancelled?.Invoke (this, args);
return false;
}
}
return false;
}
/// <summary>
/// Called when the <see cref="Wizard"/> has completed transition to a new <see cref="WizardStep"/>. Fires the
/// <see cref="StepChanged"/> event.
/// </summary>
/// <param name="oldStep">The step the Wizard changed from</param>
/// <param name="newStep">The step the Wizard has changed to</param>
/// <returns>True if the change is to be cancelled.</returns>
public virtual bool OnStepChanged (WizardStep? oldStep, WizardStep? newStep)
{
var args = new StepChangeEventArgs (oldStep, newStep);
StepChanged?.Invoke (this, args);
return args.Cancel;
}
/// <summary>
/// Called when the <see cref="Wizard"/> is about to transition to another <see cref="WizardStep"/>. Fires the
/// <see cref="StepChanging"/> event.
/// </summary>
/// <param name="oldStep">The step the Wizard is about to change from</param>
/// <param name="newStep">The step the Wizard is about to change to</param>
/// <returns>True if the change is to be cancelled.</returns>
public virtual bool OnStepChanging (WizardStep? oldStep, WizardStep? newStep)
{
var args = new StepChangeEventArgs (oldStep, newStep);
StepChanging?.Invoke (this, args);
return args.Cancel;
}
/// <summary>This event is raised after the <see cref="Wizard"/> has changed the <see cref="CurrentStep"/>.</summary>
public event EventHandler<StepChangeEventArgs>? StepChanged;
/// <summary>
/// This event is raised when the current <see cref="CurrentStep"/>) is about to change. Use
/// <see cref="StepChangeEventArgs.Cancel"/> to abort the transition.
/// </summary>
public event EventHandler<StepChangeEventArgs>? StepChanging;
private void BackBtn_Accepting (object? sender, CommandEventArgs e)
{
var args = new WizardButtonEventArgs ();
MovingBack?.Invoke (this, args); MovingBack?.Invoke (this, args);
if (!args.Cancel) if (args.Cancel)
{ {
e.Handled = GoBack (); return;
}
} }
private void NextFinishBtn_Accepting (object? sender, CommandEventArgs e) e.Handled = true;
GoBack ();
}
private void NextFinishBtnOnAccepting (object? sender, CommandEventArgs e)
{ {
if (CurrentStep == GetLastStep ()) if (CurrentStep == GetLastStep ())
{ {
var args = new WizardButtonEventArgs (); if (RaiseAccepting (e.Context) is false)
Finished?.Invoke (this, args);
if (!args.Cancel)
{ {
_finishedPressed = true;
if (IsCurrentTop)
{
(sender as View)?.App?.RequestStop (this);
e.Handled = true; e.Handled = true;
} RequestStop ();
// Wizard was created as a non-modal (just added to another View).
// Do nothing
} }
} }
else else
{ {
var args = new WizardButtonEventArgs (); CancelEventArgs args = new ();
MovingNext?.Invoke (this, args); MovingNext?.Invoke (this, new ());
if (!args.Cancel) if (!args.Cancel)
{ {
@@ -476,35 +392,6 @@ public class Wizard : Dialog
} }
} }
private void SizeStep (WizardStep step)
{
if (IsModal)
{
// 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 = 0;
step.Height = Dim.Fill (
Dim.Func (
v => IsInitialized
? SubViews.First (view => view.Y.Has<PosAnchorEnd> (out _)).Frame.Height + 1
: 1)); // for button frame (+1 for lineView)
step.Width = Dim.Fill ();
}
else
{
// If we're not a modal, then we show the border around the WizardStep
step.X = step.Y = 0;
step.Height = Dim.Fill (
Dim.Func (
v => IsInitialized
? SubViews.First (view => view.Y.Has<PosAnchorEnd> (out _)).Frame.Height + 1
: 2)); // for button frame (+1 for lineView)
step.Width = Dim.Fill ();
}
}
private void UpdateButtonsAndTitle () private void UpdateButtonsAndTitle ()
{ {
if (CurrentStep is null) if (CurrentStep is null)
@@ -533,35 +420,64 @@ public class Wizard : Dialog
? CurrentStep.NextButtonText ? CurrentStep.NextButtonText
: Strings.wzNext; // "_Next..."; : Strings.wzNext; // "_Next...";
} }
SizeStep (CurrentStep);
SetNeedsLayout ();
} }
private void Wizard_IsRunningChanged (object? sender, EventArgs<bool> args) bool IDesignable.EnableForDesign ()
{ {
if (!_finishedPressed) Title = "Wizard Title";
{
var a = new WizardButtonEventArgs ();
Cancelled?.Invoke (this, a);
}
}
private void Wizard_IsModalChanged (object? sender, EventArgs<bool> args) WizardStep firstStep = new ();
{ (firstStep as IDesignable).EnableForDesign ();
if (args.Value) AddStep (firstStep);
{
CurrentStep = GetFirstStep ();
// gets the first step if CurrentStep == null
}
}
private void Wizard_TitleChanged (object? sender, EventArgs<string> e) Label schemeLabel = new ()
{ {
if (string.IsNullOrEmpty (_wizardTitle)) Title = "_Scheme:"
};
OptionSelector<Schemes> selector = new ()
{ {
_wizardTitle = e.Value; X = Pos.Right (schemeLabel) + 1,
} Title = "Select Scheme"
};
selector.ValueChanged += (_, _) =>
{
if (selector.Value is { } scheme)
{
SchemeName = SchemeManager.SchemesToSchemeName (scheme);
}
};
Label borderStyleLabel = new ()
{
Title = "_Border Style:",
X = Pos.Right (selector) + 2
};
OptionSelector<LineStyle> borderStyleSelector = new ()
{
X = Pos.Right (borderStyleLabel) + 1,
Title = "Select Border Style"
};
borderStyleSelector.ValueChanged += (_, _) =>
{
if (borderStyleSelector.Value is { } style)
{
BorderStyle = style;
}
};
WizardStep secondStep = new ()
{
Title = "Second Step",
HelpText = "This is the help text for the Second Step."
};
secondStep.Add (schemeLabel, selector, borderStyleLabel, borderStyleSelector);
AddStep (secondStep);
return true;
} }
} }

View File

@@ -1,35 +0,0 @@
#nullable disable
namespace Terminal.Gui.Views;
/// <summary><see cref="EventArgs"/> for <see cref="WizardStep"/> transition events.</summary>
public class WizardButtonEventArgs : EventArgs
{
/// <summary>Initializes a new instance of <see cref="WizardButtonEventArgs"/></summary>
public WizardButtonEventArgs () { Cancel = false; }
/// <summary>Set to true to cancel the transition to the next step.</summary>
public bool Cancel { get; set; }
}
/// <summary><see cref="EventArgs"/> for <see cref="WizardStep"/> events.</summary>
public class StepChangeEventArgs : EventArgs
{
/// <summary>Initializes a new instance of <see cref="StepChangeEventArgs"/></summary>
/// <param name="oldStep">The current <see cref="WizardStep"/>.</param>
/// <param name="newStep">The new <see cref="WizardStep"/>.</param>
public StepChangeEventArgs (WizardStep oldStep, WizardStep newStep)
{
OldStep = oldStep;
NewStep = newStep;
Cancel = false;
}
/// <summary>Event handlers can set to true before returning to cancel the step transition.</summary>
public bool Cancel { get; set; }
/// <summary>The <see cref="WizardStep"/> the <see cref="Wizard"/> is changing to or has changed to.</summary>
public WizardStep NewStep { get; }
/// <summary>The current (or previous) <see cref="WizardStep"/>.</summary>
public WizardStep OldStep { get; }
}

View File

@@ -1,29 +1,16 @@
namespace Terminal.Gui.Views; namespace Terminal.Gui.Views;
/// <summary> /// <summary>
/// Represents a basic step that is displayed in a <see cref="Wizard"/>. The <see cref="WizardStep"/> view is /// A single step in a <see cref="Wizard"/>. Can contain arbitrary <see cref="View"/>s and display help text
/// divided horizontally in two. On the left is the content view where <see cref="View"/>s can be added, On the right /// in the right <see cref="Padding"/>.
/// is the help for the step. Set <see cref="WizardStep.HelpText"/> 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 <see cref="HelpText"/> (if not
/// empty) will fill the wizard step.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// If <see cref="Button"/>s are added, do not set <see cref="Button.IsDefault"/> to true as this will conflict /// Do not set <see cref="Button.IsDefault"/> on added buttons (conflicts with Wizard navigation).
/// with the Next button of the Wizard. Subscribe to the <see cref="View.VisibleChanged"/> event to be notified when /// Use <see cref="View.VisibleChanged"/> or <see cref="Wizard.StepChanged"/> to detect when this step becomes active.
/// the step is active; see also: <see cref="Wizard.StepChanged"/>. To enable or disable a step from being shown to the /// Set <see cref="View.Enabled"/> to control whether the step is shown.
/// user, set <see cref="View.Enabled"/>.
/// </remarks> /// </remarks>
public class WizardStep : View public class WizardStep : View, IDesignable
{ {
// The contentView works like the ContentView in FrameView.
private readonly View _contentView = new ()
{
CanFocus = true,
TabStop = TabBehavior.TabStop,
Id = "WizardStep._contentView"
};
private readonly TextView _helpTextView = new () private readonly TextView _helpTextView = new ()
{ {
CanFocus = true, CanFocus = true,
@@ -31,78 +18,61 @@ public class WizardStep : View
ReadOnly = true, ReadOnly = true,
WordWrap = true, WordWrap = true,
AllowsTab = false, AllowsTab = false,
X = Pos.AnchorEnd () + 1,
Height = Dim.Fill (),
#if DEBUG
Id = "WizardStep._helpTextView" Id = "WizardStep._helpTextView"
#endif
}; };
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Wizard"/> class. /// Initializes a new instance of the <see cref="WizardStep"/> class.
/// </summary> /// </summary>
public WizardStep () public WizardStep ()
{ {
TabStop = TabBehavior.TabStop; TabStop = TabBehavior.TabStop;
CanFocus = true; CanFocus = true;
BorderStyle = LineStyle.None; Width = Dim.Fill ();
Height = Dim.Fill ();
base.Add (_contentView);
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.SetNeedsDraw ();
//};
//scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
// helpTextView.LeftColumn = scrollBar.OtherScrollBarView.Position;
// if (helpTextView.LeftColumn != scrollBar.OtherScrollBarView.Position) {
// scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn;
// }
// helpTextView.SetNeedsDraw ();
//};
//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 is { }) {
// scrollBar.OtherScrollBarView.Size = helpTextView.Maxlength;
// scrollBar.OtherScrollBarView.Position = helpTextView.LeftColumn;
// }
// scrollBar.LayoutSubViews ();
// scrollBar.Refresh ();
//};
//base.Add (scrollBar);
ShowHide ();
} }
/// <summary>Sets or gets the text for the back button. The back button will only be visible on steps after the first step.</summary> /// <inheritdoc/>
/// <remarks>The default text is "Back"</remarks> public override void EndInit ()
{
// Help text goes in the right Padding
// TODO: Enable built-in scrollbars for the help text view once TextView supports
//_helpTextView.VerticalScrollBar.AutoShow = true;
//_helpTextView.HorizontalScrollBar.AutoShow = true;
_helpTextView.Width = Dim.Func (_ => CalculateHelpPaddingWidth ());
Padding?.Add (_helpTextView);
ShowHide ();
base.EndInit ();
}
/// <summary>The text for the Back button. Defaults to "Back".</summary>
public string BackButtonText { get; set; } = string.Empty; public string BackButtonText { get; set; } = string.Empty;
/// <summary>Calculates the width for the help text padding based on the current frame width.</summary>
private int CalculateHelpPaddingWidth () => 25;
/// <inheritdoc/>
protected override void OnFrameChanged (in Rectangle frame)
{
base.OnFrameChanged (frame);
// Update padding thickness when frame changes
if (Padding is { } && _helpTextView.Text.Length > 0)
{
Padding.Thickness = Padding.Thickness with { Right = CalculateHelpPaddingWidth () };
App?.Invoke (() => Layout ());
}
}
/// <summary> /// <summary>
/// Sets or gets help text for the <see cref="WizardStep"/>.If <see cref="WizardStep.HelpText"/> is empty the help /// The help text displayed in the right <see cref="Padding"/>.
/// pane will not be visible and the content will fill the entire WizardStep. /// If empty, the right padding is hidden and content fills the entire step.
/// </summary> /// </summary>
/// <remarks>The help text is displayed using a read-only <see cref="TextView"/>.</remarks> /// <remarks>The help text is displayed using a read-only <see cref="TextView"/>.</remarks>
public string HelpText public string HelpText
@@ -111,98 +81,91 @@ public class WizardStep : View
set set
{ {
_helpTextView.Text = value; _helpTextView.Text = value;
_helpTextView.MoveHome ();
ShowHide (); ShowHide ();
SetNeedsDraw ();
} }
} }
/// <summary>Sets or gets the text for the next/finish button.</summary> /// <summary>The text for the Next/Finish button. Defaults to "Next..." or "Finish" based on position.</summary>
/// <remarks>The default text is "Next..." if the Pane is not the last pane. Otherwise it is "Finish"</remarks>
public string NextButtonText { get; set; } = string.Empty; public string NextButtonText { get; set; } = string.Empty;
/// <summary>Add the specified <see cref="View"/> to the <see cref="WizardStep"/>.</summary>
/// <param name="view"><see cref="View"/> to add to this container</param>
public override View Add (View? view)
{
_contentView.Add (view);
if (view!.CanFocus)
{
CanFocus = true;
}
ShowHide ();
return view;
}
/// <summary>Removes a <see cref="View"/> from <see cref="WizardStep"/>.</summary>
/// <remarks></remarks>
public override View? Remove (View? view)
{
SetNeedsDraw ();
View? container = view?.SuperView;
if (container == this)
{
base.Remove (view);
}
else
{
container?.Remove (view);
}
if (_contentView.InternalSubViews.Count < 1)
{
CanFocus = false;
}
ShowHide ();
return view;
}
/// <summary>Removes all <see cref="View"/>s from the <see cref="WizardStep"/>.</summary>
/// <remarks></remarks>
public override IReadOnlyCollection<View> RemoveAll ()
{
IReadOnlyCollection<View> removed = _contentView.RemoveAll ();
ShowHide ();
return removed;
}
/// <summary>Does the work to show and hide the contentView and helpView as appropriate</summary> /// <summary>Does the work to show and hide the contentView and helpView as appropriate</summary>
internal void ShowHide () internal void ShowHide ()
{ {
_contentView.Height = Dim.Fill (); // Check if views are available (might be null during disposal)
_helpTextView.Height = Dim.Height(_contentView); if (Padding is null)
_helpTextView.Width = Dim.Fill ();
if (_contentView.InternalSubViews?.Count > 0)
{ {
return;
}
if (_helpTextView.Text.Length > 0) if (_helpTextView.Text.Length > 0)
{ {
_contentView.Width = Dim.Percent (70); // Configure Padding
_helpTextView.X = Pos.Right (_contentView);
_helpTextView.Width = Dim.Fill (); Padding.CanFocus = true;
Padding.TabStop = TabBehavior.TabStop;
// Help text goes in right Padding - set thickness based on current frame width
Padding.Thickness = Padding.Thickness with { Right = CalculateHelpPaddingWidth () };
_helpTextView.Visible = true;
_helpTextView.Enabled = true;
} }
else else
{ {
_contentView.Width = Dim.Fill (); // Configure Padding
}
} Padding.CanFocus = false;
else
{ // No help text - no right padding needed
if (_helpTextView.Text.Length > 0) Padding.Thickness = Padding.Thickness with { Right = 0 };
{
_helpTextView.X = 0; _helpTextView.Visible = false;
_helpTextView.Enabled = false;
} }
// Error - no pane shown SetNeedsLayout ();
} }
_contentView.Visible = _contentView.InternalSubViews?.Count > 0; bool IDesignable.EnableForDesign ()
_helpTextView.Visible = _helpTextView.Text.Length > 0; {
Title = "Example Step";
Label label = new ()
{
Title = "_Enter Text:"
};
TextField textField = new ()
{
X = Pos.Right (label) + 1,
Width = 20
};
Add (label, textField);
label = new ()
{
Title = " _A List:",
Y = Pos.Bottom (label) + 1
};
ListView listView = new ()
{
BorderStyle = LineStyle.Dashed,
X = Pos.Right (label) + 1,
Y = Pos.Top (label),
Height = Dim.Auto (),
Width = 10,
Source = new ListWrapper<string> (["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]),
SelectedItem = 0
};
Add (label, listView);
HelpText = """
This is some help text for the WizardStep.
You can provide instructions or information to guide the user through this step of the wizard.
""";
return true;
} }
} // end of WizardStep class } // end of WizardStep class

View File

@@ -14,7 +14,7 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMissingParentheses/@EntryIndexedValue">SUGGESTION</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMissingParentheses/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeModifiersOrder/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeModifiersOrder/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNamespaceBody/@EntryIndexedValue">ERROR</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNamespaceBody/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNullCheckingPattern/@EntryIndexedValue">ERROR</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeNullCheckingPattern/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeEvident/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeEvident/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeNotEvident/@EntryIndexedValue">SUGGESTION</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeObjectCreationWhenTypeNotEvident/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeRedundantParentheses/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeRedundantParentheses/@EntryIndexedValue">WARNING</s:String>
@@ -414,12 +414,15 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Int64 x:Key="/Default/Environment/UnitTesting/ParallelProcessesCount/@EntryValue">5</s:Int64> <s:Int64 x:Key="/Default/Environment/UnitTesting/ParallelProcessesCount/@EntryValue">5</s:Int64>
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=app_002ERun/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Attribute_0020attribute/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Attribute_0020attribute/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=conhost/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=conhost/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Decscusr/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Decscusr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Depeche/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=diag/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=diag/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gainsboro/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Gainsboro/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gandolf/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Gonek/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Gonek/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Guppie/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Guppie/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=IDISPOSABLE/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=IDISPOSABLE/@EntryIndexedValue">True</s:Boolean>

View File

@@ -22,7 +22,7 @@ public class WizardTests
wizard.AddStep (step1); wizard.AddStep (step1);
var finishedFired = false; var finishedFired = false;
wizard.Finished += (s, args) => { finishedFired = true; }; wizard.Accepting += (s, args) => { finishedFired = true; };
var isRunningChangedFired = false; var isRunningChangedFired = false;
wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; }; wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; };
@@ -46,7 +46,7 @@ public class WizardTests
wizard.AddStep (step2); wizard.AddStep (step2);
finishedFired = false; finishedFired = false;
wizard.Finished += (s, args) => { finishedFired = true; }; wizard.Accepting += (s, args) => { finishedFired = true; };
isRunningChangedFired = false; isRunningChangedFired = false;
wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; }; wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; };
@@ -79,7 +79,7 @@ public class WizardTests
step1.Enabled = false; step1.Enabled = false;
finishedFired = false; finishedFired = false;
wizard.Finished += (s, args) => { finishedFired = true; }; wizard.Accepting += (s, args) => { finishedFired = true; };
isRunningChangedFired = false; isRunningChangedFired = false;
wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; }; wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; };
@@ -437,7 +437,7 @@ public class WizardTests
// this test is needed because Wizard overrides Dialog's title behavior ("Title - StepTitle") // this test is needed because Wizard overrides Dialog's title behavior ("Title - StepTitle")
public void Setting_Title_Works () public void Setting_Title_Works ()
{ {
var d = Application.Driver; IDriver d = Application.Driver;
var title = "1234"; var title = "1234";
var stepTitle = " - ABCD"; var stepTitle = " - ABCD";

View File

@@ -1,21 +1,24 @@
using Xunit.Abstractions; using UnitTests;
using Xunit.Abstractions;
namespace ViewBaseTests.Adornments; namespace ViewBaseTests.Adornments;
public class AdornmentSubViewTests () public class AdornmentSubViewTests (ITestOutputHelper output)
{ {
private readonly ITestOutputHelper _output = output;
[Fact] [Fact]
public void Setting_Thickness_Causes_Adornment_SubView_Layout () public void Setting_Thickness_Causes_Adornment_SubView_Layout ()
{ {
var view = new View (); var view = new View ();
var subView = new View (); var subView = new View ();
view.Margin!.Add (subView); view.Padding!.Add (subView);
view.BeginInit (); view.BeginInit ();
view.EndInit (); view.EndInit ();
var raised = false; var raised = false;
subView.SubViewLayout += LayoutStarted; subView.SubViewLayout += LayoutStarted;
view.Margin.Thickness = new Thickness (1, 2, 3, 4); view.Padding.Thickness = new (1, 2, 3, 4);
view.Layout (); view.Layout ();
Assert.True (raised); Assert.True (raised);
@@ -27,12 +30,12 @@ public class AdornmentSubViewTests ()
} }
[Theory] [Theory]
[InlineData (0, 0, false)] // Margin has no thickness, so false [InlineData (0, 0, false)] // Padding has no thickness, so false
[InlineData (0, 1, false)] // Margin has no thickness, so false [InlineData (0, 1, false)] // Padding has no thickness, so false
[InlineData (1, 0, true)] [InlineData (1, 0, true)]
[InlineData (1, 1, true)] [InlineData (1, 1, true)]
[InlineData (2, 1, true)] [InlineData (2, 1, true)]
public void Adornment_WithSubView_Finds (int viewMargin, int subViewMargin, bool expectedFound) public void Adornment_WithSubView_Finds (int viewPadding, int subViewPadding, bool expectedFound)
{ {
IApplication? app = Application.Create (); IApplication? app = Application.Create ();
Runnable<bool> runnable = new () Runnable<bool> runnable = new ()
@@ -42,9 +45,9 @@ public class AdornmentSubViewTests ()
}; };
app.Begin (runnable); app.Begin (runnable);
runnable.Margin!.Thickness = new Thickness (viewMargin); runnable.Padding!.Thickness = new (viewPadding);
// Turn of TransparentMouse for the test // Turn of TransparentMouse for the test
runnable.Margin!.ViewportSettings = ViewportSettingsFlags.None; runnable.Padding!.ViewportSettings = ViewportSettingsFlags.None;
var subView = new View () var subView = new View ()
{ {
@@ -53,16 +56,16 @@ public class AdornmentSubViewTests ()
Width = 5, Width = 5,
Height = 5 Height = 5
}; };
subView.Margin!.Thickness = new Thickness (subViewMargin); subView.Padding!.Thickness = new (subViewPadding);
// Turn of TransparentMouse for the test // Turn of TransparentMouse for the test
subView.Margin!.ViewportSettings = ViewportSettingsFlags.None; subView.Padding!.ViewportSettings = ViewportSettingsFlags.None;
runnable.Margin!.Add (subView); runnable.Padding!.Add (subView);
runnable.Layout (); runnable.Layout ();
var foundView = runnable.GetViewsUnderLocation (new Point (0, 0), ViewportSettingsFlags.None).LastOrDefault (); View? foundView = runnable.GetViewsUnderLocation (new (0, 0), ViewportSettingsFlags.None).LastOrDefault ();
bool found = foundView == subView || foundView == subView.Margin; bool found = foundView == subView || foundView == subView.Padding;
Assert.Equal (expectedFound, found); Assert.Equal (expectedFound, found);
} }
@@ -92,4 +95,84 @@ public class AdornmentSubViewTests ()
Assert.Equal (runnable.Padding, runnable.GetViewsUnderLocation (new Point (0, 0), ViewportSettingsFlags.None).LastOrDefault ()); Assert.Equal (runnable.Padding, runnable.GetViewsUnderLocation (new Point (0, 0), ViewportSettingsFlags.None).LastOrDefault ());
} }
[Fact]
public void Button_With_Opaque_ShadowStyle_In_Border_Should_Draw_Shadow ()
{
// Arrange
using IApplication app = Application.Create ();
app.Init ("fake");
app.Driver?.SetScreenSize (1, 4);
app.Driver!.Force16Colors = true;
using Runnable window = new ();
window.Width = Dim.Fill ();
window.Height = Dim.Fill ();
window.Text = @"XXXXXX";
window.SetScheme (new (new Attribute (Color.Black, Color.White)));
// Setup padding with some thickness so we have space for the button
window.Border!.Thickness = new (0, 3, 0, 0);
// Add a button with a transparent shadow to the Padding adornment
Button buttonInBorder = new ()
{
X = 0,
Y = 0,
Text = "B",
NoDecorations = true,
NoPadding = true,
ShadowStyle = ShadowStyle.Opaque,
};
window.Border.Add (buttonInBorder);
app.Begin (window);
DriverAssert.AssertDriverOutputIs ("""
\x1b[30m\x1b[107mB \x1b[97m\x1b[40mX
""",
_output,
app.Driver);
} }
[Fact]
public void Button_With_Opaque_ShadowStyle_In_Padding_Should_Draw_Shadow ()
{
// Arrange
using IApplication app = Application.Create ();
app.Init ("fake");
app.Driver?.SetScreenSize (1, 4);
app.Driver!.Force16Colors = true;
using Runnable window = new ();
window.Width = Dim.Fill ();
window.Height = Dim.Fill ();
window.Text = @"XXXXXX";
window.SetScheme (new (new Attribute (Color.Black, Color.White)));
// Setup padding with some thickness so we have space for the button
window.Padding!.Thickness = new (0, 3, 0, 0);
// Add a button with a transparent shadow to the Padding adornment
Button buttonInPadding = new ()
{
X = 0,
Y = 0,
Text = "B",
NoDecorations = true,
NoPadding = true,
ShadowStyle = ShadowStyle.Opaque,
};
window.Padding.Add (buttonInPadding);
app.Begin (window);
DriverAssert.AssertDriverOutputIs ("""
\x1b[97m\x1b[40mB\x1b[30m\x1b[107m \x1b[97m\x1b[40mX
""",
_output,
app.Driver);
}
}

View File

@@ -0,0 +1,25 @@
#nullable enable
using UnitTests;
using Xunit.Abstractions;
namespace ViewBaseTests.Adornments;
public class PaddingTests (ITestOutputHelper output)
{
[Fact]
public void Constructor_Defaults ()
{
View view = new () { Height = 3, Width = 3 };
Assert.True (view.Padding!.CanFocus);
Assert.Equal (TabBehavior.NoStop, view.Padding.TabStop);
Assert.Empty (view.Padding!.KeyBindings.GetBindings ());
}
[Fact]
public void Thickness_Is_Empty_By_Default ()
{
View view = new () { Height = 3, Width = 3 };
Assert.Equal (Thickness.Empty, view.Padding!.Thickness);
}
}

View File

@@ -210,7 +210,6 @@ public class GetViewsUnderLocationForRootTests
} }
[Theory] [Theory]
[InlineData ("Margin")]
[InlineData ("Border")] [InlineData ("Border")]
[InlineData ("Padding")] [InlineData ("Padding")]
public void Returns_Subview_Of_Adornment (string adornmentType) public void Returns_Subview_Of_Adornment (string adornmentType)
@@ -271,7 +270,6 @@ public class GetViewsUnderLocationForRootTests
[Theory] [Theory]
[InlineData ("Margin")]
[InlineData ("Border")] [InlineData ("Border")]
[InlineData ("Padding")] [InlineData ("Padding")]
public void Returns_OnlyParentsSuperView_Of_Adornment_If_TransparentMouse (string adornmentType) public void Returns_OnlyParentsSuperView_Of_Adornment_If_TransparentMouse (string adornmentType)

View File

@@ -0,0 +1,586 @@
namespace ViewBaseTests.Navigation;
/// <summary>
/// Tests for navigation into and out of Adornments (Padding, Border, Margin).
/// These tests prove that navigation to/from adornments is broken and need to be fixed.
/// </summary>
public class AdornmentNavigationTests
{
#region Padding Navigation Tests
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void AdvanceFocus_Into_Padding_With_Focusable_SubView ()
{
// Setup: View with a focusable subview in Padding
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Padding!.Thickness = new Thickness (1);
View paddingButton = new ()
{
Id = "paddingButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Padding.Add (paddingButton);
View contentButton = new ()
{
Id = "contentButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Add (contentButton);
view.BeginInit ();
view.EndInit ();
// Test: Advance focus should navigate to content first
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
// Expected: contentButton should have focus
// This test documents the expected behavior for navigation into padding
Assert.True (contentButton.HasFocus, "Content view should receive focus first");
Assert.False (paddingButton.HasFocus, "Padding subview should not have focus yet");
// Test: Advance focus again should go to padding
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
// Expected: paddingButton should now have focus
// This will likely FAIL, proving the bug exists
Assert.True (paddingButton.HasFocus, "Padding subview should receive focus after content");
Assert.False (contentButton.HasFocus, "Content view should no longer have focus");
view.Dispose ();
}
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void AdvanceFocus_Out_Of_Padding_To_Content ()
{
// Setup: View with focusable padding that has focus
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Padding!.Thickness = new Thickness (1);
View paddingButton = new ()
{
Id = "paddingButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Padding.Add (paddingButton);
View contentButton = new ()
{
Id = "contentButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Add (contentButton);
view.BeginInit ();
view.EndInit ();
// Set focus to padding button
paddingButton.SetFocus ();
Assert.True (paddingButton.HasFocus, "Setup: Padding button should have focus");
// Test: Advance focus should navigate from padding to content
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
// Expected: Should navigate to content
// This will likely FAIL, proving the bug exists
Assert.True (contentButton.HasFocus, "Content view should receive focus after padding");
Assert.False (paddingButton.HasFocus, "Padding button should no longer have focus");
view.Dispose ();
}
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void AdvanceFocus_Backward_Into_Padding ()
{
// Setup: View with focusable subviews in both content and padding
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Padding!.Thickness = new Thickness (1);
View paddingButton = new ()
{
Id = "paddingButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Padding.Add (paddingButton);
View contentButton = new ()
{
Id = "contentButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Add (contentButton);
view.BeginInit ();
view.EndInit ();
// Set focus to content
contentButton.SetFocus ();
Assert.True (contentButton.HasFocus, "Setup: Content button should have focus");
// Test: Advance focus backward should go to padding
view.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop);
// Expected: Should navigate to padding
// This will likely FAIL, proving the bug exists
Assert.True (paddingButton.HasFocus, "Padding button should receive focus when navigating backward");
Assert.False (contentButton.HasFocus, "Content button should no longer have focus");
view.Dispose ();
}
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void Padding_CanFocus_True_TabStop_TabStop_Should_Be_In_FocusChain ()
{
// Setup: View with focusable Padding
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Padding!.Thickness = new (1);
view.Padding.CanFocus = true;
view.Padding.TabStop = TabBehavior.TabStop;
view.BeginInit ();
view.EndInit ();
// Test: Get focus chain
View [] focusChain = view.GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop);
// Expected: Padding should be in the focus chain
// This should pass based on the GetFocusChain code
Assert.Contains (view.Padding, focusChain);
view.Dispose ();
}
#endregion
#region Border Navigation Tests
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void AdvanceFocus_Into_Border_With_Focusable_SubView ()
{
// Setup: View with a focusable subview in Border
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Border!.Thickness = new Thickness (1);
View borderButton = new ()
{
Id = "borderButton",
CanFocus = true,
TabStop = TabBehavior.TabGroup,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Border.Add (borderButton);
View contentButton = new ()
{
Id = "contentButton",
CanFocus = true,
TabStop = TabBehavior.TabGroup,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Add (contentButton);
view.BeginInit ();
view.EndInit ();
// Test: Advance focus should navigate between content and border
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup);
// Expected: One of them should have focus
var hasFocus = contentButton.HasFocus || borderButton.HasFocus;
Assert.True (hasFocus, "Either content or border button should have focus");
// Advance again
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup);
// Expected: The other one should now have focus
// This will likely FAIL, proving the bug exists
if (contentButton.HasFocus)
{
// If content has focus now, border should have had it before
Assert.False (borderButton.HasFocus, "Only one should have focus at a time");
}
else
{
Assert.True (borderButton.HasFocus, "Border should have focus if content doesn't");
}
view.Dispose ();
}
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void Border_CanFocus_True_TabStop_TabGroup_Should_NOT_Be_In_FocusChain ()
{
// Setup: View with focusable Border (default TabStop is TabGroup for Border)
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Border!.Thickness = new Thickness (1);
view.Border.CanFocus = true;
view.BeginInit ();
view.EndInit ();
// Test: Get focus chain for TabGroup
View [] focusChain = view.GetFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup);
// Expected: Border should be in the focus chain
Assert.DoesNotContain (view.Border, focusChain);
view.Dispose ();
}
#endregion
#region Margin Navigation Tests
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void Margin_CanFocus_True_Should_NOT_Be_In_FocusChain ()
{
// Setup: View with focusable Margin
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Margin!.Thickness = new Thickness (1);
view.Margin.CanFocus = true;
view.Margin.TabStop = TabBehavior.TabStop;
view.BeginInit ();
view.EndInit ();
// Test: Get focus chain
View [] focusChain = view.GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop);
// Expected: Margin should be in the focus chain
Assert.DoesNotContain (view.Margin, focusChain);
view.Dispose ();
}
#endregion
#region Mixed Scenarios
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void AdvanceFocus_Nested_Views_With_Adornment_SubViews ()
{
// Setup: Nested views where parent has adornment subviews
View parent = new ()
{
Id = "parent",
Width = 30,
Height = 30,
CanFocus = true
};
parent.Padding!.Thickness = new Thickness (2);
View parentPaddingButton = new ()
{
Id = "parentPaddingButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 8,
Height = 1
};
parent.Padding.Add (parentPaddingButton);
View child = new ()
{
Id = "child",
Width = 10,
Height = 10,
CanFocus = true,
TabStop = TabBehavior.TabStop
};
parent.Add (child);
child.Padding!.Thickness = new Thickness (1);
View childPaddingButton = new ()
{
Id = "childPaddingButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
child.Padding.Add (childPaddingButton);
parent.BeginInit ();
parent.EndInit ();
// Test: Advance focus should navigate through parent padding, child, and child padding
parent.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
// Track which views receive focus
List<string> focusedIds = new ();
// Navigate multiple times to test nested navigation (extra iteration to allow for wrapping)
for (var i = 0; i < 5; i++)
{
if (parentPaddingButton.HasFocus)
{
focusedIds.Add ("parentPaddingButton");
}
else if (child.HasFocus)
{
focusedIds.Add ("child");
}
else if (childPaddingButton.HasFocus)
{
focusedIds.Add ("childPaddingButton");
}
parent.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
}
// Expected: Navigation should reach all elements including adornment subviews
// This will likely show incomplete navigation, proving the bug exists
Assert.True (
focusedIds.Count > 0,
"At least some navigation should occur (this test documents current behavior)"
);
parent.Dispose ();
}
#endregion
#region TabGroup Behavior Tests
#endregion
#region Edge Cases
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void AdvanceFocus_Padding_With_No_Thickness_Should_Not_Participate ()
{
// Setup: View with Padding that has no thickness but has subviews
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
// Padding has default Thickness.Empty
View paddingButton = new ()
{
Id = "paddingButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Padding!.Add (paddingButton);
View contentButton = new ()
{
Id = "contentButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Add (contentButton);
view.BeginInit ();
view.EndInit ();
// Test: Navigate - should only focus content since Padding has no thickness
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
Assert.True (contentButton.HasFocus, "Content should get focus");
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
// Expected: Should wrap back to content, not go to padding
Assert.True (contentButton.HasFocus, "Should stay in content when Padding has no thickness");
Assert.False (paddingButton.HasFocus, "Padding button should not receive focus");
view.Dispose ();
}
[Fact]
[Trait ("Category", "Adornment")]
[Trait ("Category", "Navigation")]
public void AdvanceFocus_Disabled_Adornment_SubView_Should_Be_Skipped ()
{
// Setup: View with disabled subview in Padding
View view = new ()
{
Id = "view",
Width = 10,
Height = 10,
CanFocus = true
};
view.Padding!.Thickness = new Thickness (1);
View paddingButton = new ()
{
Id = "paddingButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
Enabled = false, // Disabled
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Padding.Add (paddingButton);
View contentButton = new ()
{
Id = "contentButton",
CanFocus = true,
TabStop = TabBehavior.TabStop,
X = 0,
Y = 0,
Width = 5,
Height = 1
};
view.Add (contentButton);
view.BeginInit ();
view.EndInit ();
// Test: Navigate - disabled padding button should be skipped
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
Assert.True (contentButton.HasFocus, "Content should get focus");
view.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
// Expected: Should wrap back to content, skipping disabled padding button
Assert.True (contentButton.HasFocus, "Should skip disabled padding button");
Assert.False (paddingButton.HasFocus, "Disabled padding button should not receive focus");
view.Dispose ();
}
#endregion
}

View File

@@ -26,6 +26,11 @@ public class AllViewsNavigationTests (ITestOutputHelper output) : TestsAllViews
return; return;
} }
if (view is IDesignable designable)
{
designable.EnableForDesign ();
}
IApplication app = Application.Create (); IApplication app = Application.Create ();
app.Begin (new Runnable<bool> () { CanFocus = true }); app.Begin (new Runnable<bool> () { CanFocus = true });
@@ -45,7 +50,7 @@ public class AllViewsNavigationTests (ITestOutputHelper output) : TestsAllViews
if (view.TabStop == TabBehavior.TabGroup) if (view.TabStop == TabBehavior.TabGroup)
{ {
navKeys = new [] { Key.F6, Key.F6.WithShift }; navKeys = [Key.F6, Key.F6.WithShift];
} }
var left = false; var left = false;
@@ -113,6 +118,11 @@ public class AllViewsNavigationTests (ITestOutputHelper output) : TestsAllViews
return; return;
} }
if (view is IDesignable designable)
{
designable.EnableForDesign ();
}
IApplication app = Application.Create (); IApplication app = Application.Create ();
app.Begin (new Runnable<bool> () { CanFocus = true }); app.Begin (new Runnable<bool> () { CanFocus = true });

View File

@@ -6,16 +6,14 @@ public class SubViewTests
[Fact] [Fact]
public void SuperViewChanged_Raised_On_Add () public void SuperViewChanged_Raised_On_Add ()
{ {
var super = new View { }; var super = new View ();
var sub = new View (); var sub = new View ();
int superRaisedCount = 0; var superRaisedCount = 0;
int subRaisedCount = 0; var subRaisedCount = 0;
super.SuperViewChanged += (s, e) => { superRaisedCount++; };
super.SuperViewChanged += (s, e) =>
{
superRaisedCount++;
};
sub.SuperViewChanged += (s, e) => sub.SuperViewChanged += (s, e) =>
{ {
if (sub.SuperView is { }) if (sub.SuperView is { })
@@ -34,16 +32,14 @@ public class SubViewTests
[Fact] [Fact]
public void SuperViewChanged_Raised_On_Remove () public void SuperViewChanged_Raised_On_Remove ()
{ {
var super = new View { }; var super = new View ();
var sub = new View (); var sub = new View ();
int superRaisedCount = 0; var superRaisedCount = 0;
int subRaisedCount = 0; var subRaisedCount = 0;
super.SuperViewChanged += (s, e) => { superRaisedCount++; };
super.SuperViewChanged += (s, e) =>
{
superRaisedCount++;
};
sub.SuperViewChanged += (s, e) => sub.SuperViewChanged += (s, e) =>
{ {
if (sub.SuperView is null) if (sub.SuperView is null)
@@ -95,6 +91,13 @@ public class SubViewTests
Assert.Equal (new (1, 1), view.GetContentSize ()); Assert.Equal (new (1, 1), view.GetContentSize ());
} }
[Fact]
public void Add_Margin_Throws ()
{
View view = new ();
Assert.Throws<InvalidOperationException> (() => view.Margin!.Add (new View ()));
}
[Fact] [Fact]
public void Remove_Does_Not_Impact_ContentSize () public void Remove_Does_Not_Impact_ContentSize ()
{ {
@@ -392,18 +395,12 @@ public class SubViewTests
var view = new View (); var view = new View ();
var superView = new View (); var superView = new View ();
int superViewChangedCount = 0; var superViewChangedCount = 0;
int superViewChangingCount = 0; var superViewChangingCount = 0;
view.SuperViewChanged += (s, e) => view.SuperViewChanged += (s, e) => { superViewChangedCount++; };
{
superViewChangedCount++;
};
view.SuperViewChanging += (s, e) => view.SuperViewChanging += (s, e) => { superViewChangingCount++; };
{
superViewChangingCount++;
};
// Act // Act
superView.Add (view); superView.Add (view);
@@ -411,7 +408,6 @@ public class SubViewTests
// Assert // Assert
Assert.Equal (1, superViewChangingCount); Assert.Equal (1, superViewChangingCount);
Assert.Equal (1, superViewChangedCount); Assert.Equal (1, superViewChangedCount);
} }
[Fact] [Fact]
@@ -450,7 +446,6 @@ public class SubViewTests
top2.Dispose (); top2.Dispose ();
} }
[Fact] [Fact]
public void Initialized_Event_Comparing_With_Added_Event () public void Initialized_Event_Comparing_With_Added_Event ()
{ {
@@ -563,6 +558,7 @@ public class SubViewTests
//Assert.Equal (top.Viewport.Width, svAddedTov1.Frame.Width); //Assert.Equal (top.Viewport.Width, svAddedTov1.Frame.Width);
//Assert.Equal (top.Viewport.Height, svAddedTov1.Frame.Height); //Assert.Equal (top.Viewport.Height, svAddedTov1.Frame.Height);
Assert.False (svAddedTov1.CanFocus); Assert.False (svAddedTov1.CanFocus);
//Assert.Throws<InvalidOperationException> (() => svAddedTov1.CanFocus = true); //Assert.Throws<InvalidOperationException> (() => svAddedTov1.CanFocus = true);
Assert.False (svAddedTov1.CanFocus); Assert.False (svAddedTov1.CanFocus);
}; };
@@ -639,7 +635,7 @@ public class SubViewTests
superView.Add (subView1, subView2, subView3); superView.Add (subView1, subView2, subView3);
// Act // Act
var removedViews = superView.RemoveAll (); IReadOnlyCollection<View> removedViews = superView.RemoveAll ();
// Assert // Assert
Assert.Empty (superView.SubViews); Assert.Empty (superView.SubViews);
@@ -662,7 +658,7 @@ public class SubViewTests
superView.Add (subView1, subView2, subView3, subView4); superView.Add (subView1, subView2, subView3, subView4);
// Act // Act
var removedViews = superView.RemoveAll<Button> (); IReadOnlyCollection<Button> removedViews = superView.RemoveAll<Button> ();
// Assert // Assert
Assert.Equal (3, superView.SubViews.Count); Assert.Equal (3, superView.SubViews.Count);
@@ -683,7 +679,7 @@ public class SubViewTests
superView.Add (subView1, subView2, subView3); superView.Add (subView1, subView2, subView3);
// Act // Act
var removedViews = superView.RemoveAll<Button> (); IReadOnlyCollection<Button> removedViews = superView.RemoveAll<Button> ();
// Assert // Assert
Assert.Equal (2, superView.SubViews.Count); Assert.Equal (2, superView.SubViews.Count);
@@ -700,7 +696,7 @@ public class SubViewTests
var superView = new View (); var superView = new View ();
var subView = new View (); var subView = new View ();
var events = new List<string> (); List<string> events = new ();
subView.SuperViewChanging += (s, e) => { events.Add ("SuperViewChanging"); }; subView.SuperViewChanging += (s, e) => { events.Add ("SuperViewChanging"); };
@@ -722,7 +718,7 @@ public class SubViewTests
var superView = new View (); var superView = new View ();
var subView = new View (); var subView = new View ();
View? currentValueInEvent = new View (); // Set to non-null to ensure it gets updated var currentValueInEvent = new View (); // Set to non-null to ensure it gets updated
View? newValueInEvent = null; View? newValueInEvent = null;
subView.SuperViewChanging += (s, e) => subView.SuperViewChanging += (s, e) =>
@@ -749,7 +745,7 @@ public class SubViewTests
superView.Add (subView); superView.Add (subView);
View? currentValueInEvent = null; View? currentValueInEvent = null;
View? newValueInEvent = new View (); // Set to non-null to ensure it gets updated var newValueInEvent = new View (); // Set to non-null to ensure it gets updated
subView.SuperViewChanging += (s, e) => subView.SuperViewChanging += (s, e) =>
{ {
@@ -770,7 +766,7 @@ public class SubViewTests
{ {
// Arrange // Arrange
using IApplication app = Application.Create (); using IApplication app = Application.Create ();
var runnable = new Runnable<bool> (); Runnable<bool> runnable = new ();
var subView = new View (); var subView = new View ();
runnable.Add (subView); runnable.Add (subView);
@@ -781,11 +777,11 @@ public class SubViewTests
subView.SuperViewChanging += (s, e) => subView.SuperViewChanging += (s, e) =>
{ {
Assert.NotNull (s); Assert.NotNull (s);
// At this point, SuperView is still set, so App should be accessible // At this point, SuperView is still set, so App should be accessible
appInEvent = (s as View)?.App; appInEvent = (s as View)?.App;
}; };
Assert.NotNull (runnable.App); Assert.NotNull (runnable.App);
// Act // Act
@@ -804,7 +800,7 @@ public class SubViewTests
{ {
// Arrange // Arrange
var superView = new View (); var superView = new View ();
var events = new List<string> (); List<string> events = new ();
var subView = new TestViewWithSuperViewEvents (events); var subView = new TestViewWithSuperViewEvents (events);
@@ -854,6 +850,7 @@ public class SubViewTests
protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args) protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
{ {
_events.Add ("OnSuperViewChanging"); _events.Add ("OnSuperViewChanging");
return base.OnSuperViewChanging (args); return base.OnSuperViewChanging (args);
} }
@@ -907,10 +904,7 @@ public class SubViewTests
var subView = new TestViewThatCancelsChange (); var subView = new TestViewThatCancelsChange ();
var eventRaised = false; var eventRaised = false;
subView.SuperViewChanging += (s, e) => subView.SuperViewChanging += (s, e) => { eventRaised = true; };
{
eventRaised = true;
};
// Act // Act
superView.Add (subView); superView.Add (subView);
@@ -983,4 +977,327 @@ public class SubViewTests
return true; // Always cancel the change return true; // Always cancel the change
} }
} }
#region GetSubViews Tests
[Fact]
public void GetSubViews_Returns_Empty_Collection_When_No_SubViews ()
{
// Arrange
View view = new ();
// Act
IReadOnlyCollection<View> result = view.GetSubViews ();
// Assert
Assert.NotNull (result);
Assert.Empty (result);
} }
[Fact]
public void GetSubViews_Returns_Direct_SubViews_By_Default ()
{
// Arrange
View superView = new ();
View subView1 = new () { Id = "subView1" };
View subView2 = new () { Id = "subView2" };
View subView3 = new () { Id = "subView3" };
superView.Add (subView1, subView2, subView3);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews ();
// Assert
Assert.NotNull (result);
Assert.Equal (3, result.Count);
Assert.Contains (subView1, result);
Assert.Contains (subView2, result);
Assert.Contains (subView3, result);
}
[Fact]
public void GetSubViews_Does_Not_Include_Adornment_SubViews_By_Default ()
{
// Arrange
View superView = new ();
View subView = new () { Id = "subView" };
superView.Add (subView);
superView.BeginInit ();
superView.EndInit ();
// Add a subview to the Border (e.g., ShadowView)
View borderSubView = new () { Id = "borderSubView" };
superView.Border!.Add (borderSubView);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews ();
// Assert
Assert.Single (result);
Assert.Contains (subView, result);
Assert.DoesNotContain (borderSubView, result);
}
[Fact]
public void GetSubViews_Includes_Border_SubViews_When_IncludeAdornments_Is_True ()
{
// Arrange
View superView = new ();
View subView = new () { Id = "subView" };
superView.Add (subView);
superView.BeginInit ();
superView.EndInit ();
// Add a subview to the Border
View borderSubView = new () { Id = "borderSubView" };
// Thickness matters
superView.Border!.Thickness = new (1);
superView.Border!.Add (borderSubView);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews (includeBorder: true);
// Assert
Assert.Equal (2, result.Count);
Assert.Contains (subView, result);
Assert.Contains (borderSubView, result);
}
[Fact]
public void GetSubViews_Includes_Padding_SubViews_When_IncludeAdornments_Is_True ()
{
// Arrange
View superView = new ();
View subView = new () { Id = "subView" };
superView.Add (subView);
superView.BeginInit ();
superView.EndInit ();
// Add a subview to the Padding
View paddingSubView = new () { Id = "paddingSubView" };
// Thickness matters
superView.Padding!.Thickness = new (1);
superView.Padding!.Add (paddingSubView);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews (includePadding: true);
// Assert
Assert.Equal (2, result.Count);
Assert.Contains (subView, result);
Assert.Contains (paddingSubView, result);
}
[Fact]
public void GetSubViews_Includes_All_Adornment_SubViews_When_IncludeAdornments_Is_True ()
{
// Arrange
View superView = new ();
View subView1 = new () { Id = "subView1" };
View subView2 = new () { Id = "subView2" };
superView.Add (subView1, subView2);
superView.BeginInit ();
superView.EndInit ();
// Add subviews to each adornment
View borderSubView = new () { Id = "borderSubView" };
View paddingSubView = new () { Id = "paddingSubView" };
// Thickness matters
//superView.Margin!.Thickness = new (1);
//superView.Margin!.Add (marginSubView);
superView.Border!.Thickness = new (1);
superView.Border!.Add (borderSubView);
superView.Padding!.Thickness = new (1);
superView.Padding!.Add (paddingSubView);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews (true, true, true);
// Assert
Assert.Equal (4, result.Count);
Assert.Contains (subView1, result);
Assert.Contains (subView2, result);
Assert.Contains (borderSubView, result);
Assert.Contains (paddingSubView, result);
}
[Fact]
public void GetSubViews_Returns_Correct_Order ()
{
// Arrange
View superView = new ();
View subView1 = new () { Id = "subView1" };
View subView2 = new () { Id = "subView2" };
superView.Add (subView1, subView2);
superView.BeginInit ();
superView.EndInit ();
View borderSubView = new () { Id = "borderSubView" };
View paddingSubView = new () { Id = "paddingSubView" };
// Thickness matters
superView.Border!.Thickness = new (1);
superView.Border!.Add (borderSubView);
superView.Padding!.Thickness = new (1);
superView.Padding!.Add (paddingSubView);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews (true, true, true);
List<View> resultList = result.ToList ();
// Assert - Order should be: direct SubViews, Border, Padding
Assert.Equal (4, resultList.Count);
Assert.Equal (subView1, resultList [0]);
Assert.Equal (subView2, resultList [1]);
Assert.Equal (borderSubView, resultList [2]);
Assert.Equal (paddingSubView, resultList [3]);
}
[Fact]
public void GetSubViews_Returns_Snapshot_Safe_For_Modification ()
{
// Arrange
View superView = new ();
View subView1 = new () { Id = "subView1" };
View subView2 = new () { Id = "subView2" };
superView.Add (subView1, subView2);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews ();
// Modify the SuperView's SubViews
View subView3 = new () { Id = "subView3" };
superView.Add (subView3);
// Assert - The snapshot should not include subView3
Assert.Equal (2, result.Count);
Assert.Contains (subView1, result);
Assert.Contains (subView2, result);
Assert.DoesNotContain (subView3, result);
}
[Fact]
public void GetSubViews_Multiple_SubViews_In_Each_Adornment ()
{
// Arrange
View superView = new ();
View subView = new () { Id = "subView" };
superView.Add (subView);
superView.BeginInit ();
superView.EndInit ();
// Add multiple subviews to each adornment
View borderSubView1 = new () { Id = "borderSubView1" };
View borderSubView2 = new () { Id = "borderSubView2" };
View paddingSubView1 = new () { Id = "paddingSubView1" };
View paddingSubView2 = new () { Id = "paddingSubView2" };
// Thickness matters
superView.Border!.Thickness = new (1);
superView.Border!.Add (borderSubView1, borderSubView2);
// Thickness matters
superView.Padding!.Thickness = new (1);
superView.Padding!.Add (paddingSubView1, paddingSubView2);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews (true, true, true);
// Assert
Assert.Equal (5, result.Count);
Assert.Contains (subView, result);
Assert.Contains (borderSubView1, result);
Assert.Contains (borderSubView2, result);
Assert.Contains (paddingSubView1, result);
Assert.Contains (paddingSubView2, result);
}
[Fact]
public void GetSubViews_Works_With_Adornment_Itself ()
{
// Arrange - Test that an Adornment (which is a View) can also have GetSubViews called
View view = new ();
view.BeginInit ();
view.EndInit ();
View paddingSubView = new () { Id = "paddingSubView" };
view.Padding!.Add (paddingSubView);
// Act - Call GetSubViews on the Margin itself
IReadOnlyCollection<View> result = view.Padding.GetSubViews ();
// Assert
Assert.Single (result);
Assert.Contains (paddingSubView, result);
}
[Fact]
public void GetSubViews_Handles_Null_Adornments_Gracefully ()
{
// Arrange - Create an Adornment view which doesn't have its own adornments
View view = new ();
view.BeginInit ();
view.EndInit ();
// Border is an Adornment and doesn't have Margin, Border, Padding
View borderSubView = new () { Id = "borderSubView" };
view.Border!.Add (borderSubView);
// Act - GetSubViews on Border (an Adornment) with includeAdornments
IReadOnlyCollection<View> result = view.Border.GetSubViews (true);
// Assert - Should only return direct subviews, not crash
Assert.Single (result);
Assert.Contains (borderSubView, result);
}
[Fact]
public void GetSubViews_Returns_IReadOnlyCollection ()
{
// Arrange
View superView = new ();
View subView = new () { Id = "subView" };
superView.Add (subView);
// Act
IReadOnlyCollection<View> result = superView.GetSubViews ();
// Assert
Assert.IsAssignableFrom<IReadOnlyCollection<View>> (result);
// Verify Count property is available and single item
Assert.Single (result);
}
[Fact]
public void GetSubViews_Empty_Adornments_Do_Not_Add_Nulls ()
{
// Arrange
View superView = new ();
View subView = new () { Id = "subView" };
superView.Add (subView);
superView.BeginInit ();
superView.EndInit ();
// Don't add any subviews to adornments
// Act
IReadOnlyCollection<View> result = superView.GetSubViews (true);
// Assert - Should only have the direct subview, no nulls
Assert.Single (result);
Assert.Contains (subView, result);
Assert.All (result, Assert.NotNull);
}
}
#endregion GetSubViews Tests

View File

@@ -1833,7 +1833,7 @@ public class TextViewTests
[Fact] [Fact]
public void Space_Key_Types_Space () public void Space_Key_Types_Space ()
{ {
var view = new TextView (); TextView view = new ();
view.NewKeyDownEvent (Key.Space); view.NewKeyDownEvent (Key.Space);

View File

@@ -0,0 +1,479 @@
namespace ViewsTests;
[Collection ("Global Test Setup")]
public class WizardStepTests
{
#region Constructor Tests
[Fact]
public void Constructor_Initializes_Properties ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.NotNull (step);
Assert.True (step.CanFocus);
Assert.Equal (TabBehavior.TabStop, step.TabStop);
Assert.Equal (LineStyle.None, step.BorderStyle);
Assert.IsType<DimFill> (step.Width);
Assert.IsType<DimFill> (step.Height);
}
[Fact]
public void Constructor_Sets_Default_Button_Text ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.Equal (string.Empty, step.BackButtonText);
Assert.Equal (string.Empty, step.NextButtonText);
}
#endregion Constructor Tests
#region Title Tests
[Fact]
public void Title_Can_Be_Set ()
{
// Arrange
WizardStep step = new ();
string title = "Test Step";
// Act
step.Title = title;
// Assert
Assert.Equal (title, step.Title);
}
[Fact]
public void Title_Change_Raises_TitleChanged_Event ()
{
// Arrange
WizardStep step = new ();
var eventRaised = false;
string? newTitle = null;
step.TitleChanged += (sender, args) =>
{
eventRaised = true;
newTitle = args.Value;
};
// Act
step.Title = "New Title";
// Assert
Assert.True (eventRaised);
Assert.Equal ("New Title", newTitle);
}
#endregion Title Tests
#region HelpText Tests
[Fact]
public void HelpText_Can_Be_Set ()
{
// Arrange
WizardStep step = new ();
string helpText = "This is help text";
// Act
step.HelpText = helpText;
// Assert
Assert.Equal (helpText, step.HelpText);
}
[Fact]
public void HelpText_Empty_By_Default ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.Equal (string.Empty, step.HelpText);
}
[Fact]
public void HelpText_Setting_Calls_ShowHide ()
{
// Arrange
WizardStep step = new ();
step.BeginInit ();
step.EndInit ();
// Act - Setting help text should adjust padding
step.HelpText = "Help text content";
// Assert - Padding should have right thickness when help text is present
Assert.True (step.Padding!.Thickness.Right > 0);
}
[Fact]
public void HelpText_Clearing_Removes_Padding ()
{
// Arrange
WizardStep step = new ();
step.BeginInit ();
step.EndInit ();
step.HelpText = "Help text content";
// Act - Clear help text
step.HelpText = string.Empty;
// Assert - Padding right should be 0 when help text is empty
Assert.Equal (0, step.Padding!.Thickness.Right);
}
#endregion HelpText Tests
#region BackButtonText Tests
[Fact]
public void BackButtonText_Can_Be_Set ()
{
// Arrange
WizardStep step = new ();
string buttonText = "Previous";
// Act
step.BackButtonText = buttonText;
// Assert
Assert.Equal (buttonText, step.BackButtonText);
}
[Fact]
public void BackButtonText_Empty_By_Default ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.Equal (string.Empty, step.BackButtonText);
}
#endregion BackButtonText Tests
#region NextButtonText Tests
[Fact]
public void NextButtonText_Can_Be_Set ()
{
// Arrange
WizardStep step = new ();
string buttonText = "Continue";
// Act
step.NextButtonText = buttonText;
// Assert
Assert.Equal (buttonText, step.NextButtonText);
}
[Fact]
public void NextButtonText_Empty_By_Default ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.Equal (string.Empty, step.NextButtonText);
}
#endregion NextButtonText Tests
#region SubView Tests
[Fact]
public void Can_Add_SubViews ()
{
// Arrange
WizardStep step = new ();
Label label = new () { Text = "Test Label" };
// Act
step.Add (label);
// Assert
Assert.Contains (label, step.SubViews);
}
[Fact]
public void Can_Add_Multiple_SubViews ()
{
// Arrange
WizardStep step = new ();
Label label1 = new () { Text = "Label 1" };
Label label2 = new () { Text = "Label 2" };
TextField textField = new () { Width = 10 };
// Act
step.Add (label1, label2, textField);
// Assert
Assert.Equal (3, step.SubViews.Count);
Assert.Contains (label1, step.SubViews);
Assert.Contains (label2, step.SubViews);
Assert.Contains (textField, step.SubViews);
}
#endregion SubView Tests
#region Enabled Tests
[Fact]
public void Enabled_True_By_Default ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.True (step.Enabled);
}
[Fact]
public void Enabled_Can_Be_Set_To_False ()
{
// Arrange
WizardStep step = new ();
// Act
step.Enabled = false;
// Assert
Assert.False (step.Enabled);
}
[Fact]
public void Enabled_Change_Raises_EnabledChanged_Event ()
{
// Arrange
WizardStep step = new ();
var eventRaised = false;
step.EnabledChanged += (sender, args) => { eventRaised = true; };
// Act
step.Enabled = false;
// Assert
Assert.True (eventRaised);
}
#endregion Enabled Tests
#region Visible Tests
[Fact]
public void Visible_True_By_Default ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.True (step.Visible);
}
[Fact]
public void Visible_Can_Be_Changed ()
{
// Arrange
WizardStep step = new ();
// Act
step.Visible = false;
// Assert
Assert.False (step.Visible);
}
#endregion Visible Tests
#region HelpTextView Tests
[Fact]
public void HelpTextView_Added_To_Padding ()
{
// Arrange
WizardStep step = new ();
step.BeginInit ();
step.EndInit ();
// Act
step.HelpText = "Help content";
// Assert
// The help text view should be in the Padding
Assert.True (step.Padding!.SubViews.Count > 0);
}
[Fact]
public void HelpTextView_Visible_When_HelpText_Set ()
{
// Arrange
WizardStep step = new ();
step.BeginInit ();
step.EndInit ();
// Act
step.HelpText = "Help content";
// Assert
// When help text is set, padding right should be non-zero
Assert.True (step.Padding!.Thickness.Right > 0);
}
[Fact]
public void HelpTextView_Hidden_When_HelpText_Empty ()
{
// Arrange
WizardStep step = new ();
step.BeginInit ();
step.EndInit ();
step.HelpText = "Help content";
// Act
step.HelpText = string.Empty;
// Assert
// When help text is cleared, padding right should be 0
Assert.Equal (0, step.Padding!.Thickness.Right);
}
#endregion HelpTextView Tests
#region Layout Tests
[Fact]
public void Width_Is_Fill_After_Construction ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.IsType<DimFill> (step.Width);
}
[Fact]
public void Height_Is_Fill_After_Construction ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.IsType<DimFill> (step.Height);
}
#endregion Layout Tests
#region Focus Tests
[Fact]
public void CanFocus_True_By_Default ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.True (step.CanFocus);
}
[Fact]
public void TabStop_Is_TabStop_By_Default ()
{
// Arrange & Act
WizardStep step = new ();
// Assert
Assert.Equal (TabBehavior.TabStop, step.TabStop);
}
#endregion Focus Tests
#region BorderStyle Tests
[Fact]
public void BorderStyle_Can_Be_Changed ()
{
// Arrange
WizardStep step = new ();
// Act
step.BorderStyle = LineStyle.Single;
// Assert
Assert.Equal (LineStyle.Single, step.BorderStyle);
}
#endregion BorderStyle Tests
#region Integration Tests
[Fact]
public void Step_With_HelpText_And_SubViews ()
{
// Arrange
WizardStep step = new ()
{
Title = "User Information",
HelpText = "Please enter your details"
};
Label nameLabel = new () { Text = "Name:" };
TextField nameField = new () { X = Pos.Right (nameLabel) + 1, Width = 20 };
step.Add (nameLabel, nameField);
step.BeginInit ();
step.EndInit ();
// Assert
Assert.Equal ("User Information", step.Title);
Assert.Equal ("Please enter your details", step.HelpText);
// SubViews includes the views we added
Assert.Contains (nameLabel, step.SubViews);
Assert.Contains (nameField, step.SubViews);
Assert.True (step.Padding!.Thickness.Right > 0);
}
[Fact]
public void Step_With_Custom_Button_Text ()
{
// Arrange & Act
WizardStep step = new ()
{
Title = "Confirmation",
BackButtonText = "Go Back",
NextButtonText = "Accept"
};
// Assert
Assert.Equal ("Confirmation", step.Title);
Assert.Equal ("Go Back", step.BackButtonText);
Assert.Equal ("Accept", step.NextButtonText);
}
[Fact]
public void Disabled_Step_Maintains_Properties ()
{
// Arrange
WizardStep step = new ()
{
Title = "Optional Step",
HelpText = "This step is optional",
Enabled = false
};
// Assert
Assert.Equal ("Optional Step", step.Title);
Assert.Equal ("This step is optional", step.HelpText);
Assert.False (step.Enabled);
}
#endregion Integration Tests
}

File diff suppressed because it is too large Load Diff