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;
@@ -10,12 +13,11 @@ public class Adornments : Scenario
public override void Main ()
{
Application.Init ();
using IApplication app = Application.Instance;
Window appWindow = new ()
{
Title = GetQuitKeyAndName (),
BorderStyle = LineStyle.None
};
using Window appWindow = new ();
appWindow.Title = GetQuitKeyAndName ();
appWindow.BorderStyle = LineStyle.None;
var editor = new AdornmentsEditor
{
@@ -31,7 +33,7 @@ public class Adornments : Scenario
appWindow.Add (editor);
var window = new Window
Window window = new ()
{
Title = "The _Window",
Arrangement = ViewArrangement.Overlapped | ViewArrangement.Movable,
@@ -41,29 +43,29 @@ public class Adornments : Scenario
};
appWindow.Add (window);
var tf1 = new TextField { Width = 10, Text = "TextField" };
var color = new ColorPicker16 { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () };
TextField tf1 = new () { Width = 10, Text = "TextField" };
ColorPicker16 color = new () { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd () };
color.BorderStyle = LineStyle.RoundedDotted;
color.ColorChanged += (s, e) =>
color.ColorChanged += (_, e) =>
{
color.SuperView!.SetScheme (
new (color.SuperView.GetScheme ())
{
Normal = new (
color.SuperView.GetAttributeForRole (VisualRole.Normal).Foreground,
e.Result,
color.SuperView.GetAttributeForRole (VisualRole.Normal).Style
)
});
new (color.SuperView.GetScheme ())
{
Normal = new (
color.SuperView.GetAttributeForRole (VisualRole.Normal).Foreground,
e.Result,
color.SuperView.GetAttributeForRole (VisualRole.Normal).Style
)
});
};
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) =>
MessageBox.Query (appWindow.App, 20, 7, "Hi", $"Am I a {window.GetType ().Name}?", "Yes", "No");
button.Accepting += (_, _) =>
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 (),
Y = Pos.Bottom (button),
@@ -74,9 +76,9 @@ public class Adornments : Scenario
};
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 (),
Width = 40,
@@ -87,68 +89,76 @@ public class Adornments : Scenario
window.Margin!.Data = "Margin";
window.Margin!.Text = "Margin Text";
window.Margin!.Thickness = new (0);
window.Margin!.Thickness = new (3);
window.Border!.Data = "Border";
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.Thickness = new (3);
window.Padding.SchemeName = "Error";
window.Padding.Thickness = new (4);
window.Padding!.SetScheme (SchemeManager.GetScheme (Schemes.Menu));
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."
};
longLabel.TextFormatter.WordWrap = true;
window.Add (tf1, color, button, label, btnButtonInWindow, labelAnchorEnd, longLabel);
window.Initialized += (s, e) =>
window.Initialized += (_, _) =>
{
editor.ViewToEdit = window;
editor.ShowViewIdentifier = true;
var labelInPadding = new Label { X = 0, Y = 1, Title = "_Text:" };
window.Padding.Add (labelInPadding);
// NOTE: Adding SubViews to Margin is not supported
var textFieldInPadding = new TextField
{
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
Button btnButtonInBorder = new ()
{
X = Pos.Center (),
Y = 1,
Text = "_Button in Padding Y = 1",
CanFocus = true,
HighlightStates = MouseState.None,
Text = "_Button in Border Y = 1"
};
btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (appWindow.App, 20, 7, "Hi", "Button in Padding Pressed!", "Ok");
btnButtonInPadding.BorderStyle = LineStyle.Dashed;
btnButtonInPadding.Border!.Thickness = new (1, 1, 1, 1);
btnButtonInBorder.Accepting += (_, _) => MessageBox.Query (appWindow.App!, 20, 7, "Hi", "Button in Border Pressed!", "Ok");
window.Border.Add (btnButtonInBorder);
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);
#if SUBVIEW_BASED_BORDER
btnButtonInPadding.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");
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
};
@@ -156,9 +166,6 @@ public class Adornments : Scenario
editor.AutoSelectSuperView = window;
editor.AutoSelectAdornments = true;
Application.Run (appWindow);
appWindow.Dispose ();
Application.Shutdown ();
app.Run (appWindow);
}
}

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")]
[ScenarioCategory ("Dialogs")]
@@ -6,69 +11,37 @@
[ScenarioCategory ("Runnable")]
public class Wizards : Scenario
{
private Wizard? _wizard;
private View? _actionLabel;
private TextField? _titleEdit;
public override void Main ()
{
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 (),
Y = 0,
Width = Dim.Percent (75),
SchemeName = "Base",
Height = Dim.Auto (),
Title = "Wizard Options"
};
win.Add (frame);
win.Add (settingsFrame);
var label = new Label { X = 0, Y = 0, TextAlignment = Alignment.End, Text = "_Width:", Width = 10 };
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 ()
Label label = new ()
{
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,
Text = "_Title:"
};
frame.Add (label);
settingsFrame.Add (label);
var titleEdit = new TextField
_titleEdit = new ()
{
X = Pos.Right (label) + 1,
Y = Pos.Top (label),
@@ -76,15 +49,26 @@ public class Wizards : Scenario
Height = 1,
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;
win.IsModalChanged -= Win_Loaded;
}
Title = "_Run Wizard as a modal",
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 ()
{
@@ -92,295 +76,104 @@ public class Wizards : Scenario
};
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);
}
cbRun.CheckedStateChanged += (_, a) =>
{
if (a.Value == CheckState.Checked)
{
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);
}
};
showWizardButton.Accepting += (s, e) =>
{
try
{
var width = 0;
int.TryParse (widthEdit.Text, out width);
var height = 0;
int.TryParse (heightEdit.Text, out height);
showWizardButton.Accepting += (_, _) =>
{
_wizard = CreateWizard ();
app.Run (_wizard);
_wizard.Dispose ();
};
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;
var wizard = new Wizard { Title = titleEdit.Text, Width = width, Height = height };
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;
actionLabel.Text = "Finished";
};
wizard.Cancelled += (s, args) =>
{
//args.Cancel = true;
actionLabel.Text = "Cancelled";
};
// Add 1st step
var firstStep = new WizardStep { Title = "End User License Agreement" };
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.";
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 ();
app.Run (win);
}
private void Wizard_StepChanged (object sender, StepChangeEventArgs e) { throw new NotImplementedException (); }
private Wizard CreateWizard ()
{
Wizard wizard = new ();
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) =>
{
_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;
};
((IDesignable)wizard).EnableForDesign ();
return wizard;
}
}

View File

@@ -663,7 +663,8 @@ public class UICatalog
// 'app' closed cleanly.
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 File

@@ -31,6 +31,10 @@ public class Adornment : View, IDesignable
CanFocus = false;
TabStop = TabBehavior.NoStop;
Parent = parent;
// By default, Adornments have no key bindings.
KeyBindings.Clear ();
}
/// <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);
}
/// <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
/// <inheritdoc/>

View File

