diff --git a/.editorconfig b/.editorconfig index 17e0e79ad..9faacb6d8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -34,6 +34,8 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false +csharp_style_var_elsewhere = false:suggestion +csharp_style_var_when_type_is_apparent = false:suggestion dotnet_diagnostic.bc40000.severity = warning dotnet_diagnostic.bc400005.severity = warning dotnet_diagnostic.bc40008.severity = warning @@ -545,7 +547,7 @@ resharper_formatter_tags_enabled = false resharper_format_leading_spaces_decl = false resharper_for_built_in_types = use_var_when_evident resharper_for_other_types = use_explicit_type -resharper_for_simple_types = use_var_when_evident +resharper_for_simple_types = use_explicit_type resharper_ignore_space_preservation = false resharper_include_prefix_comment_in_indent = false resharper_indent_anonymous_method_block = true @@ -864,7 +866,7 @@ resharper_arrange_default_value_when_type_evident_highlighting = suggestion resharper_arrange_default_value_when_type_not_evident_highlighting = suggestion resharper_arrange_local_function_body_highlighting = warning resharper_arrange_method_or_operator_body_highlighting = hint -resharper_arrange_null_checking_pattern_highlighting = error +resharper_arrange_null_checking_pattern_highlighting = hint resharper_arrange_object_creation_when_type_evident_highlighting = suggestion resharper_arrange_object_creation_when_type_not_evident_highlighting = warning resharper_arrange_redundant_parentheses_highlighting = warning diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42675db67..dd156ff92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,8 +10,8 @@ We welcome contributions from the community. See [Issues](https://github.com/gui Terminal.Gui uses the [GitFlow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. -* The `v1_release_` and `v2_release` branches are always stable, and always matches the most recently released Nuget package. -* The `v1__develop` and `v2_develop` branches are where new development and bug-fixes happen. `v2_develop` is the default Github branch. +* The `v1_release` and `v2_release` branches are always stable, and always match the most recently released Nuget package. +* The `v1_develop` and `v2_develop` branches are where new development and bug-fixes happen. `v2_develop` is the default Github branch. ### Forking Terminal.Gui @@ -141,34 +141,8 @@ Great care has been provided thus far in ensuring **Terminal.Gui** has great [AP ### Defining Events -The [Microsoft .NET Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/) provides these guidelines for defining events: +See https://gui-cs.github.io/Terminal.GuiV2Docs/docs/events.html -> Events always refer to some action, either one that is happening or one that has occurred. Therefore, as with methods, events are named with verbs, and verb tense is used to indicate the time when the event is raised. -> -> ✔️ DO name events with a verb or a verb phrase. -> -> Examples include Clicked, Painting, DroppedDown, and so on. -> -> ✔️ DO give events names with a concept of before and after, using the present and past tenses. -> -> For example, a close event that is raised before a window is closed would be called Closing, and one that is raised after the window is closed would be called Closed. -> -> ❌ DO NOT use "Before" or "After" prefixes or postfixes to indicate pre- and post-events. Use present and past tenses as just described. -> -> ✔️ DO name event handlers (delegates used as types of events) with the "EventHandler" suffix, as shown in the following example: -> -> ✔️ DO name event argument classes with the "EventArgs" suffix. - -1. We follow the naming guidelines provided in https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members?redirectedfrom=MSDN -2. We use the `event EventHandler` idiom. -3. For public APIs, the class that can raise the event will implement: - - A `virtual` event raising function, named as `OnEventToRaise`. Typical implementations will simply do a `EventToRaise?.Invoke(this, eventArgs)`. - - An `event` as in `public event EventHandler EventToRaise` - - Consumers of the event can do `theobject.EventToRaise += (sender, args) => {};` - - Sub-classes of the class implementing `EventToRaise` can override `OnEventToRaise` as needed. -4. Where possible, a subclass of `EventArgs` should be provided and the old and new state should be included. By doing this, event handler methods do not have to query the sender for state. - -See also: https://www.codeproject.com../docs/20550/C-Event-Implementation-Fundamentals-Best-Practices ### Defining new `View` classes diff --git a/Examples/UICatalog/Resources/config.json b/Examples/UICatalog/Resources/config.json index 3d92db91f..6a7b47b3e 100644 --- a/Examples/UICatalog/Resources/config.json +++ b/Examples/UICatalog/Resources/config.json @@ -136,7 +136,7 @@ { "UI Catalog Theme": { "Window.DefaultShadow": "Transparent", - "CheckBox.DefaultHighlightStyle": "Hover, Pressed, PressedOutside", + "CheckBox.DefaultHighlightStates": "In, Pressed, PressedOutside", "MessageBox.DefaultButtonAlignment": "Start", "StatusBar.DefaultSeparatorLineStyle": "Single", "Dialog.DefaultMinimumWidth": 80, @@ -149,7 +149,7 @@ "Button.DefaultShadow": "Transparent", "FrameView.DefaultBorderStyle": "Double", "MessageBox.DefaultMinimumHeight": 0, - "Button.DefaultHighlightStyle": "Hover, Pressed", + "Button.DefaultHighlightStates": "In, Pressed", "Menuv2.DefaultBorderStyle": "Heavy", "MenuBarv2.DefaultBorderStyle": "Heavy", "Schemes": [ diff --git a/Examples/UICatalog/Scenario.cs b/Examples/UICatalog/Scenario.cs index 702661bae..b0a6e5f36 100644 --- a/Examples/UICatalog/Scenario.cs +++ b/Examples/UICatalog/Scenario.cs @@ -181,7 +181,7 @@ public class Scenario : IDisposable private void OnApplicationOnInitializedChanged (object? s, EventArgs a) { - if (a.CurrentValue) + if (a.Value) { lock (_timeoutLock!) { @@ -196,7 +196,7 @@ public class Scenario : IDisposable cd.Refreshed += (sender, args) => { BenchmarkResults.RefreshedCount++; - if (args.CurrentValue) + if (args.Value) { BenchmarkResults.UpdatedCount++; } diff --git a/Examples/UICatalog/Scenarios/Adornments.cs b/Examples/UICatalog/Scenarios/Adornments.cs index 5ae5c5e68..aff70c09c 100644 --- a/Examples/UICatalog/Scenarios/Adornments.cs +++ b/Examples/UICatalog/Scenarios/Adornments.cs @@ -51,7 +51,7 @@ public class Adornments : Scenario { Normal = new ( color.SuperView.GetAttributeForRole (VisualRole.Normal).Foreground, - e.CurrentValue, + e.Result, color.SuperView.GetAttributeForRole (VisualRole.Normal).Style ) }); @@ -130,7 +130,7 @@ public class Adornments : Scenario Y = 1, Text = "_Button in Padding Y = 1", CanFocus = true, - HighlightStyle = HighlightStyle.None, + HighlightStates = MouseState.None, }; btnButtonInPadding.Accepting += (s, e) => MessageBox.Query (20, 7, "Hi", "Button in Padding Pressed!", "Ok"); btnButtonInPadding.BorderStyle = LineStyle.Dashed; diff --git a/Examples/UICatalog/Scenarios/Arrangement.cs b/Examples/UICatalog/Scenarios/Arrangement.cs index 29994d78f..6aae631c8 100644 --- a/Examples/UICatalog/Scenarios/Arrangement.cs +++ b/Examples/UICatalog/Scenarios/Arrangement.cs @@ -223,9 +223,9 @@ public class Arrangement : Scenario return; - void ColorPickerColorChanged (object sender, ColorEventArgs e) + void ColorPickerColorChanged (object sender, ResultEventArgs e) { - testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.CurrentValue) }); + testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.Result) }); } } diff --git a/Examples/UICatalog/Scenarios/Bars.cs b/Examples/UICatalog/Scenarios/Bars.cs index 538af7520..226e18e26 100644 --- a/Examples/UICatalog/Scenarios/Bars.cs +++ b/Examples/UICatalog/Scenarios/Bars.cs @@ -415,7 +415,7 @@ public class Bars : Scenario Title = "_File", HelpText = "File Menu", Key = Key.D0.WithAlt, - HighlightStyle = HighlightStyle.Hover + HighlightStates = MouseState.In }; var editMenuBarItem = new Shortcut @@ -423,7 +423,7 @@ public class Bars : Scenario Title = "_Edit", HelpText = "Edit Menu", Key = Key.D1.WithAlt, - HighlightStyle = HighlightStyle.Hover + HighlightStates = MouseState.In }; var helpMenuBarItem = new Shortcut @@ -431,7 +431,7 @@ public class Bars : Scenario Title = "_Help", HelpText = "Halp Menu", Key = Key.D2.WithAlt, - HighlightStyle = HighlightStyle.Hover + HighlightStates = MouseState.In }; bar.Add (fileMenuBarItem, editMenuBarItem, helpMenuBarItem); @@ -445,7 +445,7 @@ public class Bars : Scenario Title = "Z_igzag", Key = Key.I.WithCtrl, Text = "Gonna zig zag", - HighlightStyle = HighlightStyle.Hover + HighlightStates = MouseState.In }; var shortcut2 = new Shortcut @@ -453,7 +453,7 @@ public class Bars : Scenario Title = "Za_G", Text = "Gonna zag", Key = Key.G.WithAlt, - HighlightStyle = HighlightStyle.Hover + HighlightStates = MouseState.In }; var shortcut3 = new Shortcut @@ -461,7 +461,7 @@ public class Bars : Scenario Title = "_Three", Text = "The 3rd item", Key = Key.D3.WithAlt, - HighlightStyle = HighlightStyle.Hover + HighlightStates = MouseState.In }; var line = new Line () @@ -475,13 +475,13 @@ public class Bars : Scenario Title = "_Four", Text = "Below the line", Key = Key.D3.WithAlt, - HighlightStyle = HighlightStyle.Hover + HighlightStates = MouseState.In }; shortcut4.CommandView = new CheckBox () { Title = shortcut4.Title, - HighlightStyle = HighlightStyle.None, + HighlightStates = MouseState.None, CanFocus = false }; // This ensures the checkbox state toggles when the hotkey of Title is pressed. diff --git a/Examples/UICatalog/Scenarios/Buttons.cs b/Examples/UICatalog/Scenarios/Buttons.cs index cc66fafba..b4c921437 100644 --- a/Examples/UICatalog/Scenarios/Buttons.cs +++ b/Examples/UICatalog/Scenarios/Buttons.cs @@ -415,6 +415,7 @@ public class Buttons : Scenario var repeatButton = new Button { + Id = "repeatButton", X = Pos.Right (label) + 1, Y = Pos.Top (label), Title = $"Accept Co_unt: {acceptCount}", diff --git a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs index 58f467d01..4e7679b6b 100644 --- a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs +++ b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -68,13 +68,13 @@ public class CharacterMap : Scenario _charMap.SelectedCodePointChanged += (sender, args) => { - if (Rune.IsValid (args.CurrentValue)) + if (Rune.IsValid (args.Value)) { - jumpEdit.Text = ((Rune)args.CurrentValue).ToString (); + jumpEdit.Text = ((Rune)args.Value).ToString (); } else { - jumpEdit.Text = $"U+{args.CurrentValue:x5}"; + jumpEdit.Text = $"U+{args.Value:x5}"; } }; diff --git a/Examples/UICatalog/Scenarios/ColorPicker.cs b/Examples/UICatalog/Scenarios/ColorPicker.cs index 87e429177..568e6824e 100644 --- a/Examples/UICatalog/Scenarios/ColorPicker.cs +++ b/Examples/UICatalog/Scenarios/ColorPicker.cs @@ -190,9 +190,9 @@ public class ColorPickers : Scenario cbShowTextFields.CheckedStateChanging += (_, e) => { - foregroundColorPicker.Style.ShowTextFields = e.NewValue == CheckState.Checked; + foregroundColorPicker.Style.ShowTextFields = e.Result == CheckState.Checked; foregroundColorPicker.ApplyStyleChanges (); - backgroundColorPicker.Style.ShowTextFields = e.NewValue == CheckState.Checked; + backgroundColorPicker.Style.ShowTextFields = e.Result == CheckState.Checked; backgroundColorPicker.ApplyStyleChanges (); }; app.Add (cbShowTextFields); @@ -209,9 +209,9 @@ public class ColorPickers : Scenario cbShowName.CheckedStateChanging += (_, e) => { - foregroundColorPicker.Style.ShowColorName = e.NewValue == CheckState.Checked; + foregroundColorPicker.Style.ShowColorName = e.Result == CheckState.Checked; foregroundColorPicker.ApplyStyleChanges (); - backgroundColorPicker.Style.ShowColorName = e.NewValue == CheckState.Checked; + backgroundColorPicker.Style.ShowColorName = e.Result == CheckState.Checked; backgroundColorPicker.ApplyStyleChanges (); }; app.Add (cbShowName); @@ -226,7 +226,7 @@ public class ColorPickers : Scenario } /// Fired when background color is changed. - private void BackgroundColor_ColorChanged (object sender, EventArgs e) + private void BackgroundColor_ColorChanged (object sender, ResultEventArgs e) { UpdateColorLabel (_backgroundColorLabel, backgroundColorPicker.Visible ? @@ -237,7 +237,7 @@ public class ColorPickers : Scenario } /// Fired when foreground color is changed. - private void ForegroundColor_ColorChanged (object sender, EventArgs e) + private void ForegroundColor_ColorChanged (object sender, ResultEventArgs e) { UpdateColorLabel (_foregroundColorLabel, foregroundColorPicker.Visible ? diff --git a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs b/Examples/UICatalog/Scenarios/DynamicMenuBar.cs index 2741800de..fc61a7a3b 100644 --- a/Examples/UICatalog/Scenarios/DynamicMenuBar.cs +++ b/Examples/UICatalog/Scenarios/DynamicMenuBar.cs @@ -142,9 +142,9 @@ public class DynamicMenuBar : Scenario TextHotKey.TextChanging += (s, e) => { - if (!string.IsNullOrEmpty (e.NewValue) && char.IsLower (e.NewValue [0])) + if (!string.IsNullOrEmpty (e.Result) && char.IsLower (e.Result [0])) { - e.NewValue = e.NewValue.ToUpper (); + e.Result = e.Result.ToUpper (); } }; TextHotKey.TextChanged += (s, _) => TextHotKey.SelectAll (); @@ -208,20 +208,20 @@ public class DynamicMenuBar : Scenario CkbIsTopLevel.CheckedStateChanging += (s, e) => { - if ((_menuItem != null && _menuItem.Parent != null && e.NewValue == CheckState.Checked) - || (_menuItem == null && _hasParent && e.NewValue == CheckState.Checked)) + if ((_menuItem != null && _menuItem.Parent != null && e.Result == CheckState.Checked) + || (_menuItem == null && _hasParent && e.Result == CheckState.Checked)) { MessageBox.ErrorQuery ( "Invalid IsTopLevel", "Only menu bar can have top level menu item!", "Ok" ); - e.Cancel = true; + e.Handled = true; return; } - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { CkbSubMenu.CheckedState = CheckState.UnChecked; CkbSubMenu.SetNeedsDraw (); @@ -243,13 +243,13 @@ public class DynamicMenuBar : Scenario TextAction.Text = ""; TextShortcutKey.Enabled = - e.NewValue == CheckState.Checked && CkbSubMenu.CheckedState == CheckState.UnChecked; + e.Result == CheckState.Checked && CkbSubMenu.CheckedState == CheckState.UnChecked; } }; CkbSubMenu.CheckedStateChanged += (s, e) => { - if (e.CurrentValue == CheckState.Checked) + if (e.Value == CheckState.Checked) { CkbIsTopLevel.CheckedState = CheckState.UnChecked; CkbIsTopLevel.SetNeedsDraw (); @@ -275,7 +275,7 @@ public class DynamicMenuBar : Scenario if (_hasParent) { TextShortcutKey.Enabled = CkbIsTopLevel.CheckedState == CheckState.UnChecked - && e.CurrentValue == CheckState.UnChecked; + && e.Value == CheckState.UnChecked; } } }; @@ -284,7 +284,7 @@ public class DynamicMenuBar : Scenario { if (_menuItem != null) { - _menuItem.AllowNullChecked = e.CurrentValue == CheckState.Checked; + _menuItem.AllowNullChecked = e.Value == CheckState.Checked; } }; @@ -792,13 +792,13 @@ public class DynamicMenuBar : Scenario txtDelimiter.TextChanging += (s, e) => { - if (!string.IsNullOrEmpty (e.NewValue)) + if (!string.IsNullOrEmpty (e.Result)) { - Key.Separator = e.NewValue.ToRunes () [0]; + Key.Separator = e.Result.ToRunes () [0]; } else { - e.Cancel = true; + e.Handled = true; txtDelimiter.SelectAll (); } }; diff --git a/Examples/UICatalog/Scenarios/Editor.cs b/Examples/UICatalog/Scenarios/Editor.cs index ff45264ad..9cf02dc4d 100644 --- a/Examples/UICatalog/Scenarios/Editor.cs +++ b/Examples/UICatalog/Scenarios/Editor.cs @@ -901,14 +901,14 @@ public class Editor : Scenario { X = 0, Y = Pos.Top (txtToFind) + 2, CheckedState = _matchCase ? CheckState.Checked : CheckState.UnChecked, Text = "Match c_ase" }; - ckbMatchCase.CheckedStateChanging += (s, e) => _matchCase = e.NewValue == CheckState.Checked; + ckbMatchCase.CheckedStateChanging += (s, e) => _matchCase = e.Result == CheckState.Checked; d.Add (ckbMatchCase); var ckbMatchWholeWord = new CheckBox { X = 0, Y = Pos.Top (ckbMatchCase) + 1, CheckedState = _matchWholeWord ? CheckState.Checked : CheckState.UnChecked, Text = "Match _whole word" }; - ckbMatchWholeWord.CheckedStateChanging += (s, e) => _matchWholeWord = e.NewValue == CheckState.Checked; + ckbMatchWholeWord.CheckedStateChanging += (s, e) => _matchWholeWord = e.Result == CheckState.Checked; d.Add (ckbMatchWholeWord); return d; } @@ -1159,14 +1159,14 @@ public class Editor : Scenario { X = 0, Y = Pos.Top (txtToFind) + 2, CheckedState = _matchCase ? CheckState.Checked : CheckState.UnChecked, Text = "Match c_ase" }; - ckbMatchCase.CheckedStateChanging += (s, e) => _matchCase = e.NewValue == CheckState.Checked; + ckbMatchCase.CheckedStateChanging += (s, e) => _matchCase = e.Result == CheckState.Checked; d.Add (ckbMatchCase); var ckbMatchWholeWord = new CheckBox { X = 0, Y = Pos.Top (ckbMatchCase) + 1, CheckedState = _matchWholeWord ? CheckState.Checked : CheckState.UnChecked, Text = "Match _whole word" }; - ckbMatchWholeWord.CheckedStateChanging += (s, e) => _matchWholeWord = e.NewValue == CheckState.Checked; + ckbMatchWholeWord.CheckedStateChanging += (s, e) => _matchWholeWord = e.Result == CheckState.Checked; d.Add (ckbMatchWholeWord); return d; diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs index 5ad44324b..d1243b424 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AdornmentEditor.cs @@ -170,7 +170,7 @@ public class AdornmentEditor : EditorBase _diagThicknessCheckBox.CheckedStateChanging += (s, e) => { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { AdornmentToEdit!.Diagnostics |= ViewDiagnosticFlags.Thickness; } @@ -196,7 +196,7 @@ public class AdornmentEditor : EditorBase _diagRulerCheckBox.CheckedStateChanging += (s, e) => { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { AdornmentToEdit!.Diagnostics |= ViewDiagnosticFlags.Ruler; } @@ -210,7 +210,7 @@ public class AdornmentEditor : EditorBase _diagRulerCheckBox.Y = Pos.Bottom (_diagThicknessCheckBox); } - private EventHandler ColorPickerColorChanged () + private EventHandler> ColorPickerColorChanged () { return (o, a) => { diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs index 85572c710..862cc2083 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/AllViewsView.cs @@ -1,4 +1,4 @@ - +#nullable enable namespace UICatalog.Scenarios; public class AllViewsView : View @@ -80,7 +80,7 @@ public class AllViewsView : View if (view is { }) { - FrameView? frame = new () + FrameView frame = new () { CanFocus = true, Title = type.Name, @@ -181,7 +181,7 @@ public class AllViewsView : View view.Height = MAX_VIEW_FRAME_HEIGHT - 2; } - if (!view.Width.Has (out _)) + if (!view.Width!.Has (out _)) { view.Width = Dim.Fill (); } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs index 2e883cc0a..e5c6d0c18 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs @@ -91,9 +91,9 @@ public class BorderEditor : AdornmentEditor SetNeedsLayout (); } - void OnCkbTitleOnToggle (object? _, CancelEventArgs args) + void OnCkbTitleOnToggle (object? _, ResultEventArgs args) { - if (args.NewValue == CheckState.Checked) + if (args.Result == CheckState.Checked) { ((Border)AdornmentToEdit!).Settings |= BorderSettings.Title; @@ -105,9 +105,9 @@ public class BorderEditor : AdornmentEditor } } - void OnCkbGradientOnToggle (object? _, CancelEventArgs args) + void OnCkbGradientOnToggle (object? _, ResultEventArgs args) { - if (args.NewValue == CheckState.Checked) + if (args.Result == CheckState.Checked) { ((Border)AdornmentToEdit!).Settings |= BorderSettings.Gradient; diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs index 2faf3c778..13c2aee8a 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ExpanderButton.cs @@ -43,7 +43,7 @@ public class ExpanderButton : Button Orientation = Orientation.Vertical; - HighlightStyle = HighlightStyle.None; + HighlightStates = Terminal.Gui.ViewBase.MouseState.None; Initialized += ExpanderButton_Initialized; diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs index fc3c89132..9bcf2d213 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/MarginEditor.cs @@ -81,7 +81,7 @@ public class MarginEditor : AdornmentEditor _flagSelectorTransparent.ValueChanged += (_, args) => { - ((Margin)AdornmentToEdit!).ViewportSettings = (ViewportSettingsFlags)args.CurrentValue!; + ((Margin)AdornmentToEdit!).ViewportSettings = (ViewportSettingsFlags)args.Value!; }; diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/SchemeViewer.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/SchemeViewer.cs index 16659aa70..45141e143 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/SchemeViewer.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/SchemeViewer.cs @@ -32,15 +32,15 @@ public class SchemeViewer : FrameView } /// - protected override bool OnSettingSchemeName (in string? currentName, ref string? newName) + protected override bool OnSchemeNameChanging (ValueChangingEventArgs args) { - Title = newName ?? "null"; + Title = args.NewValue ?? "null"; foreach (VisualRoleViewer v in SubViews.OfType ()) { - v.SchemeName = newName; + v.SchemeName = args.NewValue; } - return base.OnSettingSchemeName (in currentName, ref newName); + return base.OnSchemeNameChanging (args); } } diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ThemeViewer.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ThemeViewer.cs index fff8172d8..0141cbfc9 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ThemeViewer.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ThemeViewer.cs @@ -106,7 +106,7 @@ public class ThemeViewer : FrameView } } - private void OnThemeManagerOnThemeChanged (object? _, StringPropertyEventArgs args) { Title = args.NewString!; } + private void OnThemeManagerOnThemeChanged (object? _, EventArgs args) { Title = args.Value!; } protected override void Dispose (bool disposing) { diff --git a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs index 74bd805b9..e54835989 100644 --- a/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs +++ b/Examples/UICatalog/Scenarios/EditorsAndHelpers/ViewportSettingsEditor.cs @@ -115,9 +115,9 @@ public sealed class ViewportSettingsEditor : EditorBase Add (_cbAllowXGreaterThanContentWidth); - void AllowNegativeXToggle (object? sender, CancelEventArgs e) + void AllowNegativeXToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowNegativeX; } @@ -127,9 +127,9 @@ public sealed class ViewportSettingsEditor : EditorBase } } - void AllowXGreaterThanContentWidthToggle (object? sender, CancelEventArgs e) + void AllowXGreaterThanContentWidthToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowXGreaterThanContentWidth; } @@ -153,9 +153,9 @@ public sealed class ViewportSettingsEditor : EditorBase Add (_cbAllowYGreaterThanContentHeight); - void AllowNegativeYToggle (object? sender, CancelEventArgs e) + void AllowNegativeYToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowNegativeY; } @@ -165,9 +165,9 @@ public sealed class ViewportSettingsEditor : EditorBase } } - void AllowYGreaterThanContentHeightToggle (object? sender, CancelEventArgs e) + void AllowYGreaterThanContentHeightToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowYGreaterThanContentHeight; } @@ -243,9 +243,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbClearContentOnly.CheckedStateChanging += ClearContentOnlyToggle; - void ClearContentOnlyToggle (object? sender, CancelEventArgs e) + void ClearContentOnlyToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.ClearContentOnly; } @@ -264,9 +264,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbClipContentOnly.CheckedStateChanging += ClipContentOnlyToggle; - void ClipContentOnlyToggle (object? sender, CancelEventArgs e) + void ClipContentOnlyToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.ClipContentOnly; } @@ -285,9 +285,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbTransparent.CheckedStateChanging += TransparentToggle; - void TransparentToggle (object? sender, CancelEventArgs e) + void TransparentToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.Transparent; } @@ -306,9 +306,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbTransparentMouse.CheckedStateChanging += TransparentMouseToggle; - void TransparentMouseToggle (object? sender, CancelEventArgs e) + void TransparentMouseToggle (object? sender, ResultEventArgs e) { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { ViewToEdit!.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.TransparentMouse; } @@ -327,9 +327,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbVerticalScrollBar.CheckedStateChanging += VerticalScrollBarToggle; - void VerticalScrollBarToggle (object? sender, CancelEventArgs e) + void VerticalScrollBarToggle (object? sender, ResultEventArgs e) { - ViewToEdit!.VerticalScrollBar.Visible = e.NewValue == CheckState.Checked; + ViewToEdit!.VerticalScrollBar.Visible = e.Result == CheckState.Checked; } _cbAutoShowVerticalScrollBar = new () @@ -341,9 +341,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbAutoShowVerticalScrollBar.CheckedStateChanging += AutoShowVerticalScrollBarToggle; - void AutoShowVerticalScrollBarToggle (object? sender, CancelEventArgs e) + void AutoShowVerticalScrollBarToggle (object? sender, ResultEventArgs e) { - ViewToEdit!.VerticalScrollBar.AutoShow = e.NewValue == CheckState.Checked; + ViewToEdit!.VerticalScrollBar.AutoShow = e.Result == CheckState.Checked; } _cbHorizontalScrollBar = new () @@ -355,9 +355,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbHorizontalScrollBar.CheckedStateChanging += HorizontalScrollBarToggle; - void HorizontalScrollBarToggle (object? sender, CancelEventArgs e) + void HorizontalScrollBarToggle (object? sender, ResultEventArgs e) { - ViewToEdit!.HorizontalScrollBar.Visible = e.NewValue == CheckState.Checked; + ViewToEdit!.HorizontalScrollBar.Visible = e.Result == CheckState.Checked; } _cbAutoShowHorizontalScrollBar = new () @@ -369,9 +369,9 @@ public sealed class ViewportSettingsEditor : EditorBase }; _cbAutoShowHorizontalScrollBar.CheckedStateChanging += AutoShowHorizontalScrollBarToggle; - void AutoShowHorizontalScrollBarToggle (object? sender, CancelEventArgs e) + void AutoShowHorizontalScrollBarToggle (object? sender, ResultEventArgs e) { - ViewToEdit!.HorizontalScrollBar.AutoShow = e.NewValue == CheckState.Checked; + ViewToEdit!.HorizontalScrollBar.AutoShow = e.Result == CheckState.Checked; } Add ( diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index 048974592..42e3d7ce7 100644 --- a/Examples/UICatalog/Scenarios/Images.cs +++ b/Examples/UICatalog/Scenarios/Images.cs @@ -111,8 +111,8 @@ public class Images : Scenario _cbSupportsSixel.CheckedStateChanging += (s, e) => { - _sixelSupportResult.IsSupported = e.NewValue == CheckState.Checked; - SetupSixelSupported (e.NewValue == CheckState.Checked); + _sixelSupportResult.IsSupported = e.Result == CheckState.Checked; + SetupSixelSupported (e.Result == CheckState.Checked); ApplyShowTabViewHack (); }; @@ -126,7 +126,7 @@ public class Images : Scenario Enabled = canTrueColor, Text = "Use true color" }; - cbUseTrueColor.CheckedStateChanging += (_, evt) => Application.Force16Colors = evt.NewValue == CheckState.UnChecked; + cbUseTrueColor.CheckedStateChanging += (_, evt) => Application.Force16Colors = evt.Result == CheckState.UnChecked; _win.Add (cbUseTrueColor); var btnOpenImage = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 0, Text = "Open Image" }; diff --git a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs index f85649da7..123e4d54a 100644 --- a/Examples/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/Examples/UICatalog/Scenarios/ListViewWithSelection.cs @@ -123,9 +123,9 @@ public class ListViewWithSelection : Scenario Application.Shutdown (); } - private void CustomRenderCB_Toggle (object sender, CancelEventArgs stateEventArgs) + private void CustomRenderCB_Toggle (object sender, ResultEventArgs stateEventArgs) { - if (stateEventArgs.CurrentValue == CheckState.Checked) + if (stateEventArgs.Result == CheckState.Checked) { _listView.SetSource (_scenarios); } @@ -137,23 +137,23 @@ public class ListViewWithSelection : Scenario _appWindow.SetNeedsDraw (); } - private void AllowsMarkingCB_Toggle (object sender, [NotNull] CancelEventArgs stateEventArgs) + private void AllowsMarkingCB_Toggle (object sender, [NotNull] ResultEventArgs stateEventArgs) { - _listView.AllowsMarking = stateEventArgs.NewValue == CheckState.Checked; + _listView.AllowsMarking = stateEventArgs.Result == CheckState.Checked; _allowMultipleCb.Enabled = _listView.AllowsMarking; _appWindow.SetNeedsDraw (); } - private void AllowsMultipleSelectionCB_Toggle (object sender, [NotNull] CancelEventArgs stateEventArgs) + private void AllowsMultipleSelectionCB_Toggle (object sender, [NotNull] ResultEventArgs stateEventArgs) { - _listView.AllowsMultipleSelection = stateEventArgs.NewValue == CheckState.Checked; + _listView.AllowsMultipleSelection = stateEventArgs.Result == CheckState.Checked; _appWindow.SetNeedsDraw (); } - private void AllowYGreaterThanContentHeightCB_Toggle (object sender, [NotNull] CancelEventArgs stateEventArgs) + private void AllowYGreaterThanContentHeightCB_Toggle (object sender, [NotNull] ResultEventArgs stateEventArgs) { - if (stateEventArgs.NewValue == CheckState.Checked) + if (stateEventArgs.Result == CheckState.Checked) { _listView.ViewportSettings |= Terminal.Gui.ViewBase.ViewportSettingsFlags.AllowYGreaterThanContentHeight; } diff --git a/Examples/UICatalog/Scenarios/Mouse.cs b/Examples/UICatalog/Scenarios/Mouse.cs index e7919c91f..658789cf6 100644 --- a/Examples/UICatalog/Scenarios/Mouse.cs +++ b/Examples/UICatalog/Scenarios/Mouse.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; +using System.Collections.ObjectModel; namespace UICatalog.Scenarios; -[ScenarioMetadata ("Mouse", "Demonstrates how to capture mouse events")] +[ScenarioMetadata ("Mouse", "Demonstrates Mouse Events and States")] [ScenarioCategory ("Mouse and Keyboard")] public class Mouse : Scenario { @@ -15,6 +12,7 @@ public class Mouse : Scenario Window win = new () { + Id = "win", Title = GetQuitKeyAndName () }; @@ -73,104 +71,151 @@ public class Mouse : Scenario win.Add (cbWantContinuousPresses); - CheckBox cbHighlightOnPress = new () + CheckBox cbHighlightOnPressed = new () { X = Pos.Right (filterSlider), Y = Pos.Bottom (cbWantContinuousPresses), - Title = "_Highlight on Press" + Title = "_Highlight on Pressed" }; - win.Add (cbHighlightOnPress); + win.Add (cbHighlightOnPressed); + + CheckBox cbHighlightOnPressedOutside = new () + { + X = Pos.Right (filterSlider), + Y = Pos.Bottom (cbHighlightOnPressed), + Title = "_Highlight on PressedOutside" + }; + + win.Add (cbHighlightOnPressedOutside); var demo = new MouseEventDemoView { + Id = "demo", X = Pos.Right (filterSlider), - Y = Pos.Bottom (cbHighlightOnPress), + Y = Pos.Bottom (cbHighlightOnPressedOutside), Width = Dim.Fill (), Height = 15, - Title = "Enter/Leave Demo", + Title = "Enter/Leave Demo" }; - demo.Padding.Initialized += DemoPaddingOnInitialized; + demo.Padding!.Initialized += DemoPaddingOnInitialized; void DemoPaddingOnInitialized (object o, EventArgs eventArgs) { - demo.Padding.Add ( - new MouseEventDemoView () - { - X = 0, - Y = 0, - Width = Dim.Fill (), - Height = Dim.Func (() => demo.Padding.Thickness.Top), - Title = "inPadding", - Id = "inPadding" - }); + demo.Padding!.Add ( + new MouseEventDemoView + { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Func (() => demo.Padding.Thickness.Top), + Title = "inPadding", + Id = "inPadding" + }); demo.Padding.Thickness = demo.Padding.Thickness with { Top = 5 }; } - View sub1 = new MouseEventDemoView () + View sub1 = new MouseEventDemoView { X = 0, Y = 0, Width = Dim.Percent (20), Height = Dim.Fill (), Title = "sub1", - Id = "sub1", + Id = "sub1" }; demo.Add (sub1); - demo.Add ( - new MouseEventDemoView () - { - X = Pos.Right (sub1) - 4, - Y = Pos.Top (sub1) + 1, - Width = Dim.Percent (20), - Height = Dim.Fill (1), - Title = "sub2", - Id = "sub2", - }); + View sub2 = new MouseEventDemoView + { + X = Pos.Right (sub1) - 4, + Y = Pos.Top (sub1) + 1, + Width = Dim.Percent (20), + Height = Dim.Fill (1), + Title = "sub2", + Id = "sub2" + }; + + demo.Add (sub2); win.Add (demo); - cbHighlightOnPress.CheckedState = demo.HighlightStyle == (HighlightStyle.Pressed | HighlightStyle.PressedOutside) ? CheckState.Checked : CheckState.UnChecked; + cbHighlightOnPressed.CheckedState = demo.HighlightStates.HasFlag (MouseState.Pressed) ? CheckState.Checked : CheckState.UnChecked; - // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/3753 - cbHighlightOnPress.CheckedStateChanging += (s, e) => - { - if (e.NewValue == CheckState.Checked) - { - demo.HighlightStyle = HighlightStyle.Pressed | HighlightStyle.PressedOutside; - } - else - { - demo.HighlightStyle = HighlightStyle.None; - } + cbHighlightOnPressed.CheckedStateChanging += (s, e) => + { + if (e.Result == CheckState.Checked) + { + demo.HighlightStates |= MouseState.Pressed; + } + else + { + demo.HighlightStates &= ~MouseState.Pressed; + } - foreach (View subview in demo.SubViews) - { - if (e.NewValue == CheckState.Checked) - { - subview.HighlightStyle = HighlightStyle.Pressed | HighlightStyle.PressedOutside; - } - else - { - subview.HighlightStyle = HighlightStyle.None; - } - } + foreach (View subview in demo.SubViews) + { + if (e.Result == CheckState.Checked) + { + subview.HighlightStates |= MouseState.Pressed; + } + else + { + subview.HighlightStates &= ~MouseState.Pressed; + } + } - foreach (View subview in demo.Padding.SubViews) - { - if (e.NewValue == CheckState.Checked) - { - subview.HighlightStyle = HighlightStyle.Pressed | HighlightStyle.PressedOutside; - } - else - { - subview.HighlightStyle = HighlightStyle.None; - } - } + foreach (View subview in demo.Padding.SubViews) + { + if (e.Result == CheckState.Checked) + { + subview.HighlightStates |= MouseState.Pressed; + } + else + { + subview.HighlightStates &= ~MouseState.Pressed; + } + } + }; - }; + cbHighlightOnPressedOutside.CheckedState = demo.HighlightStates.HasFlag (MouseState.PressedOutside) ? CheckState.Checked : CheckState.UnChecked; + + cbHighlightOnPressedOutside.CheckedStateChanging += (s, e) => + { + if (e.Result == CheckState.Checked) + { + demo.HighlightStates |= MouseState.PressedOutside; + } + else + { + demo.HighlightStates &= ~MouseState.PressedOutside; + } + + foreach (View subview in demo.SubViews) + { + if (e.Result == CheckState.Checked) + { + subview.HighlightStates |= MouseState.PressedOutside; + } + else + { + subview.HighlightStates &= ~MouseState.PressedOutside; + } + } + + foreach (View subview in demo.Padding.SubViews) + { + if (e.Result == CheckState.Checked) + { + subview.HighlightStates |= MouseState.PressedOutside; + } + else + { + subview.HighlightStates &= ~MouseState.PressedOutside; + } + } + }; cbWantContinuousPresses.CheckedStateChanging += (s, e) => { @@ -185,10 +230,8 @@ public class Mouse : Scenario { subview.WantContinuousButtonPressed = demo.WantContinuousButtonPressed; } - }; - var label = new Label { Text = "_App Events:", @@ -241,12 +284,12 @@ public class Mouse : Scenario win.Add (label, winLog); clearButton.Accepting += (s, e) => - { - appLogList.Clear (); - appLog.SetSource (appLogList); - winLogList.Clear (); - winLog.SetSource (winLogList); - }; + { + appLogList.Clear (); + appLog.SetSource (appLogList); + winLogList.Clear (); + winLog.SetSource (winLogList); + }; win.MouseEvent += (sender, a) => { @@ -277,42 +320,56 @@ public class Mouse : Scenario CanFocus = true; Id = "mouseEventDemoView"; - Padding!.Thickness = new Thickness (1, 1, 1, 1); - Initialized += OnInitialized; + MouseLeave += (s, e) => { Text = "Leave"; }; + MouseEnter += (s, e) => { Text = "Enter"; }; + + return; + void OnInitialized (object sender, EventArgs e) { TextAlignment = Alignment.Center; VerticalTextAlignment = Alignment.Center; - Padding!.SetScheme (new Scheme (new Attribute (Color.Black))); + Padding!.Thickness = new (1, 1, 1, 1); + Padding!.SetScheme (new (new Attribute (Color.Black))); + Padding.Id = $"{Id}.Padding"; - Padding.MouseEnter += PaddingOnMouseEnter; - Padding.MouseLeave += PaddingOnMouseLeave; - - void PaddingOnMouseEnter (object o, CancelEventArgs e) - { - Padding.SchemeName = "Error"; - } - - void PaddingOnMouseLeave (object o, EventArgs e) - { - Padding.SchemeName = "Dialog"; - } - - Border!.Thickness = new Thickness (1); + Border!.Thickness = new (1); Border.LineStyle = LineStyle.Rounded; + Border.Id = $"{Id}.Border"; + + MouseStateChanged += (_, args) => + { + if (args.Value.HasFlag (MouseState.PressedOutside)) + { + Border.LineStyle = LineStyle.Dotted; + } + else + { + Border.LineStyle = LineStyle.Single; + } + + SetNeedsDraw (); + }; + } + } + + /// + protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute) + { + if (role == VisualRole.Normal) + { + if (MouseState.HasFlag (MouseState.Pressed) && HighlightStates.HasFlag (MouseState.Pressed)) + { + currentAttribute = currentAttribute with { Background = currentAttribute.Foreground.GetBrighterColor () }; + + return true; + } } - MouseLeave += (s, e) => - { - Text = "Leave"; - }; - MouseEnter += (s, e) => - { - Text = "Enter"; - }; + return base.OnGettingAttributeForRole (in role, ref currentAttribute); } } } diff --git a/Examples/UICatalog/Scenarios/Navigation.cs b/Examples/UICatalog/Scenarios/Navigation.cs index ccc2eb7fa..7a3390c6f 100644 --- a/Examples/UICatalog/Scenarios/Navigation.cs +++ b/Examples/UICatalog/Scenarios/Navigation.cs @@ -220,9 +220,9 @@ public class Navigation : Scenario return; - void ColorPicker_ColorChanged (object sender, ColorEventArgs e) + void ColorPicker_ColorChanged (object sender, ResultEventArgs e) { - testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.CurrentValue) }); + testFrame.SetScheme (testFrame.GetScheme () with { Normal = new (testFrame.GetAttributeForRole (VisualRole.Normal).Foreground, e.Result) }); } } diff --git a/Examples/UICatalog/Scenarios/PosAlignDemo.cs b/Examples/UICatalog/Scenarios/PosAlignDemo.cs index f7cd5ec20..5a9317aac 100644 --- a/Examples/UICatalog/Scenarios/PosAlignDemo.cs +++ b/Examples/UICatalog/Scenarios/PosAlignDemo.cs @@ -101,14 +101,14 @@ public sealed class PosAlignDemo : Scenario { if (dimension == Dimension.Width) { - _horizAligner.AlignmentModes = e.NewValue == CheckState.Checked + _horizAligner.AlignmentModes = e.Result == CheckState.Checked ? _horizAligner.AlignmentModes | AlignmentModes.EndToStart : _horizAligner.AlignmentModes & ~AlignmentModes.EndToStart; UpdatePosAlignObjects (appWindow, dimension, _horizAligner); } else { - _vertAligner.AlignmentModes = e.NewValue == CheckState.Checked + _vertAligner.AlignmentModes = e.Result == CheckState.Checked ? _vertAligner.AlignmentModes | AlignmentModes.EndToStart : _vertAligner.AlignmentModes & ~AlignmentModes.EndToStart; UpdatePosAlignObjects (appWindow, dimension, _vertAligner); @@ -139,14 +139,14 @@ public sealed class PosAlignDemo : Scenario { if (dimension == Dimension.Width) { - _horizAligner.AlignmentModes = e.NewValue == CheckState.Checked + _horizAligner.AlignmentModes = e.Result == CheckState.Checked ? _horizAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast : _horizAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast; UpdatePosAlignObjects (appWindow, dimension, _horizAligner); } else { - _vertAligner.AlignmentModes = e.NewValue == CheckState.Checked + _vertAligner.AlignmentModes = e.Result == CheckState.Checked ? _vertAligner.AlignmentModes | AlignmentModes.IgnoreFirstOrLast : _vertAligner.AlignmentModes & ~AlignmentModes.IgnoreFirstOrLast; UpdatePosAlignObjects (appWindow, dimension, _vertAligner); @@ -177,14 +177,14 @@ public sealed class PosAlignDemo : Scenario { if (dimension == Dimension.Width) { - _horizAligner.AlignmentModes = e.NewValue == CheckState.Checked + _horizAligner.AlignmentModes = e.Result == CheckState.Checked ? _horizAligner.AlignmentModes | AlignmentModes.AddSpaceBetweenItems : _horizAligner.AlignmentModes & ~AlignmentModes.AddSpaceBetweenItems; UpdatePosAlignObjects (appWindow, dimension, _horizAligner); } else { - _vertAligner.AlignmentModes = e.NewValue == CheckState.Checked + _vertAligner.AlignmentModes = e.Result == CheckState.Checked ? _vertAligner.AlignmentModes | AlignmentModes.AddSpaceBetweenItems : _vertAligner.AlignmentModes & ~AlignmentModes.AddSpaceBetweenItems; UpdatePosAlignObjects (appWindow, dimension, _vertAligner); @@ -214,12 +214,12 @@ public sealed class PosAlignDemo : Scenario { if (dimension == Dimension.Width) { - _leftMargin = e.NewValue == CheckState.Checked ? 1 : 0; + _leftMargin = e.Result == CheckState.Checked ? 1 : 0; UpdatePosAlignObjects (appWindow, dimension, _horizAligner); } else { - _topMargin = e.NewValue == CheckState.Checked ? 1 : 0; + _topMargin = e.Result == CheckState.Checked ? 1 : 0; UpdatePosAlignObjects (appWindow, dimension, _vertAligner); } }; diff --git a/Examples/UICatalog/Scenarios/ProgressBarStyles.cs b/Examples/UICatalog/Scenarios/ProgressBarStyles.cs index 368fabd1e..638303466 100644 --- a/Examples/UICatalog/Scenarios/ProgressBarStyles.cs +++ b/Examples/UICatalog/Scenarios/ProgressBarStyles.cs @@ -268,7 +268,7 @@ public class ProgressBarStyles : Scenario ckbBidirectional.CheckedStateChanging += (s, e) => { marqueesBlocksPB.BidirectionalMarquee = - marqueesContinuousPB.BidirectionalMarquee = e.NewValue == CheckState.Checked; + marqueesContinuousPB.BidirectionalMarquee = e.Result == CheckState.Checked; }; diff --git a/Examples/UICatalog/Scenarios/ScrollBarDemo.cs b/Examples/UICatalog/Scenarios/ScrollBarDemo.cs index 50abe30c0..7376a7b54 100644 --- a/Examples/UICatalog/Scenarios/ScrollBarDemo.cs +++ b/Examples/UICatalog/Scenarios/ScrollBarDemo.cs @@ -274,7 +274,7 @@ public class ScrollBarDemo : Scenario Text = $"_AutoShow", CheckedState = scrollBar.AutoShow ? CheckState.Checked : CheckState.UnChecked }; - autoShow.CheckedStateChanging += (s, e) => scrollBar.AutoShow = e.NewValue == CheckState.Checked; + autoShow.CheckedStateChanging += (s, e) => scrollBar.AutoShow = e.Result == CheckState.Checked; demoFrame.Add (autoShow); var lblSliderPosition = new Label @@ -352,33 +352,33 @@ public class ScrollBarDemo : Scenario { scrollBar.ScrollableContentSizeChanged += (s, e) => { - eventLog.Log ($"SizeChanged: {e.CurrentValue}"); + eventLog.Log ($"SizeChanged: {e.Value}"); - if (scrollContentSize.Value != e.CurrentValue) + if (scrollContentSize.Value != e.Value) { - scrollContentSize.Value = e.CurrentValue; + scrollContentSize.Value = e.Value; } }; scrollBar.SliderPositionChanged += (s, e) => { - eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}"); + eventLog.Log ($"SliderPositionChanged: {e.Value}"); eventLog.Log ($" Position: {scrollBar.Position}"); - scrollSliderPosition.Text = e.CurrentValue.ToString (); + scrollSliderPosition.Text = e.Value.ToString (); }; scrollBar.Scrolled += (s, e) => { - eventLog.Log ($"Scrolled: {e.CurrentValue}"); + eventLog.Log ($"Scrolled: {e.Value}"); eventLog.Log ($" SliderPosition: {scrollBar.GetSliderPosition ()}"); - scrolled.Text = e.CurrentValue.ToString (); + scrolled.Text = e.Value.ToString (); }; scrollBar.PositionChanged += (s, e) => { - eventLog.Log ($"PositionChanged: {e.CurrentValue}"); - scrollPosition.Value = e.CurrentValue; - controlledList.Viewport = controlledList.Viewport with { Y = e.CurrentValue }; + eventLog.Log ($"PositionChanged: {e.Value}"); + scrollPosition.Value = e.Value; + controlledList.Viewport = controlledList.Viewport with { Y = e.Value }; }; diff --git a/Examples/UICatalog/Scenarios/Scrolling.cs b/Examples/UICatalog/Scenarios/Scrolling.cs index 319afb9c1..14fa5b193 100644 --- a/Examples/UICatalog/Scenarios/Scrolling.cs +++ b/Examples/UICatalog/Scenarios/Scrolling.cs @@ -53,7 +53,7 @@ public class Scrolling : Scenario CheckedState = demoView.HorizontalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked }; app.Add (hCheckBox); - hCheckBox.CheckedStateChanged += (sender, args) => { demoView.HorizontalScrollBar.Visible = args.CurrentValue == CheckState.Checked; }; + hCheckBox.CheckedStateChanged += (sender, args) => { demoView.HorizontalScrollBar.Visible = args.Value == CheckState.Checked; }; //// NOTE: This call to EnableScrollBar is technically not needed because the reference //// NOTE: to demoView.HorizontalScrollBar below will cause it to be lazy created. @@ -67,7 +67,7 @@ public class Scrolling : Scenario CheckedState = demoView.VerticalScrollBar.Visible ? CheckState.Checked : CheckState.UnChecked }; app.Add (vCheckBox); - vCheckBox.CheckedStateChanged += (sender, args) => { demoView.VerticalScrollBar.Visible = args.CurrentValue == CheckState.Checked; }; + vCheckBox.CheckedStateChanged += (sender, args) => { demoView.VerticalScrollBar.Visible = args.Value == CheckState.Checked; }; var ahCheckBox = new CheckBox { @@ -79,8 +79,8 @@ public class Scrolling : Scenario ahCheckBox.CheckedStateChanging += (s, e) => { - demoView.HorizontalScrollBar.AutoShow = e.NewValue == CheckState.Checked; - demoView.VerticalScrollBar.AutoShow = e.NewValue == CheckState.Checked; + demoView.HorizontalScrollBar.AutoShow = e.Result == CheckState.Checked; + demoView.VerticalScrollBar.AutoShow = e.Result == CheckState.Checked; }; app.Add (ahCheckBox); diff --git a/Examples/UICatalog/Scenarios/ShadowStyles.cs b/Examples/UICatalog/Scenarios/ShadowStyles.cs index 3c0e39f81..25a2a335c 100644 --- a/Examples/UICatalog/Scenarios/ShadowStyles.cs +++ b/Examples/UICatalog/Scenarios/ShadowStyles.cs @@ -82,7 +82,7 @@ public class ShadowStyles : Scenario colorPicker.ColorChanged += (sender, args) => { var normal = app.GetScheme ().Normal; - app.SetScheme (app.GetScheme() with {Normal = new Attribute(normal.Foreground, args.CurrentValue)}); + app.SetScheme (app.GetScheme() with {Normal = new Attribute(normal.Foreground, args.Result)}); }; app.Add (button, colorPicker); diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index 58dd77d79..0ad863e94 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -64,7 +64,7 @@ public class Shortcuts : Scenario { Text = "_Align Keys", CanFocus = false, - HighlightStyle = HighlightStyle.None + HighlightStates = MouseState.None }, Key = Key.F5.WithCtrl.WithAlt.WithShift }; @@ -85,7 +85,7 @@ public class Shortcuts : Scenario v => v is Shortcut { Width: not DimAbsolute }); IEnumerable enumerable = toAlign as View [] ?? toAlign.ToArray (); - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { max = (from Shortcut? peer in enumerable select peer.Key.ToString ().GetColumns ()).Prepend (max) @@ -118,7 +118,7 @@ public class Shortcuts : Scenario { Text = "Command _First", CanFocus = false, - HighlightStyle = HighlightStyle.None + HighlightStates = MouseState.None }, Key = Key.F.WithCtrl }; @@ -143,7 +143,7 @@ public class Shortcuts : Scenario { var peer = (Shortcut)view; - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { peer.AlignmentModes &= ~AlignmentModes.EndToStart; } @@ -181,7 +181,7 @@ public class Shortcuts : Scenario { if (peer.CanFocus) { - peer.CommandView.CanFocus = e.NewValue == CheckState.Checked; + peer.CommandView.CanFocus = e.Result == CheckState.Checked; } } } @@ -213,7 +213,7 @@ public class Shortcuts : Scenario { Title = "_Button", ShadowStyle = ShadowStyle.None, - HighlightStyle = HighlightStyle.None + HighlightStates = MouseState.None }, Key = Key.K }; @@ -449,7 +449,7 @@ public class Shortcuts : Scenario { if (o is { }) { - eventSource.Add ($"ColorChanged: {o.GetType ().Name} - {args.CurrentValue}"); + eventSource.Add ($"ColorChanged: {o.GetType ().Name} - {args.Result}"); eventLog.MoveDown (); Application.Top.SetScheme ( @@ -457,7 +457,7 @@ public class Shortcuts : Scenario { Normal = new ( Application.Top!.GetAttributeForRole (VisualRole.Normal).Foreground, - args.CurrentValue, + args.Result, Application.Top!.GetAttributeForRole (VisualRole.Normal).Style) }); } diff --git a/Examples/UICatalog/Scenarios/SpinnerStyles.cs b/Examples/UICatalog/Scenarios/SpinnerStyles.cs index 826eb04aa..e9e923ae7 100644 --- a/Examples/UICatalog/Scenarios/SpinnerStyles.cs +++ b/Examples/UICatalog/Scenarios/SpinnerStyles.cs @@ -162,9 +162,9 @@ public class SpinnerViewStyles : Scenario } }; - ckbReverse.CheckedStateChanging += (s, e) => { spinner.SpinReverse = e.NewValue == CheckState.Checked; }; + ckbReverse.CheckedStateChanging += (s, e) => { spinner.SpinReverse = e.Result == CheckState.Checked; }; - ckbBounce.CheckedStateChanging += (s, e) => { spinner.SpinBounce = e.NewValue == CheckState.Checked; }; + ckbBounce.CheckedStateChanging += (s, e) => { spinner.SpinBounce = e.Result == CheckState.Checked; }; app.Unloaded += App_Unloaded; diff --git a/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs b/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs index f8666a1bc..d1a668dd5 100644 --- a/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs +++ b/Examples/UICatalog/Scenarios/TextAlignmentAndDirection.cs @@ -501,7 +501,7 @@ public class TextAlignmentAndDirection : Scenario Enabled = false }; - justifyCheckbox.CheckedStateChanging += (s, e) => ToggleJustify (e.NewValue != CheckState.Checked); + justifyCheckbox.CheckedStateChanging += (s, e) => ToggleJustify (e.Result != CheckState.Checked); justifyOptions.SelectedItemChanged += (s, e) => { ToggleJustify (false, true); }; @@ -521,7 +521,7 @@ public class TextAlignmentAndDirection : Scenario wrapCheckbox.CheckedStateChanging += (s, e) => { - if (e.CurrentValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { foreach (View t in multiLineLabels) { diff --git a/Examples/UICatalog/Scenarios/TextEffectsScenario.cs b/Examples/UICatalog/Scenarios/TextEffectsScenario.cs index 838ef4941..15e9db4c6 100644 --- a/Examples/UICatalog/Scenarios/TextEffectsScenario.cs +++ b/Examples/UICatalog/Scenarios/TextEffectsScenario.cs @@ -56,7 +56,7 @@ public class TextEffectsScenario : Scenario cbLooping.CheckedStateChanging += (s, e) => { - _loopingGradient = e.NewValue == CheckState.Checked; + _loopingGradient = e.Result == CheckState.Checked; SetupGradientLineCanvas (w, w.Frame.Size); }; diff --git a/Examples/UICatalog/Scenarios/TextFormatterDemo.cs b/Examples/UICatalog/Scenarios/TextFormatterDemo.cs index 08fba2c60..08319eefb 100644 --- a/Examples/UICatalog/Scenarios/TextFormatterDemo.cs +++ b/Examples/UICatalog/Scenarios/TextFormatterDemo.cs @@ -124,8 +124,8 @@ public class TextFormatterDemo : Scenario { for (int i = 0; i < alignments.Count; i++) { - singleLines [i].Text = e.CurrentValue == CheckState.Checked ? text : unicode; - multipleLines [i].Text = e.CurrentValue == CheckState.Checked ? text : unicode; + singleLines [i].Text = e.Result == CheckState.Checked ? text : unicode; + multipleLines [i].Text = e.Result == CheckState.Checked ? text : unicode; } }; diff --git a/Examples/UICatalog/Scenarios/TextInputControls.cs b/Examples/UICatalog/Scenarios/TextInputControls.cs index d02a49fe0..797d83f2f 100644 --- a/Examples/UICatalog/Scenarios/TextInputControls.cs +++ b/Examples/UICatalog/Scenarios/TextInputControls.cs @@ -33,9 +33,9 @@ public class TextInputControls : Scenario textField.Autocomplete.SuggestionGenerator = singleWordGenerator; textField.TextChanging += TextFieldTextChanging; - void TextFieldTextChanging (object sender, CancelEventArgs e) + void TextFieldTextChanging (object sender, ResultEventArgs e) { - singleWordGenerator.AllSuggestions = Regex.Matches (e.NewValue, "\\w+") + singleWordGenerator.AllSuggestions = Regex.Matches (e.Result, "\\w+") .Select (s => s.Value) .Distinct () .ToList (); @@ -121,7 +121,7 @@ public class TextInputControls : Scenario { X = Pos.Left (textView), Y = Pos.Bottom (textView), CheckedState = textView.ReadOnly ? CheckState.Checked : CheckState.UnChecked, Text = "Read_Only" }; - chxReadOnly.CheckedStateChanging += (sender, args) => textView.ReadOnly = args.NewValue == CheckState.Checked; + chxReadOnly.CheckedStateChanging += (sender, args) => textView.ReadOnly = args.Result == CheckState.Checked; win.Add (chxReadOnly); // By default TextView is a multi-line control. It can be forced to @@ -139,7 +139,7 @@ public class TextInputControls : Scenario CheckedState = textView.WordWrap ? CheckState.Checked : CheckState.UnChecked, Text = "_Word Wrap" }; - chxWordWrap.CheckedStateChanging += (s, e) => textView.WordWrap = e.NewValue == CheckState.Checked; + chxWordWrap.CheckedStateChanging += (s, e) => textView.WordWrap = e.Result == CheckState.Checked; win.Add (chxWordWrap); // TextView captures Tabs (so users can enter /t into text) by default; @@ -155,7 +155,7 @@ public class TextInputControls : Scenario chxMultiline.CheckedStateChanging += (s, e) => { - textView.Multiline = e.NewValue == CheckState.Checked; + textView.Multiline = e.Result == CheckState.Checked; if (!textView.Multiline && chxWordWrap.CheckedState == CheckState.Checked) { @@ -173,7 +173,7 @@ public class TextInputControls : Scenario chxCaptureTabs.CheckedStateChanging += (s, e) => { - if (e.NewValue == CheckState.Checked) + if (e.Result == CheckState.Checked) { textView.KeyBindings.Add (keyTab, Command.Tab); textView.KeyBindings.Add (keyBackTab, Command.BackTab); @@ -184,7 +184,7 @@ public class TextInputControls : Scenario textView.KeyBindings.Remove (keyBackTab); } - textView.AllowsTab = e.NewValue == CheckState.Checked; + textView.AllowsTab = e.Result == CheckState.Checked; }; win.Add (chxCaptureTabs); diff --git a/Examples/UICatalog/Scenarios/TextStyles.cs b/Examples/UICatalog/Scenarios/TextStyles.cs index 902ad7399..92c9c723b 100644 --- a/Examples/UICatalog/Scenarios/TextStyles.cs +++ b/Examples/UICatalog/Scenarios/TextStyles.cs @@ -81,8 +81,13 @@ public sealed class TestStyles : Scenario { return; } - args.NewValue = args.NewValue with { Style = style }; - args.Cancel = true; + + if (args.Result is { }) + { + args.Result = args.Result.Value with { Style = style }; + } + + args.Handled = true; }; appWindow.Add (button); @@ -125,8 +130,12 @@ public sealed class TestStyles : Scenario }; button.GettingAttributeForRole += (_, args) => { - args.NewValue = args.NewValue with { Style = combination }; - args.Cancel = true; + if (args.Result is { }) + { + args.Result = args.Result.Value with { Style = combination }; + } + + args.Handled = true; }; appWindow.Add (button); } diff --git a/Examples/UICatalog/Scenarios/Themes.cs b/Examples/UICatalog/Scenarios/Themes.cs index 95b71f9f3..af849b6b3 100644 --- a/Examples/UICatalog/Scenarios/Themes.cs +++ b/Examples/UICatalog/Scenarios/Themes.cs @@ -125,26 +125,26 @@ public sealed class Themes : Scenario viewListView.SelectedItem = 0; - themeViewer.SettingSchemeName += (sender, args) => - { - if (_view is { }) - { - Application.Top!.SchemeName = args.NewString; + themeViewer.SchemeNameChanging += (sender, args) => + { + if (_view is { }) + { + Application.Top!.SchemeName = args.NewValue; - if (_view.HasScheme) - { - _view.SetScheme (null); - } + if (_view.HasScheme) + { + _view.SetScheme (null); + } - _view.SchemeName = args.NewString; - } - }; + _view.SchemeName = args.NewValue; + } + }; AllViewsView? allViewsView = null; allViewsCheckBox.CheckedStateChanged += (sender, args) => { - if (args.CurrentValue == CheckState.Checked) + if (args.Value == CheckState.Checked) { viewListView.Visible = false; appWindow.Remove (viewFrame); diff --git a/Examples/UICatalog/Scenarios/TrueColors.cs b/Examples/UICatalog/Scenarios/TrueColors.cs index fb6670903..43e52dec3 100644 --- a/Examples/UICatalog/Scenarios/TrueColors.cs +++ b/Examples/UICatalog/Scenarios/TrueColors.cs @@ -46,7 +46,7 @@ public class TrueColors : Scenario Enabled = canTrueColor, Text = "Force 16 colors" }; - cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.NewValue == CheckState.Checked; }; + cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.Result == CheckState.Checked; }; app.Add (cbUseTrueColor); y += 2; diff --git a/Examples/UICatalog/Scenarios/ViewportSettings.cs b/Examples/UICatalog/Scenarios/ViewportSettings.cs index 4f447233c..7e488d0c7 100644 --- a/Examples/UICatalog/Scenarios/ViewportSettings.cs +++ b/Examples/UICatalog/Scenarios/ViewportSettings.cs @@ -143,7 +143,7 @@ public class ViewportSettings : Scenario { Normal = new ( colorPicker.SuperView.GetAttributeForRole (VisualRole.Normal).Foreground, - e.CurrentValue + e.Result ) }); }; diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 410e75517..fbd1296e8 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -406,7 +406,7 @@ public class UICatalog void ApplicationOnInitializedChanged (object? sender, EventArgs e) { - if (e.CurrentValue) + if (e.Value) { sw.Start (); } diff --git a/Examples/UICatalog/UICatalogTop.cs b/Examples/UICatalog/UICatalogTop.cs index d80ae86d5..f574713c9 100644 --- a/Examples/UICatalog/UICatalogTop.cs +++ b/Examples/UICatalog/UICatalogTop.cs @@ -168,9 +168,9 @@ public class UICatalogTop : Toplevel _force16ColorsMenuItemCb.CheckedStateChanged += (sender, args) => { - Application.Force16Colors = args.CurrentValue == CheckState.Checked; + Application.Force16Colors = args.Value == CheckState.Checked; - _force16ColorsShortcutCb!.CheckedState = args.CurrentValue; + _force16ColorsShortcutCb!.CheckedState = args.Value; Application.LayoutAndDraw (); }; @@ -186,7 +186,7 @@ public class UICatalogTop : Toplevel { _themesRg = new () { - HighlightStyle = HighlightStyle.None, + HighlightStates = Terminal.Gui.ViewBase.MouseState.None, }; _themesRg.SelectedItemChanged += (_, args) => @@ -210,7 +210,7 @@ public class UICatalogTop : Toplevel _topSchemeRg = new () { - HighlightStyle = HighlightStyle.None, + HighlightStates = Terminal.Gui.ViewBase.MouseState.None, }; _topSchemeRg.SelectedItemChanged += (_, args) => @@ -261,7 +261,7 @@ public class UICatalogTop : Toplevel { CanFocus = true, Styles = FlagSelectorStyles.ShowNone, - HighlightStyle = HighlightStyle.None, + HighlightStates = Terminal.Gui.ViewBase.MouseState.None, }; _diagnosticFlagsSelector.UsedHotKeys.Add (Key.D); _diagnosticFlagsSelector.AssignHotKeysToCheckBoxes = true; @@ -287,7 +287,7 @@ public class UICatalogTop : Toplevel CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked }; - _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.CurrentValue == CheckState.Checked; }; + _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; }; menuItems.Add ( new MenuItemv2 @@ -310,7 +310,7 @@ public class UICatalogTop : Toplevel AssignHotKeysToCheckBoxes = true, Options = Enum.GetNames (), SelectedItem = logLevels.ToList ().IndexOf (Enum.Parse (UICatalog.Options.DebugLogLevel)), - HighlightStyle = HighlightStyle.Hover + HighlightStates = Terminal.Gui.ViewBase.MouseState.In }; _logLevelRg.SelectedItemChanged += (_, args) => @@ -634,8 +634,8 @@ public class UICatalogTop : Toplevel _force16ColorsShortcutCb.CheckedStateChanging += (sender, args) => { - Application.Force16Colors = args.NewValue == CheckState.Checked; - _force16ColorsMenuItemCb!.CheckedState = args.NewValue; + Application.Force16Colors = args.Result == CheckState.Checked; + _force16ColorsMenuItemCb!.CheckedState = args.Result; Application.LayoutAndDraw (); }; diff --git a/Terminal.Gui/App/CWP/CWPEventHelper.cs b/Terminal.Gui/App/CWP/CWPEventHelper.cs new file mode 100644 index 000000000..d85d184d5 --- /dev/null +++ b/Terminal.Gui/App/CWP/CWPEventHelper.cs @@ -0,0 +1,56 @@ +#nullable enable +namespace Terminal.Gui.App; + +using System; + + +/// +/// Provides helper methods for executing event-driven workflows in the Cancellable Work Pattern (CWP). +/// +/// +/// +/// Used for workflows where an event is raised to allow cancellation or customization of a result, +/// such as in . The method invokes an +/// event handler and returns whether the operation was handled, supporting result production +/// scenarios with . +/// +/// +/// +public static class CWPEventHelper +{ + /// + /// Executes an event-driven CWP workflow by raising an event. + /// + /// The type of the result in the event arguments. + /// The event handler to invoke, or null if no handler is subscribed. + /// The event arguments, containing a result and handled status. + /// True if the event was handled, false otherwise. + /// Thrown if is null. + /// + /// + /// EventHandler<ResultEventArgs<Key>>? keyDownHandler = (sender, args) => + /// { + /// if (args.Result?.KeyCode == KeyCode.Q | KeyCode.CtrlMask) + /// { + /// args.Handled = true; + /// } + /// }; + /// ResultEventArgs<Key> args = new(new Key(KeyCode.Q | KeyCode.CtrlMask)); + /// bool handled = CWPEventHelper.Execute(keyDownHandler, args); + /// + /// + public static bool Execute ( + EventHandler>? eventHandler, + ResultEventArgs args) + { + ArgumentNullException.ThrowIfNull (args); + + if (eventHandler == null) + { + return false; + } + + eventHandler.Invoke (null, args); + return args.Handled; + } +} \ No newline at end of file diff --git a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs new file mode 100644 index 000000000..fe8ec485c --- /dev/null +++ b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs @@ -0,0 +1,102 @@ +namespace Terminal.Gui.App; + +#nullable enable + +/// +/// Provides helper methods for executing property change workflows in the Cancellable Work Pattern (CWP). +/// +/// +/// +/// Used for workflows where a property value is modified, such as in or +/// , allowing pre- and post-change events to customize or cancel the change. +/// +/// +/// +/// +public static class CWPPropertyHelper +{ + /// + /// Executes a CWP workflow for a property change, with pre- and post-change events. + /// + /// + /// The type of the property value, which may be a nullable reference type (e.g., + /// ?). + /// + /// The current property value, which may be null for nullable types. + /// The proposed new property value, which may be null for nullable types. + /// The virtual method invoked before the change, returning true to cancel. + /// The pre-change event raised to allow modification or cancellation. + /// The virtual method invoked after the change. + /// The post-change event raised to notify of the completed change. + /// + /// The final value after the workflow, reflecting any modifications, which may be null for + /// nullable types. + /// + /// True if the property was changed, false if cancelled. + /// + /// Thrown if is null for non-nullable reference types after the + /// workflow. + /// + /// + /// + /// string? current = null; + /// string? proposed = "Base"; + /// Func<ValueChangingEventArgs<string?>, bool> onChanging = args => false; + /// EventHandler<ValueChangingEventArgs<string?>>? changingEvent = null; + /// Action<ValueChangedEventArgs<string?>>? onChanged = args => + /// Console.WriteLine($"SchemeName changed to {args.NewValue ?? "none"}."); + /// EventHandler<ValueChangedEventArgs<string?>>? changedEvent = null; + /// bool changed = CWPPropertyHelper.ChangeProperty( + /// current, proposed, onChanging, changingEvent, onChanged, changedEvent, out string? final); + /// + /// + public static bool ChangeProperty ( + T currentValue, + T newValue, + Func, bool> onChanging, + EventHandler>? changingEvent, + Action>? onChanged, + EventHandler>? changedEvent, + out T finalValue + ) + { + if (EqualityComparer.Default.Equals (currentValue, newValue)) + { + finalValue = currentValue; + + return false; + } + + ValueChangingEventArgs args = new (currentValue, newValue); + bool cancelled = onChanging (args) || args.Handled; + + if (cancelled) + { + finalValue = currentValue; + + return false; + } + + changingEvent?.Invoke (null, args); + + if (args.Handled) + { + finalValue = currentValue; + + return false; + } + + // Validate NewValue for non-nullable reference types + if (args.NewValue is null && !typeof (T).IsValueType && !Nullable.GetUnderlyingType (typeof (T))?.IsValueType == true) + { + throw new InvalidOperationException ("NewValue cannot be null for non-nullable reference types."); + } + + finalValue = args.NewValue; + ValueChangedEventArgs changedArgs = new (currentValue, finalValue); + onChanged?.Invoke (changedArgs); + changedEvent?.Invoke (null, changedArgs); + + return true; + } +} diff --git a/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs b/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs new file mode 100644 index 000000000..401f17fb8 --- /dev/null +++ b/Terminal.Gui/App/CWP/CWPWorkflowHelper.cs @@ -0,0 +1,129 @@ +#nullable enable +namespace Terminal.Gui.App; + +using System; + + +/// +/// Provides helper methods for executing single-phase and result-producing workflows in the Cancellable Work Pattern (CWP). +/// +/// +/// +/// Used for workflows that allow customization or cancellation, such as command execution +/// (e.g., ) or scheme resolution (e.g., ). +/// The method handles workflows without results, while +/// handles workflows producing results. +/// +/// +/// +public static class CWPWorkflowHelper +{ + /// + /// Executes a single-phase CWP workflow with a virtual method, event, and optional default action. + /// + /// The type of the result in the event arguments. + /// The virtual method invoked first, returning true to mark the workflow as handled. + /// The event handler to invoke, or null if no handlers are subscribed. + /// The event arguments containing a result and handled status. + /// The default action to execute if the workflow is not handled, or null if none. + /// True if the workflow was handled, false if not, or null if no event handlers are subscribed. + /// + /// Thrown if or is null. + /// + /// + /// + /// ResultEventArgs<bool> args = new(); + /// Func<ResultEventArgs<bool>, bool> onAccepting = _ => false; + /// EventHandler<ResultEventArgs<bool>>? acceptingHandler = null; + /// Action? defaultAction = () => args.Result = true; + /// bool? handled = CWPWorkflowHelper.Execute(onAccepting, acceptingHandler, args, defaultAction); + /// + /// + public static bool? Execute ( + Func, bool> onMethod, + EventHandler>? eventHandler, + ResultEventArgs args, + Action? defaultAction = null) + { + ArgumentNullException.ThrowIfNull (onMethod); + ArgumentNullException.ThrowIfNull (args); + + bool handled = onMethod (args) || args.Handled; + if (handled) + { + return true; + } + + eventHandler?.Invoke (null, args); + if (args.Handled) + { + return true; + } + + if (defaultAction is {}) + { + defaultAction (); + return true; + } + + return eventHandler is null ? null : false; + } + + /// + /// Executes a CWP workflow that produces a result, suitable for methods like . + /// + /// The type of the result, which may be a nullable reference type (e.g., ?). + /// The virtual method invoked first, returning true to mark the workflow as handled. + /// The event handler to invoke, or null if no handlers are subscribed. + /// The event arguments containing a result and handled status. + /// The default action that produces the result if the workflow is not handled. + /// The result from the event arguments or the default action. + /// + /// Thrown if , , or is null. + /// + /// + /// Thrown if is null for non-nullable reference types when is true. + /// + /// + /// + /// ResultEventArgs<Scheme?> args = new(); + /// Func<ResultEventArgs<Scheme?>, bool> onGettingScheme = _ => false; + /// EventHandler<ResultEventArgs<Scheme?>>? gettingSchemeHandler = null; + /// Func<Scheme> defaultAction = () => SchemeManager.GetScheme("Base"); + /// Scheme scheme = CWPWorkflowHelper.ExecuteWithResult(onGettingScheme, gettingSchemeHandler, args, defaultAction); + /// + /// + public static TResult ExecuteWithResult ( + Func, bool> onMethod, + EventHandler>? eventHandler, + ResultEventArgs args, + Func defaultAction) + { + ArgumentNullException.ThrowIfNull (onMethod); + ArgumentNullException.ThrowIfNull (args); + ArgumentNullException.ThrowIfNull (defaultAction); + + bool handled = onMethod (args) || args.Handled; + if (handled) + { + if (args.Result is null && !typeof (TResult).IsValueType && !Nullable.GetUnderlyingType (typeof (TResult))?.IsValueType == true) + { + throw new InvalidOperationException ("Result cannot be null for non-nullable reference types when Handled is true."); + } + return args.Result!; + } + + eventHandler?.Invoke (null, args); + + if (!args.Handled) + { + return defaultAction (); + } + + if (args.Result is null && !typeof (TResult).IsValueType && !Nullable.GetUnderlyingType (typeof (TResult))?.IsValueType == true) + { + throw new InvalidOperationException ("Result cannot be null for non-nullable reference types when Handled is true."); + } + return args.Result!; + } +} \ No newline at end of file diff --git a/Terminal.Gui/ViewBase/CancelEventArgs.cs b/Terminal.Gui/App/CWP/CancelEventArgs.cs similarity index 74% rename from Terminal.Gui/ViewBase/CancelEventArgs.cs rename to Terminal.Gui/App/CWP/CancelEventArgs.cs index 2bdd5c20e..7378b722a 100644 --- a/Terminal.Gui/ViewBase/CancelEventArgs.cs +++ b/Terminal.Gui/App/CWP/CancelEventArgs.cs @@ -1,19 +1,20 @@ #nullable enable using System.ComponentModel; -namespace Terminal.Gui.ViewBase; +namespace Terminal.Gui.App; #pragma warning disable CS1711 /// -/// for events that convey changes to a property of type . +/// Provides data for events that can be cancelled without a changeable result in a cancellable workflow in the Cancellable Work Pattern (CWP). /// -/// The type of the value that was part of the change being canceled. /// -/// Events that use this class can be cancellable. Where applicable, the property -/// should be set to -/// to prevent the state change from occurring. +/// Used for workflows where a change (e.g., a simple property change) can be cancelled, but the +/// value being changed is not directly modified by the event handlers. /// +/// The type of the value that is being changed. +/// +/// public class CancelEventArgs : CancelEventArgs where T : notnull { /// Initializes a new instance of the class. diff --git a/Terminal.Gui/App/CWP/EventArgs.cs b/Terminal.Gui/App/CWP/EventArgs.cs new file mode 100644 index 000000000..fe7644264 --- /dev/null +++ b/Terminal.Gui/App/CWP/EventArgs.cs @@ -0,0 +1,22 @@ +#nullable enable +namespace Terminal.Gui.App; + +#pragma warning disable CS1711 +/// +/// Provides data for events that convey the current value of a property or other value in a cancellable workflow (CWP). +/// +/// +/// Used for workflows where the current value of a property or value is being conveyed, such as +/// when a property has been changed. +/// +/// The type of the value. +public class EventArgs : EventArgs /*where T : notnull*/ +{ + /// Initializes a new instance of the class. + /// The current value of the property. + /// The type of the value. + public EventArgs (in T currentValue) { Value = currentValue; } + + /// The current value of the property. + public T Value { get; } +} diff --git a/Terminal.Gui/App/CWP/ResultEventArgs.cs b/Terminal.Gui/App/CWP/ResultEventArgs.cs new file mode 100644 index 000000000..d75627c3b --- /dev/null +++ b/Terminal.Gui/App/CWP/ResultEventArgs.cs @@ -0,0 +1,45 @@ +#nullable enable +namespace Terminal.Gui.App; + +using System; + +#pragma warning disable CS1711 + +/// +/// Provides data for events that produce a result in a cancellable workflow in the Cancellable Work Pattern (CWP). +/// +/// +/// Used for workflows where a result (e.g., outcome, resolution) is +/// being produced or cancelled, such as for methods like . +/// +/// The type of the result. +/// +/// +public class ResultEventArgs +{ + /// + /// Gets or sets the result of the operation, which may be null if no result is provided. + /// + public T? Result { get; set; } + + /// + /// Gets or sets a value indicating whether the operation has been handled. + /// If true, the operation is considered handled and may use the provided result. + /// + public bool Handled { get; set; } + + /// + /// Initializes a new instance of the class with no initial result. + /// + public ResultEventArgs () { } + + /// + /// Initializes a new instance of the class with an initial result. + /// + /// The initial result, which may be null for optional outcomes. + public ResultEventArgs (T? result) + { + Result = result; + } +} +#pragma warning restore CS1711 \ No newline at end of file diff --git a/Terminal.Gui/App/CWP/ValueChangedEventArgs.cs b/Terminal.Gui/App/CWP/ValueChangedEventArgs.cs new file mode 100644 index 000000000..d04c42825 --- /dev/null +++ b/Terminal.Gui/App/CWP/ValueChangedEventArgs.cs @@ -0,0 +1,39 @@ +#nullable enable +namespace Terminal.Gui.App; + +/// +/// Provides data for events that notify of a completed property change in the Cancellable Work Pattern (CWP). +/// +/// +/// +/// Used in post-change events raised by to notify +/// subscribers of a property change, such as in when the +/// property is updated or when the scheme name changes. +/// +/// +/// The type of the property value, which may be a nullable reference type (e.g., ?). +/// +/// +public class ValueChangedEventArgs +{ + /// + /// Gets the value before the change, which may be null for nullable types. + /// + public T OldValue { get; } + + /// + /// Gets the value after the change, which may be null for nullable types. + /// + public T NewValue { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The value before the change, which may be null for nullable types. + /// The value after the change, which may be null for nullable types. + public ValueChangedEventArgs (T oldValue, T newValue) + { + OldValue = oldValue; + NewValue = newValue; + } +} diff --git a/Terminal.Gui/App/CWP/ValueChangingEventArgs.cs b/Terminal.Gui/App/CWP/ValueChangingEventArgs.cs new file mode 100644 index 000000000..fed087b8c --- /dev/null +++ b/Terminal.Gui/App/CWP/ValueChangingEventArgs.cs @@ -0,0 +1,44 @@ +#nullable enable +namespace Terminal.Gui.App; + +/// +/// Provides data for events that allow modification or cancellation of a property change in the Cancellable Work Pattern (CWP). +/// +/// +/// +/// Used in pre-change events raised by to allow handlers to +/// modify the proposed value or cancel the change, such as for or +/// . +/// +/// +/// The type of the property value, which may be a nullable reference type (e.g., ?). +/// +/// +public class ValueChangingEventArgs +{ + /// + /// Gets the current value before the change. + /// + public T CurrentValue { get; } + + /// + /// Gets or sets the proposed new value, which can be modified by event handlers. + /// + public T NewValue { get; set; } + + /// + /// Gets or sets a value indicating whether the change has been handled. If true, the change is cancelled. + /// + public bool Handled { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The current value before the change, which may be null for nullable types. + /// The proposed new value, which may be null for nullable types. + public ValueChangingEventArgs (T currentValue, T newValue) + { + CurrentValue = currentValue; + NewValue = newValue; + } +} \ No newline at end of file diff --git a/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs b/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs index 3ac43d9ee..24a325ab7 100644 --- a/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs +++ b/Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs @@ -9,3 +9,15 @@ public class ConfigurationManagerEventArgs : EventArgs public ConfigurationManagerEventArgs () { } } + +//public class ConfigurationLoadEventArgs : ResultEventArgs +//{ +// public ConfigLocations Location { get; } +// public string? Path { get; } + +// public ConfigurationLoadEventArgs (ConfigLocations location, string? path) +// { +// Location = location; +// Path = path; +// } +//} diff --git a/Terminal.Gui/Configuration/SourceGenerationContext.cs b/Terminal.Gui/Configuration/SourceGenerationContext.cs index 82cd4bb84..15c74cb90 100644 --- a/Terminal.Gui/Configuration/SourceGenerationContext.cs +++ b/Terminal.Gui/Configuration/SourceGenerationContext.cs @@ -24,7 +24,7 @@ namespace Terminal.Gui.Configuration; [JsonSerializable (typeof (AlignmentModes))] [JsonSerializable (typeof (LineStyle))] [JsonSerializable (typeof (ShadowStyle))] -[JsonSerializable (typeof (HighlightStyle))] +[JsonSerializable (typeof (MouseState))] [JsonSerializable (typeof (TextStyle))] [JsonSerializable (typeof (Dictionary))] [JsonSerializable (typeof (Dictionary))] diff --git a/Terminal.Gui/Configuration/ThemeManager.cs b/Terminal.Gui/Configuration/ThemeManager.cs index ef4daaccf..8b1de8fed 100644 --- a/Terminal.Gui/Configuration/ThemeManager.cs +++ b/Terminal.Gui/Configuration/ThemeManager.cs @@ -296,12 +296,12 @@ public static class ThemeManager internal static void OnThemeChanged (string previousThemeName, string newThemeName) { Logging.Debug ($"Themes.OnThemeChanged({previousThemeName}) -> {Theme}"); - StringPropertyEventArgs args = new StringPropertyEventArgs (in previousThemeName, ref newThemeName!); + EventArgs args = new (newThemeName); ThemeChanged?.Invoke (null, args); } /// Raised when the selected theme has changed. - public static event EventHandler? ThemeChanged; + public static event EventHandler>? ThemeChanged; /// /// Validates all themes in the dictionary. diff --git a/Terminal.Gui/Drawing/Color/ColorEventArgs.cs b/Terminal.Gui/Drawing/Color/ColorEventArgs.cs deleted file mode 100644 index 8823a45bb..000000000 --- a/Terminal.Gui/Drawing/Color/ColorEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -#nullable enable - - -namespace Terminal.Gui.Drawing; - -/// Event arguments for the events. -public class ColorEventArgs : EventArgs -{ - /// Initializes a new instance of - /// The value that is being changed to. - public ColorEventArgs (Color newColor) :base(newColor) { } -} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/Scheme.cs b/Terminal.Gui/Drawing/Scheme.cs index acbc699cc..24aa49fd5 100644 --- a/Terminal.Gui/Drawing/Scheme.cs +++ b/Terminal.Gui/Drawing/Scheme.cs @@ -493,7 +493,7 @@ public record Scheme : IEqualityOperators private readonly Attribute? _highlight; /// - /// The visual role for elements that are highlighted (e.g., when the mouse is hovering over a ). + /// The visual role for elements that are highlighted (e.g., when the mouse is inside a ). /// If not explicitly set, will be a derived value. See the description for for details on the /// algorithm used. /// diff --git a/Terminal.Gui/Drawing/SchemeEventArgs.cs b/Terminal.Gui/Drawing/SchemeEventArgs.cs deleted file mode 100644 index e9546cafb..000000000 --- a/Terminal.Gui/Drawing/SchemeEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -#nullable enable -using System.ComponentModel; - -namespace Terminal.Gui.Drawing; - -/// Event args for draw events -public class SchemeEventArgs : CancelEventArgs -{ - /// Creates a new instance of the class. - public SchemeEventArgs (in Scheme? currentScheme, ref Scheme? newScheme) - { - CurrentScheme = currentScheme; - NewScheme = newScheme; - } - - /// Gets the View's current . - public Scheme? CurrentScheme { get; } - - /// Gets or sets the View's new . - public Scheme? NewScheme { get; set; } -} diff --git a/Terminal.Gui/Drawing/VisualRole.cs b/Terminal.Gui/Drawing/VisualRole.cs index 661c3fc9b..abef07420 100644 --- a/Terminal.Gui/Drawing/VisualRole.cs +++ b/Terminal.Gui/Drawing/VisualRole.cs @@ -43,7 +43,7 @@ public enum VisualRole HotActive, /// - /// The visual role for elements that are highlighted (e.g., when the mouse is hovering over a ). + /// The visual role for elements that are highlighted (e.g., when the mouse is inside over a ). /// Highlight, diff --git a/Terminal.Gui/Drawing/VisualRoleEventArgs.cs b/Terminal.Gui/Drawing/VisualRoleEventArgs.cs index 62659de0a..5f0389967 100644 --- a/Terminal.Gui/Drawing/VisualRoleEventArgs.cs +++ b/Terminal.Gui/Drawing/VisualRoleEventArgs.cs @@ -1,30 +1,63 @@ #nullable enable - namespace Terminal.Gui.Drawing; -/// Args for events that relate . -public class VisualRoleEventArgs : CancelEventArgs +using System; + +#pragma warning disable CS1711 + +/// +/// Provides data for cancellable workflow events that resolve an for a specific +/// in the Cancellable Work Pattern (CWP). +/// +/// +/// +/// Used in events like to allow customization or cancellation +/// of attribute resolution for a , such as determining the appearance of a +/// based on its state (e.g., focused, disabled). +/// +/// +/// Inherits from with T = , providing a +/// cancellable result workflow where event handlers can supply a custom or mark +/// the operation as handled. +/// +/// +/// The type of the result, constrained to . +/// +/// +/// View view = new(); +/// view.GettingAttributeForRole += (sender, args) => +/// { +/// if (args.Role == VisualRole.Focus) +/// { +/// args.Result = new Attribute(Color.BrightCyan, Color.Black); +/// args.Handled = true; +/// } +/// }; +/// Attribute attribute = view.GetAttributeForRole(VisualRole.Focus); +/// +/// +/// +/// +/// +/// +public class VisualRoleEventArgs : ResultEventArgs { - /// - public VisualRoleEventArgs (in VisualRole role, ref readonly Attribute currentValue, ref Attribute newValue, bool cancel = false) : base ( - in currentValue, - ref newValue, - cancel) - { - Role = role; - } - - /// - protected VisualRoleEventArgs (in VisualRole role, ref readonly Attribute currentValue, ref Attribute newValue) : base (currentValue, newValue) - { - Role = role; - } - - /// - public VisualRoleEventArgs (in VisualRole role, ref Attribute newValue) : base (default (Attribute), newValue) { Role = role; } + /// + /// Gets the for which an is being resolved. + /// + public VisualRole Role { get; } /// - /// The that is being set. + /// Initializes a new instance of the class with the specified + /// and initial result. /// - public VisualRole Role { get; set; } + /// The for which the attribute is being resolved. + /// The initial attribute result, which may be null if no result is provided. + public VisualRoleEventArgs (in VisualRole role, Attribute? result) + : base (result) + { + Role = role; + } } + +#pragma warning restore CS1711 \ No newline at end of file diff --git a/Terminal.Gui/Drivers/V2/MainLoop.cs b/Terminal.Gui/Drivers/V2/MainLoop.cs index 392a9934f..e40dfc66b 100644 --- a/Terminal.Gui/Drivers/V2/MainLoop.cs +++ b/Terminal.Gui/Drivers/V2/MainLoop.cs @@ -184,7 +184,7 @@ public class MainLoop : IMainLoop if (v.NeedsDraw || v.NeedsLayout) { - Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); + // Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); return true; } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs index 8af14dbb9..3683f0231 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs @@ -921,17 +921,21 @@ internal class WindowsDriver : ConsoleDriver } await Task.Delay (delay); - var me = new MouseEventArgs - { - ScreenPosition = _pointMove, - Flags = mouseFlag - }; - //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}"); if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { + Point pointMove = _pointMove; // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. - Application.Invoke (() => OnMouseEvent (me)); + Application.Invoke (() => + { + var me = new MouseEventArgs + { + ScreenPosition = pointMove, + Position = pointMove, + Flags = mouseFlag + }; + OnMouseEvent (me); + }); } } } diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md index 4a448a8ee..fd4d7d6d1 100644 --- a/Terminal.Gui/README.md +++ b/Terminal.Gui/README.md @@ -1,58 +1,78 @@ # Terminal.Gui Project -All files required to build the **Terminal.Gui** library (and NuGet package). +**Terminal.Gui** is a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. This repository contains all files required to build the **Terminal.Gui** library and NuGet package, enabling developers to create rich terminal applications with ease. + +## Project Overview + +**Terminal.Gui** provides a comprehensive framework for building interactive console applications with support for keyboard and mouse input, customizable views, and a robust event system. It is designed to work across Windows, macOS, and Linux, leveraging platform-specific console capabilities where available. ## Project Folder Structure -- `\` - The root folder contains the source code for the library. - - `Terminal.Gui.sln` - The Visual Studio solution - - `Application\` - The core `Application` logic, including `Application.cs`, which is is a `static` class that provides the base 'application engine', `RunState`, and `MainLoop`. +- `\` - The root folder contains the core solution and project files for the library. + - `Terminal.Gui.sln` - The Visual Studio solution file for the project. + - `Terminal.Gui.csproj` - The project file defining build configurations and dependencies. + - `App\` - Contains the core `Application` logic, including `Application.cs`, a `static` class that serves as the base 'application engine', managing `RunState` and `MainLoop`. -- `ConsoleDrivers\` - - `IConsoleDriver.cs` - Definition for the Console Driver API. - - Source files for the three `IConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. +- `Configuration\` - Classes related to the `ConfigurationManager` for handling application settings. -- `Configuration\` - Classes related the `ConfigurationManager`. +- `Drivers\` - Contains the console driver implementations: + - `IConsoleDriver.cs` - Defines the Console Driver API. + - Driver implementations for .NET (`NetDriver`), Unix & macOS (`UnixDriver`), and Windows (`WindowsDriver`). -- `Clipboard\` - Classes related to clipboard access. +- `Drawing\` - Classes related to rendering graphical elements in the console. -- `Input\` - Classes relating to keyboard and mouse input. - - `Events.cs` - Defines keyboard and mouse-related structs & classes. - - etc... +- `FileServices\` - Utility classes for file operations and services. -- `Text\` - Classes related to text processing +- `Input\` - Classes handling keyboard and mouse input: + - `Events.cs` - Defines structs and classes for keyboard and mouse events. -- `Drawing\` - Classes related to drawing +- `Resources\` - Assets and resources used by the library. -- `View\` - The `View` class heirarchy, not including any sub-classes +- `Text\` - Classes for text processing and formatting. + +- `View\` - Core `View` class hierarchy (excluding specific sub-classes): - `View.cs` - The base class for non-modal visual elements such as controls. - - `Layout\` - - `PosDim.cs` - Implements *Computed Layout* system. These classes have deep dependencies on `View`. + - Related subdirectories for layout and positioning logic. -- `Views\` - Sub-classes of `View` - - `Toplevel` - Derived from `View`, the base class for modal visual elements such as top-level windows and dialogs. Supports the concept of `MenuBar` and `StatusBar`. - - `Window` - Derived from `TopLevel`; implements Toplevel views with a visible frame and Title. - - `Dialog` - - - etc... +- `ViewBase\` - Base classes and utilities for views. -- `FileServcies/` - File services classes. +- `Views\` - Specific sub-classes of `View`: + - `Toplevel` - Base class for modal visual elements like top-level windows and dialogs, supporting `MenuBar` and `StatusBar`. + - `Window` - Implements framed top-level views with titles. + - `Dialog` - Specialized windows for user interaction. + - Other specialized view classes. -## Version numbers +## Showcase -Version info for Terminal.Gui is managed by [gitversion](https://gitversion.net). +See the [Showcase](docs/showcase.md) to find independent applications and examples built with Terminal.Gui. -Install `gitversion`: +## Getting Started + +For instructions on how to start using **Terminal.Gui**, refer to the [Getting Started Guide](https://gui-cs.github.io/Terminal.Gui/docs/getting-started.html) in our documentation. + +## Documentation + +Comprehensive documentation for **Terminal.Gui** is available at [gui-cs.github.io/Terminal.Gui](https://gui-cs.github.io/Terminal.Gui). Key resources include: +- [Events Deep Dive](https://gui-cs.github.io/Terminal.Gui/docs/events.html) - Detailed guide on event handling and the Cancellable Work Pattern. +- [View Documentation](https://gui-cs.github.io/Terminal.Gui/docs/View.html) - Information on creating and customizing views. +- [Keyboard Handling](https://gui-cs.github.io/Terminal.Gui/docs/keyboard.html) - Guide to managing keyboard input. +- [Mouse Support](https://gui-cs.github.io/Terminal.Gui/docs/mouse.html) - Details on implementing mouse interactions. +- [Showcase](https://gui-cs.github.io/Terminal.Gui/docs/showcase.html) - A collection of applications and examples built with Terminal.Gui. + +For information on generating and updating the API documentation locally, refer to the [DocFX README](../docfx/README.md) in the `docfx` folder. + +## Versioning + +Version information for Terminal.Gui is managed by [gitversion](https://gitversion.net). To install `gitversion`: ```powershell dotnet tool install --global GitVersion.Tool dotnet-gitversion ``` -The project version (the nuget package and in `Terminal.Gui.dll`) is determined from the latest `git tag`. +The project version (used in the NuGet package and `Terminal.Gui.dll`) is determined from the latest `git tag`. The format of version numbers is `major.minor.patch.build.height` and follows [Semantic Versioning](https://semver.org/) rules. -The format of version numbers is `vmajor.minor.patch.build.height` and follows the [Semantic Versioning](https://semver.org/) rules. - -To define a new version (e.g. with a higher `major`, `minor`, `patch`, or `build` value) tag a commit using `git tag`: +To define a new version, tag a commit using `git tag`: ```powershell git tag v1.3.4-beta.5 -a -m "Release v1.3.4 Beta 5" @@ -60,111 +80,81 @@ dotnet-gitversion /updateprojectfiles dotnet build -c Release ``` -**DO NOT COMMIT AFTER USING `/updateprojectfiles`!** - -Doing so will update the `.csproj` files in your branch with version info, which we do not want. +**DO NOT COMMIT AFTER USING `/updateprojectfiles`!** Doing so will update the `.csproj` files in your branch with version info, which we do not want. ## Publishing a Release of Terminal.Gui -First, use the [Semantic Versioning](https://semver.org/) rules.to determine the new verison number. +To release a new version, follow these steps based on [Semantic Versioning](https://semver.org/) rules: -Given a version number MAJOR.MINOR.PATCH, increment the: +- **MAJOR** version for incompatible API changes. +- **MINOR** version for backwards-compatible functionality additions. +- **PATCH** version for backwards-compatible bug fixes. -* MAJOR version when you make incompatible API changes -* MINOR version when you add functionality in a backwards compatible manner -* PATCH version when you make backwards compatible bug fixes +### Steps for Release: -Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format. +1. **Verify the `develop` branch is ready for release**: + - Ensure all changes are committed and pushed to the `develop` branch. + - Ensure your local `develop` branch is up-to-date with `upstream/develop`. -To release a new version (e.g. with a higher `major`, `minor`, or `patch` value) tag a commit using `git tag` and then push that tag directly to the `main` branch on `github.com/gui-cs/Terminal.Gui` (`upstream`). +2. **Create a pull request for the release in the `develop` branch**: + - Title the PR as "Release vX.Y.Z". + ```powershell + git checkout develop + git pull upstream develop + git checkout -b vX_Y_Z + git add . + git commit -m "Release vX.Y.Z" + git push + ``` + - Go to the link printed by `git push` and fill out the Pull Request. -The `tag` must be of the form `v..`, e.g. `v2.3.4`. +3. **On github.com, verify the build action worked on your fork, then merge the PR**. -`patch` can indicate pre-release or not (e.g. `pre`, `beta`, `rc`, etc...). +4. **Pull the merged `develop` from `upstream`**: + ```powershell + git checkout develop + git pull upstream develop + ``` -### 1) Verify the `develop` branch is ready for release +5. **Merge `develop` into `main`**: + ```powershell + git checkout main + git pull upstream main + git merge develop + ``` + - Fix any merge errors. -* Ensure everything is committed and pushed to the `develop` branch -* Ensure your local `develop` branch is up-to-date with `upstream/develop` +6. **Create a new annotated tag for the release on `main`**: + ```powershell + git tag vX.Y.Z -a -m "Release vX.Y.Z" + ``` -### 2) Create a pull request for the release in the `develop` branch +7. **Push the new tag to `main` on `upstream`**: + ```powershell + git push --atomic upstream main vX.Y.Z + ``` -The PR title should be of the form "Release v2.3.4" +8. **Monitor Github Actions to ensure the NuGet publishing worked**: + - Check [GitHub Actions](https://github.com/gui-cs/Terminal.Gui/actions). -```powershell -git checkout develop -git pull upstream develop -git checkout -b v2_3_4 -git add . -git commit -m "Release v2.3.4" -git push -``` +9. **Check NuGet to see the new package version (wait a few minutes)**: + - Visit [NuGet Package](https://www.nuget.org/packages/Terminal.Gui). -Go to the link printed by `git push` and fill out the Pull Request. +10. **Add a new Release in Github**: + - Go to [GitHub Releases](https://github.com/gui-cs/Terminal.Gui/releases) and generate release notes with the list of PRs since the last release. -### 3) On github.com, verify the build action worked on your fork, then merge the PR +11. **Update the `develop` branch with the new version**: + ```powershell + git checkout develop + git pull upstream develop + git merge main + git push upstream develop + ``` -### 4) Pull the merged `develop` from `upstream` +## NuGet -```powershell -git checkout develop -git pull upstream develop -``` - -### 5) Merge `develop` into `main` - -```powershell -git checkout main -git pull upstream main -git merge develop -``` - -Fix any merge errors. - -### 6) Create a new annotated tag for the release on `main` - -```powershell -git tag v2.3.4 -a -m "Release v2.3.4" -``` - -### 7) Push the new tag to `main` on `upstream` - -```powershell -git push --atomic upstream main v2.3.4 -``` - -*See https://stackoverflow.com/a/3745250/297526* - -### 8) Monitor Github Actions to ensure the Nuget publishing worked. - -https://github.com/gui-cs/Terminal.Gui/actions - -### 9) Check Nuget to see the new package version (wait a few minutes) -https://www.nuget.org/packages/Terminal.Gui - -### 10) Add a new Release in Github: https://github.com/gui-cs/Terminal.Gui/releases - -Generate release notes with the list of PRs since the last release. - -### 11) Update the `develop` branch with the new version - -```powershell -git checkout develop -git pull upstream develop -git merge main -git push upstream develop -``` - -## Nuget - -https://www.nuget.org/packages/Terminal.Gui - -When a new version tag is defined and merged into `main`, a Nuget package will be generated by a Github Action. - -If the version is pre-release (includes a hyphen, e.g. `1.3.4-beta.5`) the Nuget package will be tagged as pre-release. - -Miguel & Tig can hide defunct/old Nuget packages. +The official NuGet package for Terminal.Gui is available at [https://www.nuget.org/packages/Terminal.Gui](https://www.nuget.org/packages/Terminal.Gui). When a new version tag is defined and merged into `main`, a NuGet package is automatically generated by a GitHub Action. Pre-release versions (e.g., `1.3.4-beta.5`) are tagged as pre-release on NuGet. ## Contributing -See [CONTRIBUTING.md](https://github.com/gui-cs/Terminal.Gui/blob/master/CONTRIBUTING.md). +We welcome contributions from the community. For detailed guidelines on how to contribute, including coding style, unit tests, and pull request processes, please refer to [CONTRIBUTING.md](https://github.com/gui-cs/Terminal.Gui/blob/master/CONTRIBUTING.md). diff --git a/Terminal.Gui/Text/StringPropertyEventArgs.cs b/Terminal.Gui/Text/StringPropertyEventArgs.cs deleted file mode 100644 index 6dccf5cd9..000000000 --- a/Terminal.Gui/Text/StringPropertyEventArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -#nullable enable -using System.ComponentModel; - -namespace Terminal.Gui.Text; - -/// Event args for type property events -public class StringPropertyEventArgs : CancelEventArgs -{ - /// Creates a new instance of the class. - public StringPropertyEventArgs (in string? currentString, ref string? newString) - { - CurrentString = currentString; - NewString = newString; - } - - /// Gets the current . - public string? CurrentString { get; } - - /// Gets or sets the new . - public string? NewString { get; set; } -} diff --git a/Terminal.Gui/ViewBase/Adornment/Adornment.cs b/Terminal.Gui/ViewBase/Adornment/Adornment.cs index 19b95bfca..9d5675b6b 100644 --- a/Terminal.Gui/ViewBase/Adornment/Adornment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Adornment.cs @@ -95,13 +95,12 @@ public class Adornment : View, IDesignable return true; } - /// /// - protected override bool OnSettingScheme (in Scheme? scheme) + protected override bool OnSettingScheme (ValueChangingEventArgs args) { Parent?.SetNeedsDraw (); - _scheme = scheme; + _scheme = args.NewValue; return false; } diff --git a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs index 46fdf6891..a76005138 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.Arrangment.cs @@ -35,6 +35,8 @@ public partial class Border return false; } + MouseState |= MouseState.Pressed; + // Add Commands and KeyBindings - Note it's ok these get added each time. KeyBindings are cleared in EndArrange() AddArrangeModeKeyBindings (); @@ -425,6 +427,8 @@ public partial class Border // Debug.Assert (_arranging != ViewArrangement.Fixed); Arranging = ViewArrangement.Fixed; + MouseState &= ~MouseState.Pressed; + Application.MouseEvent -= ApplicationOnMouseEvent; if (Application.MouseGrabView == this && _dragPosition.HasValue) @@ -496,8 +500,6 @@ public partial class Border _dragPosition = mouseEvent.Position; Application.GrabMouse (this); - SetPressedHighlight (HighlightStyle); - // Determine the mode based on where the click occurred ViewArrangement arrangeMode = DetermineArrangeModeFromClick (); EnterArrangeMode (arrangeMode); @@ -522,7 +524,6 @@ public partial class Border { _dragPosition = null; Application.UngrabMouse (); - SetPressedHighlight (HighlightStyle.None); EndArrangeMode (); diff --git a/Terminal.Gui/ViewBase/Adornment/Border.cs b/Terminal.Gui/ViewBase/Adornment/Border.cs index 46a3f894a..259a45998 100644 --- a/Terminal.Gui/ViewBase/Adornment/Border.cs +++ b/Terminal.Gui/ViewBase/Adornment/Border.cs @@ -53,14 +53,10 @@ public partial class Border : Adornment Application.GrabbingMouse += Application_GrabbingMouse; Application.UnGrabbingMouse += Application_UnGrabbingMouse; - HighlightStyle |= HighlightStyle.Pressed; - ThicknessChanged += OnThicknessChanged; } - // TODO: Move DrawIndicator out of Border and into View - private void OnThicknessChanged (object? sender, EventArgs e) { if (IsInitialized) @@ -117,7 +113,15 @@ public partial class Border : Adornment { base.BeginInit (); + if (Parent is null) + { + return; + } + ShowHideDrawIndicator (); + + HighlightStates |= (Parent.Arrangement != ViewArrangement.Fixed ? MouseState.Pressed : MouseState.None); + #if SUBVIEW_BASED_BORDER if (Parent is { }) { @@ -275,6 +279,7 @@ public partial class Border : Adornment LineStyle lineStyle = LineStyle; + if (Settings.FastHasFlags (BorderSettings.Title)) { if (Thickness.Top == 2) @@ -332,9 +337,16 @@ public partial class Border : Adornment bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1 && Frame.Height > 1; bool drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0); - Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default; + //Attribute prevAttr = Driver?.GetAttribute () ?? Attribute.Default; - SetAttributeForRole (VisualRole.Normal); + Attribute normalAttribute = GetAttributeForRole (VisualRole.Normal); + + if (MouseState.HasFlag (MouseState.Pressed)) + { + normalAttribute = GetAttributeForRole (VisualRole.Highlight); + } + + SetAttribute (normalAttribute); if (drawTop) { @@ -348,7 +360,7 @@ public partial class Border : Adornment borderBounds.Width, Orientation.Horizontal, lineStyle, - Driver?.GetAttribute () + normalAttribute ); } else @@ -363,7 +375,7 @@ public partial class Border : Adornment Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver?.GetAttribute () + normalAttribute ); } @@ -377,7 +389,7 @@ public partial class Border : Adornment Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver?.GetAttribute () + normalAttribute ); lc?.AddLine ( @@ -385,7 +397,7 @@ public partial class Border : Adornment Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver?.GetAttribute () + normalAttribute ); } @@ -396,7 +408,7 @@ public partial class Border : Adornment 2, Orientation.Horizontal, lineStyle, - Driver?.GetAttribute () + normalAttribute ); // Add a vert line for ╔╡ @@ -405,7 +417,7 @@ public partial class Border : Adornment titleBarsLength, Orientation.Vertical, LineStyle.Single, - Driver?.GetAttribute () + normalAttribute ); // Add a vert line for ╞ @@ -420,7 +432,7 @@ public partial class Border : Adornment titleBarsLength, Orientation.Vertical, LineStyle.Single, - Driver?.GetAttribute () + normalAttribute ); // Add the right hand line for ╞═════╗ @@ -435,7 +447,7 @@ public partial class Border : Adornment borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, lineStyle, - Driver?.GetAttribute () + normalAttribute ); } } @@ -449,7 +461,7 @@ public partial class Border : Adornment sideLineLength, Orientation.Vertical, lineStyle, - Driver?.GetAttribute () + normalAttribute ); } #endif @@ -461,7 +473,7 @@ public partial class Border : Adornment borderBounds.Width, Orientation.Horizontal, lineStyle, - Driver?.GetAttribute () + normalAttribute ); } @@ -472,11 +484,11 @@ public partial class Border : Adornment sideLineLength, Orientation.Vertical, lineStyle, - Driver?.GetAttribute () + normalAttribute ); } - SetAttribute (prevAttr); + // SetAttribute (prevAttr); // TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler if (Diagnostics.HasFlag (ViewDiagnosticFlags.Ruler)) diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs index 6d14b1c84..4695d3ee5 100644 --- a/Terminal.Gui/ViewBase/Adornment/Margin.cs +++ b/Terminal.Gui/ViewBase/Adornment/Margin.cs @@ -1,5 +1,6 @@ #nullable enable +using System.Runtime.InteropServices; namespace Terminal.Gui.ViewBase; @@ -32,11 +33,6 @@ public class Margin : Adornment /// public Margin (View parent) : base (parent) { - /* Do nothing; View.CreateAdornment requires a constructor that takes a parent */ - - // BUGBUG: We should not set HighlightStyle.Pressed here, but wherever it is actually needed - // HighlightStyle |= HighlightStyle.Pressed; - Highlight += Margin_Highlight; SubViewLayout += Margin_LayoutStarted; // Margin should not be focusable @@ -81,7 +77,7 @@ public class Margin : Adornment { var view = stack.Pop (); - if (view.Margin?.GetCachedClip() != null) + if (view.Margin?.GetCachedClip () != null) { view.Margin.NeedsDraw = true; Region? saved = GetClip (); @@ -113,16 +109,10 @@ public class Margin : Adornment } ShadowStyle = base.ShadowStyle; + + Parent.MouseStateChanged += OnParentOnMouseStateChanged; } - ///// - //protected override bool OnGettingScheme (out Scheme? scheme) - //{ - // scheme = Parent?.SuperView?.GetScheme () ?? SchemeManager.GetScheme (Schemes.Base); - - // return true; - //} - /// protected override bool OnClearingViewport () { @@ -153,12 +143,12 @@ public class Margin : Adornment /// protected override bool OnDrawingText () { - return ViewportSettings.HasFlag(ViewportSettingsFlags.Transparent); + return ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent); } #region Shadow - private bool _pressed; + // private bool _pressed; private ShadowView? _bottomShadow; private ShadowView? _rightShadow; @@ -228,14 +218,22 @@ public class Margin : Adornment set => base.ShadowStyle = SetShadow (value); } - private void Margin_Highlight (object? sender, CancelEventArgs e) + private void OnParentOnMouseStateChanged (object? sender, EventArgs args) { - if (Thickness == Thickness.Empty || ShadowStyle == ShadowStyle.None) + if (sender is not View parent || Thickness == Thickness.Empty || ShadowStyle == ShadowStyle.None) { return; } - if (_pressed && e.NewValue == HighlightStyle.None) + bool pressed = args.Value.HasFlag (MouseState.Pressed) && parent.HighlightStates.HasFlag(MouseState.Pressed); + bool pressedOutside = args.Value.HasFlag (MouseState.PressedOutside) && parent.HighlightStates.HasFlag (MouseState.PressedOutside); ; + + if (pressedOutside) + { + pressed = false; + } + + if (MouseState.HasFlag (MouseState.Pressed) && !pressed) { // If the view is pressed and the highlight is being removed, move the shadow back. // Note, for visual effects reasons, we only move horizontally. @@ -256,14 +254,14 @@ public class Margin : Adornment _bottomShadow.Visible = true; } - _pressed = false; + MouseState &= ~MouseState.Pressed; return; } - if (!_pressed && e.NewValue.HasFlag (HighlightStyle.Pressed)) + if (!MouseState.HasFlag (MouseState.Pressed) && pressed) { - // If the view is not pressed and we want highlight move the shadow + // If the view is not pressed, and we want highlight move the shadow // Note, for visual effects reasons, we only move horizontally. // TODO: Add a setting or flag that lets the view move vertically as well. Thickness = new ( @@ -271,7 +269,7 @@ public class Margin : Adornment Thickness.Top + PRESS_MOVE_VERTICAL, Thickness.Right - PRESS_MOVE_HORIZONTAL, Thickness.Bottom - PRESS_MOVE_VERTICAL); - _pressed = true; + MouseState |= MouseState.Pressed; if (_rightShadow is { }) { diff --git a/Terminal.Gui/ViewBase/DrawAdornmentsEventArgs.cs b/Terminal.Gui/ViewBase/DrawAdornmentsEventArgs.cs new file mode 100644 index 000000000..97092cf91 --- /dev/null +++ b/Terminal.Gui/ViewBase/DrawAdornmentsEventArgs.cs @@ -0,0 +1,35 @@ +#nullable enable +namespace Terminal.Gui.ViewBase; + +/// +/// Provides data for events that allow cancellation of adornment drawing in the Cancellable Work Pattern (CWP). +/// +/// +/// +/// Used in events raised by to allow handlers to cancel the drawing +/// of , , and adornments. +/// +/// +/// +/// +public class DrawAdornmentsEventArgs +{ + /// + /// Gets the draw context for tracking drawn regions, or null if not tracking. + /// + public DrawContext? Context { get; } + + /// + /// Gets or sets a value indicating whether the adornment drawing is handled. If true, drawing is cancelled. + /// + public bool Handled { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The draw context, or null if not tracking. + public DrawAdornmentsEventArgs (DrawContext? context) + { + Context = context; + } +} diff --git a/Terminal.Gui/ViewBase/EventArgs.cs b/Terminal.Gui/ViewBase/EventArgs.cs deleted file mode 100644 index 2ddba6383..000000000 --- a/Terminal.Gui/ViewBase/EventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -#nullable enable -namespace Terminal.Gui.ViewBase; - -#pragma warning disable CS1711 -/// -/// for events that convey changes to a property of type . -/// -/// The type of the value that was part of the change being canceled. -public class EventArgs : EventArgs where T : notnull -{ - /// Initializes a new instance of the class. - /// The current value of the property. - /// The type of the value. - public EventArgs (in T currentValue) { CurrentValue = currentValue; } - - /// The current value of the property. - public T CurrentValue { get; } -} diff --git a/Terminal.Gui/ViewBase/HighlightStyle.cs b/Terminal.Gui/ViewBase/HighlightStyle.cs deleted file mode 100644 index 8c2993761..000000000 --- a/Terminal.Gui/ViewBase/HighlightStyle.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Terminal.Gui.ViewBase; - -/// -/// Describes the highlight style of a view when the mouse is over it. -/// -[JsonConverter (typeof (JsonStringEnumConverter))] -[Flags] -public enum HighlightStyle -{ - /// - /// No highlight. - /// - None = 0, - - /// - /// The mouse is hovering over the view (but not pressed). See . - /// - Hover = 1, - - /// - /// The mouse is pressed within the . - /// - Pressed = 2, - - /// - /// The mouse is pressed but moved outside the . - /// - PressedOutside = 4 -} diff --git a/Terminal.Gui/ViewBase/MouseState.cs b/Terminal.Gui/ViewBase/MouseState.cs new file mode 100644 index 000000000..8950526fe --- /dev/null +++ b/Terminal.Gui/ViewBase/MouseState.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Terminal.Gui.ViewBase; + +/// +/// Used to describe the state of the mouse in relation to a () and to +/// specify visual effects, +/// such as highlighting a button when the mouse is over it or changing the appearance of a view when the mouse is +/// pressed (). +/// +/// +/// +[JsonConverter (typeof (JsonStringEnumConverter))] +[Flags] +public enum MouseState +{ + /// + /// No mouse interaction with the view is occurring. + /// + None = 0, + + /// + /// The mouse is in the (but not pressed). Set between the + /// and events. + /// + In = 1, + + /// + /// The mouse is in the and is pressed. + /// + Pressed = 2, + + /// + /// The mouse is outside the and is pressed. If + /// is true, + /// this flag is ignored so that the view remains in the pressed state until the mouse is released. + /// + PressedOutside = 4 +} diff --git a/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs b/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs index 9149c2680..55cc41c6e 100644 --- a/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs +++ b/Terminal.Gui/ViewBase/Navigation/FocusEventArgs.cs @@ -1,5 +1,7 @@ namespace Terminal.Gui.ViewBase; +// TODO: CWP: FocusChanging should use an event arg type derived from ResultEventArgs so that its more obvious +// TODO: the result can be changed. /// The event arguments for events. public class HasFocusEventArgs : CancelEventArgs { diff --git a/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs b/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs index 4aa98cfe4..a7079128c 100644 --- a/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/ViewBase/Orientation/OrientationHelper.cs @@ -1,6 +1,8 @@ #nullable enable namespace Terminal.Gui.ViewBase; + + /// /// Helper class for implementing . /// @@ -135,3 +137,33 @@ public class OrientationHelper /// public event EventHandler>? OrientationChanged; } + + +//public class OrientationHelper +//{ +// private Orientation _orientation; + +// public Orientation Orientation +// { +// get => _orientation; +// set +// { +// CWPPropertyHelper.ChangeProperty ( +// currentValue: _orientation, +// newValue: ref value, +// onChanging: args => OnOrientationChanging (args), +// changingEvent: OrientationChanging, +// onChanged: args => OnOrientationChanged (args), +// changedEvent: OrientationChanged +// ); +// _orientation = value; +// } +// } + +// public event EventHandler>? OrientationChanging; +// public event EventHandler>? OrientationChanged; + +// protected virtual bool OnOrientationChanging (CancelEventArgs args) => false; +// protected virtual void OnOrientationChanged (EventArgs args) { } +//} + diff --git a/Terminal.Gui/ViewBase/View.Command.cs b/Terminal.Gui/ViewBase/View.Command.cs index deaa709f3..b6ea67ec7 100644 --- a/Terminal.Gui/ViewBase/View.Command.cs +++ b/Terminal.Gui/ViewBase/View.Command.cs @@ -219,7 +219,7 @@ public partial class View // Command APIs /// protected bool? RaiseSelecting (ICommandContext? ctx) { - Logging.Debug ($"{Title} ({ctx?.Source?.Title})"); + //Logging.Debug ($"{Title} ({ctx?.Source?.Title})"); CommandEventArgs args = new () { Context = ctx }; // Best practice is to invoke the virtual method first. @@ -266,7 +266,7 @@ public partial class View // Command APIs protected bool? RaiseHandlingHotKey () { CommandEventArgs args = new () { Context = new CommandContext { Command = Command.HotKey } }; - Logging.Debug ($"{Title} ({args.Context?.Source?.Title})"); + //Logging.Debug ($"{Title} ({args.Context?.Source?.Title})"); // Best practice is to invoke the virtual method first. // This allows derived classes to handle the event and potentially cancel it. diff --git a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs index a91aa6e58..b349ac007 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Attribute.cs @@ -12,21 +12,21 @@ public partial class View public Attribute GetCurrentAttribute () { return Driver?.GetAttribute () ?? Attribute.Default; } /// - /// Gets the associated with a specified - /// from the . + /// Gets the associated with a specified + /// from the . /// /// Raises / /// which can cancel the default behavior, and optionally change the attribute in the event args. /// /// - /// If is , + /// If is , /// will be used instead of . /// To override this behavior use / /// to cancel the method, and return a different attribute. /// /// - /// If is not and is , - /// the will be used instead of . + /// If is not and is + /// the will be used instead of . /// To override this behavior use / /// to cancel the method, and return a different attribute. /// @@ -43,18 +43,21 @@ public partial class View return schemeAttribute; } - VisualRoleEventArgs args = new (role, newValue: ref schemeAttribute, currentValue: ref schemeAttribute); + VisualRoleEventArgs args = new (role, result: schemeAttribute); GettingAttributeForRole?.Invoke (this, args); - if (args.Cancel) + if (args is { Handled: true, Result: { } }) { // A handler may have changed the attribute - return args.NewValue; + return args.Result.Value; } - if (HighlightStyle != HighlightStyle.None) + if (role != VisualRole.Disabled && HighlightStates != MouseState.None) { - if (MouseHovering && HighlightStyle.HasFlag (HighlightStyle.Hover) && role != VisualRole.Highlight && role != VisualRole.Disabled) + // The default behavior for HighlightStates of MouseState.Over is to use the Highlight role + if (((HighlightStates.HasFlag (MouseState.In) && MouseState.HasFlag (MouseState.In)) + || (HighlightStates.HasFlag (MouseState.Pressed) && MouseState.HasFlag (MouseState.Pressed))) + && role != VisualRole.Highlight && !HasFocus) { schemeAttribute = GetAttributeForRole (VisualRole.Highlight); } diff --git a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs index 89efe0e17..d55765e9e 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs @@ -8,53 +8,96 @@ public partial class View private string? _schemeName; /// - /// Gets or sets the name of the Scheme to use for this View. If set, it will override the scheme inherited from the - /// SuperView. If a Scheme was explicitly set ( is ), - /// this property will be ignored. + /// Gets or sets the name of the scheme to use for this . If set, it overrides the scheme + /// inherited from the . If a scheme was explicitly set ( is + /// true), this property is ignored. /// + /// + /// + /// Setting this property raises pre- and post-change events via , + /// allowing customization or cancellation of the change. The event + /// is raised before the change, and is raised after. + /// + /// + /// The scheme name, or null if no scheme name is set. + /// + /// public string? SchemeName { get => _schemeName; set { - if (_schemeName == value) + bool changed = CWPPropertyHelper.ChangeProperty ( + _schemeName, + value, + OnSchemeNameChanging, + SchemeNameChanging, + OnSchemeNameChanged, + SchemeNameChanged, + out string? finalValue); + + if (changed) { - return; + _schemeName = finalValue; } - - if (OnSettingSchemeName (in _schemeName, ref value)) - { - _schemeName = value; - return; - } - - StringPropertyEventArgs args = new (in _schemeName, ref value); - SettingSchemeName?.Invoke (this, args); - - if (args.Cancel) - { - _schemeName = args.NewString; - - return; - } - - _schemeName = value; } } /// - /// Called when the for the View is to be set. + /// Called before the property changes, allowing subclasses to cancel or modify the change. /// - /// - /// - /// to stop default behavior. - protected virtual bool OnSettingSchemeName (in string? currentName, ref string? newName) { return false; } + /// The event arguments containing the current and proposed new scheme name. + /// True to cancel the change, false to proceed. + protected virtual bool OnSchemeNameChanging (ValueChangingEventArgs args) + { + return false; + } - /// Raised when the for the View is to be set. - /// - /// Set to to stop default behavior. - /// - public event EventHandler? SettingSchemeName; + /// + /// Called after the property changes, allowing subclasses to react to the change. + /// + /// The event arguments containing the old and new scheme name. + protected virtual void OnSchemeNameChanged (ValueChangedEventArgs args) + { + } + + /// + /// Raised before the property changes, allowing handlers to modify or cancel the change. + /// + /// + /// Set to true to cancel the change or modify + /// to adjust the proposed value. + /// + /// + /// + /// view.SchemeNameChanging += (sender, args) => + /// { + /// if (args.NewValue == "InvalidScheme") + /// { + /// args.Handled = true; + /// Console.WriteLine("Invalid scheme name cancelled."); + /// } + /// }; + /// + /// + public event EventHandler>? SchemeNameChanging; + + /// + /// Raised after the property changes, notifying handlers of the completed change. + /// + /// + /// Provides the old and new scheme name via and + /// , which may be null. + /// + /// + /// + /// view.SchemeNameChanged += (sender, args) => + /// { + /// Console.WriteLine($"SchemeName changed from {args.OldValue ?? "none"} to {args.NewValue ?? "none"}."); + /// }; + /// + /// + public event EventHandler>? SchemeNameChanged; // Both holds the set Scheme and is used to determine if a Scheme has been set or not private Scheme? _scheme; @@ -66,112 +109,165 @@ public partial class View public bool HasScheme => _scheme is { }; /// - /// Gets the Scheme for the View. If the Scheme has not been explicitly set (see ), gets - /// 's Scheme. + /// Gets the scheme for the . If the scheme has not been explicitly set + /// (see ), gets the 's scheme or falls back to the base scheme. /// - /// + /// The resolved scheme, never null. + /// + /// + /// This method uses the Cancellable Work Pattern (CWP) via + /// to allow customization or cancellation of scheme resolution through the method + /// and event. + /// + /// + /// + /// + /// view.GettingScheme += (sender, args) => + /// { + /// args.Result = SchemeManager.GetScheme("Custom"); + /// args.Handled = true; + /// }; + /// Scheme scheme = view.GetScheme(); + /// + /// public Scheme GetScheme () { - if (OnGettingScheme (out Scheme? newScheme)) + ResultEventArgs args = new (); + + return CWPWorkflowHelper.ExecuteWithResult ( + onMethod: args => + { + bool cancelled = OnGettingScheme (out Scheme? newScheme); + args.Result = newScheme; + return cancelled; + }, + eventHandler: GettingScheme, + args, + DefaultAction); + + Scheme DefaultAction () { - return newScheme!; + if (!HasScheme && !string.IsNullOrEmpty (SchemeName)) + { + return SchemeManager.GetScheme (SchemeName); + } + + if (!HasScheme) + { + return SuperView?.GetScheme () ?? SchemeManager.GetScheme (Schemes.Base); + } + + return _scheme!; } - - var args = new SchemeEventArgs (in _scheme, ref newScheme); - GettingScheme?.Invoke (this, args); - - if (args.Cancel) - { - return args.NewScheme!; - } - - if (!HasScheme && !string.IsNullOrEmpty (SchemeName)) - { - return SchemeManager.GetScheme (SchemeName); - } - - if (!HasScheme) - { - return SuperView?.GetScheme () ?? SchemeManager.GetScheme (Schemes.Base); - } - - return _scheme!; } /// - /// Called when the for the View is being retrieved. Overrides can return - /// to - /// stop further processing and optionally set to a different value. + /// Called when the for the is being retrieved. Subclasses can return + /// true to stop further processing and optionally set to a different value. /// - /// to stop default behavior. + /// The scheme to use, or null to continue processing. + /// True to stop default behavior, false to proceed. protected virtual bool OnGettingScheme (out Scheme? scheme) { scheme = null; - return false; } /// - /// Raised when the for the View is being retrieved. Overrides can return - /// to - /// stop further processing and optionally set the in the event args to a different value. + /// Raised when the for the is being retrieved. Handlers can set + /// to true to stop further processing and optionally set + /// to a different value. /// - /// - /// Set `Cancel` to to stop default behavior. - /// - public event EventHandler? GettingScheme; + public event EventHandler>? GettingScheme; + /// - /// Sets the Scheme for the View. Raises event before setting the scheme. + /// Sets the scheme for the , marking it as explicitly set. /// - /// - /// The scheme to set. If will be - /// . - /// - /// if the scheme was set. + /// The scheme to set, or null to clear the explicit scheme. + /// True if the scheme was set, false if unchanged or cancelled. + /// + /// + /// This method uses the Cancellable Work Pattern (CWP) via + /// to allow customization or cancellation of the scheme change through the method + /// and event. The event is raised after a successful change. + /// + /// + /// If set to null, will be false, and the view will inherit the scheme from its + /// or fall back to the base scheme. + /// + /// + /// + /// + /// view.SchemeChanging += (sender, args) => + /// { + /// if (args.NewValue is null) + /// { + /// args.Handled = true; + /// Console.WriteLine("Null scheme cancelled."); + /// } + /// }; + /// view.SchemeChanged += (sender, args) => + /// { + /// Console.WriteLine($"Scheme changed to {args.NewValue?.Name ?? "none"}."); + /// }; + /// bool set = view.SetScheme(SchemeManager.GetScheme("Base")); + /// + /// public bool SetScheme (Scheme? scheme) { - if (_scheme == scheme) + bool changed = CWPPropertyHelper.ChangeProperty ( + _scheme, + scheme, + OnSettingScheme, + SchemeChanging, + OnSchemeChanged, + SchemeChanged, + out Scheme? finalValue); + + if (changed) { - return false; + _scheme = finalValue; + return true; } - - if (OnSettingScheme (in scheme)) - { - return false; - } - - var args = new CancelEventArgs (); - SettingScheme?.Invoke (this, args); - - if (args.Cancel) - { - return false; - } - - _scheme = scheme; - - // BUGBUG: This should be in Border.cs somehow - if (Border is { } && Border.LineStyle != LineStyle.None && Border.HasScheme) - { - Border.SetScheme (_scheme); - } - - SetNeedsDraw (); - - return true; + return false; } /// - /// Called when the for the View is to be set. + /// Called before the scheme is set, allowing subclasses to cancel or modify the change. /// - /// - /// to stop default behavior. - protected virtual bool OnSettingScheme (in Scheme? scheme) { return false; } + /// The event arguments containing the current and proposed new scheme. + /// True to cancel the change, false to proceed. + protected virtual bool OnSettingScheme (ValueChangingEventArgs args) + { + return false; + } + + /// + /// Called after the scheme is set, allowing subclasses to react to the change. + /// + /// The event arguments containing the old and new scheme. + protected virtual void OnSchemeChanged (ValueChangedEventArgs args) + { + SetNeedsDraw (); + } + + /// + /// Raised before the scheme is set, allowing handlers to modify or cancel the change. + /// + /// + /// Set to true to cancel the change or modify + /// to adjust the proposed scheme. + /// + public event EventHandler>? SchemeChanging; + + /// + /// Raised after the scheme is set, notifying handlers of the completed change. + /// + /// + /// Provides the old and new scheme via and + /// , which may be null. + /// + public event EventHandler>? SchemeChanged; - /// Raised when the for the View is to be set. - /// - /// Set to to stop default behavior. - /// - public event EventHandler? SettingScheme; } diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index 30482fd6b..c581401a3 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -164,7 +164,7 @@ public partial class View // Drawing APIs } } - private void DoDrawAdornments (Region? originalClip) + internal void DoDrawAdornments (Region? originalClip) { if (this is Adornment) { diff --git a/Terminal.Gui/ViewBase/View.Mouse.cs b/Terminal.Gui/ViewBase/View.Mouse.cs index 71a992d7b..a59513ef0 100644 --- a/Terminal.Gui/ViewBase/View.Mouse.cs +++ b/Terminal.Gui/ViewBase/View.Mouse.cs @@ -46,12 +46,6 @@ public partial class View // Mouse APIs #region MouseEnterLeave - /// - /// Gets whether the mouse is currently hovering over the View's . Is after - /// has been raised, and before is raised. - /// - public bool MouseHovering { get; internal set; } - /// /// INTERNAL Called by when the mouse moves over the View's /// . @@ -86,9 +80,9 @@ public partial class View // Mouse APIs return true; } - MouseHovering = true; + MouseState |= MouseState.In; - if (HighlightStyle != HighlightStyle.None) + if (HighlightStates != MouseState.None) { SetNeedsDraw (); } @@ -96,36 +90,6 @@ public partial class View // Mouse APIs return false; } - /// - /// Gets the to use when the view is highlighted. The highlight colorscheme - /// is based on the current , using . - /// - /// The highlight scheme. - public Scheme GetHighlightScheme () - { - Scheme cs = GetScheme (); - - return cs with - { - Normal = new ( - GetAttributeForRole (VisualRole.Normal).Foreground.GetBrighterColor (), - GetAttributeForRole (VisualRole.Normal).Background, - GetAttributeForRole (VisualRole.Normal).Style), - HotNormal = new ( - GetAttributeForRole (VisualRole.HotNormal).Foreground.GetBrighterColor (), - GetAttributeForRole (VisualRole.HotNormal).Background, - GetAttributeForRole (VisualRole.HotNormal).Style), - Focus = new ( - GetAttributeForRole (VisualRole.Focus).Foreground.GetBrighterColor (), - GetAttributeForRole (VisualRole.Focus).Background, - GetAttributeForRole (VisualRole.Focus).Style), - HotFocus = new ( - GetAttributeForRole (VisualRole.HotFocus).Foreground.GetBrighterColor (), - GetAttributeForRole (VisualRole.HotFocus).Background, - GetAttributeForRole (VisualRole.HotFocus).Style) - }; - } - /// /// Called when the mouse moves over the View's and no other non-SubView occludes it. /// will @@ -143,7 +107,7 @@ public partial class View // Mouse APIs /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// - /// See for more information. + /// See for more information. /// /// /// @@ -174,7 +138,7 @@ public partial class View // Mouse APIs /// prevents Views higher in the visible hierarchy from receiving Enter/Leave events. /// /// - /// See for more information. + /// See for more information. /// /// public event EventHandler? MouseEnter; @@ -192,7 +156,7 @@ public partial class View // Mouse APIs /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// - /// See for more information. + /// See for more information. /// /// internal void NewMouseLeaveEvent () @@ -204,9 +168,11 @@ public partial class View // Mouse APIs MouseLeave?.Invoke (this, EventArgs.Empty); - MouseHovering = false; + MouseState &= ~MouseState.In; - if (HighlightStyle != HighlightStyle.None) + // TODO: Should we also MouseState &= ~MouseState.Pressed; ?? + + if (HighlightStates != MouseState.None) { SetNeedsDraw (); } @@ -220,7 +186,7 @@ public partial class View // Mouse APIs /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// - /// See for more information. + /// See for more information. /// /// protected virtual void OnMouseLeave () { } @@ -233,7 +199,7 @@ public partial class View // Mouse APIs /// Adornments receive MouseEnter/Leave events when the mouse is over the Adornment's . /// /// - /// See for more information. + /// See for more information. /// /// public event EventHandler? MouseLeave; @@ -242,8 +208,13 @@ public partial class View // Mouse APIs #region Low Level Mouse Events - /// Gets or sets whether the wants continuous button pressed events. - public virtual bool WantContinuousButtonPressed { get; set; } + /// + /// Gets or sets whether the wants continuous button pressed events. When set to + /// , + /// and the user presses and holds the mouse button, will be + /// repeatedly called with the same for as long as the mouse button remains pressed. + /// + public bool WantContinuousButtonPressed { get; set; } /// Gets or sets whether the wants mouse position reports. /// if mouse position reports are wanted; otherwise, . @@ -263,13 +234,9 @@ public partial class View // Mouse APIs /// mouse buttons was clicked, the / event will be raised /// /// - /// See for more information. - /// - /// - /// If is , the / - /// event - /// will be raised on any new mouse event where indicates a button - /// is pressed. + /// If is , and the user presses and holds the + /// mouse button, will be repeatedly called with the same for + /// as long as the mouse button remains pressed. /// /// /// @@ -300,7 +267,7 @@ public partial class View // Mouse APIs } // Post-Conditions - if (HighlightStyle != HighlightStyle.None || WantContinuousButtonPressed) + if (HighlightStates != MouseState.None || WantContinuousButtonPressed) { if (WhenGrabbedHandlePressed (mouseEvent)) { @@ -318,7 +285,7 @@ public partial class View // Mouse APIs } } - // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and + // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent, and // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked if (mouseEvent.IsSingleDoubleOrTripleClicked) { @@ -375,7 +342,7 @@ public partial class View // Mouse APIs /// /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event /// (typically - /// when or are set). + /// when or are set). /// /// /// Marked internal just to support unit tests @@ -390,7 +357,9 @@ public partial class View // Mouse APIs { if (Application.MouseGrabView == this) { - SetPressedHighlight (HighlightStyle.None); + //Logging.Debug ($"{Id} - {MouseState}"); + MouseState &= ~MouseState.Pressed; + MouseState &= ~MouseState.PressedOutside; } return mouseEvent.Handled = true; @@ -402,7 +371,7 @@ public partial class View // Mouse APIs /// /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event /// (typically - /// when or are set). + /// when or are set). /// /// /// @@ -433,19 +402,26 @@ public partial class View // Mouse APIs if (Viewport.Contains (mouseEvent.Position)) { - if (this is not Adornment - && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None)) + //Logging.Debug ($"{Id} - Inside Viewport: {MouseState}"); + // The mouse is inside. + if (HighlightStates.HasFlag (MouseState.Pressed)) { - return true; + MouseState |= MouseState.Pressed; } - } - else - { - if (this is not Adornment - && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None)) + // Always clear PressedOutside when the mouse is pressed inside the Viewport + MouseState &= ~MouseState.PressedOutside; + } + + if (!Viewport.Contains (mouseEvent.Position)) + { + // Logging.Debug ($"{Id} - Outside Viewport: {MouseState}"); + // The mouse is outside. + // When WantContinuousButtonPressed is set we want to keep the mouse state as pressed (e.g. a repeating button). + // This shows the user that the button is doing something, even if the mouse is outside the Viewport. + if (HighlightStates.HasFlag (MouseState.PressedOutside) && !WantContinuousButtonPressed) { - return true; + MouseState |= MouseState.PressedOutside; } } @@ -486,7 +462,6 @@ public partial class View // Mouse APIs } // Cancellable event - if (OnMouseClick (args) || args.Handled) { return args.Handled; @@ -540,7 +515,7 @@ public partial class View // Mouse APIs /// /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the click event /// (typically - /// when or are set). + /// when or are set). /// /// /// Marked internal just to support unit tests @@ -556,10 +531,10 @@ public partial class View // Mouse APIs // We're grabbed. Clicked event comes after the last Release. This is our signal to ungrab Application.UngrabMouse (); - if (SetPressedHighlight (HighlightStyle.None)) - { - return true; - } + // TODO: Prove we need to unset MouseState.Pressed and MouseState.PressedOutside here + // TODO: There may be perf gains if we don't unset these flags here + MouseState &= ~MouseState.Pressed; + MouseState &= ~MouseState.PressedOutside; // If mouse is still in bounds, generate a click if (!WantMousePositionReports && Viewport.Contains (mouseEvent.Position)) @@ -626,90 +601,83 @@ public partial class View // Mouse APIs #endregion Mouse Wheel Events - #region Highlight Handling + #region MouseState Handling + private MouseState _mouseState; /// - /// Gets or sets whether the will be highlighted visually by mouse interaction. + /// Gets the state of the mouse relative to the View. When changed, the / + /// + /// event will be raised. /// - public HighlightStyle HighlightStyle { get; set; } - - /// - /// INTERNAL Raises the event. Returns if the event was handled, - /// otherwise. - /// - /// - /// - private bool RaiseHighlight (CancelEventArgs args) + public MouseState MouseState { - if (OnHighlight (args)) + get => _mouseState; + internal set { - return true; + if (_mouseState == value) + { + return; + } + + EventArgs args = new (value); + + RaiseMouseStateChanged (args); + + _mouseState = value; } - - Highlight?.Invoke (this, args); - - return args.Cancel; } /// - /// Called when the view is to be highlighted. The passed in the event indicates the - /// highlight style that will be applied. The view can modify the highlight style by setting the - /// property. - /// - /// - /// Set the property to , to cancel, indicating custom - /// highlighting. - /// - /// , to cancel, indicating custom highlighting. - protected virtual bool OnHighlight (CancelEventArgs args) { return false; } - - /// - /// Raised when the view is to be highlighted. The passed in the event indicates the - /// highlight style that will be applied. The view can modify the highlight style by setting the - /// property. - /// Set to , to cancel, indicating custom highlighting. - /// - public event EventHandler>? Highlight; - - /// - /// INTERNAL Enables the highlight for the view when the mouse is pressed. Called from OnMouseEvent. + /// Gets or sets which changes should cause the View to change its appearance. /// /// /// - /// Set to and/or - /// to enable. + /// is set by default, which means the View will be highlighted when the + /// mouse is over it. The default behavior of + /// is to use the role for the highlight Attribute. /// /// - /// Calls and raises the event. + /// means the View will be highlighted when the mouse is pressed over it. + /// 's default behavior is to use + /// the role when the Border is pressed for Arrangement. + /// 's default behavior, when shadows are enabled, is to move the shadow providing + /// a pressed effect. /// /// - /// Marked internal just to support unit tests + /// means the View will be highlighted when the mouse was pressed + /// inside it and then moved outside of it, unless is set to + /// , in which case the flag has no effect. /// /// - /// , if the Highlight event was handled, otherwise. - internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) + public MouseState HighlightStates { get; set; } + + /// + /// INTERNAL Raises the event. + /// + /// + private void RaiseMouseStateChanged (EventArgs args) { - // TODO: Make the highlight colors configurable - if (!CanFocus) - { - return false; - } + //Logging.Debug ($"{Id} - {args.Value} -> {args.Value}"); - HighlightStyle copy = HighlightStyle; - CancelEventArgs args = new (ref copy, ref newHighlightStyle); + OnMouseStateChanged (args); - if (RaiseHighlight (args) || args.Cancel) - { - return true; - } - - // For 3D Pressed Style - Note we don't care about canceling the event here - Margin?.RaiseHighlight (args); - return args.Cancel; + MouseStateChanged?.Invoke (this, args); } - #endregion Highlight Handling + /// + /// Called when has changed, indicating the View should be highlighted or not. The passed in the event + /// indicates the highlight style that will be applied. + /// + protected virtual void OnMouseStateChanged (EventArgs args) { } + + /// + /// RaisedCalled when has changed, indicating the View should be highlighted or not. The passed in the event + /// indicates the highlight style that will be applied. + /// + public event EventHandler>? MouseStateChanged; + + #endregion MouseState Handling private void DisposeMouse () { } } diff --git a/Terminal.Gui/ViewBase/View.Navigation.cs b/Terminal.Gui/ViewBase/View.Navigation.cs index 2a6d1f35b..733a53071 100644 --- a/Terminal.Gui/ViewBase/View.Navigation.cs +++ b/Terminal.Gui/ViewBase/View.Navigation.cs @@ -637,6 +637,8 @@ public partial class View // Focus and cross-view navigation management (TabStop return (true, false); } + // TODO: CWP: FocusChanging should use an event arg type derived from ResultEventArgs so that its more obvious + // TODO: the result can be changed. private bool RaiseFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) { Debug.Assert (currentFocused is null || currentFocused is { HasFocus: true }); @@ -882,6 +884,7 @@ public partial class View // Focus and cross-view navigation management (TabStop SetNeedsDraw (); } + // TODO: CWP: FocusChanged should not be using event args derived from CancelEventArgs, as it is not cancellable. private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { // If we are the most focused view, we need to set the focused view in Application.Navigation diff --git a/Terminal.Gui/ViewBase/View.ScrollBars.cs b/Terminal.Gui/ViewBase/View.ScrollBars.cs index 6c084bfb0..9f79bcf98 100644 --- a/Terminal.Gui/ViewBase/View.ScrollBars.cs +++ b/Terminal.Gui/ViewBase/View.ScrollBars.cs @@ -139,7 +139,7 @@ public partial class View { Viewport = Viewport with { - Y = Math.Min (args.CurrentValue, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize) + Y = Math.Min (args.Value, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize) }; }; @@ -160,7 +160,7 @@ public partial class View { Viewport = Viewport with { - X = Math.Min (args.CurrentValue, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize) + X = Math.Min (args.Value, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize) }; }; diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 5a45c642b..894af07d6 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -39,13 +39,13 @@ public class Button : View, IDesignable /// Gets or sets the default Highlight Style. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static HighlightStyle DefaultHighlightStyle { get; set; } = HighlightStyle.Pressed | HighlightStyle.Hover; + public static MouseState DefaultHighlightStates { get; set; } = MouseState.In | MouseState.Pressed | MouseState.PressedOutside; /// Initializes a new instance of . public Button () { - TextAlignment = Alignment.Center; - VerticalTextAlignment = Alignment.Center; + base.TextAlignment = Alignment.Center; + base.VerticalTextAlignment = Alignment.Center; _leftBracket = Glyphs.LeftBracket; _rightBracket = Glyphs.RightBracket; @@ -67,8 +67,8 @@ public class Button : View, IDesignable TitleChanged += Button_TitleChanged; MouseClick += Button_MouseClick; - ShadowStyle = DefaultShadow; - HighlightStyle = DefaultHighlightStyle; + base.ShadowStyle = DefaultShadow; + HighlightStates = DefaultHighlightStates; } private bool? HandleHotKeyCommand (ICommandContext commandContext) @@ -99,33 +99,6 @@ public class Button : View, IDesignable return false; } - - private bool _wantContinuousButtonPressed; - - /// - public override bool WantContinuousButtonPressed - { - get => _wantContinuousButtonPressed; - set - { - if (value == _wantContinuousButtonPressed) - { - return; - } - - _wantContinuousButtonPressed = value; - - if (_wantContinuousButtonPressed) - { - HighlightStyle |= HighlightStyle.PressedOutside; - } - else - { - HighlightStyle &= ~HighlightStyle.PressedOutside; - } - } - } - private void Button_MouseClick (object sender, MouseEventArgs e) { if (e.Handled) @@ -139,7 +112,7 @@ public class Button : View, IDesignable private void Button_TitleChanged (object sender, EventArgs e) { - base.Text = e.CurrentValue; + base.Text = e.Value; TextFormatter.HotKeySpecifier = HotKeySpecifier; } diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index b6d318e8f..d62e9df93 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -106,10 +106,10 @@ public class CharMap : View, IDesignable { if (e.Role != VisualRole.Focus && e.Role != VisualRole.Active) { - e.NewValue = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active); + e.Result = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Active); } - e.Cancel = true; + e.Handled = true; } private bool? Move (ICommandContext? commandContext, int cpOffset) @@ -442,9 +442,6 @@ public class CharMap : View, IDesignable #region Mouse Handling - // TODO: Use this to demonstrate using a popover to show glyph info on hover - // public event EventHandler? Hover; - private bool? HandleSelectCommand (ICommandContext? commandContext) { Point position = GetCursor (SelectedCodePoint); diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 626f4516d..b6165186b 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -14,7 +14,7 @@ public class CheckBox : View /// Gets or sets the default Highlight Style. /// [ConfigurationProperty (Scope = typeof (ThemeScope))] - public static HighlightStyle DefaultHighlightStyle { get; set; } = HighlightStyle.PressedOutside | HighlightStyle.Pressed | HighlightStyle.Hover; + public static MouseState DefaultHighlightStates { get; set; } = MouseState.PressedOutside | MouseState.Pressed | MouseState.In; /// /// Initializes a new instance of . @@ -46,7 +46,7 @@ public class CheckBox : View TitleChanged += Checkbox_TitleChanged; - HighlightStyle = DefaultHighlightStyle; + HighlightStates = DefaultHighlightStates; } private bool? AdvanceAndSelect (ICommandContext? commandContext) @@ -68,7 +68,7 @@ public class CheckBox : View private void Checkbox_TitleChanged (object? sender, EventArgs e) { - base.Text = e.CurrentValue; + base.Text = e.Value; TextFormatter.HotKeySpecifier = HotKeySpecifier; } @@ -152,7 +152,7 @@ public class CheckBox : View return null; } - CancelEventArgs e = new (in _checkedState, ref value); + ResultEventArgs e = new (value); if (OnCheckedStateChanging (e)) { @@ -161,9 +161,9 @@ public class CheckBox : View CheckedStateChanging?.Invoke (this, e); - if (e.Cancel) + if (e.Handled) { - return e.Cancel; + return e.Handled; } _checkedState = value; @@ -184,7 +184,7 @@ public class CheckBox : View /// The state change can be cancelled by setting the args.Cancel to . /// /// - protected virtual bool OnCheckedStateChanging (CancelEventArgs args) { return false; } + protected virtual bool OnCheckedStateChanging (ResultEventArgs args) { return false; } /// Raised when the state is changing. /// @@ -192,7 +192,7 @@ public class CheckBox : View /// This event can be cancelled. If cancelled, the will not change its state. /// /// - public event EventHandler>? CheckedStateChanging; + public event EventHandler>? CheckedStateChanging; /// Called when the state has changed. protected virtual void OnCheckedStateChanged (EventArgs args) { } @@ -221,32 +221,32 @@ public class CheckBox : View public bool? AdvanceCheckState () { CheckState oldValue = CheckedState; - CancelEventArgs e = new (in _checkedState, ref oldValue); + ResultEventArgs e = new (oldValue); switch (CheckedState) { case CheckState.None: - e.NewValue = CheckState.Checked; + e.Result = CheckState.Checked; break; case CheckState.Checked: - e.NewValue = CheckState.UnChecked; + e.Result = CheckState.UnChecked; break; case CheckState.UnChecked: if (AllowCheckStateNone) { - e.NewValue = CheckState.None; + e.Result = CheckState.None; } else { - e.NewValue = CheckState.Checked; + e.Result = CheckState.Checked; } break; } - bool? cancelled = ChangeCheckedState (e.NewValue); + bool? cancelled = ChangeCheckedState (e.Result); return cancelled; } diff --git a/Terminal.Gui/Views/Color/ColorPicker.16.cs b/Terminal.Gui/Views/Color/ColorPicker.16.cs index 221883399..8c76f2648 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.16.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.16.cs @@ -53,7 +53,7 @@ public class ColorPicker16 : View } /// Fired when a color is picked. - public event EventHandler? ColorChanged; + public event EventHandler>? ColorChanged; /// Cursor for the selected color. public Point Cursor @@ -280,7 +280,7 @@ public class ColorPicker16 : View private void SetInitialProperties () { - HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed; + HighlightStates = ViewBase.MouseState.PressedOutside | ViewBase.MouseState.Pressed; CanFocus = true; AddCommands (); diff --git a/Terminal.Gui/Views/Color/ColorPicker.cs b/Terminal.Gui/Views/Color/ColorPicker.cs index cc44fa32d..2a60e536b 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.cs @@ -96,7 +96,7 @@ public partial class ColorPicker : View, IDesignable /// /// Fired when color is changed. /// - public event EventHandler? ColorChanged; + public event EventHandler>? ColorChanged; /// protected override bool OnDrawingContent () diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 209f94bae..158c7cf23 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -111,10 +111,10 @@ public class ComboBox : View, IDesignable } /// - protected override bool OnSettingScheme (in Scheme scheme) + protected override bool OnSettingScheme (ValueChangingEventArgs args) { - _listview.SetScheme(scheme); - return base.OnSettingScheme (in scheme); + _listview.SetScheme(args.NewValue); + return base.OnSettingScheme (args); } /// Gets or sets if the drop-down list can be hide with a button click event. diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/Terminal.Gui/Views/FileDialog.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Terminal.Gui/Views/FlagSelector.cs b/Terminal.Gui/Views/FlagSelector.cs index 7e2298c32..00e8544f7 100644 --- a/Terminal.Gui/Views/FlagSelector.cs +++ b/Terminal.Gui/Views/FlagSelector.cs @@ -307,7 +307,7 @@ public class FlagSelector : View, IOrientation, IDesignable Title = nameWithHotKey, Id = name, Data = flag, - HighlightStyle = HighlightStyle.Hover + HighlightStates = ViewBase.MouseState.In }; checkbox.GettingAttributeForRole += (_, e) => @@ -320,36 +320,36 @@ public class FlagSelector : View, IOrientation, IDesignable switch (e.Role) { case VisualRole.Normal: - e.Cancel = true; + e.Handled = true; if (!HasFocus) { - e.NewValue = GetAttributeForRole (VisualRole.Focus); + e.Result = GetAttributeForRole (VisualRole.Focus); } else { // If _scheme was set, it's because of Hover if (checkbox.HasScheme) { - e.NewValue = checkbox.GetAttributeForRole (VisualRole.Normal); + e.Result = checkbox.GetAttributeForRole (VisualRole.Normal); } else { - e.NewValue = GetAttributeForRole (VisualRole.Normal); + e.Result = GetAttributeForRole (VisualRole.Normal); } } break; case VisualRole.HotNormal: - e.Cancel = true; + e.Handled = true; if (!HasFocus) { - e.NewValue = GetAttributeForRole (VisualRole.HotFocus); + e.Result = GetAttributeForRole (VisualRole.HotFocus); } else { - e.NewValue = GetAttributeForRole (VisualRole.HotNormal); + e.Result = GetAttributeForRole (VisualRole.HotNormal); } break; diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 0483f42cb..d315fe502 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -40,7 +40,7 @@ public class Label : View, IDesignable private void Label_TitleChanged (object sender, EventArgs e) { - base.Text = e.CurrentValue; + base.Text = e.Value; TextFormatter.HotKeySpecifier = HotKeySpecifier; } diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index 7f3392913..5f1917861 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -198,7 +198,7 @@ public class MenuBarv2 : Menuv2, IDesignable { if (sender is MenuBarItemv2 mbi) { - if (e.CurrentValue) + if (e.Value) { Active = true; } @@ -541,7 +541,7 @@ public class MenuBarv2 : Menuv2, IDesignable { Normal = new ( GetAttributeForRole (VisualRole.Normal).Foreground, - args.CurrentValue, + args.Result, GetAttributeForRole (VisualRole.Normal).Style) }); }; diff --git a/Terminal.Gui/Views/OptionSelector.cs b/Terminal.Gui/Views/OptionSelector.cs index 0d6491261..17da53568 100644 --- a/Terminal.Gui/Views/OptionSelector.cs +++ b/Terminal.Gui/Views/OptionSelector.cs @@ -179,7 +179,7 @@ public class OptionSelector : View, IOrientation, IDesignable Title = nameWithHotKey, Id = name, Data = index, - //HighlightStyle = HighlightStyle.Hover, + //HighlightStates = HighlightStates.Hover, RadioStyle = true }; @@ -193,37 +193,37 @@ public class OptionSelector : View, IOrientation, IDesignable switch (e.Role) { case VisualRole.Normal: - e.Cancel = true; + e.Handled = true; if (!HasFocus) { - e.NewValue = GetAttributeForRole (VisualRole.Focus); + e.Result = GetAttributeForRole (VisualRole.Focus); } else { // If _scheme was set, it's because of Hover if (checkbox.HasScheme) { - e.NewValue = checkbox.GetAttributeForRole(VisualRole.Normal); + e.Result = checkbox.GetAttributeForRole(VisualRole.Normal); } else { - e.NewValue = GetAttributeForRole (VisualRole.Normal); + e.Result = GetAttributeForRole (VisualRole.Normal); } } break; case VisualRole.HotNormal: - e.Cancel = true; + e.Handled = true; if (!HasFocus) { - e.NewValue = GetAttributeForRole (VisualRole.HotFocus); + e.Result = GetAttributeForRole (VisualRole.HotFocus); } else { - e.NewValue = GetAttributeForRole (VisualRole.HotNormal); + e.Result = GetAttributeForRole (VisualRole.HotNormal); } break; diff --git a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs index 8d36f41b8..c625962c0 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs @@ -434,7 +434,7 @@ public class ScrollBar : View, IOrientation, IDesignable return; } - RaiseSliderPositionChangeEvents (_sliderPosition, e.CurrentValue); + RaiseSliderPositionChangeEvents (_sliderPosition, e.Value); } private void SliderOnScroll (object? sender, EventArgs e) @@ -446,14 +446,14 @@ public class ScrollBar : View, IOrientation, IDesignable int calculatedSliderPos = CalculateSliderPositionFromContentPosition ( _position, - e.CurrentValue >= 0 ? NavigationDirection.Forward : NavigationDirection.Backward); + e.Value >= 0 ? NavigationDirection.Forward : NavigationDirection.Backward); if (calculatedSliderPos == _sliderPosition) { return; } - int sliderScrolledAmount = e.CurrentValue; + int sliderScrolledAmount = e.Value; int calculatedPosition = CalculatePositionFromSliderPosition (calculatedSliderPos + sliderScrolledAmount); Position = calculatedPosition; @@ -610,7 +610,7 @@ public class ScrollBar : View, IOrientation, IDesignable { OrientationChanged += (sender, args) => { - if (args.CurrentValue == Orientation.Vertical) + if (args.Value == Orientation.Vertical) { Width = 1; Height = Dim.Fill (); diff --git a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index db0511a51..c0558192a 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs @@ -26,7 +26,7 @@ public class ScrollSlider : View, IOrientation, IDesignable OnOrientationChanged (Orientation); - HighlightStyle = HighlightStyle.Hover; + HighlightStates = ViewBase.MouseState.In; } #region IOrientation members diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index c031cd737..371245ba0 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -60,7 +60,7 @@ public class Shortcut : View, IOrientation, IDesignable /// The help text to display. public Shortcut (Key key, string? commandText, Action? action, string? helpText = null) { - HighlightStyle = HighlightStyle.None; + HighlightStates = ViewBase.MouseState.None; CanFocus = true; if (Border is { }) @@ -481,7 +481,7 @@ public class Shortcut : View, IOrientation, IDesignable CommandView.VerticalTextAlignment = Alignment.Center; CommandView.TextAlignment = Alignment.Start; CommandView.TextFormatter.WordWrap = false; - //CommandView.HighlightStyle = HighlightStyle.None; + //CommandView.HighlightStates = HighlightStates.None; CommandView.GettingAttributeForRole += SubViewOnGettingAttributeForRole; } @@ -492,16 +492,16 @@ public class Shortcut : View, IOrientation, IDesignable case VisualRole.Normal: if (HasFocus) { - e.Cancel = true; - e.NewValue = GetAttributeForRole (VisualRole.Focus); + e.Handled = true; + e.Result = GetAttributeForRole (VisualRole.Focus); } break; case VisualRole.HotNormal: if (HasFocus) { - e.Cancel = true; - e.NewValue = GetAttributeForRole (VisualRole.HotFocus); + e.Handled = true; + e.Result = GetAttributeForRole (VisualRole.HotFocus); } break; } @@ -547,7 +547,7 @@ public class Shortcut : View, IOrientation, IDesignable HelpView.VerticalTextAlignment = Alignment.Center; HelpView.TextAlignment = Alignment.Start; HelpView.TextFormatter.WordWrap = false; - HelpView.HighlightStyle = HighlightStyle.None; + HelpView.HighlightStates = ViewBase.MouseState.None; HelpView.GettingAttributeForRole += SubViewOnGettingAttributeForRole; } @@ -681,14 +681,14 @@ public class Shortcut : View, IOrientation, IDesignable KeyView.TextAlignment = Alignment.End; KeyView.VerticalTextAlignment = Alignment.Center; KeyView.KeyBindings.Clear (); - KeyView.HighlightStyle = HighlightStyle.None; + KeyView.HighlightStates = ViewBase.MouseState.None; KeyView.GettingAttributeForRole += (sender, args) => { if (args.Role == VisualRole.Normal) { - args.NewValue = SuperView?.GetAttributeForRole (HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal) ?? Attribute.Default; - args.Cancel = true; + args.Result = SuperView?.GetAttributeForRole (HasFocus ? VisualRole.HotFocus : VisualRole.HotNormal) ?? Attribute.Default; + args.Handled = true; } }; KeyView.ClearingViewport += (sender, args) => diff --git a/Terminal.Gui/Views/TextInput/DateField.cs b/Terminal.Gui/Views/TextInput/DateField.cs index e85ceb60a..ac156724d 100644 --- a/Terminal.Gui/Views/TextInput/DateField.cs +++ b/Terminal.Gui/Views/TextInput/DateField.cs @@ -184,15 +184,15 @@ public class DateField : TextField } } - private void DateField_Changing (object sender, CancelEventArgs e) + private void OnTextChanging (object sender, ResultEventArgs e) { try { var spaces = 0; - for (var i = 0; i < e.NewValue.Length; i++) + for (var i = 0; i < e.Result.Length; i++) { - if (e.NewValue [i] == ' ') + if (e.Result [i] == ' ') { spaces++; } @@ -203,21 +203,22 @@ public class DateField : TextField } spaces += FormatLength; - string trimmedText = e.NewValue [..spaces]; + string trimmedText = e.Result [..spaces]; spaces -= FormatLength; trimmedText = trimmedText.Replace (new string (' ', spaces), " "); var date = Convert.ToDateTime (trimmedText).ToString (_format.Trim ()); - if ($" {date}" != e.NewValue) + if ($" {date}" != e.Result) { - e.NewValue = $" {date}".Replace (RightToLeftMark, ""); + // Change the date format to match the current culture + e.Result = $" {date}".Replace (RightToLeftMark, ""); } AdjCursorPosition (CursorPosition); } catch (Exception) { - e.Cancel = true; + e.Handled = true; } } @@ -375,7 +376,7 @@ public class DateField : TextField _separator = GetDataSeparator (Culture.DateTimeFormat.DateSeparator); Date = date; CursorPosition = 1; - TextChanging += DateField_Changing; + TextChanging += OnTextChanging; // Things this view knows how to do AddCommand ( diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index b87308009..0eaa40181 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -518,10 +518,10 @@ public class TextField : View, IDesignable } string newText = value.Replace ("\t", "").Split ("\n") [0]; - CancelEventArgs args = new (ref oldText, ref newText); - OnTextChanging (args); + ResultEventArgs args = new (newText); + RaiseTextChanging (args); - if (args.Cancel) + if (args.Handled) { if (_cursorPosition > _text.Count) { @@ -534,7 +534,7 @@ public class TextField : View, IDesignable ClearAllSelection (); // Note we use NewValue here; TextChanging subscribers may have changed it - _text = args.NewValue.EnumerateRunes ().ToList (); + _text = args.Result.EnumerateRunes ().ToList (); if (!Secret && !_historyText.IsFromHistory) { @@ -1067,16 +1067,21 @@ public class TextField : View, IDesignable return true; } - /// Virtual method that invoke the event if it's defined. + /// Raises the event, enabling canceling the change or adjusting the text. /// The event arguments. - /// if the event was cancelled. - public bool OnTextChanging (CancelEventArgs args) + /// if the event was cancelled or the text was adjusted by the event. + public bool RaiseTextChanging (ResultEventArgs args) { + // TODO: CWP: Add an OnTextChanging protected virtual method that can be overridden to handle text changing events. + TextChanging?.Invoke (this, args); - return args.Cancel; + return args.Handled; } + /// Raised before changes. The change can be canceled the text adjusted. + public event EventHandler> TextChanging; + /// Paste the selected text from the clipboard. public virtual void Paste () { @@ -1167,8 +1172,6 @@ public class TextField : View, IDesignable ///// //public event EventHandler> TextChanged; - /// Changing event, raised before the changes and can be canceled or changing the new text. - public event EventHandler> TextChanging; /// Undoes the latest changes. public void Undo () diff --git a/Terminal.Gui/Views/TextInput/TimeField.cs b/Terminal.Gui/Views/TextInput/TimeField.cs index 69a372267..083ccf0e1 100644 --- a/Terminal.Gui/Views/TextInput/TimeField.cs +++ b/Terminal.Gui/Views/TextInput/TimeField.cs @@ -432,15 +432,15 @@ public class TimeField : TextField return true; } - private void TextField_TextChanging (object sender, CancelEventArgs e) + private void TextField_TextChanging (object sender, ResultEventArgs e) { try { var spaces = 0; - for (var i = 0; i < e.NewValue.Length; i++) + for (var i = 0; i < e.Result.Length; i++) { - if (e.NewValue [i] == ' ') + if (e.Result [i] == ' ') { spaces++; } @@ -451,31 +451,31 @@ public class TimeField : TextField } spaces += FieldLength; - string trimmedText = e.NewValue [..spaces]; + string trimmedText = e.Result [..spaces]; spaces -= FieldLength; trimmedText = trimmedText.Replace (new string (' ', spaces), " "); - if (trimmedText != e.NewValue) + if (trimmedText != e.Result) { - e.NewValue = trimmedText; + e.Result = trimmedText; } if (!TimeSpan.TryParseExact ( - e.NewValue.Trim (), + e.Result.Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result )) { - e.Cancel = true; + e.Handled = true; } AdjCursorPosition (CursorPosition); } catch (Exception) { - e.Cancel = true; + e.Handled = true; } } } diff --git a/Terminal.Gui/Views/View.cs b/Terminal.Gui/Views/View.cs new file mode 100644 index 000000000..0519ecba6 --- /dev/null +++ b/Terminal.Gui/Views/View.cs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index 3b826674a..7fc288b72 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -563,7 +563,7 @@ public class Wizard : Dialog { if (string.IsNullOrEmpty (_wizardTitle)) { - _wizardTitle = e.CurrentValue; + _wizardTitle = e.Value; } } } diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 7456c9bf2..fd588071f 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -381,6 +381,7 @@ False True True + CWP LL LR UI @@ -411,6 +412,7 @@ True True 5 + True True True True diff --git a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs index ab85b1391..ebb606213 100644 --- a/Tests/IntegrationTests/UICatalog/ScenarioTests.cs +++ b/Tests/IntegrationTests/UICatalog/ScenarioTests.cs @@ -87,7 +87,7 @@ public class ScenarioTests : TestsAllViews void OnApplicationOnInitializedChanged (object? s, EventArgs a) { - if (a.CurrentValue) + if (a.Value) { Application.Iteration += OnApplicationOnIteration; initialized = true; @@ -103,7 +103,7 @@ public class ScenarioTests : TestsAllViews shutdownGracefully = true; } - _output.WriteLine ($"Initialized == {a.CurrentValue}; shutdownGracefully == {shutdownGracefully}."); + _output.WriteLine ($"Initialized == {a.Value}; shutdownGracefully == {shutdownGracefully}."); } // If the scenario doesn't close within abortTime ms, this will force it to quit diff --git a/Tests/StressTests/ScenariosStressTests.cs b/Tests/StressTests/ScenariosStressTests.cs index 30e4bc131..dc133ff5e 100644 --- a/Tests/StressTests/ScenariosStressTests.cs +++ b/Tests/StressTests/ScenariosStressTests.cs @@ -90,7 +90,7 @@ public class ScenariosStressTests : TestsAllViews void OnApplicationOnInitializedChanged (object? s, EventArgs a) { - if (a.CurrentValue) + if (a.Value) { lock (_timeoutLock) { @@ -106,7 +106,7 @@ public class ScenariosStressTests : TestsAllViews { refreshedCount++; - if (args.CurrentValue) + if (args.Value) { updatedCount++; } @@ -124,7 +124,7 @@ public class ScenariosStressTests : TestsAllViews stopwatch!.Stop (); } - _output.WriteLine ($"Initialized == {a.CurrentValue}"); + _output.WriteLine ($"Initialized == {a.Value}"); } void OnApplicationOnIteration (object? s, IterationEventArgs a) diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs index 4273a806e..eb9e8f908 100644 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ b/Tests/UnitTests/Application/ApplicationTests.cs @@ -80,7 +80,7 @@ public class ApplicationTests void OnApplicationOnInitializedChanged (object s, EventArgs a) { - if (a.CurrentValue) + if (a.Value) { Application.Iteration += OnApplicationOnIteration; initialized = true; @@ -447,7 +447,7 @@ public class ApplicationTests void OnApplicationOnInitializedChanged (object s, EventArgs a) { - if (a.CurrentValue) + if (a.Value) { initialized = true; } diff --git a/Tests/UnitTests/Application/KeyboardTests.cs b/Tests/UnitTests/Application/KeyboardTests.cs index 1b80e16b6..77db195ac 100644 --- a/Tests/UnitTests/Application/KeyboardTests.cs +++ b/Tests/UnitTests/Application/KeyboardTests.cs @@ -447,9 +447,9 @@ public class KeyboardTests void OnApplicationOnInitializedChanged (object s, EventArgs a) { - _output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.CurrentValue); + _output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.Value); - if (a.CurrentValue) + if (a.Value) { Application.Iteration += OnApplicationOnIteration; initialized = true; diff --git a/Tests/UnitTests/Configuration/SettingsScopeTests.cs b/Tests/UnitTests/Configuration/SettingsScopeTests.cs index 4b1bd4344..123cb41ad 100644 --- a/Tests/UnitTests/Configuration/SettingsScopeTests.cs +++ b/Tests/UnitTests/Configuration/SettingsScopeTests.cs @@ -51,7 +51,7 @@ public class SettingsScopeTests ThemeScope scope = dict [ThemeManager.DEFAULT_THEME_NAME]; Assert.NotNull (scope); - Assert.Equal (HighlightStyle.Hover | HighlightStyle.Pressed, scope ["Button.DefaultHighlightStyle"].PropertyValue); + Assert.Equal (MouseState.In | MouseState.Pressed | MouseState.PressedOutside, scope ["Button.DefaultHighlightStates"].PropertyValue); RuntimeConfig = """ @@ -60,13 +60,13 @@ public class SettingsScopeTests { "Default": { - "Button.DefaultHighlightStyle": "None" + "Button.DefaultHighlightStates": "None" } }, { "NewTheme": { - "Button.DefaultHighlightStyle": "Hover" + "Button.DefaultHighlightStates": "In" } } ] @@ -77,8 +77,8 @@ public class SettingsScopeTests // assert Assert.Equal (2, ThemeManager.GetThemes ().Count); - Assert.Equal (HighlightStyle.None, (HighlightStyle)ThemeManager.GetCurrentTheme () ["Button.DefaultHighlightStyle"].PropertyValue!); - Assert.Equal (HighlightStyle.Hover, (HighlightStyle)ThemeManager.GetThemes () ["NewTheme"] ["Button.DefaultHighlightStyle"].PropertyValue!); + Assert.Equal (MouseState.None, (MouseState)ThemeManager.GetCurrentTheme () ["Button.DefaultHighlightStates"].PropertyValue!); + Assert.Equal (MouseState.In, (MouseState)ThemeManager.GetThemes () ["NewTheme"] ["Button.DefaultHighlightStates"].PropertyValue!); RuntimeConfig = """ { @@ -86,7 +86,7 @@ public class SettingsScopeTests { "Default": { - "Button.DefaultHighlightStyle": "Pressed" + "Button.DefaultHighlightStates": "Pressed" } } ] @@ -96,8 +96,8 @@ public class SettingsScopeTests // assert Assert.Equal (2, ThemeManager.GetThemes ().Count); - Assert.Equal (HighlightStyle.Pressed, (HighlightStyle)ThemeManager.Themes! [ThemeManager.DEFAULT_THEME_NAME] ["Button.DefaultHighlightStyle"].PropertyValue!); - Assert.Equal (HighlightStyle.Hover, (HighlightStyle)ThemeManager.Themes! ["NewTheme"] ["Button.DefaultHighlightStyle"].PropertyValue!); + Assert.Equal (MouseState.Pressed, (MouseState)ThemeManager.Themes! [ThemeManager.DEFAULT_THEME_NAME] ["Button.DefaultHighlightStates"].PropertyValue!); + Assert.Equal (MouseState.In, (MouseState)ThemeManager.Themes! ["NewTheme"] ["Button.DefaultHighlightStates"].PropertyValue!); // clean up Disable (resetToHardCodedDefaults: true); @@ -272,11 +272,11 @@ public class SettingsScopeTests // Arrange: Create a ThemeScope and verify a property exists ThemeScope defaultThemeScope = new ThemeScope (); defaultThemeScope.LoadHardCodedDefaults (); - Assert.True (defaultThemeScope.ContainsKey ("Button.DefaultHighlightStyle")); + Assert.True (defaultThemeScope.ContainsKey ("Button.DefaultHighlightStates")); ThemeScope darkThemeScope = new ThemeScope (); darkThemeScope.LoadHardCodedDefaults (); - Assert.True (darkThemeScope.ContainsKey ("Button.DefaultHighlightStyle")); + Assert.True (darkThemeScope.ContainsKey ("Button.DefaultHighlightStates")); // Create a Themes list with two themes List> themesList = diff --git a/Tests/UnitTests/Dialogs/WizardTests.cs b/Tests/UnitTests/Dialogs/WizardTests.cs index 71504333b..a45e1803e 100644 --- a/Tests/UnitTests/Dialogs/WizardTests.cs +++ b/Tests/UnitTests/Dialogs/WizardTests.cs @@ -598,7 +598,7 @@ public class WizardTests () Assert.Equal (string.Empty, r.Title); var expected = string.Empty; - r.TitleChanged += (s, args) => { Assert.Equal (r.Title, args.CurrentValue); }; + r.TitleChanged += (s, args) => { Assert.Equal (r.Title, args.Value); }; expected = "title"; r.Title = expected; diff --git a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs index 1a0efb5ea..6866ffe47 100644 --- a/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs +++ b/Tests/UnitTests/View/Adornment/ShadowStyleTests.cs @@ -144,7 +144,7 @@ public class ShadowStyleTests (ITestOutputHelper output) Width = Dim.Auto (), Height = Dim.Auto (), Text = "0123", - HighlightStyle = HighlightStyle.Pressed, + HighlightStates = MouseState.Pressed, ShadowStyle = style, CanFocus = true }; diff --git a/Tests/UnitTests/View/Mouse/MouseTests.cs b/Tests/UnitTests/View/Mouse/MouseTests.cs index 999ffda5a..f32e208f6 100644 --- a/Tests/UnitTests/View/Mouse/MouseTests.cs +++ b/Tests/UnitTests/View/Mouse/MouseTests.cs @@ -291,66 +291,54 @@ public class MouseTests : TestsAllViews Assert.Equal (2, clickedCount); me.Handled = false; + // Stay in Viewport + me.Flags = MouseFlags.Button1Pressed; + me.Position = me.Position with { X = 0 }; + view.NewMouseEvent (me); + Assert.Equal (3, clickedCount); + me.Handled = false; + view.Dispose (); // Button1Pressed, Button1Released cause Application.MouseGrabView to be set Application.ResetState (true); } - [Theory (Skip = "This test needs to be redone.")] - [InlineData (HighlightStyle.None, 0, 0)] - [InlineData (HighlightStyle.Pressed | HighlightStyle.PressedOutside, 1, 1)] - public void HighlightOnPress_Fires_Events_And_Highlights (HighlightStyle highlightOnPress, int expectedEnabling, int expectedDisabling) - { - var view = new View - { - CanFocus = true, - HighlightStyle = highlightOnPress, - Height = 1, - Width = 1 - }; + //[Theory] + //[InlineData (true, MouseState.None, 0, 0, 0, 0)] + //[InlineData (true, MouseState.In, 0, 0, 0, 0)] + //[InlineData (true, MouseState.Pressed, 0, 0, 1, 0)] + //[InlineData (true, MouseState.PressedOutside, 0, 0, 0, 1)] + //public void MouseState_Button1_Pressed_Then_Released_Outside (bool inViewport, MouseState highlightFlags, int noneCount, int expectedInCount, int expectedPressedCount, int expectedPressedOutsideCount) + //{ + // var testView = new MouseEventTestView + // { + // HighlightStates = highlightFlags + // }; - var enablingHighlight = 0; - var disablingHighlight = 0; - view.Highlight += ViewHighlight; - view.SetScheme (new (new Attribute (ColorName16.Red, ColorName16.Blue))); - Scheme originalScheme = view.GetScheme (); + // Assert.Equal (0, testView.MouseStateInCount); + // Assert.Equal (0, testView.MouseStatePressedCount); + // Assert.Equal (0, testView.MouseStatePressedOutsideCount); + // Assert.Equal (0, testView.MouseStateNoneCount); - view.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + // testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed, Position = new (inViewport ? 0 : 1, 0) }); + // Assert.Equal (expectedInCount, testView.MouseStateInCount); + // Assert.Equal (expectedPressedCount, testView.MouseStatePressedCount); + // Assert.Equal (expectedPressedOutsideCount, testView.MouseStatePressedOutsideCount); + // Assert.Equal (noneCount, testView.MouseStateNoneCount); - if (highlightOnPress != HighlightStyle.None) - { - Assert.NotEqual (originalScheme, view.GetScheme ()); - } - else - { - Assert.Equal (originalScheme, view.GetScheme ()); - } + // testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Released, Position = new (inViewport ? 0 : 1, 0) }); + // Assert.Equal (expectedInCount, testView.MouseStateInCount); + // Assert.Equal (expectedPressedCount, testView.MouseStatePressedCount); + // Assert.Equal (expectedPressedOutsideCount, testView.MouseStatePressedOutsideCount); + // Assert.Equal (noneCount, testView.MouseStateNoneCount); - view.NewMouseEvent (new () { Flags = MouseFlags.Button1Released }); - Assert.Equal (originalScheme, view.GetScheme ()); - Assert.Equal (expectedEnabling, enablingHighlight); - Assert.Equal (expectedDisabling, disablingHighlight); + // testView.Dispose (); - view.Dispose (); + // // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + // Application.ResetState (true); - // Button1Pressed, Button1Released cause Application.MouseGrabView to be set - Application.ResetState (true); - - return; - - void ViewHighlight (object sender, CancelEventArgs e) - { - if (e.NewValue == HighlightStyle.None) - { - disablingHighlight++; - } - else - { - enablingHighlight++; - } - } - } + //} // TODO: Add tests for each combination of HighlightFlags @@ -358,70 +346,298 @@ public class MouseTests : TestsAllViews [InlineData (0)] [InlineData (1)] [InlineData (10)] - public void HighlightOnPress_Move_Keeps_Highlight (int x) + public void MouseState_None_Button1_Pressed_Move_No_Changes (int x) { - var view = new View + var testView = new MouseEventTestView { - CanFocus = true, - HighlightStyle = HighlightStyle.Pressed | HighlightStyle.PressedOutside, - Height = 1, - Width = 1 + HighlightStates = MouseState.None }; - var enablingHighlight = 0; - var disablingHighlight = 0; - view.Highlight += ViewHighlight; - bool inViewport = view.Viewport.Contains (x, 0); + + bool inViewport = testView.Viewport.Contains (x, 0); // Start at 0,0 ; in viewport - view.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); - Assert.Equal (1, enablingHighlight); - Assert.Equal (0, disablingHighlight); + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); // Move to x,0 - view.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed, Position = new (x, 0) }); if (inViewport) { - Assert.Equal (2, enablingHighlight); - Assert.Equal (0, disablingHighlight); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); } else { - Assert.Equal (2, enablingHighlight); - Assert.Equal (0, disablingHighlight); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); } // Move backto 0,0 ; in viewport - view.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); if (inViewport) { - Assert.Equal (3, enablingHighlight); - Assert.Equal (0, disablingHighlight); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); } else { - Assert.Equal (3, enablingHighlight); - Assert.Equal (0, disablingHighlight); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); } - view.Dispose (); + testView.Dispose (); // Button1Pressed, Button1Released cause Application.MouseGrabView to be set Application.ResetState (true); + } - return; - - void ViewHighlight (object sender, CancelEventArgs e) + [Theory] + [InlineData (0)] + [InlineData (1)] + [InlineData (10)] + public void MouseState_Pressed_Button1_Pressed_Move_Keeps_Pressed (int x) + { + var testView = new MouseEventTestView { - if (e.NewValue == HighlightStyle.None) + HighlightStates = MouseState.Pressed + }; + + bool inViewport = testView.Viewport.Contains (x, 0); + + // Start at 0,0 ; in viewport + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (1, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + + // Move to x,0 + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed, Position = new (x, 0) }); + + if (inViewport) + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (1, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + else + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (1, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + + // Move backto 0,0 ; in viewport + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + + if (inViewport) + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (1, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + else + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (1, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + + testView.Dispose (); + + // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + Application.ResetState (true); + } + + [Theory] + [InlineData (0)] + [InlineData (1)] + [InlineData (10)] + public void MouseState_PressedOutside_Button1_Pressed_Move_Raises_PressedOutside (int x) + { + var testView = new MouseEventTestView + { + HighlightStates = MouseState.PressedOutside, + WantContinuousButtonPressed = false + }; + + bool inViewport = testView.Viewport.Contains (x, 0); + + // Start at 0,0 ; in viewport + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + + // Move to x,0 + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed, Position = new (x, 0) }); + + if (inViewport) + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + else + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (1, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + + // Move backto 0,0 ; in viewport + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + + if (inViewport) + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + else + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (1, testView.MouseStatePressedOutsideCount); + Assert.Equal (1, testView.MouseStateNoneCount); + } + + testView.Dispose (); + + // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + Application.ResetState (true); + } + + + [Theory] + [InlineData (0)] + [InlineData (1)] + [InlineData (10)] + public void MouseState_PressedOutside_Button1_Pressed_Move_Raises_PressedOutside_WantContinuousButtonPressed (int x) + { + var testView = new MouseEventTestView + { + HighlightStates = MouseState.PressedOutside, + WantContinuousButtonPressed = true + }; + + bool inViewport = testView.Viewport.Contains (x, 0); + + // Start at 0,0 ; in viewport + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + + // Move to x,0 + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed, Position = new (x, 0) }); + + if (inViewport) + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + else + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + + // Move backto 0,0 ; in viewport + testView.NewMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); + + if (inViewport) + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + else + { + Assert.Equal (0, testView.MouseStateInCount); + Assert.Equal (0, testView.MouseStatePressedCount); + Assert.Equal (0, testView.MouseStatePressedOutsideCount); + Assert.Equal (0, testView.MouseStateNoneCount); + } + + testView.Dispose (); + + // Button1Pressed, Button1Released cause Application.MouseGrabView to be set + Application.ResetState (true); + } + private class MouseEventTestView : View + { + public int MouseEnterCount { get; private set; } + public int MouseLeaveCount { get; private set; } + public int MouseStatePressedOutsideCount { get; private set; } + public int MouseStateInCount { get; private set; } + public int MouseStatePressedCount { get; private set; } + public int MouseStateNoneCount { get; private set; } + + public MouseEventTestView () + { + Height = 1; + Width = 1; + CanFocus = true; + Id = "mouseEventTestView"; + + MouseLeave += (s, e) => { MouseEnterCount++; }; + MouseEnter += (s, e) => { MouseLeaveCount++; }; + } + + /// + protected override void OnMouseStateChanged (EventArgs args) + { + switch (args.Value) { - disablingHighlight++; - } - else - { - enablingHighlight++; + case MouseState.None: + MouseStateNoneCount++; + + break; + case MouseState.In: + MouseStateInCount++; + + break; + + case MouseState.Pressed: + MouseStatePressedCount++; + + break; + + case MouseState.PressedOutside: + MouseStatePressedOutsideCount++; + + break; } + + base.OnMouseStateChanged (args); } } } diff --git a/Tests/UnitTests/Views/CheckBoxTests.cs b/Tests/UnitTests/Views/CheckBoxTests.cs index 8c4c31c76..a5ae20d6a 100644 --- a/Tests/UnitTests/Views/CheckBoxTests.cs +++ b/Tests/UnitTests/Views/CheckBoxTests.cs @@ -623,10 +623,10 @@ public class CheckBoxTests (ITestOutputHelper output) return; - void OnCheckedStateChanging (object sender, CancelEventArgs e) + void OnCheckedStateChanging (object sender, ResultEventArgs e) { checkedInvoked = true; - e.Cancel = true; + e.Handled = true; } } } diff --git a/Tests/UnitTests/Views/ColorPickerTests.cs b/Tests/UnitTests/Views/ColorPickerTests.cs index 77105f5c7..22144245d 100644 --- a/Tests/UnitTests/Views/ColorPickerTests.cs +++ b/Tests/UnitTests/Views/ColorPickerTests.cs @@ -16,9 +16,9 @@ public class ColorPickerTests cp.ColorChanged += (s, e) => { count++; - newColor = e.CurrentValue; + newColor = e.Result; - Assert.Equal (cp.SelectedColor, e.CurrentValue); + Assert.Equal (cp.SelectedColor, e.Result); }; cp.SelectedColor = new (1, 2, 3); diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 11d723274..4f808c21f 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -348,11 +348,11 @@ public class TextFieldTests (ITestOutputHelper output) _textField.TextChanging += TextFieldTextChanging; - void TextFieldTextChanging (object sender, CancelEventArgs e) + void TextFieldTextChanging (object sender, ResultEventArgs e) { - if (e.NewValue.GetRuneCount () > 11) + if (e.Result.GetRuneCount () > 11) { - e.NewValue = e.NewValue [..11]; + e.Result = e.Result [..11]; } } @@ -416,8 +416,8 @@ public class TextFieldTests (ITestOutputHelper output) tf.TextChanging += (s, e) => { - newText = e.NewValue; - oldText = e.CurrentValue; + newText = e.Result; + oldText = tf.Text; }; var top = new Toplevel (); @@ -957,11 +957,11 @@ public class TextFieldTests (ITestOutputHelper output) _textField.TextChanging += (s, e) => { - Assert.Equal ("changing", e.NewValue); + Assert.Equal ("changing", e.Result); if (cancel) { - e.Cancel = true; + e.Handled = true; } }; diff --git a/Tests/UnitTestsParallelizable/Application/CWP/ResultEventArgsTests.cs b/Tests/UnitTestsParallelizable/Application/CWP/ResultEventArgsTests.cs new file mode 100644 index 000000000..1ed0087b7 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/CWP/ResultEventArgsTests.cs @@ -0,0 +1,342 @@ +#nullable enable +using System; +using Terminal.Gui.App; +using Xunit; + +public class ResultEventArgsTests +{ + [Fact] + public void DefaultConstructor_InitializesProperties () + { + var args = new ResultEventArgs (); + + Assert.Null (args.Result); + Assert.False (args.Handled); + } + + [Fact] + public void Constructor_WithResult_SetsResult () + { + var args = new ResultEventArgs (42); + + Assert.Equal (42, args.Result); + Assert.False (args.Handled); + } + + [Fact] + public void Constructor_WithNullResult_AllowsNull () + { + var args = new ResultEventArgs (null); + + Assert.Null (args.Result); + Assert.False (args.Handled); + } + + [Fact] + public void Result_CanBeSetAndRetrieved () + { + var args = new ResultEventArgs (); + args.Result = "foo"; + + Assert.Equal ("foo", args.Result); + + args.Result = null; + Assert.Null (args.Result); + } + + [Fact] + public void Handled_CanBeSetAndRetrieved () + { + var args = new ResultEventArgs (); + Assert.False (args.Handled); + + args.Handled = true; + Assert.True (args.Handled); + + args.Handled = false; + Assert.False (args.Handled); + } + + [Fact] + public void WorksWithValueTypes () + { + var args = new ResultEventArgs (); + Assert.Equal (0, args.Result); // default(int) is 0 + + args.Result = 123; + Assert.Equal (123, args.Result); + } + + [Fact] + public void WorksWithReferenceTypes () + { + var obj = new object (); + var args = new ResultEventArgs (obj); + + Assert.Same (obj, args.Result); + + args.Result = null; + Assert.Null (args.Result); + } + + // Simulate an event pattern + public event EventHandler>? StringResultEvent; + + [Fact] + public void EventHandler_CanChangeResult_AndCallerSeesChange () + { + // Arrange + var args = new ResultEventArgs ("initial"); + StringResultEvent += (sender, e) => + { + // Handler changes the result + e.Result = "changed by handler"; + }; + + // Act + StringResultEvent?.Invoke (this, args); + + // Assert + Assert.Equal ("changed by handler", args.Result); + } + + + + [Fact] + public void EventHandler_CanSetResultToNull () + { + // Arrange + var args = new ResultEventArgs ("not null"); + StringResultEvent += (sender, e) => + { + e.Result = null; + }; + + // Act + StringResultEvent?.Invoke (this, args); + + // Assert + Assert.Null (args.Result); + } + + [Fact] + public void MultipleHandlers_LastHandlerWins () + { + // Arrange + var args = new ResultEventArgs (1); + EventHandler>? intEvent = null; + intEvent += (s, e) => e.Result = 2; + intEvent += (s, e) => e.Result = 3; + + // Act + intEvent?.Invoke (this, args); + + // Assert + Assert.Equal (3, args.Result); + } + + // Value type: int + [Fact] + public void EventHandler_CanChangeResult_Int () + { + EventHandler> handler = (s, e) => e.Result = 99; + var args = new ResultEventArgs (1); + handler.Invoke (this, args); + Assert.Equal (99, args.Result); + } + + // Value type: double + [Fact] + public void EventHandler_CanChangeResult_Double () + { + EventHandler> handler = (s, e) => e.Result = 2.718; + var args = new ResultEventArgs (3.14); + handler.Invoke (this, args); + Assert.Equal (2.718, args.Result); + } + + // Value type: bool + [Fact] + public void EventHandler_CanChangeResult_Bool () + { + EventHandler> handler = (s, e) => e.Result = false; + var args = new ResultEventArgs (true); + handler.Invoke (this, args); + Assert.False (args.Result); + } + + // Enum + enum MyEnum { A, B, C } + [Fact] + public void EventHandler_CanChangeResult_Enum () + { + EventHandler> handler = (s, e) => e.Result = MyEnum.C; + var args = new ResultEventArgs (MyEnum.A); + handler.Invoke (this, args); + Assert.Equal (MyEnum.C, args.Result); + } + + // Struct + struct MyStruct { public int X; } + [Fact] + public void EventHandler_CanChangeResult_Struct () + { + EventHandler> handler = (s, e) => e.Result = new MyStruct { X = 42 }; + var args = new ResultEventArgs (new MyStruct { X = 1 }); + handler.Invoke (this, args); + Assert.Equal (42, args.Result.X); + } + + // Reference type: string + [Fact] + public void EventHandler_CanChangeResult_String () + { + EventHandler> handler = (s, e) => e.Result = "changed"; + var args = new ResultEventArgs ("original"); + handler.Invoke (this, args); + Assert.Equal ("changed", args.Result); + } + + // Reference type: object + [Fact] + public void EventHandler_CanChangeResult_Object () + { + var newObj = new object (); + EventHandler> handler = (s, e) => e.Result = newObj; + var args = new ResultEventArgs (new object ()); + handler.Invoke (this, args); + Assert.Same (newObj, args.Result); + } + + // Nullable value type + [Fact] + public void EventHandler_CanChangeResult_NullableInt () + { + EventHandler> handler = (s, e) => e.Result = null; + var args = new ResultEventArgs (42); + handler.Invoke (this, args); + Assert.Null (args.Result); + } + + // Array + [Fact] + public void EventHandler_CanChangeResult_Array () + { + var newArr = new [] { "x", "y" }; + EventHandler> handler = (s, e) => e.Result = newArr; + var args = new ResultEventArgs (new [] { "a", "b" }); + handler.Invoke (this, args); + Assert.Equal (newArr, args.Result); + } + + // List + [Fact] + public void EventHandler_CanChangeResult_List () + { + var newList = new List { 1, 2, 3 }; + EventHandler>> handler = (s, e) => e.Result = newList; + var args = new ResultEventArgs> (new List { 9 }); + handler.Invoke (this, args); + Assert.Equal (newList, args.Result); + } + + // Dictionary + [Fact] + public void EventHandler_CanChangeResult_Dictionary () + { + var newDict = new Dictionary { ["a"] = 1 }; + EventHandler>> handler = (s, e) => e.Result = newDict; + var args = new ResultEventArgs> (new Dictionary ()); + handler.Invoke (this, args); + Assert.Equal (newDict, args.Result); + } + + // Record + public record MyRecord (int Id, string Name); + [Fact] + public void EventHandler_CanChangeResult_Record () + { + var rec = new MyRecord (1, "foo"); + EventHandler> handler = (s, e) => e.Result = rec; + var args = new ResultEventArgs (null); + handler.Invoke (this, args); + Assert.Equal (rec, args.Result); + } + + // Nullable int + [Fact] + public void EventHandler_CanChangeResult_NullableInt_ToValue_AndNull () + { + EventHandler> handler = (s, e) => e.Result = 123; + var args = new ResultEventArgs (null); + handler.Invoke (this, args); + Assert.Equal (123, args.Result); + + handler = (s, e) => e.Result = null; + args = new ResultEventArgs (456); + handler.Invoke (this, args); + Assert.Null (args.Result); + } + + // Nullable double + [Fact] + public void EventHandler_CanChangeResult_NullableDouble_ToValue_AndNull () + { + EventHandler> handler = (s, e) => e.Result = 3.14; + var args = new ResultEventArgs (null); + handler.Invoke (this, args); + Assert.Equal (3.14, args.Result); + + handler = (s, e) => e.Result = null; + args = new ResultEventArgs (2.71); + handler.Invoke (this, args); + Assert.Null (args.Result); + } + + // Nullable custom struct + [Fact] + public void EventHandler_CanChangeResult_NullableStruct_ToValue_AndNull () + { + EventHandler> handler = (s, e) => e.Result = new MyStruct { X = 7 }; + var args = new ResultEventArgs (null); + handler.Invoke (this, args); + Assert.Equal (7, args.Result?.X); + + handler = (s, e) => e.Result = null; + args = new ResultEventArgs (new MyStruct { X = 8 }); + handler.Invoke (this, args); + Assert.Null (args.Result); + } + + // Nullable string (reference type) + [Fact] + public void EventHandler_CanChangeResult_NullableString_ToValue_AndNull () + { + EventHandler> handler = (s, e) => e.Result = "hello"; + var args = new ResultEventArgs (null); + handler.Invoke (this, args); + Assert.Equal ("hello", args.Result); + + handler = (s, e) => e.Result = null; + args = new ResultEventArgs ("world"); + handler.Invoke (this, args); + Assert.Null (args.Result); + } + + // Nullable custom class + class MyClass { public int Y { get; set; } } + [Fact] + public void EventHandler_CanChangeResult_NullableClass_ToValue_AndNull () + { + EventHandler> handler = (s, e) => e.Result = new MyClass { Y = 42 }; + var args = new ResultEventArgs (null); + handler.Invoke (this, args); + Assert.NotNull (args.Result); + Assert.Equal (42, args.Result?.Y); + + handler = (s, e) => e.Result = null; + args = new ResultEventArgs (new MyClass { Y = 99 }); + handler.Invoke (this, args); + Assert.Null (args.Result); + } +} diff --git a/Tests/UnitTestsParallelizable/View/Adornment/AdornmentTests.cs b/Tests/UnitTestsParallelizable/View/Adornment/AdornmentTests.cs index 468158d26..b7d0ec390 100644 --- a/Tests/UnitTestsParallelizable/View/Adornment/AdornmentTests.cs +++ b/Tests/UnitTestsParallelizable/View/Adornment/AdornmentTests.cs @@ -1,8 +1,63 @@ -namespace Terminal.Gui.ViewTests; +#nullable enable +namespace Terminal.Gui.ViewTests; [Collection ("Global Test Setup")] public class AdornmentTests { + //private class TestView : View + //{ + // public bool BorderDrawn { get; set; } + // public bool PaddingDrawn { get; set; } + + // /// + // protected override bool OnDrawingContent () + // { + // if (Border is { } && Border.Thickness != Thickness.Empty) + // { + // BorderDrawn = true; + // Border.Draw (); + // } + // if (Padding is { } && Padding.Thickness != Thickness.Empty) + // { + // PaddingDrawn = true; + // Padding.Draw (); + // } + + // return base.OnDrawingContent (); + // } + //} + + //[Fact] + //public void DrawAdornments_UsesCWPEventHelper () + //{ + // var view = new TestView + // { + // Id = "view" + // }; + // view.Border!.Thickness = new Thickness (1); + // view.Padding!.Thickness = new Thickness (1); + + // // Test cancellation + // view.DrawingAdornments += OnDrawingAdornmentsHandled; + // view.DoDrawAdornments (originalClip: null); + // Assert.False (view.BorderDrawn); + // Assert.False (view.PaddingDrawn); + // view.DrawingAdornments -= OnDrawingAdornmentsHandled; + + // // Test successful drawing + // view.DrawingAdornments += OnDrawingAdornmentsAssert; + // view.BorderDrawn = false; + // view.PaddingDrawn = false; + // view.DoDrawAdornments (originalClip: null); + // Assert.True (view.BorderDrawn); + // Assert.True (view.PaddingDrawn); + + // view.Dispose (); + + // void OnDrawingAdornmentsHandled (object? sender, DrawAdornmentsEventArgs args) => args.Handled = true; + // void OnDrawingAdornmentsAssert (object? sender, DrawAdornmentsEventArgs args) => Assert.Null (args.Context); + //} + [Fact] public void Viewport_Location_Always_Empty_Size_Correct () { diff --git a/Tests/UnitTestsParallelizable/View/Adornment/ShadowStyletests.cs b/Tests/UnitTestsParallelizable/View/Adornment/ShadowStyletests.cs index 487729d9b..ecf379b26 100644 --- a/Tests/UnitTestsParallelizable/View/Adornment/ShadowStyletests.cs +++ b/Tests/UnitTestsParallelizable/View/Adornment/ShadowStyletests.cs @@ -44,7 +44,7 @@ public class ShadowStyleTests Width = Dim.Auto (), Height = Dim.Auto (), Text = "0123", - HighlightStyle = HighlightStyle.Pressed, + HighlightStates = MouseState.Pressed, ShadowStyle = style, CanFocus = true }; diff --git a/Tests/UnitTestsParallelizable/View/SchemeTests.cs b/Tests/UnitTestsParallelizable/View/SchemeTests.cs index 7ff9bbc9f..f1472cd54 100644 --- a/Tests/UnitTestsParallelizable/View/SchemeTests.cs +++ b/Tests/UnitTestsParallelizable/View/SchemeTests.cs @@ -139,15 +139,17 @@ public class SchemeTests public void GettingScheme_Event_CanOverrideScheme () { var view = new View (); - var customScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"]; + var customScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"]! with { Normal = Attribute.Default }; + Assert.NotEqual (Attribute.Default, view.GetScheme ().Normal); view.GettingScheme += (sender, args) => - { - args.NewScheme = customScheme; - args.Cancel = true; - }; + { + args.Result = customScheme; + args.Handled = true; + }; Assert.Equal (customScheme, view.GetScheme ()); + Assert.Equal (Attribute.Default, view.GetScheme ().Normal); view.Dispose (); } @@ -157,7 +159,7 @@ public class SchemeTests var view = new View (); var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"]; - view.SettingScheme += (sender, args) => args.Cancel = true; + view.SchemeChanging += (sender, args) => args.Handled = true; view.SetScheme (dialogScheme); @@ -175,8 +177,8 @@ public class SchemeTests { if (args.Role == VisualRole.Focus) { - args.NewValue = customAttribute; - args.Cancel = true; + args.Result = customAttribute; + args.Handled = true; } }; @@ -208,7 +210,7 @@ public class SchemeTests subView.SchemeName = "Error"; var errorScheme = SchemeManager.GetHardCodedSchemes ()? ["Error"]; - Assert.Equal (errorScheme, subView.GetScheme()); + Assert.Equal (errorScheme, subView.GetScheme ()); subView.Dispose (); superView.Dispose (); @@ -220,7 +222,7 @@ public class SchemeTests var view = new View (); var baseScheme = SchemeManager.GetHardCodedSchemes ()? ["Base"]; - Assert.Equal (baseScheme, view.GetScheme()); + Assert.Equal (baseScheme, view.GetScheme ()); view.Dispose (); } @@ -231,7 +233,7 @@ public class SchemeTests view.SchemeName = "Dialog"; var dialogScheme = SchemeManager.GetHardCodedSchemes ()? ["Dialog"]; - Assert.Equal (dialogScheme, view.GetScheme()); + Assert.Equal (dialogScheme, view.GetScheme ()); view.Dispose (); } @@ -244,7 +246,7 @@ public class SchemeTests return true; } - protected override bool OnSettingScheme (in Scheme? scheme) + protected override bool OnSettingScheme (ValueChangingEventArgs args) { return true; // Prevent setting the scheme } diff --git a/Tests/UnitTestsParallelizable/View/TitleTests.cs b/Tests/UnitTestsParallelizable/View/TitleTests.cs index c808e66e4..b0435141e 100644 --- a/Tests/UnitTestsParallelizable/View/TitleTests.cs +++ b/Tests/UnitTestsParallelizable/View/TitleTests.cs @@ -18,7 +18,7 @@ public class TitleTests r.TitleChanged += (s, args) => { - Assert.Equal (r.Title, args.CurrentValue); + Assert.Equal (r.Title, args.Value); }; expected = "title"; diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs index 4d6f630a7..e9ad057bb 100644 --- a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs @@ -13,7 +13,7 @@ public class TextFieldTests Assert.Equal ("A", tf.Text); // cancel the next keystroke - tf.TextChanging += (s, e) => e.Cancel = e.NewValue == "AB"; + tf.TextChanging += (s, e) => e.Handled = e.Result == "AB"; tf.NewKeyDownEvent (Key.B.WithShift); // B was canceled so should just be A diff --git a/docfx/README.md b/docfx/README.md index 1d59cdd59..bf2acba6a 100644 --- a/docfx/README.md +++ b/docfx/README.md @@ -4,13 +4,26 @@ The API documentation is generated via a GitHub Action (`.github/workflows/api-d ## To Generate the Docs Locally -0. Install DotFX https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html -1. Run `./docfx/scripts/Build.ps1` +0. Install DocFX: https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html +1. Run `./docfx/scripts/build.ps1` 2. Browse to http://localhost:8080 and verify everything looks good. -3. Hit ctrl-c to stop the script. +3. Hit Ctrl-C to stop the script. -## To update `views.md` +## To Update `views.md` 0. Switch to the `./docfx` folder 1. Run `./scripts/generate-views-doc.ps1` 2. Commit the changes to `docs/views.md` + +## API Documentation Overview + +The API documentation for Terminal.Gui is a critical resource for developers, providing detailed information on classes, methods, properties, and events within the library. This documentation is hosted at [gui-cs.github.io/Terminal.Gui](https://gui-cs.github.io/Terminal.Gui) and includes both auto-generated API references and conceptual guides. For a broader overview of the Terminal.Gui project, including project structure and contribution guidelines, refer to the main [Terminal.Gui README](../Terminal.Gui/README.md). + +### Scripts for Documentation Generation + +The `scripts` folder contains PowerShell scripts to assist in generating and updating documentation: +- `build.ps1`: A script to build the documentation locally. Running this script with DocFX installed will generate the documentation site, which can be viewed at `http://localhost:8080`. +- `generate-views-doc.ps1`: A script specifically for updating the `views.md` file in the `docs` directory. This script automates the process of documenting the various view classes in Terminal.Gui, ensuring that the documentation remains current with the codebase. +- `OutputView/`: A directory likely used for storing output or intermediate files related to the documentation generation process. + +These scripts streamline the process of maintaining up-to-date documentation, ensuring that contributors can easily generate and verify documentation changes locally before committing them. diff --git a/docfx/docs/cancellable-work-pattern.md b/docfx/docs/cancellable-work-pattern.md index b0a418e6b..b39262d69 100644 --- a/docfx/docs/cancellable-work-pattern.md +++ b/docfx/docs/cancellable-work-pattern.md @@ -2,39 +2,100 @@ The *Cancellable Work Pattern* is a core design pattern in Terminal.Gui, used to structure workflows that can be executed in a default manner, modified by external code or subclasses, or cancelled entirely. This pattern is prevalent across various components of Terminal.Gui, including the `View` class for rendering, keyboard input, and command execution, as well as application-level input handling and property changes. Unlike traditional inheritance-based approaches that rely on overriding virtual methods (which often require subclasses to understand base class implementation details), the *Cancellable Work Pattern* prioritizes events for loose coupling, supplemented by optional virtual methods for flexibility. -This deep dive defines the *Cancellable Work Pattern*, outlines its components and goals, and illustrates its implementation through examples in `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper`. +This document is a conceptual definition of *Cancellable Work Pattern* and outlines its components and goals, and illustrates its implementation through examples in `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper`. + +See the [Events Deep Dive](events.md) for a concrete deep dive and tutorial. + +> [!NOTE] +> Some terms in this document are based on a yet-to-be addressed Issue: https://github.com/gui-cs/Terminal.Gui/issues/4050 ## Definition The *Cancellable Work Pattern* is a design pattern for executing a structured workflow with one or more phases, where each phase can: + - Proceed in a default manner. - Be modified by external code or subclasses. - Be cancelled to halt further processing. The pattern uses events as the primary mechanism for notification and customization, supplemented by virtual methods for subclassing when needed. It is a specialization of the **Observer Pattern**, extended with structured workflows, explicit cancellation mechanisms, and context-aware notifications. It also incorporates elements of the **Template Method Pattern** (via virtual methods) and **Pipeline Pattern** (via sequential phases). -## Goals - -The *Cancellable Work Pattern* is designed to achieve the following: -1. **Default Execution**: Provide a standard process that executes unless interrupted, ensuring predictable behavior out of the box. -2. **Modification**: Allow external code or subclasses to customize specific phases without requiring deep knowledge of the implementation. -3. **Cancellation**: Enable halting of a phase or the entire workflow, giving consumers control over the process. -4. **Decoupling**: Use events to reduce reliance on inheritance, minimizing the need for subclasses to understand base class details. - ## Lexicon and Taxonomy [!INCLUDE [Events Lexicon](~/includes/events-lexicon.md)] +## Core Concept + +At its core, CWP defines a workflow as a sequence of one or more distinct phases, each representing a unit of work within a larger operation. For each phase, the pattern provides mechanisms to: + +- **Execute Default Behavior**: A predefined implementation that executes if no external intervention occurs, ensuring the system remains functional out of the box. +- **Allow Customization**: Through event subscriptions or method overrides, external code or subclasses can inject custom logic to alter the phase's behavior without needing to replace the entire workflow. +- **Support Cancellation**: A explicit mechanism to halt the execution of a phase or the entire workflow, preventing further processing when certain conditions are met (e.g., user intervention, error states, or logical constraints). + +This triadic structure—default execution, customization, and cancellation—distinguishes CWP from simpler event-driven or inheritance-based approaches. It ensures that workflows are both robust (via defaults) and flexible (via customization and cancellation), making it ideal for complex systems like terminal user interfaces where multiple stakeholders (e.g., framework developers, application developers, and end-users) interact with the same processes. + +### Structural Components + +The Cancellable Work Pattern typically comprises the following components: + +1. **Workflow Container**: The entity (often a class or object) that encapsulates the overall workflow, defining the sequence of phases and orchestrating their execution. In Terminal.Gui, this might be a `View` object managing rendering or input handling. + +2. **Phases**: Individual steps within the workflow, each representing a discrete unit of work. Each phase has a default implementation and points for intervention. For example, rendering text in a view could be a single phase within a broader drawing workflow. + +3. **Notification Mechanisms**: Events or callbacks that notify external observers of a phase's impending execution, allowing them to intervene. These are typically implemented as delegate-based events (e.g., `DrawingText` event in Terminal.Gui) or virtual methods (e.g., `OnDrawingText`). + +4. **Cancellation Flags**: Boolean indicators or properties within event arguments that signal whether a phase or workflow should be halted. In Terminal.Gui, this is often seen as `Handled` or `Cancel` properties in event args objects. + +5. **Context Objects**: Data structures passed to notification handlers, providing relevant state or parameters about the phase (e.g., `DrawContext` or `Key` objects in Terminal.Gui), enabling informed decision-making by custom logic. + +### Operational Flow + +The operational flow of CWP follows a consistent pattern for each phase within a workflow: + +1. **Pre-Phase Notification**: Before executing a phase's default behavior, the workflow container raises an event or calls a virtual method to notify potential observers or subclasses. This step allows for preemptive customization or cancellation. + +2. **Cancellation Check**: If the notification mechanism indicates cancellation (e.g., a return value of `true` from a virtual method or a `Cancel` flag set in event args), the phase is aborted, and control may return or move to the next phase, depending on the workflow design. + +3. **Default Execution**: If no cancellation occurs, the phase's default behavior is executed. This ensures the workflow progresses as intended in the absence of external intervention. + +4. **Post-Phase Notification (Optional)**: In some implementations, a secondary notification occurs after the phase completes, informing observers of the outcome or updated state (e.g., `OrientationChanged` event after a property update in Terminal.Gui). + +This flow repeats for each phase, allowing granular control over complex operations. Importantly, CWP decouples the workflow's structure from its customization, as external code can subscribe to events without needing to subclass or understand the container's internal logic. + +## Advantages + +- **Flexibility**: Developers can modify specific phases without altering the entire workflow, supporting a wide range of use cases from minor tweaks to complete overrides. + +- **Decoupling**: By prioritizing events over inheritance, CWP reduces tight coupling between base and derived classes, adhering to principles of loose coupling in software design. + +- **Robustness**: Default behaviors ensure the system remains operational even if no customization is provided, reducing the risk of incomplete implementations. + +- **Control**: Cancellation mechanisms provide precise control over workflow execution, critical in interactive systems where user input or state changes may necessitate halting operations. + +## Limitations + +- **Complexity**: Multi-phase workflows can become intricate, especially when numerous events and cancellation points are involved, potentially leading to debugging challenges. + +- **Performance Overhead**: Raising events and checking cancellation flags for each phase introduces minor performance costs, which may accumulate in high-frequency operations like rendering. + +- **Learning Curve**: Understanding the pattern's structure and knowing when to use events versus overrides requires familiarity, which may pose a barrier to novice developers. + +## Applicability + +CWP is particularly suited to domains where workflows must balance standardization with adaptability, such as user interface frameworks (e.g., Terminal.Gui), game engines, or workflow automation systems. It excels in scenarios where operations are inherently interruptible—such as responding to user input, rendering dynamic content, or managing state transitions—and where multiple components or developers need to collaborate on the same process without tight dependencies. + +In the context of Terminal.Gui, CWP underpins critical functionalities like view rendering, keyboard input processing, command execution, and property change handling, ensuring that these operations are both predictable by default and customizable as needed by application developers. + ## Implementation in Terminal.Gui -The *Cancellable Work Pattern* is implemented consistently across several key areas of Terminal.Gui’s `v2_develop` branch. Below are five primary examples, each illustrating the pattern in a different domain: rendering, keyboard input at the view level, command execution, application-level keyboard input, and property changes. +The *Cancellable Work Pattern* is implemented consistently across several key areas of Terminal.Gui's `v2_develop` branch. Below are five primary examples, each illustrating the pattern in a different domain: rendering, keyboard input at the view level, command execution, application-level keyboard input, and property changes. ### 1. View.Draw: Rendering Workflow The `View.Draw` method orchestrates the rendering of a view, including its adornments (margin, border, padding), viewport, text, content, subviews, and line canvas. It is a multi-phase workflow where each phase can be customized or cancelled. #### Example: `DoDrawText` -The `DoDrawText` method, responsible for drawing a view’s text, exemplifies the pattern: + +The `DoDrawText` method, responsible for drawing a view's text, exemplifies the pattern: ```csharp private void DoDrawText(DrawContext? context = null) { @@ -55,20 +116,22 @@ private void DoDrawText(DrawContext? context = null) DrawText(context); // Default behavior } ``` + - **Workflow**: Single phase for text drawing within the broader `Draw` workflow. - **Notifications**: `OnDrawingText` (virtual), `DrawingText` (event). - **Cancellation**: `OnDrawingText` returning `true` or `dev.Cancel = true`. - **Context**: `DrawContext` and `DrawEventArgs` provide rendering details. -- **Default Behavior**: `DrawText` renders the view’s text. +- **Default Behavior**: `DrawText` renders the view's text. - **Use Case**: Allows customization of text rendering (e.g., custom formatting) or cancellation (e.g., skipping text for performance). ### 2. View.Keyboard: View-Level Keyboard Input -The `View.ProcessKeyDown` method processes keyboard input for a view, mapping keys to commands or handling them directly. It is a linear workflow with a single phase per key event. +The `View.NewKeyDownEvent` method processes keyboard input for a view, mapping keys to commands or handling them directly. It is a linear workflow with a single phase per key event. + +#### Example: `NewKeyDownEvent` -#### Example: `ProcessKeyDown` ```csharp -public virtual bool ProcessKeyDown(Key key) +public bool NewKeyDownEvent(Key key) { if (OnKeyDown(key)) // Virtual method { @@ -94,28 +157,30 @@ public virtual bool ProcessKeyDown(Key key) The `View.Command` APIs execute commands like `Command.Activate` and `Command.Accept`, used for state changes (e.g., `CheckBox` toggling) and action confirmation (e.g., dialog submission). It is a per-unit workflow, with one phase per command. -#### Example: `RaiseActivating` -The `RaiseActivating` method handles `Command.Activate`: +#### Example: `RaiseAccepting` + ```csharp -protected bool? RaiseActivating(ICommandContext? ctx) +protected bool? RaiseAccepting(ICommandContext? ctx) { CommandEventArgs args = new() { Context = ctx }; - if (OnActivating(args) || args.Handled) + if (OnAccepting(args) || args.Handled) { return true; } - Activating?.Invoke(this, args); - return Activating is null ? null : args.Handled; + Accepting?.Invoke(this, args); + return Accepting is null ? null : args.Handled; } ``` -- **Workflow**: Single phase for `Command.Activate`. -- **Notifications**: `OnActivating` (virtual), `Activating` (event). -- **Cancellation**: `OnActivating` returning `true` or `args.Handled = true`. + +- **Workflow**: Single phase for `Command.Accept`. +- **Notifications**: `OnAccepting` (virtual), `Accepting` (event). +- **Cancellation**: `OnAccepting` returning `true` or `args.Handled = true`. - **Context**: `ICommandContext` provides `Command`, `Source`, and `Binding`. -- **Default Behavior**: `SetFocus` for `Command.Activate` (in `SetupCommands`). +- **Default Behavior**: Propagates to `SuperView` or default button if not handled. - **Use Case**: Allows customization of state changes (e.g., `CheckBox` toggling) or cancellation (e.g., preventing focus in `MenuItemv2`). #### Propagation Challenge + - `Command.Activate` is local, limiting hierarchical coordination (e.g., `MenuBarv2` popovers). A proposed `PropagatedCommands` property addresses this, as detailed in the appendix. ### 4. Application.Keyboard: Application-Level Keyboard Input @@ -123,6 +188,7 @@ protected bool? RaiseActivating(ICommandContext? ctx) The `Application.OnKeyDown` method processes application-wide keyboard input, raising events for global key handling. It is an event-driven workflow, with a single phase per key event. #### Example: `OnKeyDown` + ```csharp public static bool OnKeyDown(Key key) { @@ -134,6 +200,7 @@ public static bool OnKeyDown(Key key) return key.Handled; // Check for cancellation } ``` + - **Workflow**: Event-driven, processing one key event. - **Notifications**: `KeyDown` (event, no virtual method). - **Cancellation**: `key.Handled = true`. @@ -146,6 +213,7 @@ public static bool OnKeyDown(Key key) The `OrientationHelper` class manages orientation changes (e.g., in `StackPanel`), raising events for property updates. It is an event-driven workflow, with a single phase per change. #### Example: `Orientation` Setter + ```csharp public Orientation Orientation { @@ -174,6 +242,7 @@ public Orientation Orientation } } ``` + - **Workflow**: Event-driven, processing one property change. - **Notifications**: `OnOrientationChanging` (virtual), `OrientationChanging` (event), `OnOrientationChanged`, `OrientationChanged` (post-event). - **Cancellation**: `OnOrientationChanging` returning `true` or `args.Cancel = true`. @@ -181,58 +250,3 @@ public Orientation Orientation - **Default Behavior**: Updates `_orientation` and notifies via `OrientationChanged`. - **Use Case**: Allows customization of orientation changes (e.g., adjusting layout) or cancellation (e.g., rejecting invalid orientations). -## Proposed Enhancement: Command Propagation - -The *Cancellable Work Pattern* in `View.Command` currently supports local `Command.Activate` and propagating `Command.Accept`. To address hierarchical coordination needs (e.g., `MenuBarv2` popovers, `Dialog` closing), a `PropagatedCommands` property is proposed (Issue #4050): - -- **Change**: Add `IReadOnlyList PropagatedCommands` to `View`, defaulting to `[Command.Accept]`. `Raise*` methods propagate if the command is in `SuperView?.PropagatedCommands` and `args.Handled` is `false`. -- **Example**: - ```csharp - public IReadOnlyList PropagatedCommands { get; set; } = new List { Command.Accept }; - protected bool? RaiseActivating(ICommandContext? ctx) - { - CommandEventArgs args = new() { Context = ctx }; - if (OnActivating(args) || args.Handled) - { - return true; - } - Activating?.Invoke(this, args); - if (!args.Handled && SuperView?.PropagatedCommands.Contains(Command.Activate) == true) - { - return SuperView.InvokeCommand(Command.Activate, ctx); - } - return Activating is null ? null : args.Handled; - } - ``` -- **Impact**: Enables `Command.Activate` propagation for `MenuBarv2` while preserving `Command.Accept` propagation, maintaining decoupling and avoiding noise from irrelevant commands. - -## Challenges and Recommendations - -1. **Conflation in FlagSelector**: - - **Issue**: `CheckBox.Activating` triggers `Accepting`, conflating state change and confirmation. - - **Recommendation**: Refactor to separate `Activating` and `Accepting`: - ```csharp - checkbox.Activating += (sender, args) => - { - if (RaiseActivating(args.Context) is true) - { - args.Handled = true; - } - }; - ``` - -2. **Propagation Limitations**: - - **Issue**: Local `Command.Activate` restricts `MenuBarv2` coordination; `Command.Accept` uses hacks (#3925). - - **Recommendation**: Adopt `PropagatedCommands` to enable targeted propagation, as proposed. - -3. **Documentation Gaps**: - - **Issue**: The pattern’s phases and `Handled` semantics are not fully documented. - - **Recommendation**: Document the pattern’s structure, phases, and examples across `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper`. - -4. **Complexity in Multi-Phase Workflows**: - - **Issue**: `View.Draw`’s multi-phase workflow can be complex for developers to customize. - - **Recommendation**: Provide clearer phase-specific documentation and examples. - -## Conclusion - -The *Cancellable Work Pattern* is a foundational design in Terminal.Gui, enabling extensible, cancellable, and decoupled workflows across rendering, input handling, command execution, and property changes. Its implementation in `View.Draw`, `View.Keyboard`, `View.Command`, `Application.Keyboard`, and `OrientationHelper` supports diverse use cases, from `Menuv2`’s hierarchical menus to `CheckBox`’s state toggling. The proposed `PropagatedCommands` property enhances the pattern’s applicability in `View.Command`, addressing propagation needs while maintaining its core principles. By refining implementation flaws (e.g., `FlagSelector`) and improving documentation, Terminal.Gui can further leverage this pattern for robust, flexible UI interactions. \ No newline at end of file diff --git a/docfx/docs/dimauto.md b/docfx/docs/dimauto.md index abab0b52b..fa5f5db2a 100644 --- a/docfx/docs/dimauto.md +++ b/docfx/docs/dimauto.md @@ -7,26 +7,25 @@ ## Overview -The `Dim.Auto` type is a type of `Dim` that automatically sizes the view based on its content. This is useful when you want to size a view based on the content it contains. That content can either be the `Text`, the `SubViews`, or something else defined by the view. +The `Dim.Auto` type is a specialized `Dim` class in Terminal.Gui v2 that enables automatic sizing of a `View` based on its content. This is particularly useful for dynamically sizing views to accommodate varying content such as text, subviews, or explicitly set content areas. Unlike other `Dim` types like `Dim.Absolute` or `Dim.Fill`, `Dim.Auto` calculates dimensions at runtime based on specified criteria, making it ideal for responsive UI design in terminal applications. -Like all `Dim` types, `Dim.Auto` is used to set the `Width` or `Height` of a view and can be combined with other `Dim` types using addition or subtraction (see. `DimCombine`). +Like all `Dim` types, `Dim.Auto` is used to set the `Width` or `Height` of a view and can be combined with other `Dim` types using addition or subtraction (see `DimCombine`). -The `DimAutoStyle` enum defines the different ways that `Dim.Auto` can be used to size a view. The `DimAutoStyle` enum has the following values: +The `DimAutoStyle` enum defines the different strategies that `Dim.Auto` can employ to size a view. The `DimAutoStyle` enum has the following values: -* `Text` - The view is sized based on the `Text` property and `TextFormatter` settings. -* `Content` - The view is sized based on either the value returned by `View.SetContentSize()` or the `Subviews` property. If the content size is not explicitly set (via `View.SetContentSize()`), the view is sized based on the Subview with the largest relvant dimension plus location. If the content size is explicitly set, the view is sized based on the value returned by `View.SetContentSize()`. -* `Auto` - The view is sized based on both the text and content, whichever is larger. +- **Text**: The view is sized based on the `Text` property and `TextFormatter` settings. This considers the formatted text dimensions, constrained by any specified maximum dimensions. +- **Content**: The view is sized based on either the value returned by `View.GetContentSize()` or the `Subviews` property. If the content size is explicitly set (via `View.SetContentSize()`), the view is sized based on that value. Otherwise, it considers the subview with the largest relevant dimension plus its position. +- **Auto**: The view is sized based on both the text and content, whichever results in the larger dimension. ## Using Dim.Auto `Dim.Auto` is defined as: ```cs -public static Dim Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim minimumContentDim = null, Dim max = null) +public static Dim Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim minimumContentDim = null, Dim maximumContentDim = null) ``` -To use `Dim.Auto`, set the `Width` or `Height` property of a view to `Dim.Auto (DimAutoStyle.Text)` or `Dim.Auto (DimAutoStyle.Content)`. - +To use `Dim.Auto`, set the `Width` or `Height` property of a view to `Dim.Auto(DimAutoStyle.Text)`, `Dim.Auto(DimAutoStyle.Content)`, or `Dim.Auto(DimAutoStyle.Auto)`. For example, to create a `View` that is sized based on the `Text` property, you can do this: @@ -34,8 +33,8 @@ For example, to create a `View` that is sized based on the `Text` property, you View view = new () { Text = "Hello, World!", - Width = Dim.Auto (DimAutoStyle.Text), - Height = Dim.Auto (DimAutoStyle.Text), + Width = Dim.Auto(DimAutoStyle.Text), + Height = Dim.Auto(DimAutoStyle.Text), }; ``` @@ -46,15 +45,15 @@ To create a `View` that is sized based on its `Subviews`, you can do this: ```cs View view = new () { - Width = Dim.Auto (DimAutoStyle.Content), - Height = Dim.Auto (DimAutoStyle.Content), + Width = Dim.Auto(DimAutoStyle.Content), + Height = Dim.Auto(DimAutoStyle.Content), }; -view.Add (new Label () { Text = "Hello, World!" }); +view.Add(new Label() { Text = "Hello, World!" }); ``` In this example, the `View` will be sized based on the size of the `Label` that is added to it. -### Specifying a miniumum size +### Specifying a Minimum Size You can specify a minimum size by passing a `Dim` object to the `minimumContentDim` parameter. For example, to create a `View` that is sized based on the `Text` property, but has a minimum width of 10 columns, you can do this: @@ -62,8 +61,8 @@ You can specify a minimum size by passing a `Dim` object to the `minimumContentD View view = new () { Text = "Hello, World!", - Width = Dim.Auto (DimAutoStyle.Text, minimumContentDim: Dim.Absolute (10)), // Same as `minimumContentDim: 10` - Height = Dim.Auto (DimAutoStyle.Text), + Width = Dim.Auto(DimAutoStyle.Text, minimumContentDim: Dim.Absolute(10)), // Same as `minimumContentDim: 10` + Height = Dim.Auto(DimAutoStyle.Text), }; ``` @@ -72,71 +71,120 @@ Sometimes it's useful to have the minimum size be dynamic. Use `Dim.Func` as fol ```cs View view = new () { - Width = Dim.Auto (DimAutoStyle.Content, minimumContentDim: Dim.Func (GetDynamicMinSize)), - Height = Dim.Auto (DimAutoStyle.Text), + Width = Dim.Auto(DimAutoStyle.Content, minimumContentDim: Dim.Func(GetDynamicMinSize)), + Height = Dim.Auto(DimAutoStyle.Text), }; -int GetDynamicMinSize () +int GetDynamicMinSize() { return someDynamicInt; } ``` -### Specifying a maximum size +### Specifying a Maximum Size -It is common to want to constrain how large a View can be sized. The `maximumContentDim` parameter to the `Dim.Auto ()` method enables this. Like `minimumContentDim` it is of type `Dim` and thus can represent a dynamic value. For example, by default `Dialog` specifies `maximumContentDim` as `Dim.Percent (90)` to ensure a Dialog box is never larger than 90% of the screen. +It is common to want to constrain how large a View can be sized. The `maximumContentDim` parameter to the `Dim.Auto()` method enables this. Like `minimumContentDim`, it is of type `Dim` and thus can represent a dynamic value. For example, by default, `Dialog` specifies `maximumContentDim` as `Dim.Percent(90)` to ensure a dialog box is never larger than 90% of the screen. + +```cs +View dialog = new () +{ + Width = Dim.Auto(DimAutoStyle.Content, maximumContentDim: Dim.Percent(90)), + Height = Dim.Auto(DimAutoStyle.Content, maximumContentDim: Dim.Percent(90)), +}; +``` + +## Technical Details + +### Calculation Logic + +The `Dim.Auto` class calculates dimensions dynamically during the layout process. Here's how it works under the hood, based on the codebase analysis: + +- **Text-Based Sizing (`DimAutoStyle.Text`)**: When using `Text` style, the dimension is determined by the formatted text size as computed by `TextFormatter`. For width, it uses `ConstrainToWidth`, and for height, it uses `ConstrainToHeight`. These values are set based on the formatted text size, constrained by any maximum dimensions provided. +- **Content-Based Sizing (`DimAutoStyle.Content`)**: For `Content` style, if `ContentSizeTracksViewport` is `false` and there are no subviews, it uses the explicitly set content size from `GetContentSize()`. Otherwise, it iterates through subviews to calculate the maximum dimension needed based on their positions and sizes. +- **Auto Sizing (`DimAutoStyle.Auto`)**: This combines both `Text` and `Content` strategies, taking the larger of the two calculated dimensions. + +The calculation in `DimAuto.Calculate` method also respects `minimumContentDim` and `maximumContentDim`: +- The final size is at least the minimum specified (if any), and at most the maximum specified (if any). +- Adornments (like margins, borders, and padding) are added to the calculated content size to ensure the view's frame includes these visual elements. + +### Handling Subviews + +When sizing based on subviews, `Dim.Auto` employs a sophisticated approach to handle dependencies: +- It categorizes subviews based on their `Pos` and `Dim` types to manage layout dependencies. For instance, it processes subviews with absolute positions and dimensions first, then handles more complex cases like `PosAnchorEnd` or `DimView`. +- This ensures that views dependent on other views' sizes or positions are calculated correctly, avoiding circular dependencies and ensuring accurate sizing. + +### Adornments Consideration + +The size calculation includes the thickness of adornments (margin, border, padding) to ensure the view's total frame size accounts for these elements. This is evident in the code where `adornmentThickness` is added to the computed content size. ## Limitations -`Dim.Auto` is not always the best choice for sizing a view. For example, if you want a view to fill the entire width of the Superview, you should use `Dim.Fill ()` instead of `Dim.Auto (DimAutoStyle.Content)`. +`Dim.Auto` is not always the best choice for sizing a view. Consider the following limitations: -`Dim.Auto` is also not always the most efficient way to size a view. If you know the size of the content ahead of time, you can set the `Width` and `Height` properties to `Dim.Absolute (n)` instead of using `Dim.Auto`. +- **Performance Overhead**: `Dim.Auto` can introduce performance overhead due to the dynamic calculation of sizes, especially with many subviews or complex text formatting. If the size is known and static, `Dim.Absolute(n)` might be more efficient. +- **Not Suitable for Full-Screen Layouts**: If you want a view to fill the entire width or height of the superview, `Dim.Fill()` is more appropriate than `Dim.Auto(DimAutoStyle.Content)` as it directly uses the superview's dimensions without content-based calculations. +- **Dependency Complexity**: When subviews themselves use `Dim.Auto` or other dependent `Dim` types, the layout process can become complex and may require multiple iterations to stabilize, potentially leading to unexpected results if not carefully managed. -## Behavior of other Pos/Dim Types when used within a Dim.Auto-sized View +## Behavior of Other Pos/Dim Types When Used Within a Dim.Auto-Sized View -The table below descibes the behavior of the various Pos/Dim types when used by subviews of a View that uses `Dim.Auto` for it's `Width` or `Height`: +The table below describes the behavior of various `Pos` and `Dim` types when used by subviews of a view that uses `Dim.Auto` for its `Width` or `Height`. This reflects how these types influence the automatic sizing: -| Type | Impacts Dimension | Notes | -|-------------|-------------------|---------------------------------------------------------------------------------------------------------| -| PosAlign | Yes | The subviews with the same `GroupId` will be aligned at the maximimum dimension to enable them to not be clipped. This dimension plus the group's position will determine the minimum `Dim.Auto` dimension. | -| PosView | Yes | The position plus the dimension of `subview.Target` will determine the minimum `Dim.Auto` dimension. | -| PosCombine | Yes | | -| PosAnchorEnd| Yes | The `Dim.Auto` dimension will be increased by the dimension of the subview. | -| PosCenter | No | | -| PosPercent | No | | -| PosAbsolute | Yes | | -| PosFunc | Yes | | -| DimView | Yes | The position plus the dimension of `subview.Target` will determine the minimum `Dim.Auto` dimension. | -| DimCombine | Yes | | -| DimFill | No | | -| DimPercent | No | | -| DimAuto | Yes | | -| DimAbsolute | Yes | | -| DimFunc | Yes | | +| Type | Impacts Dimension | Notes | +|---------------|-------------------|---------------------------------------------------------------------------------------------------| +| **PosAlign** | Yes | The subviews with the same `GroupId` will be aligned at the maximum dimension to enable them to not be clipped. This dimension plus the group's position will determine the minimum `Dim.Auto` dimension. | +| **PosView** | Yes | The position plus the dimension of `subview.Target` will determine the minimum `Dim.Auto` dimension. | +| **PosCombine**| Yes | Impacts dimension if it includes a `Pos` type that affects dimension (like `PosView` or `PosAnchorEnd`). | +| **PosAnchorEnd**| Yes | The `Dim.Auto` dimension will be increased by the dimension of the subview to accommodate its anchored position. | +| **PosCenter** | No | Does not impact the dimension as it centers based on superview size, not content. | +| **PosPercent**| No | Does not impact dimension unless combined with other impacting types; based on superview size. | +| **PosAbsolute**| Yes | Impacts dimension if the absolute position plus subview dimension exceeds current content size. | +| **PosFunc** | Yes | Impacts dimension if the function returns a value that, combined with subview dimension, exceeds content size. | +| **DimView** | Yes | The dimension of `subview.Target` will contribute to the minimum `Dim.Auto` dimension. | +| **DimCombine**| Yes | Impacts dimension if it includes a `Dim` type that affects dimension (like `DimView` or `DimAuto`). | +| **DimFill** | No | Does not impact dimension as it fills remaining space, not contributing to content-based sizing. | +| **DimPercent**| No | Does not impact dimension as it is based on superview size, not content. | +| **DimAuto** | Yes | Contributes to dimension based on its own content or text sizing, potentially increasing the superview's size. | +| **DimAbsolute**| Yes | Impacts dimension if the absolute size plus position exceeds current content size. | +| **DimFunc** | Yes | Impacts dimension if the function returns a size that, combined with position, exceeds content size. | +## Building Dim.Auto Friendly Views -## Building Dim.Auto friendly View +It is common to build view classes that have a natural size based on their content. For example, the `Label` class is sized based on the `Text` property. -It is common to build View classes that have a natrual size based on their content. For example, the `Label` class is a view that is sized based on the `Text` property. +`Slider` is a good example of a sophisticated `Dim.Auto`-friendly view. Developers using these views shouldn't need to know the details of how the view is sized; they should just be able to use the view and have it size itself correctly. -`Slider` is a good example of sophsticated Dim.Auto friendly view. - -Developers using these views shouldn't need to know the details of how the view is sized, they should just be able to use the view and have it size itself correctly. - -For example, a vertical `Slider` with 3 options may be created like this: which is size based on the number of options it has, it's orientation, etc... +For example, a vertical `Slider` with 3 options may be created like this, sized based on the number of options, its orientation, etc.: ```cs -List options = new () { "Option 1", "Option 2", "Option 3" }; -Slider slider = new (options) +List options = new() { "Option 1", "Option 2", "Option 3" }; +Slider slider = new(options) { Orientation = Orientation.Vertical, Type = SliderType.Multiple, }; -view.Add (slider); +view.Add(slider); ``` -Note the developer does not need to specify the size of the `Slider`, it will size itself based on the number of options and the orientation. +Note the developer does not need to specify the size of the `Slider`; it will size itself based on the number of options and the orientation. -Views like `Slider` do this by setting `Width` and `Height` to `Dim.Auto (DimAutoStyle.Content)` in the constructor and calling `SetContentSize()` whenever the desired content size changes. The View will then be sized to be big enough to fit the content. +Views like `Slider` achieve this by setting `Width` and `Height` to `Dim.Auto(DimAutoStyle.Content)` in the constructor and calling `SetContentSize()` whenever the desired content size changes. The view will then be sized to be big enough to fit the content. -Views that use `Text` for their content can just set `Width` and `Height` to `Dim.Auto (DimAutoStyle.Text)`. It is recommended to use `Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1)` to ensure the View can show at least one line of text. +Views that use `Text` for their content can set `Width` and `Height` to `Dim.Auto(DimAutoStyle.Text)`. It is recommended to use `Height = Dim.Auto(DimAutoStyle.Text, minimumContentDim: 1)` to ensure the view can show at least one line of text. + +### Best Practices for Custom Views + +- **Set Appropriate DimAutoStyle**: Choose `Text`, `Content`, or `Auto` based on what drives the view's size. Use `Text` for text-driven views like labels, `Content` for container-like views with subviews or explicit content sizes, and `Auto` for mixed content. +- **Update Content Size Dynamically**: If your view's content changes (e.g., text updates or subviews are added/removed), call `SetContentSize()` or ensure properties like `Text` are updated to trigger re-layout. +- **Consider Minimum and Maximum Constraints**: Use `minimumContentDim` to prevent views from becoming too small to be usable, and `maximumContentDim` to prevent them from growing excessively large, especially in constrained terminal environments. +- **Handle Adornments**: Be aware that `Dim.Auto` accounts for adornments in its sizing. If your view has custom adornments, ensure they are properly factored into the layout by the base `View` class. + +## Debugging Dim.Auto Issues + +If you encounter unexpected sizing with `Dim.Auto`, consider the following debugging steps based on the codebase's diagnostic capabilities: + +- **Enable Validation**: Set `ValidatePosDim` to `true` on the view to enable runtime validation of `Pos` and `Dim` settings. This will throw exceptions if invalid configurations are detected, helping identify issues like circular dependencies or negative sizes. +- **Check Content Size**: Verify if `ContentSizeTracksViewport` is behaving as expected. If set to `false`, ensure `SetContentSize()` is called with the correct dimensions. Use logging to track `GetContentSize()` outputs. +- **Review Subview Dependencies**: Look for subviews with `Pos` or `Dim` types that impact dimension (like `PosAnchorEnd` or `DimView`). Ensure their target views are laid out before the current view to avoid incorrect sizing. +- **Inspect Text Formatting**: For `Text` style, check `TextFormatter` settings and constraints (`ConstrainToWidth`, `ConstrainToHeight`). Ensure text is formatted correctly before sizing calculations. + +By understanding the intricacies of `Dim.Auto` as implemented in Terminal.Gui v2, developers can create responsive and adaptive terminal UIs that automatically adjust to content changes, enhancing user experience and maintainability. diff --git a/docfx/docs/events.md b/docfx/docs/events.md index 3a1038d18..52f6363d7 100644 --- a/docfx/docs/events.md +++ b/docfx/docs/events.md @@ -1,6 +1,6 @@ # Terminal.Gui Event Deep Dive -Terminal.Gui exposes and uses events in many places. This deep dive covers the patterns used, where they are used, and notes any exceptions. +This document provides a comprehensive overview of how events work in Terminal.Gui. For the conceptual overview of the Cancellable Work Pattern, see [Cancellable Work Pattern](cancellable-work-pattern.md). ## See Also @@ -8,16 +8,334 @@ Terminal.Gui exposes and uses events in many places. This deep dive covers the p * [Command Deep Dive](command.md) * [Lexicon & Taxonomy](lexicon.md) -## Tenets for Terminal.Gui Events (Unless you know better ones...) - -Tenets higher in the list have precedence over tenets lower in the list. - -* **UI Interaction and Live Data Are Different Beasts** - TG distinguishes between events used for human interaction and events for live data. We don't believe in a one-size-fits-all eventing model. For UI interactions we use `EventHandler`. For data binding we think `INotifyPropertyChanged` is groovy. For some callbacks we use `Action`. - ## Lexicon and Taxonomy [!INCLUDE [Events Lexicon](~/includes/events-lexicon.md)] +## Event Categories + +Terminal.Gui uses several types of events: + +1. **UI Interaction Events**: Events triggered by user input (keyboard, mouse) +2. **View Lifecycle Events**: Events related to view creation, activation, and disposal +3. **Property Change Events**: Events for property value changes +4. **Drawing Events**: Events related to view rendering +5. **Command Events**: Events for command execution and workflow control + +## Event Patterns + +### 1. Cancellable Work Pattern (CWP) + +The [Cancellable Work Pattern (CWP)](cancellable-work-pattern.md) is a core pattern in Terminal.Gui that provides a consistent way to handle cancellable operations. An "event" has two components: + +1. **Virtual Method**: `protected virtual OnMethod()` that can be overridden in a subclass so the subclass can participate +2. **Event**: `public event EventHandler<>` that allows external subscribers to participate + +The virtual method is called first, letting subclasses have priority. Then the event is invoked. + +Optional CWP Helper Classes are provided to provide consistency. + +#### Manual CWP Implementation + +The basic CWP pattern combines a protected virtual method with a public event: + +```csharp +public class MyView : View +{ + // Public event + public event EventHandler? MouseEvent; + + // Protected virtual method + protected virtual bool OnMouseEvent(MouseEventArgs args) + { + // Return true to handle the event and stop propagation + return false; + } + + // Internal method to raise the event + internal bool RaiseMouseEvent(MouseEventArgs args) + { + // Call virtual method first + if (OnMouseEvent(args) || args.Handled) + { + return true; + } + + // Then raise the event + MouseEvent?.Invoke(this, args); + return args.Handled; + } +} +``` + +#### CWP with Helper Classes + +Terminal.Gui provides static helper classes to implement CWP: + +#### Property Changes + +For property changes, use `CWPPropertyHelper.ChangeProperty`: + +```csharp +public class MyView : View +{ + private string _text = string.Empty; + public event EventHandler>? TextChanging; + public event EventHandler>? TextChanged; + + public string Text + { + get => _text; + set + { + if (CWPPropertyHelper.ChangeProperty( + currentValue: _text, + newValue: value, + onChanging: args => OnTextChanging(args), + changingEvent: TextChanging, + onChanged: args => OnTextChanged(args), + changedEvent: TextChanged, + out string finalValue)) + { + _text = finalValue; + } + } + } + + // Virtual method called before the change + protected virtual bool OnTextChanging(ValueChangingEventArgs args) + { + // Return true to cancel the change + return false; + } + + // Virtual method called after the change + protected virtual void OnTextChanged(ValueChangedEventArgs args) + { + // React to the change + } +} +``` + +#### Workflows + +For general workflows, use `CWPWorkflowHelper`: + +```csharp +public class MyView : View +{ + public bool? ExecuteWorkflow() + { + ResultEventArgs args = new(); + return CWPWorkflowHelper.Execute( + onMethod: args => OnExecuting(args), + eventHandler: Executing, + args: args, + defaultAction: () => + { + // Main execution logic + DoWork(); + args.Result = true; + }); + } + + // Virtual method called before execution + protected virtual bool OnExecuting(ResultEventArgs args) + { + // Return true to cancel execution + return false; + } + + public event EventHandler>? Executing; +} +``` + +### 2. Action Callbacks + +For simple callbacks without cancellation, use `Action`. For example, in `Shortcut`: + +```csharp +public class Shortcut : View +{ + /// + /// Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the + /// mouse. + /// + /// + /// Note, the event is fired first, and if cancelled, the event will not be invoked. + /// + public Action? Action { get; set; } + + internal virtual bool? DispatchCommand(ICommandContext? commandContext) + { + bool cancel = base.DispatchCommand(commandContext) == true; + + if (cancel) + { + return true; + } + + if (Action is { }) + { + Logging.Debug($"{Title} ({commandContext?.Source?.Title}) - Invoke Action..."); + Action.Invoke(); + + // Assume if there's a subscriber to Action, it's handled. + cancel = true; + } + + return cancel; + } +} +``` + +### 3. Property Change Notifications + +For property change notifications, implement `INotifyPropertyChanged`. For example, in `Aligner`: + +```csharp +public class Aligner : INotifyPropertyChanged +{ + private Alignment _alignment; + public event PropertyChangedEventHandler? PropertyChanged; + + public Alignment Alignment + { + get => _alignment; + set + { + _alignment = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Alignment))); + } + } +} +``` + +### 4. Event Propagation + +Events in Terminal.Gui often propagate through the view hierarchy. For example, in `Button`, the `Selecting` and `Accepting` events are raised as part of the command handling process: + +```csharp +private bool? HandleHotKeyCommand (ICommandContext commandContext) +{ + bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario where IsDefault changes + + if (RaiseSelecting (commandContext) is true) + { + return true; + } + + bool? handled = RaiseAccepting (commandContext); + + if (handled == true) + { + return true; + } + + SetFocus (); + + // If Accept was not handled... + if (cachedIsDefault && SuperView is { }) + { + return SuperView.InvokeCommand (Command.Accept); + } + + return false; +} +``` + +This example shows how `Button` first raises the `Selecting` event, and if not canceled, proceeds to raise the `Accepting` event. If `Accepting` is not handled and the button is the default, it invokes the `Accept` command on the `SuperView`, demonstrating event propagation up the view hierarchy. + +## Event Context + +### Event Arguments + +Terminal.Gui provides rich context through event arguments. For example, `CommandEventArgs`: + +```csharp +public class CommandEventArgs : EventArgs +{ + public ICommandContext? Context { get; set; } + public bool Handled { get; set; } + public bool Cancel { get; set; } +} +``` + +### Command Context + +Command execution includes context through `ICommandContext`: + +```csharp +public interface ICommandContext +{ + View Source { get; } + object? Parameter { get; } + IDictionary State { get; } +} +``` + +## Best Practices + +1. **Event Naming**: + - Use past tense for completed events (e.g., `Clicked`, `Changed`) + - Use present tense for ongoing events (e.g., `Clicking`, `Changing`) + - Use "ing" suffix for cancellable events + +2. **Event Handler Implementation**: + - Keep handlers short and focused + - Use async/await for long-running tasks + - Unsubscribe from events in Dispose + - Use weak event patterns for long-lived subscriptions + +3. **Event Context**: + - Provide rich context in event args + - Include source view and binding details + - Add view-specific state when needed + +4. **Event Propagation**: + - Use appropriate propagation mechanisms + - Avoid unnecessary event bubbling + - Consider using `PropagatedCommands` for hierarchical views + +## Common Pitfalls + +1. **Memory Leaks**: + ```csharp + // BAD: Potential memory leak + view.Activating += OnActivating; + + // GOOD: Unsubscribe in Dispose + protected override void Dispose(bool disposing) + { + if (disposing) + { + view.Activating -= OnActivating; + } + base.Dispose(disposing); + } + ``` + +2. **Incorrect Event Cancellation**: + ```csharp + // BAD: Using Cancel for event handling + args.Cancel = true; // Wrong for MouseEventArgs + + // GOOD: Using Handled for event handling + args.Handled = true; // Correct for MouseEventArgs + + // GOOD: Using Cancel for operation cancellation + args.Cancel = true; // Correct for CancelEventArgs + ``` + +3. **Missing Context**: + ```csharp + // BAD: Missing context + Activating?.Invoke(this, new CommandEventArgs()); + + // GOOD: Including context + Activating?.Invoke(this, new CommandEventArgs { Context = ctx }); + ``` + ## Useful External Documentation * [.NET Naming Guidelines - Names of Events](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members?redirectedfrom=MSDN#names-of-events) @@ -28,28 +346,52 @@ Tenets higher in the list have precedence over tenets lower in the list. TG follows the *naming* advice provided in [.NET Naming Guidelines - Names of Events](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members?redirectedfrom=MSDN#names-of-events). -## Common Event Patterns +## Known Issues -### Cancellable Work Pattern +### Proposed Enhancement: Command Propagation -The [Cancellable Work Pattern](cancellable-work-pattern.md) is a pattern that allows for the cancellation of work. +The *Cancellable Work Pattern* in `View.Command` currently supports local `Command.Activate` and propagating `Command.Accept`. To address hierarchical coordination needs (e.g., `MenuBarv2` popovers, `Dialog` closing), a `PropagatedCommands` property is proposed (Issue #4050): -### OnEvent/Event +- **Change**: Add `IReadOnlyList PropagatedCommands` to `View`, defaulting to `[Command.Accept]`. `Raise*` methods propagate if the command is in `SuperView?.PropagatedCommands` and `args.Handled` is `false`. +- **Example**: -The primary pattern for events is the `OnEvent/Event` idiom. + ```csharp + public IReadOnlyList PropagatedCommands { get; set; } = new List { Command.Accept }; + protected bool? RaiseAccepting(ICommandContext? ctx) + { + CommandEventArgs args = new() { Context = ctx }; + if (OnAccepting(args) || args.Handled) + { + return true; + } + Accepting?.Invoke(this, args); + if (!args.Handled && SuperView?.PropagatedCommands.Contains(Command.Accept) == true) + { + return SuperView.InvokeCommand(Command.Accept, ctx); + } + return Accepting is null ? null : args.Handled; + } + ``` -* Implement a helper method for raising the event: `RaisexxxEvent`. - * If the event is cancelable, the return type should be either `bool` or `bool?`. - * Can be `private`, `internal`, or `public` depending on the situation. `internal` should only be used to enable unit tests. -* Raising an event involves FIRST calling the `protected virtual` method, THEN invoking -the `EventHandler`. +- **Impact**: Enables `Command.Activate` propagation for `MenuBarv2` while preserving `Command.Accept` propagation, maintaining decoupling and avoiding noise from irrelevant commands. -### Action - -We use the `Action` idiom sparingly. - -### INotifyPropertyChanged - -We support `INotifyPropertyChanged` in cases where data binding is relevant. +### **Conflation in FlagSelector**: + - **Issue**: `CheckBox.Activating` triggers `Accepting`, conflating state change and confirmation. + - **Recommendation**: Refactor to separate `Activating` and `Accepting`: + ```csharp + checkbox.Activating += (sender, args) => + { + if (RaiseAccepting(args.Context) is true) + { + args.Handled = true; + } + }; + ``` +### **Propagation Limitations**: + - **Issue**: Local `Command.Activate` restricts `MenuBarv2` coordination; `Command.Accept` uses hacks (#3925). + - **Recommendation**: Adopt `PropagatedCommands` to enable targeted propagation, as proposed. +### **Complexity in Multi-Phase Workflows**: + - **Issue**: `View.Draw`'s multi-phase workflow can be complex for developers to customize. + - **Recommendation**: Provide clearer phase-specific documentation and examples. diff --git a/docfx/docs/mouse.md b/docfx/docs/mouse.md index e80e4613d..552a6c585 100644 --- a/docfx/docs/mouse.md +++ b/docfx/docs/mouse.md @@ -13,31 +13,54 @@ Tenets higher in the list have precedence over tenets lower in the list. * **Keyboard Required; Mouse Optional** - Terminal users expect full functionality without having to pick up the mouse. At the same time they love being able to use the mouse when it makes sense to do so. We strive to ensure anything that can be done with the keyboard is also possible with the mouse. We avoid features that are only useable with the mouse. -* **Be Consistent With the User's Platform** - Users get to choose the platform they run *Terminal.Gui* apps on and those apps should respond to mouse input in a way that is consistent with the platform. For example, on Windows ??? +* **Be Consistent With the User's Platform** - Users get to choose the platform they run *Terminal.Gui* apps on and those apps should respond to mouse input in a way that is consistent with the platform. For example, on Windows, right-click typically shows context menus, double-click activates items, and the mouse wheel scrolls content. On other platforms, Terminal.Gui respects the platform's conventions for mouse interactions. ## Mouse APIs *Terminal.Gui* provides the following APIs for handling mouse input: * **MouseEventArgs** - @Terminal.Gui.Input.MouseEventArgs provides a platform-independent abstraction for common mouse operations. It is used for processing mouse input and raising mouse events. -* **Mouse Bindings** - Mouse Bindings provide a declarative method for handling mouse input in View implementations. The View calls Terminal.Gui.ViewBase.View.AddCommand* to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command. -* **Mouse Events** - The Mouse Bindings API is rich enough to support the majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. drag & drop). Use @Terminal.Gui.ViewBase.View.MouseEvent and related events in these cases. + +* **Mouse Bindings** - Mouse Bindings provide a declarative method for handling mouse input in View implementations. The View calls @Terminal.Gui.ViewBase.View.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command. + +* **Mouse Events** - The Mouse Bindings API is rich enough to support the majority of use-cases. However, in some cases subscribing directly to mouse events is needed (e.g. drag & drop). Use @Terminal.Gui.ViewBase.View.MouseEvent and related events in these cases. + +* **Mouse State** - @Terminal.Gui.ViewBase.View.MouseState provides an abstraction for the current state of the mouse, enabling views to do interesting things like change their appearance based on the mouse state. Each of these APIs are described more fully below. ## Mouse Bindings -Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls Terminal.Gui.ViewBase.View.AddCommand* to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command. For example, if a View wants to respond to the user using the mouse wheel to scroll up, it would do this: +Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls @Terminal.Gui.ViewBase.View.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.Input.MouseBindings to indicate which mouse events will invoke the command. For example, if a View wants to respond to the user using the mouse wheel to scroll up, it would do this: ```cs -public MyView : View +public class MyView : View { - AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); - MouseBindings.Add (MouseFlags.Button1DoubleClick, Command.ScrollUp); + public MyView() + { + AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); + MouseBindings.Add (MouseFlags.WheelUp, Command.ScrollUp); + + AddCommand (Command.ScrollDown, () => ScrollVertical (1)); + MouseBindings.Add (MouseFlags.WheelDown, Command.ScrollDown); + + AddCommand (Command.Select, () => SelectItem()); + MouseBindings.Add (MouseFlags.Button1Clicked, Command.Select); + } } ``` -The [Command](~/api/Terminal.Gui.Input.Command.yml) enum lists generic operations that are implemented by views. +The @Terminal.Gui.Input.Command enum lists generic operations that are implemented by views. + +### Common Mouse Bindings + +Here are some common mouse binding patterns used throughout Terminal.Gui: + +* **Click Events**: `MouseFlags.Button1Clicked` for primary selection/activation +* **Double-Click Events**: `MouseFlags.Button1DoubleClicked` for default actions (like opening/accepting) +* **Right-Click Events**: `MouseFlags.Button3Clicked` for context menus +* **Scroll Events**: `MouseFlags.WheelUp` and `MouseFlags.WheelDown` for scrolling content +* **Drag Events**: `MouseFlags.Button1Pressed` combined with mouse move tracking for drag operations ## Mouse Events @@ -45,19 +68,153 @@ At the core of *Terminal.Gui*'s mouse API is the @Terminal.Gui.Input.MouseEventA When the user does something with the mouse, the `ConsoleDriver` maps the platform-specific mouse event into a `MouseEventArgs` and calls `Application.RaiseMouseEvent`. Then, `Application.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `Application` will then call the appropriate high-level helper APIs. For example, if the user double-clicks the mouse, `View.OnMouseClick` will be called/`View.MouseClick` will be raised with the event arguments indicating which mouse button was double-clicked. +### Mouse Event Processing Flow + +Mouse events are processed through the following workflow using the [Cancellable Work Pattern](cancellable-work-pattern.md): + +1. **Driver Level**: The ConsoleDriver captures platform-specific mouse events and converts them to `MouseEventArgs` +2. **Application Level**: `Application.RaiseMouseEvent` determines the target view and routes the event +3. **View Level**: The target view processes the event through: + - `OnMouseEvent` (virtual method that can be overridden) + - `MouseEvent` event (for event subscribers) + - Mouse bindings (if the event wasn't handled) + - High-level events like `OnMouseClick`, `MouseEnter`, `MouseLeave` + +### Handling Mouse Events Directly + +For scenarios requiring direct mouse event handling (such as custom drag-and-drop operations), subscribe to the `MouseEvent` or override `OnMouseEvent`: + +```cs +public class CustomView : View +{ + public CustomView() + { + MouseEvent += OnMouseEventHandler; + } + + private void OnMouseEventHandler(object sender, MouseEventArgs e) + { + if (e.Flags.HasFlag(MouseFlags.Button1Pressed)) + { + // Handle drag start + e.Handled = true; + } + } + + // Alternative: Override the virtual method + protected override bool OnMouseEvent(MouseEventArgs mouseEvent) + { + if (mouseEvent.Flags.HasFlag(MouseFlags.Button1Pressed)) + { + // Handle drag start + return true; // Event was handled + } + return base.OnMouseEvent(mouseEvent); + } +} +``` + +## Mouse State + +The @Terminal.Gui.ViewBase.View.MouseState property provides an abstraction for the current state of the mouse, enabling views to do interesting things like change their appearance based on the mouse state. + +Mouse states include: +* **Normal** - Default state when mouse is not interacting with the view +* **Over** - Mouse is positioned over the view +* **Pressed** - Mouse button is pressed down while over the view +* **Clicked** - Mouse was clicked on the view + +It works in conjunction with the @Terminal.Gui.ViewBase.View.HighlightStates which is a list of mouse states that will cause a view to become highlighted. + +Subscribe to the @Terminal.Gui.ViewBase.View.MouseStateChanged event to be notified when the mouse state changes: + +```cs +view.MouseStateChanged += (sender, e) => +{ + switch (e.NewState) + { + case MouseState.Over: + // Change appearance when mouse hovers + break; + case MouseState.Pressed: + // Change appearance when pressed + break; + } +}; +``` + ## Mouse Button and Movement Concepts * **Down** - Indicates the user pushed a mouse button down. * **Pressed** - Indicates the mouse button is down; for example if the mouse was pressed down and remains down for a period of time. * **Released** - Indicates the user released a mouse button. * **Clicked** - Indicates the user pressed then released the mouse button while over a particular View. +* **Double-Clicked** - Indicates the user clicked twice in rapid succession. * **Moved** - Indicates the mouse moved to a new location since the last mouse event. +* **Wheel** - Indicates the mouse wheel was scrolled up or down. -## **Global Mouse Handling** +## Global Mouse Handling -The @Terminal.Gui.App.Application.MouseEvent event can be used if an application wishes to receive all mouse events. +The @Terminal.Gui.App.Application.MouseEvent event can be used if an application wishes to receive all mouse events before they are processed by individual views: + +```cs +Application.MouseEvent += (sender, e) => +{ + // Handle application-wide mouse events + if (e.Flags.HasFlag(MouseFlags.Button3Clicked)) + { + ShowGlobalContextMenu(e.Position); + e.Handled = true; + } +}; +``` ## Mouse Enter/Leave Events -The @Terminal.Gui.ViewBase.View.MouseEnter and @Terminal.Gui.ViewBase.View.MouseLeave events enable a View to take action when the mouse is over the view. Internally, this is used to enable @Terminal.Gui.ViewBase.View.Highlight. +The @Terminal.Gui.ViewBase.View.MouseEnter and @Terminal.Gui.ViewBase.View.MouseLeave events enable a View to take action when the mouse enters or exits the view boundary. Internally, this is used to enable @Terminal.Gui.ViewBase.View.Highlight functionality: + +```cs +view.MouseEnter += (sender, e) => +{ + // Mouse entered the view + UpdateTooltip("Hovering over button"); +}; + +view.MouseLeave += (sender, e) => +{ + // Mouse left the view + HideTooltip(); +}; +``` + +## Mouse Coordinate Systems + +Mouse coordinates in Terminal.Gui are provided in multiple coordinate systems: + +* **Screen Coordinates** - Relative to the entire terminal screen (0,0 is top-left of terminal) +* **View Coordinates** - Relative to the view's content area (0,0 is top-left of view's viewport) + +The `MouseEventArgs` provides both coordinate systems: +* `MouseEventArgs.Position` - Screen coordinates +* `MouseEventArgs.ViewPosition` - View-relative coordinates (when available) + +## Best Practices + +* **Use Mouse Bindings** when possible for simple mouse interactions - they integrate well with the Command system +* **Handle Mouse Events directly** for complex interactions like drag-and-drop or custom gestures +* **Respect platform conventions** - use right-click for context menus, double-click for default actions +* **Provide keyboard alternatives** - ensure all mouse functionality has keyboard equivalents +* **Test with different terminals** - mouse support varies between terminal applications +* **Use Mouse State** to provide visual feedback when users hover or interact with views + +## Limitations and Considerations + +* Not all terminal applications support mouse input - always provide keyboard alternatives +* Mouse wheel support may vary between platforms and terminals +* Some terminals may not support all mouse buttons or modifier keys +* Mouse coordinates are limited to character cell boundaries - sub-character precision is not available +* Performance can be impacted by excessive mouse move event handling - use mouse enter/leave events when appropriate rather than tracking all mouse moves + + + diff --git a/docfx/docs/navigation.md b/docfx/docs/navigation.md index 5d953d53b..0421670dd 100644 --- a/docfx/docs/navigation.md +++ b/docfx/docs/navigation.md @@ -1,8 +1,9 @@ # Navigation Deep Dive +This document covers Terminal.Gui's navigation system, which determines: + - What are the visual cues that help the user know which element of an application is receiving keyboard and mouse input (which one has focus)? - How does the user change which element of an application has focus? -- How does the user change which element of an application has focus? - What are the visual cues that help the user know what keystrokes will change the focus? - What are the visual cues that help the user know what keystrokes will cause action in elements of the application that don't currently have focus? - What is the order in which UI elements are traversed when using keyboard navigation? @@ -24,14 +25,49 @@ See the [Keyboard Tenets](keyboard.md) as they apply as well. Tenets higher in the list have precedence over tenets lower in the list. -* **One Focus Per App** - It should not be possible to have two views be the "most focused" view in an application. +* **One Focus Per App** - It should not be possible to have two views be the "most focused" view in an application. There is always exactly one view that is the target of keyboard input. -* **There's Always a Way With The Keyboard** - The framework strives to ensure users' wanting to use the keyboard can't get into a situation where some element of the application is not accessible via the keyboard. For example, we have unit tests that ensure built-in Views will all have at least one navigation key that advances focus. Another example: As long as a View with a HotKey is visible and enabled, regardless of view-hierarchy, if the user presses that hotkey, the action defined by the hotkey will happen (and, by default the View that defines it will be focused). +* **There's Always a Way With The Keyboard** - The framework strives to ensure users wanting to use the keyboard can't get into a situation where some element of the application is not accessible via the keyboard. For example, we have unit tests that ensure built-in Views will all have at least one navigation key that advances focus. Another example: As long as a View with a HotKey is visible and enabled, regardless of view-hierarchy, if the user presses that hotkey, the action defined by the hotkey will happen (and, by default the View that defines it will be focused). * **Flexible Overrides** - The framework makes it easy for navigation changes to be made from code and enables changing of behavior to be done in flexible ways. For example a view can be prevented from getting focus by setting `CanFocus` to `false` or overriding `OnHasFocusChanging` and returning `true` to cancel. * **Decouple Concepts** - In v1 `CanFocus` is tightly coupled with `HasFocus`, `TabIndex`, `TabIndexes`, and `TabStop` and vice-versa. There was a bunch of "magic" logic that automatically attempted to keep these concepts aligned. This resulted in a poorly specified, hard-to-test, and fragile API. In v2 we strive to keep the related navigation concepts decoupled. For example, `CanFocus` and `TabStop` are decoupled. A view with `CanFocus == true` can have `TabStop == NoStop` and still be focusable with the mouse. +## Answering the Key Navigation Questions + +### Visual Cues for Focus + +**Current Focus Indicator:** +- Views with focus are rendered using their `ColorScheme.Focus` attribute +- The focused view may display a cursor (for text input views) +- Views in the focus chain (SuperViews of the focused view) also use focused styling + +**Navigation Cues:** +- HotKeys are indicated by underlined characters in Labels, Buttons, and MenuItems +- Tab order is generally left-to-right, top-to-bottom within containers +- Focus indicators (such as highlight rectangles) show which view will receive input + +### Changing Focus + +**Keyboard Methods:** +- `Tab` / `Shift+Tab` - Navigate between TabStop views +- `F6` / `Shift+F6` - Navigate between TabGroup containers +- Arrow keys - Navigate within containers or between adjacent views +- HotKeys - Direct navigation to specific views (Alt+letter combinations) +- `Enter` / `Space` - Activate the focused view + +**Mouse Methods:** +- Click on any focusable view to give it focus +- Focus behavior depends on whether the view was previously focused (RestoreFocus vs AdvanceFocus) + +### Navigation Order + +Views are traversed based on their `TabStop` behavior and position in the view hierarchy: + +1. **TabStop Views** - Navigated with Tab/Shift+Tab in layout order +2. **TabGroup Views** - Containers navigated with F6/Shift+F6 +3. **NoStop Views** - Skipped during keyboard navigation but can receive mouse focus + ## Keyboard Navigation The majority of the Terminal.Gui Navigation system is dedicated to enabling the keyboard to be used to navigate Views. @@ -47,9 +83,29 @@ Terminal.Gui defines these keys for keyboard navigation: - `Application.NextTabGroupKey` (`Key.F6`) - Navigates to the next view in the view-hierarchy that is a `TabGroup` (see below). If there is no next, the first view that is a `TabGroup` will gain focus. - `Application.PrevTabGroupKey` (`Key.F6.WithShift`) - Opposite of `Application.NextTabGroupKey`. -`F6` was chosen to match [Windows](https://learn.microsoft.com/en-us/windows/apps/design/input/keyboard-accelerators#common-keyboard-accelerators) +`F6` was chosen to match [Windows](https://learn.microsoft.com/en-us/windows/apps/design/input/keyboard-accelerators#common-keyboard-accelerators) conventions. -These keys are all registered as `KeyBindingScope.Application` key bindings by `Application`. Because application-scoped key bindings have the lowest priority, Views can override the behaviors of these keys (e.g. `TextView` overrides `Key.Tab` by default, enabling the user to enter `\t` into text). The `AllViews_AtLeastOneNavKey_Leaves` unit test ensures all built-in Views have at least one of the above keys that can advance. +These keys are all registered as `KeyBindingScope.Application` key bindings by `Application`. Because application-scoped key bindings have the lowest priority, Views can override the behaviors of these keys (e.g. `TextView` overrides `Key.Tab` by default, enabling the user to enter `\t` into text). The `AllViews_AtLeastOneNavKey_Leaves` unit test ensures all built-in Views have at least one of the above keys that can advance focus. + +### Navigation Examples + +```csharp +// Basic focus management +var button = new Button() { Text = "Click Me", CanFocus = true, TabStop = TabBehavior.TabStop }; +var textField = new TextField() { Text = "", CanFocus = true, TabStop = TabBehavior.TabStop }; + +// Container with group navigation +var frameView = new FrameView() +{ + Title = "Options", + CanFocus = true, + TabStop = TabBehavior.TabGroup +}; + +// Programmatic focus control +button.SetFocus(); // Give focus to specific view +Application.Navigation.AdvanceFocus(NavigationDirection.Forward, TabBehavior.TabStop); +``` ### HotKeys @@ -57,7 +113,16 @@ See also [Keyboard](keyboard.md) where HotKey is covered more deeply... `HotKeys` can be used to navigate across the entire application view-hierarchy. They work independently of `Focus`. This enables a user to navigate across a complex UI of nested subviews if needed (even in overlapped scenarios). An example use case is the `AllViewsTester` Scenario. -Additionally, multiple Views in an application (even within the same SuperView) can have the same HotKey. Each press of the HotKey will invoke the next HotKey across the View hierarchy (NOT IMPLEMENTED YET see https://github.com/gui-cs/Terminal.Gui/issues/3554). +HotKeys are defined using the `HotKey` property and are activated using `Alt+` the specified key: + +```csharp +var saveButton = new Button() { Text = "_Save", HotKey = Key.S }; +var exitButton = new Button() { Text = "E_xit", HotKey = Key.X }; + +// Alt+S will activate save, Alt+X will activate exit, regardless of current focus +``` + +Additionally, multiple Views in an application (even within the same SuperView) can have the same HotKey. ## Mouse Navigation @@ -71,19 +136,42 @@ The answer to both questions is: If the View was previously focused, the system keeps a record of the SubView that was previously most-focused and restores focus to that SubView (`RestoreFocus()`). -If the View was not previously focused, `AdvanceFocus()` is called. +If the View was not previously focused, `AdvanceFocus()` is called to find the next appropriate focus target. For this to work properly, there must be logic that removes the focus-cache used by `RestoreFocus()` if something changes that makes the previously-focusable view not focusable (e.g. if Visible has changed). +### Mouse Focus Examples + +```csharp +// Mouse click behavior +view.MouseEvent += (sender, e) => +{ + if (e.Flags.HasFlag(MouseFlags.Button1Clicked) && view.CanFocus) + { + view.SetFocus(); + e.Handled = true; + } +}; + +// Focus on mouse enter (optional behavior) +view.MouseEnter += (sender, e) => +{ + if (view.CanFocus && focusOnHover) + { + view.SetFocus(); + } +}; +``` + ## Application Level Navigation At the application level, navigation is encapsulated within the @Terminal.Gui.ApplicationNavigation helper class which is publicly exposed via the @Terminal.Gui.App.Application.Navigation property. @Terminal.Gui.App.ApplicationNavigation.GetFocused gets the most-focused View in the application. Will return `null` if there is no view with focus (an extremely rare situation). This replaces `View.MostFocused` in v1. -The @Terminal.Gui.App.ApplicationNavigation.FocusedChanged and @Terminal.Gui.App.ApplicationNavigation.FocusedChanging events are raised when the most-focused View in the application is changing or has changed. `FocusedChanged` is useful for apps that want to do something with the most-focused view (e.g. see `AdornmentsEditor`). `FocusChanging` is useful apps that want to override what view can be focused across an entire app. +The @Terminal.Gui.App.ApplicationNavigation.FocusedChanged and @Terminal.Gui.App.ApplicationNavigation.FocusedChanging events are raised when the most-focused View in the application is changing or has changed. `FocusedChanged` is useful for apps that want to do something with the most-focused view (e.g. see `AdornmentsEditor`). `FocusChanging` is useful for apps that want to override what view can be focused across an entire app. -The @Terminal.Gui.App.ApplicationNavigation.AdvanceFocus* method causes the focus to advance (forward or backwards) to the next View in the application view-hierarchy, using `behavior` as a filter. +The @Terminal.Gui.App.ApplicationNavigation.AdvanceFocus method causes the focus to advance (forward or backwards) to the next View in the application view-hierarchy, using `behavior` as a filter. The implementation is simple: @@ -95,27 +183,104 @@ This method is called from the `Command` handlers bound to the application-scope This method replaces about a dozen functions in v1 (scattered across `Application` and `Toplevel`). +### Application Navigation Examples + +```csharp +// Listen for global focus changes +Application.Navigation.FocusedChanged += (sender, e) => +{ + var focused = Application.Navigation.GetFocused(); + StatusBar.Text = $"Focused: {focused?.GetType().Name ?? "None"}"; +}; + +// Prevent certain views from getting focus +Application.Navigation.FocusedChanging += (sender, e) => +{ + if (e.NewView is SomeRestrictedView) + { + e.Cancel = true; // Prevent focus change + } +}; + +// Programmatic navigation +Application.Navigation.AdvanceFocus(NavigationDirection.Forward, TabBehavior.TabStop); +Application.Navigation.AdvanceFocus(NavigationDirection.Backward, TabBehavior.TabGroup); +``` + ## View Level Navigation -@Terminal.Gui.ViewBase.View.AdvanceFocus* is the primary method for developers to cause a view to gain or lose focus. +@Terminal.Gui.ViewBase.View.AdvanceFocus is the primary method for developers to cause a view to gain or lose focus. Various events are raised when a View's focus is changing. For example, @Terminal.Gui.ViewBase.View.HasFocusChanging and @Terminal.Gui.ViewBase.View.HasFocusChanged. +### View Focus Management + +```csharp +// Basic focus control +public class CustomView : View +{ + protected override void OnHasFocusChanging(CancelEventArgs e) + { + if (SomeCondition) + { + e.Cancel = true; // Prevent focus change + return; + } + base.OnHasFocusChanging(e); + } + + protected override void OnHasFocusChanged(EventArgs e) + { + if (e.CurrentValue) + { + // View gained focus + UpdateAppearance(); + } + base.OnHasFocusChanged(e); + } +} +``` + ## What makes a View focusable? First, only Views that are visible and enabled can gain focus. Both `Visible` and `Enabled` must be `true` for a view to be focusable. For visible and enabled Views, the `CanFocus` property is then used to determine whether the `View` is focusable. `CanFocus` must be `true` for a View to gain focus. However, even if `CanFocus` is `true`, other factors can prevent the view from gaining focus... -A visible, enabled, and `CanFocus == true` view can be focused if the user uses the mouse to clicks on it or if code explicitly calls `View.SetFocus()`. Of course, the view itself or some other code can cancel the focus (e.g. by overriding `OnEnter`). +A visible, enabled, and `CanFocus == true` view can be focused if the user uses the mouse to clicks on it or if code explicitly calls `View.SetFocus()`. Of course, the view itself or some other code can cancel the focus (e.g. by overriding `OnHasFocusChanging`). For keyboard navigation, the `TabStop` property is a filter for which views are focusable from the current most-focused. `TabStop` has no impact on mouse navigation. `TabStop` is of type `TabBehavior`. -* `null` - This View is still being initialized; acts as a signal to `set_CanFocus` to set `TabStop` to `TabBehavior.TabStop` as convince for the most common use-case. Equivalent to `TabBehavior.NoStop` when determining if a view is focusable by the keyboard or not. -* `TabBehavior.NoStop` - Prevents the user from using keyboard navigation to cause view (and by definition it's subviews) to gain focus. Note: The view can still be focused using code or the mouse. +### TabBehavior Values + +* `null` - This View is still being initialized; acts as a signal to `set_CanFocus` to set `TabStop` to `TabBehavior.TabStop` as convenience for the most common use-case. Equivalent to `TabBehavior.NoStop` when determining if a view is focusable by the keyboard or not. + +* `TabBehavior.NoStop` - Prevents the user from using keyboard navigation to cause view (and by definition its subviews) to gain focus. Note: The view can still be focused using code or the mouse. + * `TabBehavior.TabStop` - Indicates a View is a focusable view with no focusable subviews. `Application.Next/PrevTabStopKey` will advance ONLY through the peer-Views (`SuperView.SubViews`). -* `TabBehavior.GroupStop` - Indicates a View is a focusable container for other focusable views and enables keyboard navigation across these containers. This applies to both tiled and overlapped views. For example, `FrameView` is a simple view designed to be a visible container of other views tiled scenarios. It has `TabStop` set to `TabBehavior.GroupStop` (and `Arrangement` set to `ViewArrangement.Fixed`). Likewise, `Window` is a simple view designed to be a visible container of other views in overlapped scenarios. It has `TabStop` set to `TabBehavior.GroupStop` (and `Arrangement` set to `ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped`). `Application.Next/PrevGroupStopKey` will advance across all `GroupStop` views in the application (unless blocked by a `NoStop` SuperView). +* `TabBehavior.TabGroup` - Indicates a View is a focusable container for other focusable views and enables keyboard navigation across these containers. This applies to both tiled and overlapped views. For example, `FrameView` is a simple view designed to be a visible container of other views in tiled scenarios. It has `TabStop` set to `TabBehavior.TabGroup` (and `Arrangement` set to `ViewArrangement.Fixed`). Likewise, `Window` is a simple view designed to be a visible container of other views in overlapped scenarios. It has `TabStop` set to `TabBehavior.TabGroup` (and `Arrangement` set to `ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped`). `Application.Next/PrevGroupStopKey` will advance across all `TabGroup` views in the application (unless blocked by a `NoStop` SuperView). + +### Focus Requirements Summary + +For a view to be focusable: + +1. **Visible** = `true` +2. **Enabled** = `true` +3. **CanFocus** = `true` +4. **TabStop** != `TabBehavior.NoStop` (for keyboard navigation only) + +```csharp +// Example: Make a view focusable +var view = new Label() +{ + Text = "Focusable Label", + Visible = true, // Must be visible + Enabled = true, // Must be enabled + CanFocus = true, // Must be able to focus + TabStop = TabBehavior.TabStop // Keyboard navigable +}; +``` ## How To Tell if a View has focus? And which view is the most-focused? @@ -123,21 +288,44 @@ For keyboard navigation, the `TabStop` property is a filter for which views are Setting this property to `true` has the same effect as calling `View.SetFocus ()`, which also means the focus may not change as a result. -If `v.HasFocus == true` then +If `v.HasFocus == true` then: - All views up `v`'s superview-hierarchy must be focusable. - All views up `v`'s superview-hierarchy will also have `HasFocus == true`. - The deepest-subview of `v` that is focusable will also have `HasFocus == true` -In other words, `v.HasFocus == true` does not necessarily mean `v` is the most-focused view, receiving input. If it has focusable sub-views, one of those (or a further subview) will be the most-focused (`Application.Navigation.Focused`). +In other words, `v.HasFocus == true` does not necessarily mean `v` is the most-focused view, receiving input. If it has focusable sub-views, one of those (or a further subview) will be the most-focused (`Application.Navigation.GetFocused()`). The `private bool _hasFocus` field backs `HasFocus` and is the ultimate source of truth whether a View has focus or not. +### Focus Chain Example + +```csharp +// In a hierarchy: Window -> Dialog -> Button +// If Button has focus, then: +window.HasFocus == true // Part of focus chain +dialog.HasFocus == true // Part of focus chain +button.HasFocus == true // Actually focused + +// Application.Navigation.GetFocused() returns button +var mostFocused = Application.Navigation.GetFocused(); // Returns button +``` + ### How does a user tell? -In short: `ColorScheme.Focused`. +In short: `ColorScheme.Focus` - Views in the focus chain render with focused colors. -(More needed for HasFocus SuperViews. The current `ColorScheme` design is such that this is awkward. See [Issue #2381](https://github.com/gui-cs/Terminal.Gui/issues/2381#issuecomment-1890814959)) +Views use their `ColorScheme.Focus` attribute when they are part of the focus chain. This provides visual feedback about which part of the application is active. + +```csharp +// Custom focus styling +protected override void OnDrawContent(Rectangle viewport) +{ + var attribute = HasFocus ? GetFocusColor() : GetNormalColor(); + Driver.SetAttribute(attribute); + // ... draw content +} +``` ## How to make a View become focused? @@ -145,13 +333,49 @@ The primary `public` method for developers to cause a view to get focus is `View Unlike v1, in v2, this method can return `false` if the focus change doesn't happen (e.g. because the view wasn't focusable, or the focus change was cancelled). +```csharp +// Programmatic focus control +if (myButton.SetFocus()) +{ + Console.WriteLine("Button now has focus"); +} +else +{ + Console.WriteLine("Could not focus button"); +} + +// Alternative: Set HasFocus property (same effect) +myButton.HasFocus = true; +``` + ## How to make a View become NOT focused? The typical method to make a view lose focus is to have another View gain focus. +```csharp +// Focus another view to remove focus from current +otherView.SetFocus(); + +// Or advance focus programmatically +Application.Navigation.AdvanceFocus(NavigationDirection.Forward, TabBehavior.TabStop); + +// Focus can also be lost when views become non-focusable +myView.CanFocus = false; // Will lose focus if it had it +myView.Visible = false; // Will lose focus if it had it +myView.Enabled = false; // Will lose focus if it had it +``` + ## Determining the Most Focused SubView -In v1 `View` had `MostFocused` property that traversed up the view-hierarchy returning the last view found with `HasFocus == true`. In v2, `Application.Focused` provides the same functionality with less overhead. +In v1 `View` had `MostFocused` property that traversed up the view-hierarchy returning the last view found with `HasFocus == true`. In v2, `Application.Navigation.GetFocused()` provides the same functionality with less overhead. + +```csharp +// v2 way to get the most focused view +var focused = Application.Navigation.GetFocused(); + +// This replaces the v1 pattern: +// var focused = Application.Top.MostFocused; +``` ## How Does `View.Add/Remove` Work? @@ -161,28 +385,181 @@ Also, in v1, if `view.CanFocus == true`, `Add` would automatically set `TabStop` In v2, developers need to explicitly set `CanFocus` for any view in the view-hierarchy where focus is desired. This simplifies the implementation significantly and removes confusing behavior. -In v2, the automatic setting of `TabStop` in `Add` is retained because it is not overly complex to do so and is a nice convenience for developers to not have to set both `Tabstop` and `CanFocus`. Note we do NOT automatically change `CanFocus` if `TabStop` is changed. +In v2, the automatic setting of `TabStop` in `Add` is retained because it is not overly complex to do so and is a nice convenience for developers to not have to set both `TabStop` and `CanFocus`. Note we do NOT automatically change `CanFocus` if `TabStop` is changed. + +```csharp +// v2 explicit focus setup +var container = new FrameView() +{ + Title = "Container", + CanFocus = true, // Must be explicitly set + TabStop = TabBehavior.TabGroup +}; + +var button = new Button() +{ + Text = "Click Me", + CanFocus = true, // Must be explicitly set + TabStop = TabBehavior.TabStop // Set automatically by Add(), but can override +}; + +container.Add(button); // Does not automatically set CanFocus on container +``` ## Knowing When a View's Focus is Changing @Terminal.Gui.ViewBase.View.HasFocusChanging and @Terminal.Gui.ViewBase.View.HasFocusChanged are raised when a View's focus is changing. +```csharp +// Monitor focus changes +view.HasFocusChanging += (sender, e) => +{ + if (e.NewValue && !ValidateCanFocus()) + { + e.Cancel = true; // Prevent gaining focus + } +}; + +view.HasFocusChanged += (sender, e) => +{ + if (e.CurrentValue) + { + OnViewGainedFocus(); + } + else + { + OnViewLostFocus(); + } +}; +``` + ## Built-In Views Interactivity -| | | | | **Keyboard** | | | | **Mouse** | | | | | -|----------------|-------------------------|------------|---------------|--------------|-----------------------|------------------------------|---------------------------|------------------------------|------------------------------|------------------------------|----------------|---------------| -| | **Number
of States** | **Static** | **IsDefault** | **Hotkeys** | **Select
Command** | **Accept
Command** | **Hotkey
Command** | **CanFocus
Click** | **CanFocus
DblCLick** | **!CanFocus
Click** | **RightClick** | **GrabMouse** | -| **View** | 1 | Yes | No | 1 | OnSelect | OnAccept | Focus | Focus | | | | No | -| **Label** | 1 | Yes | No | 1 | OnSelect | OnAccept | FocusNext | Focus | | FocusNext | | No | -| **Button** | 1 | No | Yes | 1 | OnSelect | Focus
OnAccept | Focus
OnAccept | HotKey | | Select | | No | -| **Checkbox** | 3 | No | No | 1 | OnSelect
Advance | OnAccept | OnAccept | Select | | Select | | No | -| **RadioGroup** | > 1 | No | No | 2+ | Advance | Set SelectedItem
OnAccept | Focus
Set SelectedItem | SetFocus
Set _cursor | | SetFocus
Set _cursor | | No | -| **Slider** | > 1 | No | No | 1 | SetFocusedOption | SetFocusedOption
OnAccept | Focus | SetFocus
SetFocusedOption | | SetFocus
SetFocusedOption | | Yes | -| **ListView** | > 1 | No | No | 1 | MarkUnMarkRow | OpenSelectedItem
OnAccept | OnAccept | SetMark
OnSelectedChanged | OpenSelectedItem
OnAccept | | | No | +The following table summarizes how built-in views respond to various input methods: -## Accessibility Tenets +| View | States | Static | Default | HotKeys | Select Cmd | Accept Cmd | HotKey Cmd | Click Focus | DblClick | RightClick | GrabMouse | +|------|--------|--------|---------|---------|------------|------------|------------|-------------|----------|------------|-----------| +| **View** | 1 | Yes | No | 1 | OnSelect | OnAccept | Focus | Focus | - | - | No | +| **Label** | 1 | Yes | No | 1 | OnSelect | OnAccept | FocusNext | Focus | - | FocusNext | No | +| **Button** | 1 | No | Yes | 1 | OnSelect | Focus+OnAccept | Focus+OnAccept | HotKey | - | Select | No | +| **CheckBox** | 3 | No | No | 1 | OnSelect+Advance | OnAccept | OnAccept | Select | - | Select | No | +| **RadioGroup** | >1 | No | No | 2+ | Advance | SetSelected+OnAccept | Focus+SetSelected | SetFocus+SetCursor | - | SetFocus+SetCursor | No | +| **Slider** | >1 | No | No | 1 | SetFocusedOption | SetFocusedOption+OnAccept | Focus | SetFocus+SetOption | - | SetFocus+SetOption | Yes | +| **ListView** | >1 | No | No | 1 | MarkUnMarkRow | OpenSelected+OnAccept | OnAccept | SetMark+OnSelectedChanged | OpenSelected+OnAccept | - | No | +| **TextField** | 1 | No | No | 1 | - | OnAccept | Focus | Focus | SelectAll | ContextMenu | No | +| **TextView** | 1 | No | No | 1 | - | OnAccept | Focus | Focus | - | ContextMenu | Yes | -See https://devblogs.microsoft.com/dotnet/the-journey-to-accessible-apps-keyboard-accessible/ +### Table Legend -https://github.com/dotnet/maui/issues/1646 +- **States**: Number of visual/functional states the view can have +- **Static**: Whether the view is primarily for display (non-interactive) +- **Default**: Whether the view can be a default button (activated by Enter) +- **HotKeys**: Number of hotkeys the view typically supports +- **Select Cmd**: What happens when Command.Select is invoked +- **Accept Cmd**: What happens when Command.Accept is invoked +- **HotKey Cmd**: What happens when the view's hotkey is pressed +- **Click Focus**: Behavior when clicked (if CanFocus=true) +- **DblClick**: Behavior on double-click +- **RightClick**: Behavior on right-click +- **GrabMouse**: Whether the view captures mouse for drag operations + +## Common Navigation Patterns + +### Dialog Navigation + +```csharp +var dialog = new Dialog() +{ + Title = "Settings", + CanFocus = true, + TabStop = TabBehavior.TabGroup +}; + +var okButton = new Button() { Text = "OK", IsDefault = true }; +var cancelButton = new Button() { Text = "Cancel" }; + +// Tab navigates between buttons, Enter activates default +dialog.Add(okButton, cancelButton); +``` + +### Container Navigation + +```csharp +var leftPanel = new FrameView() +{ + Title = "Options", + TabStop = TabBehavior.TabGroup, + X = 0, + Width = Dim.Percent(50) +}; + +var rightPanel = new FrameView() +{ + Title = "Preview", + TabStop = TabBehavior.TabGroup, + X = Pos.Right(leftPanel), + Width = Dim.Fill() +}; + +// F6 navigates between panels, Tab navigates within panels +``` + +### List Navigation + +```csharp +var listView = new ListView() +{ + CanFocus = true, + TabStop = TabBehavior.TabStop +}; + +// Arrow keys navigate items, Enter selects, Space toggles +listView.KeyBindings.Add(Key.CursorUp, Command.Up); +listView.KeyBindings.Add(Key.CursorDown, Command.Down); +listView.KeyBindings.Add(Key.Enter, Command.Accept); +``` + +## Accessibility Considerations + +Terminal.Gui's navigation system is designed with accessibility in mind: + +### Keyboard Accessibility +- All functionality must be accessible via keyboard +- Tab order should be logical and predictable +- HotKeys provide direct access to important functions +- Arrow keys provide fine-grained navigation within controls + +### Visual Accessibility +- Focus indicators must be clearly visible +- Color is not the only indicator of focus state +- Text and background contrast meets accessibility standards +- HotKeys are visually indicated (underlined characters) + +### Screen Reader Support +- Focus changes are announced through system events +- View titles and labels provide context +- Status information is available programmatically + +### Best Practices for Accessible Navigation + +```csharp +// Provide meaningful labels +var button = new Button() { Text = "_Save Document", HotKey = Key.S }; + +// Set logical tab order +container.TabStop = TabBehavior.TabGroup; +foreach (var view in container.Subviews) +{ + view.TabStop = TabBehavior.TabStop; +} + +// Provide keyboard alternatives to mouse actions +view.KeyBindings.Add(Key.F10, Command.Context); // Right-click equivalent +view.KeyBindings.Add(Key.Space, Command.Select); // Click equivalent +``` + +For more information on accessibility standards, see: +- [Web Content Accessibility Guidelines (WCAG)](https://www.w3.org/WAI/WCAG21/quickref/) +- [Microsoft Accessibility Guidelines](https://learn.microsoft.com/en-us/windows/apps/design/accessibility/) +- [.NET Accessibility Documentation](https://learn.microsoft.com/en-us/dotnet/desktop/winforms/advanced/walkthrough-creating-an-accessible-windows-based-application) diff --git a/docfx/docs/newinv2.md b/docfx/docs/newinv2.md index 90d0af68b..1cda3e6c4 100644 --- a/docfx/docs/newinv2.md +++ b/docfx/docs/newinv2.md @@ -1,88 +1,175 @@ # Terminal.Gui v2 -This document provides an overview of the new features and improvements in Terminal.Gui v2. +This document provides an in-depth overview of the new features, improvements, and architectural changes in Terminal.Gui v2 compared to v1. For information on how to port code from v1 to v2, see the [v1 To v2 Migration Guide](migratingfromv1.md). -## Modern Look & Feel +## Architectural Overhaul and Design Philosophy -Apps built with Terminal.Gui now feel modern thanks to these improvements: +Terminal.Gui v2 represents a fundamental rethinking of the library's architecture, driven by the need for better maintainability, performance, and developer experience. The primary design goals in v2 include: -* *TrueColor support* - 24-bit color support for Windows, Mac, and Linux. Legacy 16-color systems are still supported, automatically. See [TrueColor](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#truecolor) for details. -* *Enhanced Borders and Padding* - Terminal.Gui now supports a `Border`, `Margin`, and `Padding` property on all views. This simplifies View development and enables a sophisticated look and feel. See [Adornments](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#adornments) for details. -* *User Configurable Color Themes* - See [Color Themes](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#color-themes) for details. -* *Enhanced Unicode/Wide Character support* - Terminal.Gui now supports the full range of Unicode/wide characters. See [Unicode](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#unicode) for details. -* [LineCanvas](~/api/Terminal.Gui.Drawing.LineCanvas.yml) - Terminal.Gui now supports a line canvas enabling high-performance drawing of lines and shapes using box-drawing glyphs. `LineCanvas` provides *auto join*, a smart TUI drawing system that automatically selects the correct line/box drawing glyphs for intersections making drawing complex shapes easy. See [Line Canvas](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#line-canvas) for details. +- **Decoupling of Concepts**: In v1, many concepts like focus management, layout, and input handling were tightly coupled, leading to fragile and hard-to-predict behavior. v2 explicitly separates these concerns, resulting in a more modular and testable codebase. +- **Performance Optimization**: v2 reduces overhead in rendering, event handling, and view management by streamlining internal data structures and algorithms. +- **Modern .NET Practices**: The API has been updated to align with contemporary .NET conventions, such as using events with `EventHandler` and leveraging modern C# features like target-typed `new` and file-scoped namespaces. +- **Accessibility and Usability**: v2 places a stronger emphasis on ensuring that terminal applications are accessible, with improved keyboard navigation and visual feedback. -## Simplified API +This architectural shift has resulted in the removal of thousands of lines of redundant or overly complex code from v1, replaced with cleaner, more focused implementations. -The entire library has been reviewed and simplified. As a result, the API is more consistent and uses modern .NET API standards (e.g. for events). This refactoring resulted in the removal of thousands of lines of code, better unit tests, and higher performance than v1. +## Modern Look & Feel - Technical Details -## [View](~/api/Terminal.Gui.ViewBase.View.yml) Improvements -* *Improved!* View Lifetime Management is Now Deterministic - In v1 the rules for lifetime management of `View` objects was unclear and led to non-dterministic behavior and hard to diagnose bugs. This was particularly acute in the behavior of `Application.Run`. In v2, the rules are clear and the code and unit test infrastructure tries to enforce them. See [Migrating From v1 To v2](migratingfromv1.md) for more details. -* *New!* Adornments - Adornments are a special form of View that appear outside the `Viewport`: @Terminal.Gui.ViewBase.View.Margin, @Terminal.Gui.ViewBase.View.Border, and @Terminal.Gui.ViewBase.View.Padding. -* *New!* Built-in Scrolling/Virtual Content Area - In v1, to have a view a user could scroll required either a bespoke scrolling implementation, inheriting from `ScrollView`, or managing the complexity of `ScrollBarView` directly. In v2, the base-View class supports scrolling inherently. The area of a view visible to the user at a given moment was previously a rectangle called `Bounds`. `Bounds.Location` was always `Point.Empty`. In v2 the visible area is a rectangle called `Viewport` which is a portal into the Views content, which can be bigger (or smaller) than the area visible to the user. Causing a view to scroll is as simple as changing `View.Viewport.Location`. The View's content described by `View.GetContentSize()`. See [Layout](layout.md) for details. -* *Improved!* @Terminal.Gui.Views.ScrollBar replaces `ScrollBarView` with a much cleaner implementation of a scrollbar. In addition, @Terminal.Gui.ViewBase.View.VerticalScrollBar and @Terminal.Gui.ViewBase.View.HorizontalScrollBar provide a simple way to enable scroll bars in any View with almost no code. See See [Scrolling Deep Dive](scrolling.md) for more. -* *New!* @Terminal.Gui.ViewBase.DimAuto - Automatically sizes the view to fit the view's Text, SubViews, or ContentArea. -* *Improved!* @Terminal.Gui.ViewBase.PosAnchorEnd - New to v2 is `Pos.AnchorEnd ()` (with no parameters) which allows a view to be anchored to the right or bottom of the SuperView. -* *New!* @Terminal.Gui.ViewBase.PosAlign - Aligns a set of views horizontally or vertically (left, right, center, etc...). -* *New!* @Terminal.Gui.ViewBase.View.Arrangement enables tiled and overlapped view arrangement and moving/resizing Views with the keyboard and mouse. See [Arrangement](arrangement.md). -* *Improved!* Keyboard [Navigation](navigation.md) has been revamped to be more reliability and ensure TUI apps built with Terminal.Gui are accessible. -* *New!* Sizable/Movable views - Any view can now be set to have resizeable borders and/or be dragged around. -* *Improved!* Consistent tabbing behavior - Tab navigation now behaves as expected, cleanly and consistently. +### TrueColor Support +- **Implementation**: v2 introduces 24-bit color support by extending the `Attribute` class to handle RGB values, with fallback to 16-color mode for older terminals. This is evident in the `ConsoleDriver` implementations, which now map colors to the appropriate terminal escape sequences. +- **Impact**: Developers can now use a full spectrum of colors without manual palette management, as seen in v1. The `Color` struct in v2 supports direct RGB input, and drivers handle the translation to terminal capabilities. +- **Usage**: See the `ColorPicker` view for an example of how TrueColor is leveraged to provide a rich color selection UI. -## New and Improved Built-in Views +### Enhanced Borders and Padding (Adornments) +- **Implementation**: v2 introduces a new `Adornment` class hierarchy, with `Margin`, `Border`, and `Padding` as distinct view-like entities that wrap content. This is a significant departure from v1, where borders were often hardcoded or required custom drawing. +- **Code Change**: In v1, `View` had rudimentary border support via properties like `BorderStyle`. In v2, `View` has a `Border` property of type `Border`, which is itself a configurable entity with properties like `Thickness` and `Effect3D`. +- **Impact**: This allows for consistent border rendering across all views and simplifies custom view development by providing a reusable adornment framework. -* *[DatePicker](~/api/Terminal.Gui.Views.DatePicker.yml)* - NEW! -* *ScrollView* - Replaced by built-in scrolling. -* *@"Terminal.Gui.Views.ScrollBar"* - Replaces *ScrollBarView* with a much simpler view. -* *[Slider](~/api/Terminal.Gui.Views.Slider.yml)* - NEW! -* *[Shortcut](~/api/Terminal.Gui.Views.Shortcut.yml)* - NEW! An opinionated (visually & API) View for displaying a command, helptext, key. -* *[Bar](~/api/Terminal.Gui.Views.Bar.yml)* - NEW! Building-block View for containing Shortcuts. Opinionated relative to Orientation but minimially so. The basis for the new StatusBar, MenuBar, and Menu views. -* *[StatusBar](~/api/Terminal.Gui.Views.StatusBar.yml)* - New implementation based on `Bar` -* *[MenuBar](~/api/Terminal.Gui.Views.MenuBar.yml)* - COMING SOON! New implementation based on `Bar` -* *[PopoverMenu](~/api/Terminal.Gui.Views.PopoverMenu.yml)* - COMING SOON! New implementation based on `Bar` -* *[FileDialog](~/api/Terminal.Gui.Views.FileDialog.yml)* - The new, modern file dialog includes icons (in TUI!) for files/folders, search, and a `TreeView`. -* *[TableView](~/api/Terminal.Gui.Views.TableView.yml)* - No longer just DataTable, now supports any collections, checkboxes and even expandable trees -* *@"Terminal.Gui.Views.ColorPicker"* - Fully supports TrueColor with the ability to choose a color using HSV, RGB, or HSL as well as W3C standard color names. +### User Configurable Color Themes +- **Implementation**: v2 adds a `ConfigurationManager` that supports loading and saving color schemes from configuration files. Themes are applied via `ColorScheme` objects, which can be customized per view or globally. +- **Impact**: Unlike v1, where color schemes were static or required manual override, v2 enables end-users to personalize the UI without code changes, enhancing accessibility and user preference support. -## Beauty +### Enhanced Unicode/Wide Character Support +- **Implementation**: v2 improves Unicode handling by correctly managing wide characters in text rendering and input processing. The `TextFormatter` class now accounts for Unicode width in layout calculations. +- **Impact**: This fixes v1 issues where wide characters (e.g., CJK scripts) could break layout or input handling, making Terminal.Gui v2 suitable for international applications. -Terminal.Gui has never been prettier +### LineCanvas +- **Implementation**: A new `LineCanvas` class provides a drawing API for creating lines and shapes using box-drawing characters. It includes logic for auto-joining lines at intersections, selecting appropriate glyphs dynamically. +- **Code Example**: In v2, `LineCanvas` is used internally by views like `Border` to draw clean, connected lines, a feature absent in v1. +- **Impact**: Developers can create complex diagrams or UI elements with minimal effort, improving the visual fidelity of terminal applications. -* *ShowBorders* - Get that 3D 'pop' for your buttons -* *Gradient* - Render beautiful true color borders, titles etc with the new Gradient API +## Simplified API - Under the Hood +### API Consistency and Reduction +- **Change**: v2 revisits every public API, consolidating redundant methods and properties. For example, v1 had multiple focus-related methods scattered across `View` and `Application`; v2 centralizes these in `ApplicationNavigation`. +- **Impact**: This reduces the learning curve for new developers and minimizes the risk of using deprecated or inconsistent APIs. +- **Example**: The v1 `View.MostFocused` property is replaced by `Application.Navigation.GetFocused()`, reducing traversal overhead and clarifying intent. -## Configuration Manager +### Modern .NET Standards +- **Change**: Events in v2 use `EventHandler` instead of v1's custom delegate types. Methods follow consistent naming (e.g., `OnHasFocusChanged` vs. v1's varied naming). +- **Impact**: Developers familiar with .NET conventions will find v2 more intuitive, and tools like IntelliSense provide better support due to standardized signatures. -Terminal.Gui now supports a configuration manager enabling library and app settings to be persisted and loaded from the file system. See [Configuration Manager](https://gui-cs.github.io/Terminal.GuiV2Docs/docs/overview.html#configuration-manager) for details. +### Performance Gains +- **Change**: v2 optimizes rendering by minimizing unnecessary redraws through a smarter `NeedsDisplay` system and reducing object allocations in hot paths like event handling. +- **Impact**: Applications built with v2 will feel snappier, especially in complex UIs with many views or frequent updates, addressing v1 performance bottlenecks. -## Logging & Metrics +## View Improvements - Deep Dive -Terminal.Gui now features multi level logging of engine internals and system performance metrics (redraws, invoke durations etc). Never again wonder why your frame rate is low, or a given terminal/distro does not behave as expected. -See [Logging](logging.md) for details. +### Deterministic View Lifetime Management +- **v1 Issue**: Lifetime rules for `View` objects were unclear, leading to memory leaks or premature disposal, especially with `Application.Run`. +- **v2 Solution**: v2 defines explicit rules for view disposal and ownership, enforced by unit tests. `Application.Run` now clearly manages the lifecycle of `Toplevel` views, ensuring deterministic cleanup. +- **Impact**: Developers can predict when resources are released, reducing bugs related to dangling references or uninitialized states. -## Sixel Image Support +### Adornments Framework +- **Technical Detail**: Adornments are implemented as nested views that surround the content area, each with its own drawing and layout logic. For instance, `Border` can draw 3D effects or custom glyphs. +- **Code Change**: In v2, `View` has properties like `Margin`, `Border`, and `Padding`, each configurable independently, unlike v1's limited border support. +- **Impact**: This modular approach allows for reusable UI elements and simplifies creating visually consistent applications. -Recently added to Windows Terminal and long supported in mainstream linux terminals, this graphics protcol allows images and even animations to be rendered directly into the console. +### Built-in Scrolling/Virtual Content Area +- **v1 Issue**: Scrolling required using `ScrollView` or manual offset management, which was error-prone. +- **v2 Solution**: Every `View` in v2 has a `Viewport` rectangle representing the visible portion of a potentially larger content area defined by `GetContentSize()`. Changing `Viewport.Location` scrolls the content. +- **Code Example**: In v2, `TextView` uses this to handle large text buffers without additional wrapper views. +- **Impact**: Simplifies implementing scrollable content and reduces the need for specialized container views. -## Updated Keyboard API +### Improved ScrollBar +- **Change**: v2 replaces `ScrollBarView` with `ScrollBar`, a cleaner implementation integrated with the built-in scrolling system. `VerticalScrollBar` and `HorizontalScrollBar` properties on `View` enable scroll bars with minimal code. +- **Impact**: Developers can add scroll bars to any view without managing separate view hierarchies, a significant usability improvement over v1. -The API for handling keyboard input is significantly improved. See [Keyboard API](keyboard.md). +### DimAuto, PosAnchorEnd, and PosAlign +- **DimAuto**: Automatically sizes views based on content or subviews, reducing manual layout calculations. +- **PosAnchorEnd**: Allows anchoring to the right or bottom of a superview, enabling flexible layouts not easily achievable in v1. +- **PosAlign**: Provides alignment options (left, center, right) for multiple views, streamlining UI design. +- **Impact**: These features reduce boilerplate layout code and support responsive designs in terminal constraints. -* The `Key` class replaces the `KeyEvent` struct and provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level `KeyCode` enum when possible. See [Key](~/api/Terminal.Gui.Input.Key.yml) for more details. -* The preferred way to handle single keystrokes is to use **Key Bindings**. Key Bindings map a key press to a [Command](~/api/Terminal.Gui.Input.Command.yml). A view can declare which commands it supports, and provide a lambda that implements the functionality of the command, using `View.AddCommand()`. Use the `View.Keybindings` to configure the key bindings. -* For better consistency and user experience, the default key for closing an app or `Toplevel` is now `Esc` (it was previously `Ctrl+Q`). +### View Arrangement +- **Technical Detail**: The `Arrangement` property on `View` supports flags like `Movable`, `Resizable`, and `Overlapped`, enabling dynamic UI interactions via keyboard and mouse. +- **Code Example**: `Window` in v2 uses `Arrangement` to allow dragging and resizing, a feature requiring custom logic in v1. +- **Impact**: Developers can create desktop-like experiences in the terminal with minimal effort. -## Updated Mouse API +### Keyboard Navigation Overhaul +- **v1 Issue**: Navigation was inconsistent, with coupled concepts like `CanFocus` and `TabStop` leading to unpredictable focus behavior. +- **v2 Solution**: v2 decouples these concepts, introduces `TabBehavior` enum for clearer intent (`TabStop`, `TabGroup`, `NoStop`), and centralizes navigation logic in `ApplicationNavigation`. +- **Impact**: Ensures accessibility by guaranteeing keyboard access to all focusable elements, with unit tests enforcing navigation keys on built-in views. -The API for mouse input is now internally consistent and easiser to use. +### Sizable/Movable Views +- **Implementation**: Any view can be made resizable or movable by setting `Arrangement` flags, with built-in mouse and keyboard handlers for interaction. +- **Impact**: Enhances user experience by allowing runtime UI customization, a feature limited to specific views like `Window` in v1. -* The `MouseEvent` class replaces `MouseEventEventArgs`. -* More granular APIs are provided to ease handling specific mouse actions. See [Mouse API](mouse.md). -* Views can use the `View.Highlight` event to have the view be visibly highlighted on various mouse events. -* Views can set `View.WantContinousButtonPresses = true` to have their `Command.Accept` command be invoked repeatedly as the user holds a mouse button down on the view. +## New and Improved Built-in Views - Detailed Analysis -## AOT support -*AOT/single file app support* now works out of the box. \ No newline at end of file +### New Views +- **DatePicker**: Provides a calendar-based date selection UI, leveraging v2's improved drawing and navigation systems. +- **Slider**: A new control for range selection, using `LineCanvas` for smooth rendering and supporting TrueColor for visual feedback. +- **Shortcut**: An opinionated view for command display with key bindings, simplifying status bar or toolbar creation. +- **Bar**: A foundational view for horizontal or vertical layouts of `Shortcut` or other items, used in `StatusBar`, `MenuBar`, and `PopoverMenu`. +- **FileDialog**: Modernized with a `TreeView` for navigation, icons using Unicode glyphs, and search functionality, far surpassing v1's basic dialog. +- **ColorPicker**: Leverages TrueColor for a comprehensive color selection experience, supporting multiple color models (HSV, RGB, HSL). + +### Improved Views +- **ScrollView**: Deprecated in favor of built-in scrolling on `View`, reducing complexity and view hierarchy depth. +- **TableView**: Now supports generic collections, checkboxes, and tree structures, moving beyond v1's `DataTable` limitation, with improved rendering performance. +- **StatusBar**: Rebuilt on `Bar`, providing a more flexible and visually appealing status display. + +## Beauty - Visual Enhancements + +### Borders +- **Implementation**: Uses the `Border` adornment to render 3D effects or custom styles, configurable per view. +- **Impact**: Adds visual depth to UI elements, making applications feel more polished compared to v1's flat borders. + +### Gradient +- **Implementation**: A new `Gradient` API allows rendering color transitions across view elements, using TrueColor for smooth effects. +- **Impact**: Enables modern-looking UI elements like gradient borders or backgrounds, not possible in v1 without custom drawing. + +## Configuration Manager - Persistence and Customization +- **Technical Detail**: `ConfigurationManager` in v2 uses JSON or other formats to persist settings like themes, key bindings, and view properties to disk. +- **Code Change**: Unlike v1, where settings were ephemeral or hardcoded, v2 provides a centralized system for loading/saving configurations. +- **Impact**: Allows for user-specific customizations and library-wide settings without recompilation, enhancing flexibility. + +## Logging & Metrics - Debugging and Performance +- **Implementation**: v2 introduces a multi-level logging system for internal operations (e.g., rendering, input handling) and metrics for performance tracking (e.g., frame rate, redraw times). +- **Impact**: Developers can diagnose issues like slow redraws or terminal compatibility problems, a capability absent in v1, reducing guesswork in debugging. + +## Sixel Image Support - Graphics in Terminal +- **Technical Detail**: v2 supports the Sixel protocol for rendering images and animations directly in compatible terminals (e.g., Windows Terminal, xterm). +- **Code Change**: New rendering logic in console drivers detects terminal support and handles Sixel data transmission. +- **Impact**: Brings graphical capabilities to terminal applications, far beyond v1's text-only rendering, opening up new use cases like image previews. + +## Updated Keyboard API - Comprehensive Input Handling + +### Key Class +- **Change**: Replaces v1's `KeyEvent` struct with a `Key` class, providing a high-level abstraction over raw key codes with properties for modifiers and key type. +- **Impact**: Simplifies keyboard handling by abstracting platform differences, making code more portable and readable. + +### Key Bindings +- **Implementation**: v2 introduces a binding system mapping keys to `Command` enums via `View.KeyBindings`, with scopes (`Application`, `Focused`, `HotKey`) for priority. +- **Impact**: Replaces v1's ad-hoc key handling with a structured approach, allowing views to declare supported commands and customize responses easily. +- **Example**: `TextField` in v2 binds `Key.Tab` to text insertion rather than focus change, customizable by developers. + +### Default Close Key +- **Change**: Changed from `Ctrl+Q` in v1 to `Esc` in v2 for closing apps or `Toplevel` views. +- **Impact**: Aligns with common user expectations, improving UX consistency across terminal applications. + +## Updated Mouse API - Enhanced Interaction + +### MouseEvent Class +- **Change**: Replaces `MouseEventEventArgs` with `MouseEvent`, providing a cleaner structure for mouse data (position, flags). +- **Impact**: Simplifies event handling with a more intuitive API, reducing errors in mouse interaction logic. + +### Granular Mouse Handling +- **Implementation**: v2 offers specific events for clicks, double-clicks, and movement, with flags for button states. +- **Impact**: Developers can handle complex mouse interactions (e.g., drag-and-drop) more easily than in v1. + +### Highlight Event and Continuous Button Presses +- **Highlight**: Views can visually respond to mouse hover or click via the `Highlight` event. +- **Continuous Presses**: Setting `WantContinuousButtonPresses = true` repeats `Command.Accept` during button hold, useful for sliders or buttons. +- **Impact**: Enhances interactive feedback, making terminal UIs feel more responsive. + +## AOT Support - Deployment and Performance +- **Implementation**: v2 ensures compatibility with Ahead-of-Time compilation and single-file applications by avoiding reflection patterns problematic for AOT. +- **Impact**: Simplifies deployment for environments requiring AOT (e.g., .NET Native), a feature not explicitly supported in v1, reducing runtime overhead. + +## Conclusion + +Terminal.Gui v2 is a transformative update, addressing core limitations of v1 through architectural redesign, performance optimizations, and feature enhancements. From TrueColor and adornments for visual richness to decoupled navigation and modern input APIs for usability, v2 provides a robust foundation for building sophisticated terminal applications. The detailed changes in view management, configuration, and debugging tools empower developers to create more maintainable and user-friendly applications. \ No newline at end of file diff --git a/Showcase.md b/docfx/docs/showcase.md similarity index 65% rename from Showcase.md rename to docfx/docs/showcase.md index 12f968898..c72b585ac 100644 --- a/Showcase.md +++ b/docfx/docs/showcase.md @@ -1,43 +1,37 @@ -# Showcase # +# Showcase -* **[UI Catalog](https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog)** - The UI Catalog project provides an easy to use and extend sample illustrating the capabilities of **Terminal.Gui**. Run `dotnet run --project UICatalog` to run the UI Catalog. - ![Sample app](docfx/images/sample.gif) - ⠀ -* **[PowerShell's `Out-ConsoleGridView`](https://github.com/PowerShell/GraphicalTools)** - `OCGV` sends the output from a command to an interactive table. - ![OutConsoleGridView.png](docfx/images/OutConsoleGridView.png) - ⠀ -* **[F7History](https://github.com/gui-cs/F7History)** - Graphical Command History for PowerShell (built on PowerShell's `Out-ConsoleGridView`). - ![F7History.gif](docfx/images/F7History.gif) - ⠀ -* **[PoshRedisViewer](https://github.com/En3Tho/PoshRedisViewer)** - A compact Redis viewer module for PowerShell written in F#. - ![PoshRedisViewer.png](docfx/images/PoshRedisViewer.png) - ⠀ -* **[PoshDotnetDumpAnalyzeViewer](https://github.com/En3Tho/PoshDotnetDumpAnalyzeViewer)** - dotnet-dump UI module for PowerShell. - ![PoshDotnetDumpAnalyzerViewer.png](docfx/images/PoshDotnetDumpAnalyzerViewer.png) - ⠀ -* **[TerminalGuiDesigner](https://github.com/tznind/TerminalGuiDesigner)** - Cross platform view designer for building Terminal.Gui applications. - ![TerminalGuiDesigner.gif](docfx/images/TerminalGuiDesigner.gif) - -* **[Capital and Cargo](https://github.com/dhorions/Capital-and-Cargo)** - A retro console game where you buy, sell, produce and transport goods built with Terminal.Gui - ![image](https://github.com/gui-cs/Terminal.Gui/assets/1682004/ed89f3d6-020f-4a8a-ae18-e057514f4c43) +## Applications Built with Terminal.Gui +- **[UI Catalog](https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog)** - The UI Catalog project provides an easy to use and extend sample illustrating the capabilities of **Terminal.Gui**. Run `dotnet run --project UICatalog` to run the UI Catalog. + ![Sample app](../images/sample.gif) + ⠀ +- **[PowerShell's `Out-ConsoleGridView`](https://github.com/PowerShell/GraphicalTools)** - `OCGV` sends the output from a command to an interactive table. + ![OutConsoleGridView](../images/OutConsoleGridView.png) + ⠀ +- **[F7History](https://github.com/gui-cs/F7History)** - Graphical Command History for PowerShell (built on PowerShell's `Out-ConsoleGridView`). + ![F7History](../images/F7History.gif) + ⠀ +- **[PoshRedisViewer](https://github.com/En3Tho/PoshRedisViewer)** - A compact Redis viewer module for PowerShell written in F#. + ![PoshRedisViewer](../images/PoshRedisViewer.png) + ⠀ +- **[PoshDotnetDumpAnalyzeViewer](https://github.com/En3Tho/PoshDotnetDumpAnalyzeViewer)** - dotnet-dump UI module for PowerShell. + ![PoshDotnetDumpAnalyzerViewer](../images/PoshDotnetDumpAnalyzerViewer.png) + ⠀ +- **[TerminalGuiDesigner](https://github.com/tznind/TerminalGuiDesigner)** - Cross platform view designer for building Terminal.Gui applications. + ![TerminalGuiDesigner](../images/TerminalGuiDesigner.gif) +- **[Capital and Cargo](https://github.com/dhorions/Capital-and-Cargo)** - A retro console game where you buy, sell, produce and transport goods built with Terminal.Gui + ![image](https://github.com/gui-cs/Terminal.Gui/assets/1682004/ed89f3d6-020f-4a8a-ae18-e057514f4c43) - **[Falcon](https://github.com/MaciekWin3/Falcon)** - Terminal chat application that uses SignalR and Terminal.Gui. ![Falcon](https://github.com/user-attachments/assets/d505cba3-75d3-43ea-b270-924dfd257a65) - - **[Muse](https://github.com/MaciekWin3/Muse)** - Muse is terminal music player built with Terminal.Gui and NAudio on .NET platform. ![Muse](https://github.com/user-attachments/assets/94aeb559-a889-4b52-bb0d-453b3e19b290) - - **[Whale](https://github.com/MaciekWin3/Whale)** - Lightweight terminal user interface application that helps software engineers manage Docker containers. ![Whale](https://github.com/user-attachments/assets/7ef6e348-c36b-4aee-a63c-4e5c60c3aad2) - - **[TermKeyVault](https://github.com/MaciekWin3/TermKeyVault)** - Terminal based password manager built with F# and Terminal.Gui. ![TermKeyVault](https://github.com/user-attachments/assets/c40e17ed-2614-4ad4-8547-e93c1b1d8937) - -# Examples # +## Examples -* **[C# Example](https://github.com/gui-cs/Terminal.Gui/tree/master/Example)** - Run `dotnet run` in the `Example` directory to run the C# Example. - -* **[F# Example](https://github.com/gui-cs/Terminal.Gui/tree/master/FSharpExample)** - An example showing how to build a Terminal.Gui app using F#. - -* **[Reactive Example](https://github.com/gui-cs/Terminal.Gui/tree/master/ReactiveExample)** - A sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [Pharmacist](https://github.com/reactiveui/pharmacist) — a tool that converts all events in a NuGet package into observable wrappers. +- **[C# Example](https://github.com/gui-cs/Terminal.Gui/tree/master/Example)** - Run `dotnet run` in the `Example` directory to run the C# Example. +- **[F# Example](https://github.com/gui-cs/Terminal.Gui/tree/master/FSharpExample)** - An example showing how to build a Terminal.Gui app using F#. +- **[Reactive Example](https://github.com/gui-cs/Terminal.Gui/tree/master/ReactiveExample)** - A sample app that shows how to use `System.Reactive` and `ReactiveUI` with `Terminal.Gui`. The app uses the MVVM architecture that may seem familiar to folks coming from WPF, Xamarin Forms, UWP, Avalonia, or Windows Forms. In this app, we implement the data bindings using ReactiveUI `WhenAnyValue` syntax and [Pharmacist](https://github.com/reactiveui/pharmacist) — a tool that converts all events in a NuGet package into observable wrappers. \ No newline at end of file diff --git a/docfx/index.md b/docfx/index.md index 1aef8ba67..9a2ee0b01 100644 --- a/docfx/index.md +++ b/docfx/index.md @@ -1,3 +1,16 @@ +# Terminal.Gui Documentation + +Welcome to the official documentation for Terminal.Gui, a cross-platform UI toolkit for creating console-based graphical user interfaces in .NET. + +## Key Resources + +- [Getting Started](docs/getting-started.md) - Learn how to start using Terminal.Gui. +- [Events Deep Dive](docs/events.md) - Detailed guide on event handling and the Cancellable Work Pattern. +- [View Documentation](docs/View.md) - Information on creating and customizing views. +- [Keyboard Handling](docs/keyboard.md) - Guide to managing keyboard input. +- [Mouse Support](docs/mouse.md) - Details on implementing mouse interactions. +- [Showcase](docs/showcase.md) - Explore applications and examples built with Terminal.Gui. + # Terminal.Gui v2 - Cross Platform Terminal UI toolkit for .NET A toolkit for building rich console apps for .NET that run on Windows, the Mac, and Linux. diff --git a/docfx/scripts/OutputView/OutputView.sln b/docfx/scripts/OutputView/OutputView.sln new file mode 100644 index 000000000..0b75e083f --- /dev/null +++ b/docfx/scripts/OutputView/OutputView.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.35906.104 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutputView", "OutputView.csproj", "{DE15A308-8649-94BA-072D-8F2D35DAA789}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Terminal.Gui", "..\..\..\Terminal.Gui\Terminal.Gui.csproj", "{4DB1A280-EA90-4E91-80BE-D17A627AEC65}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DE15A308-8649-94BA-072D-8F2D35DAA789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE15A308-8649-94BA-072D-8F2D35DAA789}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE15A308-8649-94BA-072D-8F2D35DAA789}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE15A308-8649-94BA-072D-8F2D35DAA789}.Release|Any CPU.Build.0 = Release|Any CPU + {4DB1A280-EA90-4E91-80BE-D17A627AEC65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DB1A280-EA90-4E91-80BE-D17A627AEC65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DB1A280-EA90-4E91-80BE-D17A627AEC65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DB1A280-EA90-4E91-80BE-D17A627AEC65}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D6EAC099-6A1E-462C-9D34-C8C4D21CD91C} + EndGlobalSection +EndGlobal diff --git a/docfx/scripts/OutputView/Properties/launchSettings.json b/docfx/scripts/OutputView/Properties/launchSettings.json new file mode 100644 index 000000000..7edd91873 --- /dev/null +++ b/docfx/scripts/OutputView/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "OutputView": { + "commandName": "Project", + "commandLineArgs": "--view=FileDialog" + } + } +} \ No newline at end of file diff --git a/docfx/toc.yml b/docfx/toc.yml index 94ab08013..f6ef66e50 100644 --- a/docfx/toc.yml +++ b/docfx/toc.yml @@ -5,4 +5,21 @@ href: api/ - name: Source - href: https://github.com/gui-cs/Terminal.Gui \ No newline at end of file + href: https://github.com/gui-cs/Terminal.Gui + +- name: Documentation + href: docs/ + items: + - name: Getting Started + href: docs/getting-started.md + - name: Events Deep Dive + href: docs/events.md + - name: View Documentation + href: docs/View.md + - name: Keyboard Handling + href: docs/keyboard.md + - name: Mouse Support + href: docs/mouse.md + - name: Showcase + href: docs/showcase.md + # Add other existing documentation entries as needed \ No newline at end of file