@@ -371,7 +371,7 @@ public partial class Border
});
AddCommand (
Command.Tab,
Command.NextTabStop,
() =>
{
// BUGBUG: If an arrangeable view has only arrangeable subviews, it's not possible to activate
@@ -386,7 +386,7 @@ public partial class Border
});
AddCommand (
Command.BackTab,
Command.PreviousTabStop,
() =>
{
AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop);
@@ -396,14 +396,17 @@ public partial class Border
});
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.CursorDown, Command.Down);
HotKeyBindings.Add (Key.CursorLeft, Command.Left);
HotKeyBindings.Add (Key.CursorRight, Command.Right);
HotKeyBindings.Add (Key.Tab, Command.Tab);
HotKeyBindings.Add (Key.Tab.WithShift, Command.BackTab);
KeyBindings.Remove (App!.Keyboard.NextTabKey);
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)

View File

@@ -53,7 +53,7 @@ public class Margin : Adornment
// QUESTION: Why can't this just be the NeedsDisplay region?
private Region? _cachedClip;
internal Region? GetCachedClip () { return _cachedClip; }
internal Region? GetCachedClip () => _cachedClip;
internal void ClearCachedClip () { _cachedClip = null; }
@@ -67,15 +67,18 @@ public class Margin : Adornment
}
/// <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.
/// </summary>
/// <remarks>
/// Non-transparent margins are drawn as-normal in <see cref="View.DrawAdornments"/>.
/// </remarks>
/// <param name="views"></param>
/// <returns><see langword="true"/></returns>
internal static bool DrawTransparentMargins (IEnumerable<View> views)
/// <returns>
/// <see langword="true"/>
/// </returns>
internal static bool DrawMargins (IEnumerable<View> views)
{
Stack<View> stack = new (views);
@@ -96,7 +99,10 @@ public class Margin : Adornment
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);
}
@@ -141,17 +147,13 @@ public class Margin : Adornment
if (ShadowStyle != ShadowStyle.None)
{
// Don't clear where the shadow goes
screen = Rectangle.Inflate (screen, -ShadowSize.Width, -ShadowSize.Height);
}
return true;
}
/// <inheritdoc />
protected override bool OnDrawingText ()
{
return ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent);
}
/// <inheritdoc/>
protected override bool OnDrawingText () => ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent);
#region Shadow
@@ -186,14 +188,23 @@ public class Margin : Adornment
if (ShadowStyle != ShadowStyle.None)
{
// 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)
{
// Turn on shadow
_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;
}
@@ -279,7 +290,7 @@ public class Margin : Adornment
{
result = newValue;
bool wasValid = true;
var wasValid = true;
if (newValue.Width < 0)
{
@@ -288,7 +299,6 @@ public class Margin : Adornment
wasValid = false;
}
if (newValue.Height < 0)
{
result = ShadowStyle is ShadowStyle.Opaque or ShadowStyle.Transparent ? result with { Height = 1 } : originalValue;
@@ -301,7 +311,7 @@ public class Margin : Adornment
return false;
}
bool wasUpdated = false;
var wasUpdated = false;
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.
// TODO: Add a setting or flag that lets the view move vertically as well.
_isThicknessChanging = true;
Thickness = new (
Thickness.Left - PRESS_MOVE_HORIZONTAL,
Thickness.Top - PRESS_MOVE_VERTICAL,
@@ -369,6 +380,7 @@ public class Margin : Adornment
// Note, for visual effects reasons, we only move horizontally.
// TODO: Add a setting or flag that lets the view move vertically as well.
_isThicknessChanging = true;
Thickness = new (
Thickness.Left + PRESS_MOVE_HORIZONTAL,
Thickness.Top + PRESS_MOVE_VERTICAL,
@@ -421,5 +433,4 @@ public class Margin : Adornment
}
#endregion Shadow
}

View File

@@ -1,5 +1,3 @@
namespace Terminal.Gui.ViewBase;
/// <summary>The Padding for a <see cref="View"/>. Accessed via <see cref="View.Padding"/></summary>
@@ -10,24 +8,25 @@ public class Padding : Adornment
{
/// <inheritdoc/>
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/>
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>
/// <remarks>
/// <para>
/// The coordinates are relative to <see cref="View.Viewport"/>.
/// </para>
/// <para>
/// A mouse click on the Padding will cause the Parent to focus.
/// </para>
/// <para>
/// The coordinates are relative to <see cref="View.Viewport"/>.
/// </para>
/// <para>
/// A mouse click on the Padding will cause the Parent to focus.
/// </para>
/// </remarks>
/// <param name="mouseEvent"></param>
/// <returns><see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
@@ -44,6 +43,7 @@ public class Padding : Adornment
{
Parent.SetFocus ();
Parent.SetNeedsDraw ();
return mouseEvent.Handled = true;
}
}
@@ -51,4 +51,49 @@ public class Padding : Adornment
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
{
@@ -158,7 +157,7 @@ public partial class View // Adornments
/// <summary>
/// Called when the <see cref="BorderStyle"/> has changed.
/// </summary>
protected virtual bool OnBorderStyleChanged () { return false; }
protected virtual bool OnBorderStyleChanged () => false;
/// <summary>
/// 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>
public Thickness GetAdornmentsThickness ()
{
Thickness result = Thickness.Empty;
var result = Thickness.Empty;
if (Margin is { })
{

View File

@@ -1,5 +1,4 @@
using System.ComponentModel;
using System.Diagnostics;
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).
/// </summary>
/// <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)
{
// **Snapshot once** — every recursion level gets its own frozen array
View [] viewsArray = views.Snapshot ();
// 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)
{
@@ -29,7 +31,7 @@ public partial class View // Drawing APIs
}
// 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.
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.
// We only clear the flag if ALL the SuperView's SubViews no longer need drawing.
View? lastSuperView = null;
foreach (View view in viewsArray)
{
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.
/// </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>
/// </remarks>
public void Draw (DrawContext? context = null)
@@ -78,6 +82,7 @@ public partial class View // Drawing APIs
{
return;
}
Region? originalClip = GetClip ();
// TODO: This can be further optimized by checking NeedsDraw below and only
@@ -281,7 +286,7 @@ public partial class View // Drawing APIs
{
// Only draw Margin here if it is not Transparent. Transparent Margins are drawn in a separate pass in the static View.Draw
// via Margin.DrawTransparentMargins.
if (Margin is { } && !Margin.ViewportSettings.HasFlag(ViewportSettingsFlags.Transparent) && Margin.Thickness != Thickness.Empty)
if (Margin is { } && !Margin.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) && Margin.Thickness != Thickness.Empty)
{
Margin?.Draw ();
}
@@ -298,7 +303,7 @@ public partial class View // Drawing APIs
Padding?.Draw ();
}
if (Margin is { } && Margin.Thickness != Thickness.Empty/* && Margin.ShadowStyle == ShadowStyle.None*/)
if (Margin is { } && Margin.Thickness != Thickness.Empty /* && Margin.ShadowStyle == ShadowStyle.None*/)
{
//Margin?.Draw ();
}
@@ -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.
/// </summary>
/// <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
@@ -347,6 +352,7 @@ public partial class View // Drawing APIs
{
// BUGBUG: We should add the Viewport to context.DrawRegion here?
SetNeedsDraw ();
return;
}
@@ -362,7 +368,7 @@ public partial class View // Drawing APIs
/// Called when the <see cref="Viewport"/> is to be cleared.
/// </summary>
/// <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>
/// <remarks>
@@ -463,13 +469,13 @@ public partial class View // Drawing APIs
/// </summary>
/// <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>
protected virtual bool OnDrawingText (DrawContext? context) { return false; }
protected virtual bool OnDrawingText (DrawContext? context) => false;
/// <summary>
/// Called when the <see cref="Text"/> of the View is to be drawn.
/// </summary>
/// <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>
/// <returns>
@@ -495,11 +501,11 @@ public partial class View // Drawing APIs
if (Driver is { })
{
TextFormatter.Draw (
Driver,
drawRect,
HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal),
HasFocus ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal),
Rectangle.Empty);
Driver,
drawRect,
HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal),
HasFocus ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal),
Rectangle.Empty);
}
// We assume that the text has been drawn over the entire area; ensure that the SubViews are redrawn.
@@ -529,9 +535,7 @@ public partial class View // Drawing APIs
DrawingContent?.Invoke (this, dev);
if (dev.Cancel)
{
return;
}
{ }
// 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.
/// </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
/// 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.
/// </para>
/// <para>
/// Use <see cref="DrawContext.AddDrawnRectangle"/> for simple rectangular areas, or <see cref="DrawContext.AddDrawnRegion"/>
/// 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
/// Use <see cref="DrawContext.AddDrawnRectangle"/> for simple rectangular areas, or
/// <see cref="DrawContext.AddDrawnRegion"/>
/// 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.
/// </para>
/// <para>
@@ -583,24 +592,31 @@ public partial class View // Drawing APIs
/// }
/// </code>
/// </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>
/// <remarks>
/// <para>
/// Subscribe to this event to draw custom content for the View. Use the drawing methods available on <see cref="View"/>
/// such as <see cref="View.AddRune(int, int, Rune)"/>, <see cref="View.AddStr(string)"/>, and <see cref="View.FillRect(Rectangle, Rune)"/>.
/// Subscribe to this event to draw custom content for the View. Use the drawing methods available on
/// <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>
/// 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>
/// <b>Transparency Support:</b> If the View has <see cref="ViewportSettings"/> with <see cref="ViewportSettingsFlags.Transparent"/>
/// 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.
/// <b>Transparency Support:</b> If the View has <see cref="ViewportSettings"/> with
/// <see cref="ViewportSettingsFlags.Transparent"/>
/// 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>
/// 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>
/// </remarks>
public event EventHandler<DrawEventArgs>? DrawingContent;
@@ -643,13 +659,13 @@ public partial class View // Drawing APIs
/// </summary>
/// <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>
protected virtual bool OnDrawingSubViews (DrawContext? context) { return false; }
protected virtual bool OnDrawingSubViews (DrawContext? context) => false;
/// <summary>
/// Called when the <see cref="SubViews"/> are to be drawn.
/// </summary>
/// <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>
/// <remarks>
@@ -680,6 +696,7 @@ public partial class View // Drawing APIs
{
//view.SetNeedsDraw ();
}
view.Draw (context);
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"/>.
/// </summary>
/// <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>
/// <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;
#endregion DrawComplete
}

View File

@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace Terminal.Gui.ViewBase;
@@ -19,6 +18,73 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// </remarks>
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;
/// <summary>
@@ -40,7 +106,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
{
if (_superView == value)
{
return true;
return true;
}
return CWPPropertyHelper.ChangeProperty (
@@ -87,7 +153,6 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
#region AddRemove
// TODO: Make this non-virtual once WizardStep is refactored to use events
/// <summary>Adds a SubView (child) to this view.</summary>
/// <remarks>
/// <para>
@@ -117,7 +182,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// <seealso cref="SuperViewChanging"/>
/// <seealso cref="OnSuperViewChanged"/>
/// <seealso cref="SuperViewChanged"/>
public virtual View? Add (View? view)
public View? Add (View? view)
{
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}.");
}
// 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
InternalSubViews.Add (view);
@@ -143,6 +217,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
if (!view.SetSuperView (this))
{
InternalSubViews.Remove (view);
// The change was cancelled
return null;
}
@@ -226,7 +301,6 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// </remarks>
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>
/// <remarks>
/// <para>
@@ -253,7 +327,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
/// <seealso cref="SuperViewChanging"/>
/// <seealso cref="OnSuperViewChanged"/>
/// <seealso cref="SuperViewChanged"/>
public virtual View? Remove (View? view)
public View? Remove (View? view)
{
if (view is null)
{
@@ -308,7 +382,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
return null;
}
Debug.Assert(view.SuperView is null);
Debug.Assert (view.SuperView is null);
InternalSubViews.Remove (view);
// Clean up focus stuff

View File

@@ -1,6 +1,5 @@
using System.Diagnostics;
namespace Terminal.Gui.ViewBase;
public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...)
@@ -97,12 +96,12 @@ public partial class View // Focus and cross-view navigation management (TabStop
return false;
}
// TabGroup is special-cased.
// TabGroup is special-cased.
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;
}
}
@@ -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"/>
/// otherwise.
/// </returns>
protected virtual bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) { return false; }
protected virtual bool OnAdvancingFocus (NavigationDirection direction, TabBehavior? behavior) => false;
/// <summary>
/// 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
{
View? focused = SubViews.FirstOrDefault (v => v.HasFocus);
View? focused = GetSubViews (includePadding: true).FirstOrDefault (v => v.HasFocus);
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"/>
/// otherwise.
/// </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>
/// 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
bool prevCanFocus = _canFocus;
_canFocus = false;
bool restoredFocus = applicationFocused!.RestoreFocus ();
bool restoredFocus = applicationFocused.RestoreFocus ();
_canFocus = prevCanFocus;
if (restoredFocus)
@@ -922,9 +921,6 @@ public partial class View // Focus and cross-view navigation management (TabStop
return;
}
// Get whatever peer has focus, if any so we can update our superview's _previouslyMostFocused
View? focusedPeer = superViewOrParent?.Focused;
// Set HasFocus false
_hasFocus = false;
@@ -987,7 +983,9 @@ public partial class View // Focus and cross-view navigation management (TabStop
/// This event cannot be cancelled.
/// </para>
/// </remarks>
#pragma warning disable CS0067 // Event is never used
public event EventHandler<HasFocusEventArgs>? HasFocusChanged;
#pragma warning restore CS0067 // Event is never used
#endregion HasFocus
@@ -1007,35 +1005,28 @@ public partial class View // Focus and cross-view navigation management (TabStop
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
{
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)
if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior && Padding.Thickness != Thickness.Empty)
{
filteredSubViews = filteredSubViews?.Append (Padding);
filteredSubViews = filteredSubViews.Append (Padding);
}
if (Border is { CanFocus: true, Visible: true, Enabled: true } && Border.TabStop == behavior)
{
filteredSubViews = filteredSubViews?.Append (Border);
}
if (Margin is { CanFocus: true, Visible: true, Enabled: true } && Margin.TabStop == behavior)
{
filteredSubViews = filteredSubViews?.Append (Margin);
}
// Border and Margin do not participate in focus chain navigation.
if (direction == NavigationDirection.Backward)
{
filteredSubViews = filteredSubViews?.Reverse ();
}
return filteredSubViews?.ToArray () ?? Array.Empty<View> ();
return filteredSubViews?.ToArray () ?? [];
}
private TabBehavior? _tabStop;

View File

@@ -132,7 +132,7 @@ public partial class View
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) =>
{
@@ -153,7 +153,7 @@ public partial class View
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) =>
{

View File

@@ -500,17 +500,27 @@ public partial class View : IDisposable, ISupportInitializeNotification
return;
}
if (!OnTitleChanging (ref value))
if (OnTitleChanging (ref value))
{
string old = _title;
_title = value;
TitleTextFormatter.Text = _title;
SetTitleTextFormatterSize ();
SetHotKeyFromTitle ();
SetNeedsDraw ();
OnTitleChanged ();
return;
}
CancelEventArgs<string> args = new (ref _title, ref value);
TitleChanging?.Invoke (this, args);
if (args.Cancel)
{
return;
}
_title = value;
TitleTextFormatter.Text = _title;
SetTitleTextFormatterSize ();
SetHotKeyFromTitle ();
SetNeedsDraw ();
OnTitleChanged ();
TitleChanged?.Invoke (this, new (in _title));
}
}
@@ -524,26 +534,13 @@ public partial class View : IDisposable, ISupportInitializeNotification
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>
/// Called before the <see cref="View.Title"/> changes. Invokes the <see cref="TitleChanging"/> event, which can
/// be cancelled.
/// </summary>
/// <param name="newTitle">The new <see cref="View.Title"/> to be replaced.</param>
/// <returns>`true` if an event handler canceled the Title change.</returns>
protected bool OnTitleChanging (ref string newTitle)
{
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;
protected virtual bool OnTitleChanging (ref string newTitle) => false;
/// <summary>
/// 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>
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
#if DEBUG_IDISPOSABLE

View File

@@ -4766,6 +4766,10 @@ public class TextView : View, IDesignable
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;
}

View File

@@ -1,104 +1,159 @@
using System.ComponentModel;
namespace Terminal.Gui.Views;
/// <summary>
/// Provides navigation and a user interface (UI) to collect related data across multiple steps. Each step (
/// <see cref="WizardStep"/>) can host arbitrary <see cref="View"/>s, much like a <see cref="Dialog"/>. Each step also
/// has a pane for help text. Along the bottom of the Wizard view are customizable buttons enabling the user to
/// navigate forward and backward through the Wizard.
/// A multistep user interface for collecting related data. Each <see cref="WizardStep"/> can host arbitrary
/// <see cref="View"/>s and display help text. Navigation buttons enable moving between steps.
/// </summary>
/// <remarks>
/// The Wizard can be displayed either as a modal (pop-up) <see cref="Window"/> (like <see cref="Dialog"/>) or as
/// an embedded <see cref="View"/>.
/// Can be displayed as a modal (pop-up) or embedded view.
/// </remarks>
/// <example>
/// <code>
/// 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
/// var firstStep = new WizardStep ("End User License Agreement");
/// wizard.AddStep(firstStep);
/// // Add first step
/// WizardStep firstStep = new () { Title = "License Agreement" };
/// 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
/// var secondStep = new WizardStep ("Second Step");
/// wizard.AddStep(secondStep);
/// secondStep.HelpText = "This is the help text for the Second Step.";
/// var lbl = new Label () { Text = "Name:" };
/// secondStep.Add(lbl);
/// // Add 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);
///
/// var name = new TextField { X = Pos.Right (lbl) + 1, Width = Dim.Fill () - 1 };
/// secondStep.Add(name);
///
/// wizard.Finished += (args) =>
/// wizard.Accepting += (_, e) =>
/// {
/// MessageBox.Query("Wizard", $"Finished. The Name entered is '{name.Text}'", "Ok");
/// Application.RequestStop();
/// MessageBox.Query ("Complete", $"Name: {name.Text}", "Ok");
/// e.Handled = true;
/// };
///
/// Application.TopRunnable.Add (wizard);
/// Application.Run ();
/// Application.Shutdown ();
/// app.Run (wizard);
/// </code>
/// </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;
/// <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>
/// <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 ()
{
// TODO: LastEndRestStart will enable a "Quit" button to always appear at the far left
ButtonAlignment = Alignment.Start;
ButtonAlignmentModes |= AlignmentModes.IgnoreFirstOrLast;
BorderStyle = LineStyle.Double;
TabStop = TabBehavior.TabGroup;
X = Pos.Center ();
Y = Pos.Center ();
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 ()
{
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
var separator = new Line { Orientation = Orientation.Horizontal, X = -1, Y = Pos.Top (BackButton) - 1, Length = Dim.Fill (-1) };
AddCommand (Command.Quit, QuitHandler);
KeyBindings.Add (Application.QuitKey, Command.Quit);
base.Add (separator);
AddButton (BackButton);
AddButton (NextFinishButton);
return;
BackButton.Accepting += BackBtn_Accepting;
NextFinishButton.Accepting += NextFinishBtn_Accepting;
// Add key binding for Esc when not modal - fires Cancelled event
bool? QuitHandler (ICommandContext? ctx)
{
CancelEventArgs args = new ();
Cancelled?.Invoke (this, args);
IsModalChanged += Wizard_IsModalChanged;
IsRunningChanged += Wizard_IsRunningChanged;
TitleChanged += Wizard_TitleChanged;
return args.Cancel;
}
}
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>
/// If the <see cref="CurrentStep"/> is not the first step in the wizard, this button causes the
/// <see cref="MovingBack"/> event to be fired and the wizard moves to the previous step.
/// The Back button. Navigates to the previous step and raises <see cref="MovingBack"/>.
/// Hidden on the first step.
/// </summary>
/// <remarks>Use the <see cref="MovingBack"></see> event to be notified when the user attempts to go back.</remarks>
public Button BackButton { get; }
private readonly LinkedList<WizardStep> _steps = [];
private WizardStep? _currentStep;
/// <summary>Gets or sets the currently active <see cref="WizardStep"/>.</summary>
public WizardStep? CurrentStep
{
@@ -106,113 +161,62 @@ public class Wizard : Dialog
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>
/// If the <see cref="CurrentStep"/> is the last step in the wizard, this button causes the <see cref="Finished"/>
/// event to be fired and the wizard to close. If the step is not the last step, the <see cref="MovingNext"/> event
/// will be fired and the wizard will move next step.
/// The Next/Finish button. On the last step, raises <see cref="View.Accepting"/>.
/// On other steps, raises <see cref="MovingNext"/> and navigates forward.
/// </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; }
private Size _maxStepSize = Size.Empty;
/// <summary>
/// Adds a step to the wizard. The Next and Back buttons navigate through the added steps in the order they were
/// added.
/// Adds a step to the wizard. Steps are navigated in the order added.
/// </summary>
/// <param name="newStep"></param>
/// <remarks>The "Next..." button of the last step added will read "Finish" (unless changed from default).</remarks>
/// <param name="newStep">The step to add.</param>
public void AddStep (WizardStep newStep)
{
SizeStep (newStep);
newStep.EnabledChanged += (s, e) => UpdateButtonsAndTitle ();
newStep.TitleChanged += (s, e) => UpdateButtonsAndTitle ();
newStep.EnabledChanged += (_, _) => UpdateButtonsAndTitle ();
newStep.TitleChanged += (_, _) => UpdateButtonsAndTitle ();
_steps.AddLast (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 ();
}
/// <summary>
/// Raised when the user has cancelled the <see cref="Wizard"/> by pressing the Esc key. To prevent a modal (
/// <see cref="WizardButtonEventArgs.Cancel"/> to <c>true</c> before returning from the event handler.
/// </summary>
public event EventHandler<WizardButtonEventArgs>? Cancelled;
/// <summary>Raised when the user cancels the wizard by pressing the Esc key.</summary>
public event EventHandler<CancelEventArgs>? Cancelled;
/// <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>
/// <summary>Returns the first enabled step.</summary>
public WizardStep? GetFirstStep () { return _steps.FirstOrDefault (s => s.Enabled); }
/// <summary>Returns the last enabled step in the Wizard</summary>
/// <returns>The last enabled step</returns>
/// <summary>Returns the last enabled step.</summary>
public WizardStep? GetLastStep () { return _steps.LastOrDefault (s => s.Enabled); }
/// <summary>
/// Returns the next enabled <see cref="WizardStep"/> after the current step. Takes into account steps which are
/// disabled. If <see cref="CurrentStep"/> is <c>null</c> returns the first enabled step.
/// Returns the next enabled step after <see cref="CurrentStep"/>, skipping disabled steps.
/// </summary>
/// <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>
/// <returns>The next enabled step, or <c>null</c> if none exists.</returns>
public WizardStep? GetNextStep ()
{
LinkedListNode<WizardStep>? step = null;
LinkedListNode<WizardStep>? step;
if (CurrentStep is null)
{
@@ -223,11 +227,7 @@ public class Wizard : Dialog
{
// Get the step after current
step = _steps.Find (CurrentStep);
if (step is { })
{
step = step.Next;
}
step = step?.Next;
}
// step now points to the potential next step
@@ -245,16 +245,12 @@ public class Wizard : Dialog
}
/// <summary>
/// Returns the first enabled <see cref="WizardStep"/> before the current step. Takes into account steps which are
/// disabled. If <see cref="CurrentStep"/> is <c>null</c> returns the last enabled step.
/// Returns the previous enabled step before <see cref="CurrentStep"/>, skipping disabled steps.
/// </summary>
/// <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>
/// <returns>The previous enabled step, or <c>null</c> if none exists.</returns>
public WizardStep? GetPreviousStep ()
{
LinkedListNode<WizardStep>? step = null;
LinkedListNode<WizardStep>? step;
if (CurrentStep is null)
{
@@ -265,11 +261,7 @@ public class Wizard : Dialog
{
// Get the step before current
step = _steps.Find (CurrentStep);
if (step is { })
{
step = step.Previous;
}
step = step?.Previous;
}
// step now points to the potential previous step
@@ -286,188 +278,112 @@ public class Wizard : Dialog
return null;
}
/// <summary>
/// Causes the wizard to move to the previous enabled step (or first 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>
/// <summary>Navigates to the previous enabled step.</summary>
/// <returns><see langword="true"/> if the transition succeeded; otherwise <see langword="false"/>.</returns>
public bool GoBack ()
{
WizardStep? previous = GetPreviousStep ();
if (previous is { })
{
return GoToStep (previous);
}
return false;
return previous is { } && GoToStep (previous);
}
/// <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>
/// <summary>Navigates to the next enabled step.</summary>
/// <returns><see langword="true"/> if the transition succeeded; otherwise <see langword="false"/>.</returns>
public bool GoNext ()
{
WizardStep? nextStep = GetNextStep ();
if (nextStep is { })
{
return GoToStep (nextStep);
}
return false;
return nextStep is { } && GoToStep (nextStep);
}
/// <summary>Changes to the specified <see cref="WizardStep"/>.</summary>
/// <param name="newStep">The step to go to.</param>
/// <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>
/// Raised when the user clicks the Back button. Cancel to prevent navigation.
/// </summary>
public event EventHandler<CancelEventArgs>? MovingBack;
/// <summary>
/// Raised when the user clicks Next on a non-final step. Cancel to prevent navigation.
/// </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)
{
if (OnStepChanging (_currentStep, newStep) || newStep is { Enabled: false })
{
return false;
}
return CWPPropertyHelper.ChangeProperty (
this,
ref _currentStep,
newStep,
OnStepChanging,
StepChanging,
newValue =>
{
ValueChangingEventArgs<WizardStep?> args = new (_currentStep, newValue);
StepChanging?.Invoke (this, args);
// Hide all but the new step
foreach (WizardStep step in _steps)
{
step.Visible = step == newStep;
step.ShowHide ();
}
if (args.Handled)
{
return;
}
WizardStep? oldStep = _currentStep;
_currentStep = newStep;
// Hide all but the new step
foreach (WizardStep step in _steps)
{
step.Visible = step == newValue;
UpdateButtonsAndTitle ();
step.ShowHide ();
}
// Set focus on the contentview
newStep?.SubViews.ToArray () [0].SetFocus ();
if (OnStepChanged (oldStep, _currentStep))
{
// For correctness, we do this, but it's meaningless because there's nothing to cancel
return false;
}
return true;
_currentStep = newValue;
UpdateButtonsAndTitle ();
},
OnStepChanged,
StepChanged,
out _);
}
/// <summary>
/// Raised when the Back button in the <see cref="Wizard"/> is clicked. The Back button is always the first button
/// in the array of Buttons passed to the <see cref="Wizard"/> constructor, if any.
/// </summary>
public event EventHandler<WizardButtonEventArgs>? MovingBack;
/// <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>
/// Raised when the Next/Finish button in the <see cref="Wizard"/> is clicked (or the user presses Enter). 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>? MovingNext;
/// <summary>Raised before <see cref="CurrentStep"/> changes. Set <c>Handled</c> to cancel.</summary>
public event EventHandler<ValueChangingEventArgs<WizardStep?>>? StepChanging;
/// <summary>
/// <see cref="Wizard"/> is derived from <see cref="Dialog"/> and Dialog causes <c>Esc</c> to call
/// <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
/// non-modal.
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
protected override bool OnKeyDownNotHandled (Key key)
/// <summary>Called after changing steps. Raises <see cref="StepChanged"/>.</summary>
protected virtual void OnStepChanged (ValueChangedEventArgs<WizardStep?> args) { }
/// <summary>Raised after <see cref="CurrentStep"/> changes.</summary>
public event EventHandler<ValueChangedEventArgs<WizardStep?>>? StepChanged;
private void BackBtnOnAccepting (object? sender, CommandEventArgs e)
{
// BUGBUG: Why is this not handled by a key binding???
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 ();
CancelEventArgs args = new ();
MovingBack?.Invoke (this, args);
if (!args.Cancel)
if (args.Cancel)
{
e.Handled = GoBack ();
return;
}
e.Handled = true;
GoBack ();
}
private void NextFinishBtn_Accepting (object? sender, CommandEventArgs e)
private void NextFinishBtnOnAccepting (object? sender, CommandEventArgs e)
{
if (CurrentStep == GetLastStep ())
{
var args = new WizardButtonEventArgs ();
Finished?.Invoke (this, args);
if (!args.Cancel)
if (RaiseAccepting (e.Context) is false)
{
_finishedPressed = true;
if (IsCurrentTop)
{
(sender as View)?.App?.RequestStop (this);
e.Handled = true;
}
// Wizard was created as a non-modal (just added to another View).
// Do nothing
e.Handled = true;
RequestStop ();
}
}
else
{
var args = new WizardButtonEventArgs ();
MovingNext?.Invoke (this, args);
CancelEventArgs args = new ();
MovingNext?.Invoke (this, new ());
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 ()
{
if (CurrentStep is null)
@@ -533,35 +420,64 @@ public class Wizard : Dialog
? CurrentStep.NextButtonText
: Strings.wzNext; // "_Next...";
}
SizeStep (CurrentStep);
SetNeedsLayout ();
}
private void Wizard_IsRunningChanged (object? sender, EventArgs<bool> args)
bool IDesignable.EnableForDesign ()
{
if (!_finishedPressed)
{
var a = new WizardButtonEventArgs ();
Cancelled?.Invoke (this, a);
}
}
Title = "Wizard Title";
private void Wizard_IsModalChanged (object? sender, EventArgs<bool> args)
{
if (args.Value)
{
CurrentStep = GetFirstStep ();
// gets the first step if CurrentStep == null
}
}
WizardStep firstStep = new ();
(firstStep as IDesignable).EnableForDesign ();
AddStep (firstStep);
private void Wizard_TitleChanged (object? sender, EventArgs<string> e)
{
if (string.IsNullOrEmpty (_wizardTitle))
Label schemeLabel = new ()
{
_wizardTitle = e.Value;
}
Title = "_Scheme:"
};
OptionSelector<Schemes> selector = new ()
{
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;
/// <summary>
/// Represents a basic step that is displayed in a <see cref="Wizard"/>. The <see cref="WizardStep"/> view is
/// divided horizontally in two. On the left is the content view where <see cref="View"/>s can be added, On the right
/// 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.
/// A single step in a <see cref="Wizard"/>. Can contain arbitrary <see cref="View"/>s and display help text
/// in the right <see cref="Padding"/>.
/// </summary>
/// <remarks>
/// If <see cref="Button"/>s are added, do not set <see cref="Button.IsDefault"/> to true as this will conflict
/// with the Next button of the Wizard. Subscribe to the <see cref="View.VisibleChanged"/> event to be notified when
/// the step is active; see also: <see cref="Wizard.StepChanged"/>. To enable or disable a step from being shown to the
/// user, set <see cref="View.Enabled"/>.
/// Do not set <see cref="Button.IsDefault"/> on added buttons (conflicts with Wizard navigation).
/// Use <see cref="View.VisibleChanged"/> or <see cref="Wizard.StepChanged"/> to detect when this step becomes active.
/// Set <see cref="View.Enabled"/> to control whether the step is shown.
/// </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 ()
{
CanFocus = true,
@@ -31,78 +18,61 @@ public class WizardStep : View
ReadOnly = true,
WordWrap = true,
AllowsTab = false,
X = Pos.AnchorEnd () + 1,
Height = Dim.Fill (),
#if DEBUG
Id = "WizardStep._helpTextView"
#endif
};
/// <summary>
/// Initializes a new instance of the <see cref="Wizard"/> class.
/// Initializes a new instance of the <see cref="WizardStep"/> class.
/// </summary>
public WizardStep ()
{
TabStop = TabBehavior.TabStop;
CanFocus = true;
BorderStyle = LineStyle.None;
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 ();
Width = Dim.Fill ();
Height = Dim.Fill ();
}
/// <summary>Sets or gets the text for the back button. The back button will only be visible on steps after the first step.</summary>
/// <remarks>The default text is "Back"</remarks>
/// <inheritdoc/>
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;
/// <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>
/// Sets or gets help text for the <see cref="WizardStep"/>.If <see cref="WizardStep.HelpText"/> is empty the help
/// pane will not be visible and the content will fill the entire WizardStep.
/// The help text displayed in the right <see cref="Padding"/>.
/// If empty, the right padding is hidden and content fills the entire step.
/// </summary>
/// <remarks>The help text is displayed using a read-only <see cref="TextView"/>.</remarks>
public string HelpText
@@ -111,98 +81,91 @@ public class WizardStep : View
set
{
_helpTextView.Text = value;
_helpTextView.MoveHome ();
ShowHide ();
SetNeedsDraw ();
}
}
/// <summary>Sets or gets the text for the next/finish button.</summary>
/// <remarks>The default text is "Next..." if the Pane is not the last pane. Otherwise it is "Finish"</remarks>
/// <summary>The text for the Next/Finish button. Defaults to "Next..." or "Finish" based on position.</summary>
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>
internal void ShowHide ()
{
_contentView.Height = Dim.Fill ();
_helpTextView.Height = Dim.Height(_contentView);
_helpTextView.Width = Dim.Fill ();
if (_contentView.InternalSubViews?.Count > 0)
// Check if views are available (might be null during disposal)
if (Padding is null)
{
if (_helpTextView.Text.Length > 0)
{
_contentView.Width = Dim.Percent (70);
_helpTextView.X = Pos.Right (_contentView);
_helpTextView.Width = Dim.Fill ();
}
else
{
_contentView.Width = Dim.Fill ();
}
return;
}
if (_helpTextView.Text.Length > 0)
{
// Configure Padding
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
{
if (_helpTextView.Text.Length > 0)
{
_helpTextView.X = 0;
}
// Configure Padding
// Error - no pane shown
Padding.CanFocus = false;
// No help text - no right padding needed
Padding.Thickness = Padding.Thickness with { Right = 0 };
_helpTextView.Visible = false;
_helpTextView.Enabled = false;
}
_contentView.Visible = _contentView.InternalSubViews?.Count > 0;
_helpTextView.Visible = _helpTextView.Text.Length > 0;
SetNeedsLayout ();
}
bool IDesignable.EnableForDesign ()
{
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

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/=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/=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/=ArrangeObjectCreationWhenTypeNotEvident/@EntryIndexedValue">SUGGESTION</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_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean>
<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/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/=Depeche/@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/=Gandolf/@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/=IDISPOSABLE/@EntryIndexedValue">True</s:Boolean>

View File

@@ -22,7 +22,7 @@ public class WizardTests
wizard.AddStep (step1);
var finishedFired = false;
wizard.Finished += (s, args) => { finishedFired = true; };
wizard.Accepting += (s, args) => { finishedFired = true; };
var isRunningChangedFired = false;
wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; };
@@ -46,7 +46,7 @@ public class WizardTests
wizard.AddStep (step2);
finishedFired = false;
wizard.Finished += (s, args) => { finishedFired = true; };
wizard.Accepting += (s, args) => { finishedFired = true; };
isRunningChangedFired = false;
wizard.IsRunningChanged += (s, e) => { isRunningChangedFired = true; };
@@ -79,7 +79,7 @@ public class WizardTests
step1.Enabled = false;
finishedFired = false;
wizard.Finished += (s, args) => { finishedFired = true; };
wizard.Accepting += (s, args) => { finishedFired = true; };
isRunningChangedFired = false;
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")
public void Setting_Title_Works ()
{
var d = Application.Driver;
IDriver d = Application.Driver;
var title = "1234";
var stepTitle = " - ABCD";

View File

@@ -1,21 +1,24 @@
using Xunit.Abstractions;
using UnitTests;
using Xunit.Abstractions;
namespace ViewBaseTests.Adornments;
public class AdornmentSubViewTests ()
public class AdornmentSubViewTests (ITestOutputHelper output)
{
private readonly ITestOutputHelper _output = output;
[Fact]
public void Setting_Thickness_Causes_Adornment_SubView_Layout ()
{
var view = new View ();
var subView = new View ();
view.Margin!.Add (subView);
view.Padding!.Add (subView);
view.BeginInit ();
view.EndInit ();
var raised = false;
subView.SubViewLayout += LayoutStarted;
view.Margin.Thickness = new Thickness (1, 2, 3, 4);
view.Padding.Thickness = new (1, 2, 3, 4);
view.Layout ();
Assert.True (raised);
@@ -27,12 +30,12 @@ public class AdornmentSubViewTests ()
}
[Theory]
[InlineData (0, 0, false)] // Margin has no thickness, so false
[InlineData (0, 1, false)] // Margin has no thickness, so false
[InlineData (0, 0, false)] // Padding has no thickness, so false
[InlineData (0, 1, false)] // Padding has no thickness, so false
[InlineData (1, 0, true)]
[InlineData (1, 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 ();
Runnable<bool> runnable = new ()
@@ -42,9 +45,9 @@ public class AdornmentSubViewTests ()
};
app.Begin (runnable);
runnable.Margin!.Thickness = new Thickness (viewMargin);
runnable.Padding!.Thickness = new (viewPadding);
// Turn of TransparentMouse for the test
runnable.Margin!.ViewportSettings = ViewportSettingsFlags.None;
runnable.Padding!.ViewportSettings = ViewportSettingsFlags.None;
var subView = new View ()
{
@@ -53,16 +56,16 @@ public class AdornmentSubViewTests ()
Width = 5,
Height = 5
};
subView.Margin!.Thickness = new Thickness (subViewMargin);
subView.Padding!.Thickness = new (subViewPadding);
// 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 ();
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);
}
@@ -92,4 +95,84 @@ public class AdornmentSubViewTests ()
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]
[InlineData ("Margin")]
[InlineData ("Border")]
[InlineData ("Padding")]
public void Returns_Subview_Of_Adornment (string adornmentType)
@@ -271,7 +270,6 @@ public class GetViewsUnderLocationForRootTests
[Theory]
[InlineData ("Margin")]
[InlineData ("Border")]
[InlineData ("Padding")]
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;
}
if (view is IDesignable designable)
{
designable.EnableForDesign ();
}
IApplication app = Application.Create ();
app.Begin (new Runnable<bool> () { CanFocus = true });
@@ -45,7 +50,7 @@ public class AllViewsNavigationTests (ITestOutputHelper output) : TestsAllViews
if (view.TabStop == TabBehavior.TabGroup)
{
navKeys = new [] { Key.F6, Key.F6.WithShift };
navKeys = [Key.F6, Key.F6.WithShift];
}
var left = false;
@@ -113,6 +118,11 @@ public class AllViewsNavigationTests (ITestOutputHelper output) : TestsAllViews
return;
}
if (view is IDesignable designable)
{
designable.EnableForDesign ();
}
IApplication app = Application.Create ();
app.Begin (new Runnable<bool> () { CanFocus = true });

View File

@@ -6,16 +6,14 @@ public class SubViewTests
[Fact]
public void SuperViewChanged_Raised_On_Add ()
{
var super = new View { };
var super = new View ();
var sub = new View ();
int superRaisedCount = 0;
int subRaisedCount = 0;
var superRaisedCount = 0;
var subRaisedCount = 0;
super.SuperViewChanged += (s, e) => { superRaisedCount++; };
super.SuperViewChanged += (s, e) =>
{
superRaisedCount++;
};
sub.SuperViewChanged += (s, e) =>
{
if (sub.SuperView is { })
@@ -34,23 +32,21 @@ public class SubViewTests
[Fact]
public void SuperViewChanged_Raised_On_Remove ()
{
var super = new View { };
var super = new View ();
var sub = new View ();
int superRaisedCount = 0;
int subRaisedCount = 0;
var superRaisedCount = 0;
var subRaisedCount = 0;
super.SuperViewChanged += (s, e) => { superRaisedCount++; };
super.SuperViewChanged += (s, e) =>
{
superRaisedCount++;
};
sub.SuperViewChanged += (s, e) =>
{
if (sub.SuperView is null)
{
subRaisedCount++;
}
};
{
if (sub.SuperView is null)
{
subRaisedCount++;
}
};
super.Add (sub);
Assert.True (super.SubViews.Count == 1);
@@ -95,6 +91,13 @@ public class SubViewTests
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]
public void Remove_Does_Not_Impact_ContentSize ()
{
@@ -392,18 +395,12 @@ public class SubViewTests
var view = new View ();
var superView = new View ();
int superViewChangedCount = 0;
int superViewChangingCount = 0;
var superViewChangedCount = 0;
var superViewChangingCount = 0;
view.SuperViewChanged += (s, e) =>
{
superViewChangedCount++;
};
view.SuperViewChanged += (s, e) => { superViewChangedCount++; };
view.SuperViewChanging += (s, e) =>
{
superViewChangingCount++;
};
view.SuperViewChanging += (s, e) => { superViewChangingCount++; };
// Act
superView.Add (view);
@@ -411,7 +408,6 @@ public class SubViewTests
// Assert
Assert.Equal (1, superViewChangingCount);
Assert.Equal (1, superViewChangedCount);
}
[Fact]
@@ -450,7 +446,6 @@ public class SubViewTests
top2.Dispose ();
}
[Fact]
public void Initialized_Event_Comparing_With_Added_Event ()
{
@@ -479,10 +474,10 @@ public class SubViewTests
int tc = 0, wc = 0, v1c = 0, v2c = 0, sv1c = 0;
winAddedToTop.SubViewAdded += (s, e) =>
{
Assert.Equal (e.SuperView!.Frame.Width, winAddedToTop.Frame.Width);
Assert.Equal (e.SuperView.Frame.Height, winAddedToTop.Frame.Height);
};
{
Assert.Equal (e.SuperView!.Frame.Width, winAddedToTop.Frame.Width);
Assert.Equal (e.SuperView.Frame.Height, winAddedToTop.Frame.Height);
};
v1AddedToWin.SubViewAdded += (s, e) =>
{
@@ -503,69 +498,70 @@ public class SubViewTests
};
top.Initialized += (s, e) =>
{
tc++;
Assert.Equal (1, tc);
Assert.Equal (1, wc);
Assert.Equal (1, v1c);
Assert.Equal (1, v2c);
Assert.Equal (1, sv1c);
{
tc++;
Assert.Equal (1, tc);
Assert.Equal (1, wc);
Assert.Equal (1, v1c);
Assert.Equal (1, v2c);
Assert.Equal (1, sv1c);
Assert.True (top.CanFocus);
Assert.True (winAddedToTop.CanFocus);
Assert.False (v1AddedToWin.CanFocus);
Assert.False (v2AddedToWin.CanFocus);
Assert.False (svAddedTov1.CanFocus);
Assert.True (top.CanFocus);
Assert.True (winAddedToTop.CanFocus);
Assert.False (v1AddedToWin.CanFocus);
Assert.False (v2AddedToWin.CanFocus);
Assert.False (svAddedTov1.CanFocus);
top.Layout ();
};
top.Layout ();
};
winAddedToTop.Initialized += (s, e) =>
{
wc++;
Assert.Equal (top.Viewport.Width, winAddedToTop.Frame.Width);
Assert.Equal (top.Viewport.Height, winAddedToTop.Frame.Height);
};
{
wc++;
Assert.Equal (top.Viewport.Width, winAddedToTop.Frame.Width);
Assert.Equal (top.Viewport.Height, winAddedToTop.Frame.Height);
};
v1AddedToWin.Initialized += (s, e) =>
{
v1c++;
{
v1c++;
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the v1AddedToWin.Frame be the same as the Top.Frame/Viewport
// as it is a subview of winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, v1AddedToWin.Frame.Width);
//Assert.Equal (top.Viewport.Height, v1AddedToWin.Frame.Height);
};
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the v1AddedToWin.Frame be the same as the Top.Frame/Viewport
// as it is a subview of winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, v1AddedToWin.Frame.Width);
//Assert.Equal (top.Viewport.Height, v1AddedToWin.Frame.Height);
};
v2AddedToWin.Initialized += (s, e) =>
{
v2c++;
{
v2c++;
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the v2AddedToWin.Frame be the same as the Top.Frame/Viewport
// as it is a subview of winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, v2AddedToWin.Frame.Width);
//Assert.Equal (top.Viewport.Height, v2AddedToWin.Frame.Height);
};
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the v2AddedToWin.Frame be the same as the Top.Frame/Viewport
// as it is a subview of winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, v2AddedToWin.Frame.Width);
//Assert.Equal (top.Viewport.Height, v2AddedToWin.Frame.Height);
};
svAddedTov1.Initialized += (s, e) =>
{
sv1c++;
{
sv1c++;
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the svAddedTov1.Frame be the same as the Top.Frame/Viewport
// because sv1AddedTov1 is a subview of v1AddedToWin, which is a subview of
// winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, svAddedTov1.Frame.Width);
//Assert.Equal (top.Viewport.Height, svAddedTov1.Frame.Height);
Assert.False (svAddedTov1.CanFocus);
//Assert.Throws<InvalidOperationException> (() => svAddedTov1.CanFocus = true);
Assert.False (svAddedTov1.CanFocus);
};
// Top.Frame: 0, 0, 80, 25; Top.Viewport: 0, 0, 80, 25
// BUGBUG: This is wrong, it should be 78, 23. This test has always been broken.
// in no way should the svAddedTov1.Frame be the same as the Top.Frame/Viewport
// because sv1AddedTov1 is a subview of v1AddedToWin, which is a subview of
// winAddedToTop, which has a border!
//Assert.Equal (top.Viewport.Width, svAddedTov1.Frame.Width);
//Assert.Equal (top.Viewport.Height, svAddedTov1.Frame.Height);
Assert.False (svAddedTov1.CanFocus);
//Assert.Throws<InvalidOperationException> (() => svAddedTov1.CanFocus = true);
Assert.False (svAddedTov1.CanFocus);
};
v1AddedToWin.Add (svAddedTov1);
winAddedToTop.Add (v1AddedToWin, v2AddedToWin);
@@ -639,7 +635,7 @@ public class SubViewTests
superView.Add (subView1, subView2, subView3);
// Act
var removedViews = superView.RemoveAll ();
IReadOnlyCollection<View> removedViews = superView.RemoveAll ();
// Assert
Assert.Empty (superView.SubViews);
@@ -662,7 +658,7 @@ public class SubViewTests
superView.Add (subView1, subView2, subView3, subView4);
// Act
var removedViews = superView.RemoveAll<Button> ();
IReadOnlyCollection<Button> removedViews = superView.RemoveAll<Button> ();
// Assert
Assert.Equal (3, superView.SubViews.Count);
@@ -683,7 +679,7 @@ public class SubViewTests
superView.Add (subView1, subView2, subView3);
// Act
var removedViews = superView.RemoveAll<Button> ();
IReadOnlyCollection<Button> removedViews = superView.RemoveAll<Button> ();
// Assert
Assert.Equal (2, superView.SubViews.Count);
@@ -700,7 +696,7 @@ public class SubViewTests
var superView = new View ();
var subView = new View ();
var events = new List<string> ();
List<string> events = new ();
subView.SuperViewChanging += (s, e) => { events.Add ("SuperViewChanging"); };
@@ -722,7 +718,7 @@ public class SubViewTests
var superView = 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;
subView.SuperViewChanging += (s, e) =>
@@ -749,7 +745,7 @@ public class SubViewTests
superView.Add (subView);
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) =>
{
@@ -770,7 +766,7 @@ public class SubViewTests
{
// Arrange
using IApplication app = Application.Create ();
var runnable = new Runnable<bool> ();
Runnable<bool> runnable = new ();
var subView = new View ();
runnable.Add (subView);
@@ -781,11 +777,11 @@ public class SubViewTests
subView.SuperViewChanging += (s, e) =>
{
Assert.NotNull (s);
// At this point, SuperView is still set, so App should be accessible
appInEvent = (s as View)?.App;
};
Assert.NotNull (runnable.App);
// Act
@@ -804,7 +800,7 @@ public class SubViewTests
{
// Arrange
var superView = new View ();
var events = new List<string> ();
List<string> events = new ();
var subView = new TestViewWithSuperViewEvents (events);
@@ -854,6 +850,7 @@ public class SubViewTests
protected override bool OnSuperViewChanging (ValueChangingEventArgs<View?> args)
{
_events.Add ("OnSuperViewChanging");
return base.OnSuperViewChanging (args);
}
@@ -907,10 +904,7 @@ public class SubViewTests
var subView = new TestViewThatCancelsChange ();
var eventRaised = false;
subView.SuperViewChanging += (s, e) =>
{
eventRaised = true;
};
subView.SuperViewChanging += (s, e) => { eventRaised = true; };
// Act
superView.Add (subView);
@@ -983,4 +977,327 @@ public class SubViewTests
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]
public void Space_Key_Types_Space ()
{
var view = new TextView ();
TextView view = new ();
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