From 39b39ddc7048324fc84d692486b0b8c9c53ea226 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 13 Jul 2024 19:40:33 +0100 Subject: [PATCH 01/49] Fixes #3611. Localization not working on self-contained single-file. --- SelfContained/Program.cs | 19 ++++++++++- SelfContained/SelfContained.csproj | 2 +- Terminal.Gui/Application/Application.cs | 40 ++++++++++++++++++----- UnitTests/Application/ApplicationTests.cs | 1 + 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/SelfContained/Program.cs b/SelfContained/Program.cs index b723bcbd4..f2f157670 100644 --- a/SelfContained/Program.cs +++ b/SelfContained/Program.cs @@ -1,6 +1,7 @@ // This is a simple example application for a self-contained single file. using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Terminal.Gui; namespace SelfContained; @@ -10,7 +11,23 @@ public static class Program [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run(Func, ConsoleDriver)")] private static void Main (string [] args) { - Application.Run ().Dispose (); + Application.Init (); + + if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture)) + { + System.Diagnostics.Debug.Assert (Application.SupportedCultures.Count == 0); + } + else + { + System.Diagnostics.Debug.Assert (Application.SupportedCultures.Count == 4); + System.Diagnostics.Debug.Assert (Equals (CultureInfo.CurrentCulture, Thread.CurrentThread.CurrentUICulture)); + } + + ExampleWindow app = new (); + Application.Run (app); + + // Dispose the app object before shutdown + app.Dispose (); // Before the application exits, reset Terminal.Gui for clean shutdown Application.Shutdown (); diff --git a/SelfContained/SelfContained.csproj b/SelfContained/SelfContained.csproj index b4ffbe030..af651e3ca 100644 --- a/SelfContained/SelfContained.csproj +++ b/SelfContained/SelfContained.csproj @@ -8,7 +8,7 @@ true Link true - true + false embedded diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 477553263..225eb17f3 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -48,7 +48,7 @@ public static partial class Application internal static List GetSupportedCultures () { - CultureInfo [] culture = CultureInfo.GetCultures (CultureTypes.AllCultures); + CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); // Get the assembly var assembly = Assembly.GetExecutingAssembly (); @@ -57,15 +57,37 @@ public static partial class Application string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory; // Find the resource file name of the assembly - var resourceFilename = $"{Path.GetFileNameWithoutExtension (AppContext.BaseDirectory)}.resources.dll"; + var resourceFilename = $"{assembly.GetName ().Name}.resources.dll"; - // Return all culture for which satellite folder found with culture code. - return culture.Where ( - cultureInfo => - Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) - && File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename)) - ) - .ToList (); + if (cultures.Length > 1 && Directory.Exists (Path.Combine (assemblyLocation, "pt-PT"))) + { + // Return all culture for which satellite folder found with culture code. + return cultures.Where ( + cultureInfo => + Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) + && File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename)) + ) + .ToList (); + } + + // It's called from a self-contained single.file. + try + { + // false + return + [ + new ("fr-FR"), + new ("ja-JP"), + new ("pt-PT"), + new ("zh-Hans") + ]; + } + catch (CultureNotFoundException) + { + // true + // Only the invariant culture is supported in globalization-invariant mode. + return []; + } } // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`. diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index a7c1fc64a..e2d707e3d 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -193,6 +193,7 @@ public class ApplicationTests // Internal properties Assert.False (Application._initialized); Assert.Equal (Application.GetSupportedCultures (), Application.SupportedCultures); + Assert.Equal (4, Application.SupportedCultures.Count); Assert.False (Application._forceFakeConsole); Assert.Equal (-1, Application._mainThreadId); Assert.Empty (Application._topLevels); From ba4ab0c8c6aab183a704ab2016f6c765a27b9d5c Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 14 Jul 2024 18:34:57 +0100 Subject: [PATCH 02/49] Involving assertive code within a region. --- SelfContained/Program.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/SelfContained/Program.cs b/SelfContained/Program.cs index f2f157670..b16c8cd3e 100644 --- a/SelfContained/Program.cs +++ b/SelfContained/Program.cs @@ -1,5 +1,6 @@ // This is a simple example application for a self-contained single file. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using Terminal.Gui; @@ -13,16 +14,20 @@ public static class Program { Application.Init (); + #region The code in this region is not intended for use in a self-contained single-file. It's just here to make sure there is no functionality break with localization in Terminal.Gui using single-file + if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture)) { - System.Diagnostics.Debug.Assert (Application.SupportedCultures.Count == 0); + Debug.Assert (Application.SupportedCultures.Count == 0); } else { - System.Diagnostics.Debug.Assert (Application.SupportedCultures.Count == 4); - System.Diagnostics.Debug.Assert (Equals (CultureInfo.CurrentCulture, Thread.CurrentThread.CurrentUICulture)); + Debug.Assert (Application.SupportedCultures.Count == 4); + Debug.Assert (Equals (CultureInfo.CurrentCulture, Thread.CurrentThread.CurrentUICulture)); } + #endregion + ExampleWindow app = new (); Application.Run (app); From 918299b86dcabaac9af8c65c12477afaa90ac24d Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 15 Jul 2024 22:35:39 +0100 Subject: [PATCH 03/49] Changing README explaining the purpose of this project. --- SelfContained/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SelfContained/README.md b/SelfContained/README.md index 91cc1eb5f..f4bd041ee 100644 --- a/SelfContained/README.md +++ b/SelfContained/README.md @@ -1,6 +1,6 @@ # Terminal.Gui C# SelfContained -This example shows how to use the `Terminal.Gui` library to create a simple `self-contained` `single file` GUI application in C#. +This project aims to test the `Terminal.Gui` library to create a simple `self-contained` `single-file` GUI application in C#, ensuring that all its features are available. With `Debug` the `.csproj` is used and with `Release` the latest `nuget package` is used, either in `Solution Configurations` or in `Profile Publish`. From fbd7dc745e5034798d273f6f32709269cecdd8e1 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 16 Jul 2024 23:55:58 +0100 Subject: [PATCH 04/49] Changes comment for clarification. --- SelfContained/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SelfContained/Program.cs b/SelfContained/Program.cs index b16c8cd3e..ff6dd12b9 100644 --- a/SelfContained/Program.cs +++ b/SelfContained/Program.cs @@ -1,4 +1,4 @@ -// This is a simple example application for a self-contained single file. +// This is a test application for a self-contained single file. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; From 43ed4b59ebe050df756770ed85edd3f7837c9178 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 10:58:25 -0600 Subject: [PATCH 05/49] Initial commit. Added interface. Testing with RadioGroup --- Terminal.Gui/View/Orientation/IOrientation.cs | 127 ++++++++++++++++++ .../Orientation}/Orientation.cs | 0 Terminal.Gui/Views/OrientationEventArgs.cs | 19 --- Terminal.Gui/Views/RadioGroup.cs | 108 +++++++++------ Terminal.Gui/Views/Slider.cs | 7 +- UICatalog/Scenarios/ExpanderButton.cs | 7 +- 6 files changed, 202 insertions(+), 66 deletions(-) create mode 100644 Terminal.Gui/View/Orientation/IOrientation.cs rename Terminal.Gui/{Views/GraphView => View/Orientation}/Orientation.cs (100%) delete mode 100644 Terminal.Gui/Views/OrientationEventArgs.cs diff --git a/Terminal.Gui/View/Orientation/IOrientation.cs b/Terminal.Gui/View/Orientation/IOrientation.cs new file mode 100644 index 000000000..908ce32bf --- /dev/null +++ b/Terminal.Gui/View/Orientation/IOrientation.cs @@ -0,0 +1,127 @@ + +namespace Terminal.Gui; +using System; + +/// +/// Implement this interface to provide orientation support. +/// +public interface IOrientation +{ + /// + /// Gets or sets the orientation of the View. + /// + Orientation Orientation { get; set; } + + /// + /// Raised when is changing. Can be cancelled. + /// + public event EventHandler> OrientationChanging; + + /// + /// Called when is changing. + /// + /// The current orienation. + /// The new orienation. + /// to cancel the change. + public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) { return false; } + + /// + /// + /// + public event EventHandler> OrientationChanged; + + /// + /// Called when has been changed. + /// + /// + /// + /// + public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) { return; } +} + + +/// +/// Helper class for implementing . +/// +public class OrientationHelper +{ + private Orientation _orientation = Orientation.Vertical; + private readonly IOrientation _owner; + + /// + /// Initializes a new instance of the class. + /// + /// + public OrientationHelper (IOrientation owner) + { + _owner = owner; + } + + /// + /// Gets or sets the orientation of the View. + /// + public Orientation Orientation + { + get => _orientation; + set + { + var args = new CancelEventArgs (in _orientation, ref value); + OrientationChanging?.Invoke (_owner, args); + if (args.Cancel) + { + return; + } + + if (_owner?.OnOrientationChanging (value, _orientation) ?? false) + { + return; + } + + Orientation old = _orientation; + if (_orientation != value) + { + _orientation = value; + _owner.Orientation = value; + } + + args = new CancelEventArgs (in old, ref _orientation); + OrientationChanged?.Invoke (_owner, args); + + _owner?.OnOrientationChanged (old, _orientation); + } + } + + /// + /// + /// + public event EventHandler> OrientationChanging; + + /// + /// + /// + /// + /// + /// + protected bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) + { + return _owner?.OnOrientationChanging (currentOrientation, newOrientation) ?? false; + } + + /// + /// + /// + public event EventHandler> OrientationChanged; + + /// + /// + /// + /// + /// + /// + protected void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + _owner?.OnOrientationChanged (oldOrientation, newOrientation); + } +} + + diff --git a/Terminal.Gui/Views/GraphView/Orientation.cs b/Terminal.Gui/View/Orientation/Orientation.cs similarity index 100% rename from Terminal.Gui/Views/GraphView/Orientation.cs rename to Terminal.Gui/View/Orientation/Orientation.cs diff --git a/Terminal.Gui/Views/OrientationEventArgs.cs b/Terminal.Gui/Views/OrientationEventArgs.cs deleted file mode 100644 index 8a633ca83..000000000 --- a/Terminal.Gui/Views/OrientationEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Terminal.Gui; - -/// for events. -public class OrientationEventArgs : EventArgs -{ - /// Constructs a new instance. - /// the new orientation - public OrientationEventArgs (Orientation orientation) - { - Orientation = orientation; - Cancel = false; - } - - /// If set to true, the orientation change operation will be canceled, if applicable. - public bool Cancel { get; set; } - - /// The new orientation. - public Orientation Orientation { get; set; } -} \ No newline at end of file diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index f0dc5174b..1565193b3 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -1,14 +1,14 @@ namespace Terminal.Gui; /// Displays a group of labels each with a selected indicator. Only one of those can be selected at a given time. -public class RadioGroup : View, IDesignable +public class RadioGroup : View, IDesignable, IOrientation { private int _cursor; private List<(int pos, int length)> _horizontal; private int _horizontalSpace = 2; - private Orientation _orientation = Orientation.Vertical; private List _radioLabels = []; private int _selected; + private readonly OrientationHelper _orientationHelper; /// /// Initializes a new instance of the class. @@ -103,6 +103,13 @@ public class RadioGroup : View, IDesignable return true; }); + _orientationHelper = new OrientationHelper (this); + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); + + //OrientationChanging += (sender, e) => OnOrientationChanging (e.CurrentValue, e.NewValue); + //OrientationChanged += (sender, e) => OnOrientationChanged (e.CurrentValue, e.NewValue); + SetupKeyBindings (); LayoutStarted += RadioGroup_LayoutStarted; @@ -142,15 +149,15 @@ public class RadioGroup : View, IDesignable int viewportX = e.MouseEvent.Position.X; int viewportY = e.MouseEvent.Position.Y; - int pos = _orientation == Orientation.Horizontal ? viewportX : viewportY; + int pos = Orientation == Orientation.Horizontal ? viewportX : viewportY; - int rCount = _orientation == Orientation.Horizontal + int rCount = Orientation == Orientation.Horizontal ? _horizontal.Last ().pos + _horizontal.Last ().length : _radioLabels.Count; if (pos < rCount) { - int c = _orientation == Orientation.Horizontal + int c = Orientation == Orientation.Horizontal ? _horizontal.FindIndex (x => x.pos <= viewportX && x.pos + x.length - 2 >= viewportX) : viewportY; @@ -173,7 +180,7 @@ public class RadioGroup : View, IDesignable get => _horizontalSpace; set { - if (_horizontalSpace != value && _orientation == Orientation.Horizontal) + if (_horizontalSpace != value && Orientation == Orientation.Horizontal) { _horizontalSpace = value; UpdateTextFormatterText (); @@ -182,16 +189,6 @@ public class RadioGroup : View, IDesignable } } - /// - /// Gets or sets the for this . The default is - /// . - /// - public Orientation Orientation - { - get => _orientation; - set => OnOrientationChanged (value); - } - /// /// The radio labels to display. A key binding will be added for each radio enabling the user to select /// and/or focus the radio label using the keyboard. See for details on how HotKeys work. @@ -323,30 +320,45 @@ public class RadioGroup : View, IDesignable } } - /// Called when the view orientation has changed. Invokes the event. - /// - /// True of the event was cancelled. - public virtual bool OnOrientationChanged (Orientation newOrientation) + /// + /// Gets or sets the for this . The default is + /// . + /// + public Orientation Orientation { - var args = new OrientationEventArgs (newOrientation); - OrientationChanged?.Invoke (this, args); - - if (!args.Cancel) - { - _orientation = newOrientation; - SetupKeyBindings (); - SetContentSize (); - } - - return args.Cancel; + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; } + #region IOrientation + /// + public event EventHandler> OrientationChanging; + + /// + public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) + { + return false; + } + + /// + public event EventHandler> OrientationChanged; + + /// Called when has changed. + /// + /// + public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + SetupKeyBindings (); + SetContentSize (); + } + #endregion IOrientation + // TODO: This should be cancelable /// Called whenever the current selected item changes. Invokes the event. /// /// public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem) - { + { if (_selected == selectedItem) { return; @@ -355,12 +367,6 @@ public class RadioGroup : View, IDesignable SelectedItemChanged?.Invoke (this, new (selectedItem, previousSelectedItem)); } - /// - /// Fired when the view orientation has changed. Can be cancelled by setting - /// to true. - /// - public event EventHandler OrientationChanged; - /// public override Point? PositionCursor () { @@ -429,7 +435,7 @@ public class RadioGroup : View, IDesignable private void SetContentSize () { - switch (_orientation) + switch (Orientation) { case Orientation.Vertical: var width = 0; @@ -469,3 +475,27 @@ public class RadioGroup : View, IDesignable return true; } } + +public class RadioGroupHorizontal : RadioGroup, IOrientation +{ + private bool _preventOrientationChange = false; + public RadioGroupHorizontal () : base () + { + Orientation = Orientation.Horizontal; + _preventOrientationChange = true; + + OrientationChanging += RadioGroupHorizontal_OrientationChanging; + } + + private void RadioGroupHorizontal_OrientationChanging (object sender, CancelEventArgs e) + { + //e.Cancel = _preventOrientationChange; + } + + /// + bool IOrientation.OnOrientationChanging (Orientation currrentOrientation, Orientation newOrientation) + { + return _preventOrientationChange; + } + +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index 61ac401d6..a613ce7d5 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -310,17 +310,16 @@ public class Slider : View #region Events /// - /// Fired when the slider orientation has changed. Can be cancelled by setting - /// to true. + /// Fired when the slider orientation has changed. Can be cancelled. /// - public event EventHandler OrientationChanged; + public event EventHandler> OrientationChanged; /// Called when the slider orientation has changed. Invokes the event. /// /// True of the event was cancelled. public virtual bool OnOrientationChanged (Orientation newOrientation) { - var args = new OrientationEventArgs (newOrientation); + var args = new CancelEventArgs (in _config._sliderOrientation, ref newOrientation); OrientationChanged?.Invoke (this, args); if (!args.Cancel) diff --git a/UICatalog/Scenarios/ExpanderButton.cs b/UICatalog/Scenarios/ExpanderButton.cs index 2959d338a..f0d5c8073 100644 --- a/UICatalog/Scenarios/ExpanderButton.cs +++ b/UICatalog/Scenarios/ExpanderButton.cs @@ -74,7 +74,7 @@ public class ExpanderButton : Button /// True of the event was cancelled. protected virtual bool OnOrientationChanging (Orientation newOrientation) { - var args = new OrientationEventArgs (newOrientation); + var args = new CancelEventArgs (in _orientation, ref newOrientation); OrientationChanging?.Invoke (this, args); if (!args.Cancel) @@ -105,10 +105,9 @@ public class ExpanderButton : Button } /// - /// Fired when the orientation has changed. Can be cancelled by setting - /// to true. + /// Fired when the orientation has changed. Can be cancelled. /// - public event EventHandler OrientationChanging; + public event EventHandler> OrientationChanging; /// /// The glyph to display when the view is collapsed. From 74521d831d8edbd6ed8fde5594898276925b443b Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 12:06:51 -0600 Subject: [PATCH 06/49] Added Orientation unit tests. Code cleanup --- Terminal.Gui/View/Orientation/IOrientation.cs | 15 ++- Terminal.Gui/Views/RadioGroup.cs | 6 +- UnitTests/UnitTests.csproj | 10 +- .../Orientation/OrientationHelperTests.cs | 107 ++++++++++++++++++ .../View/Orientation/OrientationTests.cs | 90 +++++++++++++++ 5 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 UnitTests/View/Orientation/OrientationHelperTests.cs create mode 100644 UnitTests/View/Orientation/OrientationTests.cs diff --git a/Terminal.Gui/View/Orientation/IOrientation.cs b/Terminal.Gui/View/Orientation/IOrientation.cs index 908ce32bf..25bdf7fcc 100644 --- a/Terminal.Gui/View/Orientation/IOrientation.cs +++ b/Terminal.Gui/View/Orientation/IOrientation.cs @@ -20,8 +20,8 @@ public interface IOrientation /// /// Called when is changing. /// - /// The current orienation. - /// The new orienation. + /// The current orientation. + /// The new orientation. /// to cancel the change. public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) { return false; } @@ -65,6 +65,11 @@ public class OrientationHelper get => _orientation; set { + if (_orientation == value) + { + return; + } + var args = new CancelEventArgs (in _orientation, ref value); OrientationChanging?.Invoke (_owner, args); if (args.Cancel) @@ -81,7 +86,11 @@ public class OrientationHelper if (_orientation != value) { _orientation = value; - _owner.Orientation = value; + + if (_owner is { }) + { + _owner.Orientation = value; + } } args = new CancelEventArgs (in old, ref _orientation); diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 1565193b3..49e821772 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -380,8 +380,10 @@ public class RadioGroup : View, IDesignable, IOrientation break; case Orientation.Horizontal: - x = _horizontal [_cursor].pos; - + if (_horizontal.Count > 0) + { + x = _horizontal [_cursor].pos; + } break; default: diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 2a9fbc394..941e25c9b 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -30,9 +30,10 @@ - - - + + + + @@ -58,6 +59,9 @@ + + + False diff --git a/UnitTests/View/Orientation/OrientationHelperTests.cs b/UnitTests/View/Orientation/OrientationHelperTests.cs new file mode 100644 index 000000000..5294c1726 --- /dev/null +++ b/UnitTests/View/Orientation/OrientationHelperTests.cs @@ -0,0 +1,107 @@ +using Moq; + +namespace Terminal.Gui.ViewTests.OrientationTests; + +public class OrientationHelperTests +{ + [Fact] + public void Orientation_Set_NewValue_InvokesChangingAndChangedEvents () + { + // Arrange + Mock mockIOrientation = new Mock (); + var orientationHelper = new OrientationHelper (mockIOrientation.Object); + var changingEventInvoked = false; + var changedEventInvoked = false; + + orientationHelper.OrientationChanging += (sender, e) => { changingEventInvoked = true; }; + orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked = true; }; + + // Act + orientationHelper.Orientation = Orientation.Horizontal; + + // Assert + Assert.True (changingEventInvoked, "OrientationChanging event was not invoked."); + Assert.True (changedEventInvoked, "OrientationChanged event was not invoked."); + } + + [Fact] + public void Orientation_Set_NewValue_InvokesOnChangingAndOnChangedOverrides () + { + // Arrange + Mock mockIOrientation = new Mock (); + var onChangingOverrideCalled = false; + var onChangedOverrideCalled = false; + + mockIOrientation.Setup (x => x.OnOrientationChanging (It.IsAny (), It.IsAny ())) + .Callback (() => onChangingOverrideCalled = true) + .Returns (false); // Ensure it doesn't cancel the change + + mockIOrientation.Setup (x => x.OnOrientationChanged (It.IsAny (), It.IsAny ())) + .Callback (() => onChangedOverrideCalled = true); + + var orientationHelper = new OrientationHelper (mockIOrientation.Object); + + // Act + orientationHelper.Orientation = Orientation.Horizontal; + + // Assert + Assert.True (onChangingOverrideCalled, "OnOrientationChanging override was not called."); + Assert.True (onChangedOverrideCalled, "OnOrientationChanged override was not called."); + } + + [Fact] + public void Orientation_Set_SameValue_DoesNotInvokeChangingOrChangedEvents () + { + // Arrange + Mock mockIOrientation = new Mock (); + var orientationHelper = new OrientationHelper (mockIOrientation.Object); + orientationHelper.Orientation = Orientation.Vertical; // Set initial orientation + var changingEventInvoked = false; + var changedEventInvoked = false; + + orientationHelper.OrientationChanging += (sender, e) => { changingEventInvoked = true; }; + orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked = true; }; + + // Act + orientationHelper.Orientation = Orientation.Vertical; // Set to the same value + + // Assert + Assert.False (changingEventInvoked, "OrientationChanging event was invoked."); + Assert.False (changedEventInvoked, "OrientationChanged event was invoked."); + } + + [Fact] + public void Orientation_Set_NewValue_OrientationChanging_CancellationPreventsChange () + { + // Arrange + Mock mockIOrientation = new Mock (); + var orientationHelper = new OrientationHelper (mockIOrientation.Object); + orientationHelper.OrientationChanging += (sender, e) => { e.Cancel = true; }; // Cancel the change + + // Act + orientationHelper.Orientation = Orientation.Horizontal; + + // Assert + Assert.Equal (Orientation.Vertical, orientationHelper.Orientation); // Initial orientation is Vertical + } + + [Fact] + public void Orientation_Set_NewValue_OnOrientationChanging_CancelsChange () + { + // Arrange + Mock mockIOrientation = new Mock (); + + mockIOrientation.Setup (x => x.OnOrientationChanging (It.IsAny (), It.IsAny ())) + .Returns (true); // Override to return true, cancelling the change + + var orientationHelper = new OrientationHelper (mockIOrientation.Object); + + // Act + orientationHelper.Orientation = Orientation.Horizontal; + + // Assert + Assert.Equal ( + Orientation.Vertical, + orientationHelper.Orientation); // Initial orientation is Vertical, and it should remain unchanged due to cancellation + } +} diff --git a/UnitTests/View/Orientation/OrientationTests.cs b/UnitTests/View/Orientation/OrientationTests.cs new file mode 100644 index 000000000..3e5734be0 --- /dev/null +++ b/UnitTests/View/Orientation/OrientationTests.cs @@ -0,0 +1,90 @@ +namespace Terminal.Gui.ViewTests.OrientationTests; + +public class OrientationTests +{ + private class CustomView : View, IOrientation + { + private readonly OrientationHelper _orientationHelper; + + public CustomView () + { + _orientationHelper = new (this); + Orientation = Orientation.Vertical; + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); + } + + public Orientation Orientation + { + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; + } + + public event EventHandler> OrientationChanging; + public event EventHandler> OrientationChanged; + + public bool CancelOnOrientationChanging { get; set; } + + public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) + { + // Custom logic before orientation changes + return CancelOnOrientationChanging; // Return true to cancel the change + } + + public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + // Custom logic after orientation has changed + } + } + + [Fact] + public void Orientation_Change_IsSuccessful () + { + // Arrange + var customView = new CustomView (); + var orientationChanged = false; + customView.OrientationChanged += (sender, e) => orientationChanged = true; + + // Act + customView.Orientation = Orientation.Horizontal; + + // Assert + Assert.True (orientationChanged, "OrientationChanged event was not invoked."); + Assert.Equal (Orientation.Horizontal, customView.Orientation); + } + + [Fact] + public void Orientation_Change_OrientationChanging_Set_Cancel_IsCancelled () + { + // Arrange + var customView = new CustomView (); + customView.OrientationChanging += (sender, e) => e.Cancel = true; // Cancel the orientation change + var orientationChanged = false; + customView.OrientationChanged += (sender, e) => orientationChanged = true; + + // Act + customView.Orientation = Orientation.Horizontal; + + // Assert + Assert.False (orientationChanged, "OrientationChanged event was invoked despite cancellation."); + Assert.Equal (Orientation.Vertical, customView.Orientation); // Assuming Vertical is the default orientation + } + + [Fact] + public void Orientation_Change_OnOrientationChanging_Return_True_IsCancelled () + { + // Arrange + var customView = new CustomView (); + customView.CancelOnOrientationChanging = true; // Cancel the orientation change + + var orientationChanged = false; + customView.OrientationChanged += (sender, e) => orientationChanged = true; + + // Act + customView.Orientation = Orientation.Horizontal; + + // Assert + Assert.False (orientationChanged, "OrientationChanged event was invoked despite cancellation."); + Assert.Equal (Orientation.Vertical, customView.Orientation); // Assuming Vertical is the default orientation + } +} From 329cd2f06c89097f5c1c4356c3df69b1139943ad Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 12:10:43 -0600 Subject: [PATCH 07/49] One class per file --- .../View/Orientation/OrientationHelper.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 Terminal.Gui/View/Orientation/OrientationHelper.cs diff --git a/Terminal.Gui/View/Orientation/OrientationHelper.cs b/Terminal.Gui/View/Orientation/OrientationHelper.cs new file mode 100644 index 000000000..f2278ed98 --- /dev/null +++ b/Terminal.Gui/View/Orientation/OrientationHelper.cs @@ -0,0 +1,94 @@ +namespace Terminal.Gui; + +/// +/// Helper class for implementing . +/// +public class OrientationHelper +{ + private Orientation _orientation = Orientation.Vertical; + private readonly IOrientation _owner; + + /// + /// Initializes a new instance of the class. + /// + /// + public OrientationHelper (IOrientation owner) + { + _owner = owner; + } + + /// + /// Gets or sets the orientation of the View. + /// + public Orientation Orientation + { + get => _orientation; + set + { + if (_orientation == value) + { + return; + } + + var args = new CancelEventArgs (in _orientation, ref value); + OrientationChanging?.Invoke (_owner, args); + if (args.Cancel) + { + return; + } + + if (_owner?.OnOrientationChanging (value, _orientation) ?? false) + { + return; + } + + Orientation old = _orientation; + if (_orientation != value) + { + _orientation = value; + + if (_owner is { }) + { + _owner.Orientation = value; + } + } + + args = new CancelEventArgs (in old, ref _orientation); + OrientationChanged?.Invoke (_owner, args); + + _owner?.OnOrientationChanged (old, _orientation); + } + } + + /// + /// + /// + public event EventHandler> OrientationChanging; + + /// + /// + /// + /// + /// + /// + protected bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) + { + return _owner?.OnOrientationChanging (currentOrientation, newOrientation) ?? false; + } + + /// + /// + /// + public event EventHandler> OrientationChanged; + + /// + /// + /// + /// + /// + /// + protected void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + _owner?.OnOrientationChanged (oldOrientation, newOrientation); + } +} From 840e198e856a97d3558ec1bc80fff4444bd7f010 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 12:25:49 -0600 Subject: [PATCH 08/49] Beefed up unit tests --- Terminal.Gui/View/Orientation/IOrientation.cs | 98 +------------------ .../View/Orientation/OrientationHelper.cs | 87 ++++++++-------- Terminal.Gui/Views/RadioGroup.cs | 51 +++------- .../View/Orientation/OrientationTests.cs | 46 +++++++++ 4 files changed, 110 insertions(+), 172 deletions(-) diff --git a/Terminal.Gui/View/Orientation/IOrientation.cs b/Terminal.Gui/View/Orientation/IOrientation.cs index 25bdf7fcc..52bf2f49f 100644 --- a/Terminal.Gui/View/Orientation/IOrientation.cs +++ b/Terminal.Gui/View/Orientation/IOrientation.cs @@ -37,100 +37,4 @@ public interface IOrientation /// /// public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) { return; } -} - - -/// -/// Helper class for implementing . -/// -public class OrientationHelper -{ - private Orientation _orientation = Orientation.Vertical; - private readonly IOrientation _owner; - - /// - /// Initializes a new instance of the class. - /// - /// - public OrientationHelper (IOrientation owner) - { - _owner = owner; - } - - /// - /// Gets or sets the orientation of the View. - /// - public Orientation Orientation - { - get => _orientation; - set - { - if (_orientation == value) - { - return; - } - - var args = new CancelEventArgs (in _orientation, ref value); - OrientationChanging?.Invoke (_owner, args); - if (args.Cancel) - { - return; - } - - if (_owner?.OnOrientationChanging (value, _orientation) ?? false) - { - return; - } - - Orientation old = _orientation; - if (_orientation != value) - { - _orientation = value; - - if (_owner is { }) - { - _owner.Orientation = value; - } - } - - args = new CancelEventArgs (in old, ref _orientation); - OrientationChanged?.Invoke (_owner, args); - - _owner?.OnOrientationChanged (old, _orientation); - } - } - - /// - /// - /// - public event EventHandler> OrientationChanging; - - /// - /// - /// - /// - /// - /// - protected bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) - { - return _owner?.OnOrientationChanging (currentOrientation, newOrientation) ?? false; - } - - /// - /// - /// - public event EventHandler> OrientationChanged; - - /// - /// - /// - /// - /// - /// - protected void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) - { - _owner?.OnOrientationChanged (oldOrientation, newOrientation); - } -} - - +} \ No newline at end of file diff --git a/Terminal.Gui/View/Orientation/OrientationHelper.cs b/Terminal.Gui/View/Orientation/OrientationHelper.cs index f2278ed98..613557e10 100644 --- a/Terminal.Gui/View/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/View/Orientation/OrientationHelper.cs @@ -3,6 +3,14 @@ /// /// Helper class for implementing . /// +/// +/// +/// Implements the standard pattern for changing/changed events. +/// +/// +/// Views that implement should add a OrientationHelper property. See as an example. +/// +/// public class OrientationHelper { private Orientation _orientation = Orientation.Vertical; @@ -11,11 +19,8 @@ public class OrientationHelper /// /// Initializes a new instance of the class. /// - /// - public OrientationHelper (IOrientation owner) - { - _owner = owner; - } + /// Specifies the object that owns this helper instance. + public OrientationHelper (IOrientation owner) { _owner = owner; } /// /// Gets or sets the orientation of the View. @@ -30,19 +35,25 @@ public class OrientationHelper return; } - var args = new CancelEventArgs (in _orientation, ref value); - OrientationChanging?.Invoke (_owner, args); - if (args.Cancel) - { - return; - } - + // Best practice is to invoke the virtual method first. + // This allows derived classes to handle the event and potentially cancel it. if (_owner?.OnOrientationChanging (value, _orientation) ?? false) { return; } + // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. + CancelEventArgs args = new (in _orientation, ref value); + OrientationChanging?.Invoke (_owner, args); + + if (args.Cancel) + { + return; + } + + // If the event is not canceled, update the value. Orientation old = _orientation; + if (_orientation != value) { _orientation = value; @@ -53,42 +64,40 @@ public class OrientationHelper } } - args = new CancelEventArgs (in old, ref _orientation); - OrientationChanged?.Invoke (_owner, args); - + // Best practice is to invoke the virtual method first. _owner?.OnOrientationChanged (old, _orientation); + + // Even though Changed is not cancelable, it is still a good practice to raise the event after. + args = new (in old, ref _orientation); + OrientationChanged?.Invoke (_owner, args); } } /// - /// + /// Raised when the orientation is changing. This is cancelable. /// + /// + /// + /// Views that implement should raise after the orientation has changed + /// (_orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);). + /// + /// + /// This event will be raised after the method is called (assuming it was not canceled). + /// + /// public event EventHandler> OrientationChanging; /// - /// - /// - /// - /// - /// - protected bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) - { - return _owner?.OnOrientationChanging (currentOrientation, newOrientation) ?? false; - } - - /// - /// + /// Raised when the orientation has changed. /// + /// + /// + /// Views that implement should raise after the orientation has changed + /// (_orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);). + /// + /// + /// This event will be raised after the method is called. + /// + /// public event EventHandler> OrientationChanged; - - /// - /// - /// - /// - /// - /// - protected void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) - { - _owner?.OnOrientationChanged (oldOrientation, newOrientation); - } } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 49e821772..1113cf9de 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -44,6 +44,7 @@ public class RadioGroup : View, IDesignable, IOrientation { return false; } + MoveDownRight (); return true; @@ -58,6 +59,7 @@ public class RadioGroup : View, IDesignable, IOrientation { return false; } + MoveHome (); return true; @@ -72,6 +74,7 @@ public class RadioGroup : View, IDesignable, IOrientation { return false; } + MoveEnd (); return true; @@ -93,6 +96,7 @@ public class RadioGroup : View, IDesignable, IOrientation ctx => { SetFocus (); + if (ctx.KeyBinding?.Context is { } && (int)ctx.KeyBinding?.Context! < _radioLabels.Count) { SelectedItem = (int)ctx.KeyBinding?.Context!; @@ -103,13 +107,10 @@ public class RadioGroup : View, IDesignable, IOrientation return true; }); - _orientationHelper = new OrientationHelper (this); + _orientationHelper = new (this); _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); - //OrientationChanging += (sender, e) => OnOrientationChanging (e.CurrentValue, e.NewValue); - //OrientationChanged += (sender, e) => OnOrientationChanged (e.CurrentValue, e.NewValue); - SetupKeyBindings (); LayoutStarted += RadioGroup_LayoutStarted; @@ -331,16 +332,14 @@ public class RadioGroup : View, IDesignable, IOrientation } #region IOrientation - /// + + /// public event EventHandler> OrientationChanging; - /// - public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) - { - return false; - } + /// + public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) { return false; } - /// + /// public event EventHandler> OrientationChanged; /// Called when has changed. @@ -351,6 +350,7 @@ public class RadioGroup : View, IDesignable, IOrientation SetupKeyBindings (); SetContentSize (); } + #endregion IOrientation // TODO: This should be cancelable @@ -363,6 +363,7 @@ public class RadioGroup : View, IDesignable, IOrientation { return; } + _selected = selectedItem; SelectedItemChanged?.Invoke (this, new (selectedItem, previousSelectedItem)); } @@ -384,6 +385,7 @@ public class RadioGroup : View, IDesignable, IOrientation { x = _horizontal [_cursor].pos; } + break; default: @@ -470,34 +472,11 @@ public class RadioGroup : View, IDesignable, IOrientation } } - /// + /// public bool EnableForDesign () { RadioLabels = new [] { "Option _1", "Option _2", "Option _3" }; + return true; } } - -public class RadioGroupHorizontal : RadioGroup, IOrientation -{ - private bool _preventOrientationChange = false; - public RadioGroupHorizontal () : base () - { - Orientation = Orientation.Horizontal; - _preventOrientationChange = true; - - OrientationChanging += RadioGroupHorizontal_OrientationChanging; - } - - private void RadioGroupHorizontal_OrientationChanging (object sender, CancelEventArgs e) - { - //e.Cancel = _preventOrientationChange; - } - - /// - bool IOrientation.OnOrientationChanging (Orientation currrentOrientation, Orientation newOrientation) - { - return _preventOrientationChange; - } - -} \ No newline at end of file diff --git a/UnitTests/View/Orientation/OrientationTests.cs b/UnitTests/View/Orientation/OrientationTests.cs index 3e5734be0..01eeda71f 100644 --- a/UnitTests/View/Orientation/OrientationTests.cs +++ b/UnitTests/View/Orientation/OrientationTests.cs @@ -25,14 +25,19 @@ public class OrientationTests public bool CancelOnOrientationChanging { get; set; } + public bool OnOrientationChangingCalled { get; private set; } + public bool OnOrientationChangedCalled { get; private set; } + public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) { + OnOrientationChangingCalled = true; // Custom logic before orientation changes return CancelOnOrientationChanging; // Return true to cancel the change } public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) { + OnOrientationChangedCalled = true; // Custom logic after orientation has changed } } @@ -87,4 +92,45 @@ public class OrientationTests Assert.False (orientationChanged, "OrientationChanged event was invoked despite cancellation."); Assert.Equal (Orientation.Vertical, customView.Orientation); // Assuming Vertical is the default orientation } + + + [Fact] + public void OrientationChanging_VirtualMethodCalledBeforeEvent () + { + // Arrange + var radioGroup = new CustomView (); + bool eventCalled = false; + + radioGroup.OrientationChanging += (sender, e) => + { + eventCalled = true; + Assert.True (radioGroup.OnOrientationChangingCalled, "OnOrientationChanging was not called before the event."); + }; + + // Act + radioGroup.Orientation = Orientation.Horizontal; + + // Assert + Assert.True (eventCalled, "OrientationChanging event was not called."); + } + + [Fact] + public void OrientationChanged_VirtualMethodCalledBeforeEvent () + { + // Arrange + var radioGroup = new CustomView (); + bool eventCalled = false; + + radioGroup.OrientationChanged += (sender, e) => + { + eventCalled = true; + Assert.True (radioGroup.OnOrientationChangedCalled, "OnOrientationChanged was not called before the event."); + }; + + // Act + radioGroup.Orientation = Orientation.Horizontal; + + // Assert + Assert.True (eventCalled, "OrientationChanged event was not called."); + } } From b04846b7eccbda530b8636e18127b215067655a3 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 12:33:02 -0600 Subject: [PATCH 09/49] Adde example --- .../View/Orientation/OrientationHelper.cs | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/View/Orientation/OrientationHelper.cs b/Terminal.Gui/View/Orientation/OrientationHelper.cs index 613557e10..0a513487a 100644 --- a/Terminal.Gui/View/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/View/Orientation/OrientationHelper.cs @@ -7,10 +7,43 @@ /// /// Implements the standard pattern for changing/changed events. /// -/// -/// Views that implement should add a OrientationHelper property. See as an example. -/// /// +/// +/// +/// private class OrientedView : View, IOrientation +/// { +/// private readonly OrientationHelper _orientationHelper; +/// +/// public OrientedView () +/// { +/// _orientationHelper = new (this); +/// Orientation = Orientation.Vertical; +/// _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); +/// _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); +/// } +/// +/// public Orientation Orientation +/// { +/// get => _orientationHelper.Orientation; +/// set => _orientationHelper.Orientation = value; +/// } +/// +/// public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging; +/// public event EventHandler<CancelEventArgs<Orientation>> OrientationChanged; +/// +/// public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) +/// { +/// // Custom logic before orientation changes +/// return false; // Return true to cancel the change +/// } +/// +/// public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) +/// { +/// // Custom logic after orientation has changed +/// } +/// } +/// +/// public class OrientationHelper { private Orientation _orientation = Orientation.Vertical; From 6d464a0b045acba9c794d74f9e35561c4c6e8ba9 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 12:33:32 -0600 Subject: [PATCH 10/49] API docs --- Terminal.Gui/View/Orientation/IOrientation.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Terminal.Gui/View/Orientation/IOrientation.cs b/Terminal.Gui/View/Orientation/IOrientation.cs index 52bf2f49f..80551f119 100644 --- a/Terminal.Gui/View/Orientation/IOrientation.cs +++ b/Terminal.Gui/View/Orientation/IOrientation.cs @@ -5,6 +5,9 @@ using System; /// /// Implement this interface to provide orientation support. /// +/// +/// See for a helper class that implements this interface. +/// public interface IOrientation { /// From 3b89159dfec306970c87c9c682a7c7beeefe8e9a Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 12:34:38 -0600 Subject: [PATCH 11/49] API docs --- .../View/Orientation/OrientationHelper.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/View/Orientation/OrientationHelper.cs b/Terminal.Gui/View/Orientation/OrientationHelper.cs index 0a513487a..3ff93d53e 100644 --- a/Terminal.Gui/View/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/View/Orientation/OrientationHelper.cs @@ -5,11 +5,11 @@ /// /// /// -/// Implements the standard pattern for changing/changed events. +/// Implements the standard pattern for changing/changed events. /// /// /// -/// +/// /// private class OrientedView : View, IOrientation /// { /// private readonly OrientationHelper _orientationHelper; @@ -52,7 +52,7 @@ public class OrientationHelper /// /// Initializes a new instance of the class. /// - /// Specifies the object that owns this helper instance. + /// Specifies the object that owns this helper instance and implements . public OrientationHelper (IOrientation owner) { _owner = owner; } /// @@ -111,11 +111,13 @@ public class OrientationHelper /// /// /// - /// Views that implement should raise after the orientation has changed + /// Views that implement should raise + /// after the orientation has changed /// (_orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);). /// /// - /// This event will be raised after the method is called (assuming it was not canceled). + /// This event will be raised after the method is called (assuming + /// it was not canceled). /// /// public event EventHandler> OrientationChanging; @@ -125,7 +127,8 @@ public class OrientationHelper /// /// /// - /// Views that implement should raise after the orientation has changed + /// Views that implement should raise + /// after the orientation has changed /// (_orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);). /// /// From 112d04390978d4bc94ef99cd58b9cd1fccf72b16 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 12:48:55 -0600 Subject: [PATCH 12/49] Upgraded Slider --- Terminal.Gui/Views/Slider.cs | 87 ++++++++++++++------------- UICatalog/Scenarios/AllViewsTester.cs | 10 ++- 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index a613ce7d5..ee0f2f4d0 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -21,7 +21,7 @@ public class Slider : Slider /// keyboard or mouse. /// /// -public class Slider : View +public class Slider : View, IOrientation { private readonly SliderConfiguration _config = new (); @@ -31,6 +31,8 @@ public class Slider : View // Options private List> _options; + private OrientationHelper _orientationHelper; + #region Initialize private void SetInitialProperties ( @@ -45,11 +47,13 @@ public class Slider : View _options = options ?? new List> (); - _config._sliderOrientation = orientation; + _orientationHelper = new (this); + _orientationHelper.Orientation = _config._sliderOrientation = orientation; + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); SetDefaultStyle (); SetCommands (); - SetContentSize (); // BUGBUG: This should not be needed - Need to ensure SetRelativeLayout gets called during EndInit @@ -222,13 +226,46 @@ public class Slider : View } } - /// Slider Orientation. + + /// + /// Gets or sets the . The default is . + /// public Orientation Orientation { - get => _config._sliderOrientation; - set => OnOrientationChanged (value); + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; } + #region IOrientation members + + /// + public event EventHandler> OrientationChanging; + + /// + public event EventHandler> OrientationChanged; + + /// + public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + _config._sliderOrientation = newOrientation; + + switch (_config._sliderOrientation) + { + case Orientation.Horizontal: + Style.SpaceChar = new () { Rune = Glyphs.HLine }; // '─' + + break; + case Orientation.Vertical: + Style.SpaceChar = new () { Rune = Glyphs.VLine }; + + break; + } + + SetKeyBindings (); + SetContentSize (); + } + #endregion + /// Legends Orientation. public Orientation LegendsOrientation { @@ -309,42 +346,6 @@ public class Slider : View #region Events - /// - /// Fired when the slider orientation has changed. Can be cancelled. - /// - public event EventHandler> OrientationChanged; - - /// Called when the slider orientation has changed. Invokes the event. - /// - /// True of the event was cancelled. - public virtual bool OnOrientationChanged (Orientation newOrientation) - { - var args = new CancelEventArgs (in _config._sliderOrientation, ref newOrientation); - OrientationChanged?.Invoke (this, args); - - if (!args.Cancel) - { - _config._sliderOrientation = newOrientation; - - switch (_config._sliderOrientation) - { - case Orientation.Horizontal: - Style.SpaceChar = new () { Rune = Glyphs.HLine }; // '─' - - break; - case Orientation.Vertical: - Style.SpaceChar = new () { Rune = Glyphs.VLine }; - - break; - } - - SetKeyBindings (); - SetContentSize (); - } - - return args.Cancel; - } - /// Event raised when the slider option/s changed. The dictionary contains: key = option index, value = T public event EventHandler> OptionsChanged; @@ -1737,7 +1738,7 @@ public class Slider : View internal bool Select () { - SetFocusedOption(); + SetFocusedOption (); return true; } diff --git a/UICatalog/Scenarios/AllViewsTester.cs b/UICatalog/Scenarios/AllViewsTester.cs index cc440b820..ed5d74bf6 100644 --- a/UICatalog/Scenarios/AllViewsTester.cs +++ b/UICatalog/Scenarios/AllViewsTester.cs @@ -272,9 +272,9 @@ public class AllViewsTester : Scenario _orientation.SelectedItemChanged += (s, selected) => { - if (_curView?.GetType ().GetProperty ("Orientation") is { } prop) + if (_curView is IOrientation orientatedView) { - prop.GetSetMethod ()?.Invoke (_curView, new object [] { _orientation.SelectedItem }); + orientatedView.Orientation = (Orientation)_orientation.SelectedItem; } }; _settingsPane.Add (label, _orientation); @@ -358,11 +358,9 @@ public class AllViewsTester : Scenario view.Title = "_Test Title"; } - // TODO: Add IOrientation so this doesn't require reflection - // If the view supports a Title property, set it so we have something to look at - if (view?.GetType ().GetProperty ("Orientation") is { } prop) + if (view is IOrientation orientatedView) { - _orientation.SelectedItem = (int)prop.GetGetMethod ()!.Invoke (view, null)!; + _orientation.SelectedItem = (int)orientatedView.Orientation; _orientation.Enabled = true; } else From e8b32b050a98245e90d761f9aaf9db407e836f6a Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 13:10:45 -0600 Subject: [PATCH 13/49] Upgraed Bar --- Terminal.Gui/Views/Bar.cs | 58 +++++++++++++++++++++---- Terminal.Gui/Views/RadioGroup.cs | 3 -- Terminal.Gui/Views/StatusBar.cs | 72 +++++++++++++++++++++++++++++++- UICatalog/Scenarios/Bars.cs | 2 +- 4 files changed, 122 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index 07447fba8..16d533cd9 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -11,8 +11,10 @@ namespace Terminal.Gui; /// align them in a specific order. /// /// -public class Bar : View +public class Bar : View, IOrientation, IDesignable { + private readonly OrientationHelper _orientationHelper; + /// public Bar () : this ([]) { } @@ -24,6 +26,10 @@ public class Bar : View Width = Dim.Auto (); Height = Dim.Auto (); + _orientationHelper = new (this); + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); + Initialized += Bar_Initialized; if (shortcuts is null) @@ -46,7 +52,7 @@ public class Bar : View Border.LineStyle = value; } - private Orientation _orientation = Orientation.Horizontal; + #region IOrientation members /// /// Gets or sets the for this . The default is @@ -58,16 +64,28 @@ public class Bar : View /// Vertical orientation arranges the command, help, and key parts of each s from left to right. /// /// + public Orientation Orientation { - get => _orientation; - set - { - _orientation = value; - SetNeedsLayout (); - } + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; } + /// + public event EventHandler> OrientationChanging; + + /// + public event EventHandler> OrientationChanged; + + /// Called when has changed. + /// + /// + public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + SetNeedsLayout (); + } + #endregion + private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd; /// @@ -226,4 +244,28 @@ public class Bar : View break; } } + + /// + public bool EnableForDesign () + { + var shortcut = new Shortcut + { + Text = "Quit", + Title = "Q_uit", + Key = Key.Z.WithCtrl, + }; + + Add (shortcut); + + shortcut = new Shortcut + { + Text = "Help Text", + Title = "Help", + Key = Key.F1, + }; + + Add (shortcut); + + return true; + } } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 1113cf9de..783aa81bc 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -336,9 +336,6 @@ public class RadioGroup : View, IDesignable, IOrientation /// public event EventHandler> OrientationChanging; - /// - public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) { return false; } - /// public event EventHandler> OrientationChanged; diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index b4df14e6b..4136335cf 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -10,7 +10,7 @@ namespace Terminal.Gui; /// to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. So for each context must be a /// new instance of a status bar. /// -public class StatusBar : Bar +public class StatusBar : Bar, IDesignable { /// public StatusBar () : this ([]) { } @@ -74,4 +74,74 @@ public class StatusBar : Bar return view; } + + /// + bool IDesignable.EnableForDesign () + { + var shortcut = new Shortcut + { + Text = "Quit", + Title = "Q_uit", + Key = Key.Z.WithCtrl, + }; + + Add (shortcut); + + shortcut = new Shortcut + { + Text = "Help Text", + Title = "Help", + Key = Key.F1, + }; + + Add (shortcut); + + shortcut = new Shortcut + { + Title = "_Show/Hide", + Key = Key.F10, + CommandView = new CheckBox + { + CanFocus = false, + Text = "_Show/Hide" + }, + }; + + Add (shortcut); + + var button1 = new Button + { + Text = "I'll Hide", + // Visible = false + }; + button1.Accept += Button_Clicked; + Add (button1); + + shortcut.Accept += (s, e) => + { + button1.Visible = !button1.Visible; + button1.Enabled = button1.Visible; + e.Handled = false; + }; + + Add (new Label + { + HotKeySpecifier = new Rune ('_'), + Text = "Fo_cusLabel", + CanFocus = true + }); + + var button2 = new Button + { + Text = "Or me!", + }; + button2.Accept += (s, e) => Application.RequestStop (); + + Add (button2); + + return true; + + void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ("Hi", $"You clicked {sender}"); } + } + } diff --git a/UICatalog/Scenarios/Bars.cs b/UICatalog/Scenarios/Bars.cs index c2027d8ec..a9ac5fe6e 100644 --- a/UICatalog/Scenarios/Bars.cs +++ b/UICatalog/Scenarios/Bars.cs @@ -408,7 +408,7 @@ public class Bars : Scenario bar.Add (shortcut1, shortcut2, line, shortcut3); } - private void ConfigStatusBar (Bar bar) + public void ConfigStatusBar (Bar bar) { var shortcut = new Shortcut { From aec901f52843342ac8187725ca855af9d9c693bb Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 13:14:06 -0600 Subject: [PATCH 14/49] Upgraded Line --- Terminal.Gui/Views/Line.cs | 48 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs index 6a25fa55b..a42f58ebb 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -1,43 +1,61 @@ namespace Terminal.Gui; /// Draws a single line using the specified by . -public class Line : View +public class Line : View, IOrientation { + private readonly OrientationHelper _orientationHelper; + /// Constructs a Line object. public Line () { BorderStyle = LineStyle.Single; Border.Thickness = new Thickness (0); SuperViewRendersLineCanvas = true; + + _orientationHelper = new (this); + _orientationHelper.Orientation = Orientation.Horizontal; + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); } - private Orientation _orientation; + #region IOrientation members /// /// The direction of the line. If you change this you will need to manually update the Width/Height of the /// control to cover a relevant area based on the new direction. /// public Orientation Orientation { - get => _orientation; - set + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; + } + + /// + public event EventHandler> OrientationChanging; + + /// + public event EventHandler> OrientationChanged; + + /// Called when has changed. + /// + /// + public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + + switch (Orientation) { - _orientation = value; + case Orientation.Horizontal: + Height = 1; - switch (Orientation) - { - case Orientation.Horizontal: - Height = 1; + break; + case Orientation.Vertical: + Width = 1; - break; - case Orientation.Vertical: - Width = 1; + break; - break; - - } } } + #endregion /// public override void SetBorderStyle (LineStyle value) From 16b374bae470e82a07f95cdd6e71da12b7c3a8f5 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 13:19:45 -0600 Subject: [PATCH 15/49] Upgraded Shortcut --- Terminal.Gui/Views/Shortcut.cs | 719 +++++++++++++++++---------------- 1 file changed, 375 insertions(+), 344 deletions(-) diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 7ddbe7c5a..245308564 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -1,10 +1,8 @@ -using System.ComponentModel; -using System.Threading.Channels; - -namespace Terminal.Gui; +namespace Terminal.Gui; /// -/// Displays a command, help text, and a key binding. When the key specified by is pressed, the command will be invoked. Useful for +/// Displays a command, help text, and a key binding. When the key specified by is pressed, the +/// command will be invoked. Useful for /// displaying a command in such as a /// menu, toolbar, or status bar. /// @@ -12,12 +10,13 @@ namespace Terminal.Gui; /// /// The following user actions will invoke the , causing the /// event to be fired: -/// - Clicking on the . -/// - Pressing the key specified by . -/// - Pressing the HotKey specified by . +/// - Clicking on the . +/// - Pressing the key specified by . +/// - Pressing the HotKey specified by . /// /// -/// If is , will invoked +/// If is , will invoked +/// /// command regardless of what View has focus, enabling an application-wide keyboard shortcut. /// /// @@ -37,8 +36,10 @@ namespace Terminal.Gui; /// If the is , the text is not displayed. /// /// -public class Shortcut : View +public class Shortcut : View, IOrientation, IDesignable { + private readonly OrientationHelper _orientationHelper; + /// /// Creates a new instance of . /// @@ -60,6 +61,10 @@ public class Shortcut : View Width = GetWidthDimAuto (); Height = Dim.Auto (DimAutoStyle.Content, 1); + _orientationHelper = new (this); + _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); + _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); + AddCommand (Command.HotKey, ctx => OnAccept (ctx)); AddCommand (Command.Accept, ctx => OnAccept (ctx)); AddCommand (Command.Select, ctx => OnSelect (ctx)); @@ -132,31 +137,49 @@ public class Shortcut : View } } - /// /// Creates a new instance of . /// public Shortcut () : this (Key.Empty, string.Empty, null) { } - private Orientation _orientation = Orientation.Horizontal; + #region IOrientation members /// - /// Gets or sets the for this . The default is - /// , which is ideal for status bar, menu bar, and tool bar items If set to - /// , - /// the Shortcut will be configured for vertical layout, which is ideal for menu items. + /// Gets or sets the for this . The default is + /// . /// + /// + /// + /// Horizontal orientation arranges the command, help, and key parts of each s from right to + /// left + /// Vertical orientation arranges the command, help, and key parts of each s from left to + /// right. + /// + /// + public Orientation Orientation { - get => _orientation; - set - { - _orientation = value; - - // TODO: Determine what, if anything, is opinionated about the orientation. - } + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; } + /// + public event EventHandler> OrientationChanging; + + /// + public event EventHandler> OrientationChanged; + + /// Called when has changed. + /// + /// + public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + { + // TODO: Determine what, if anything, is opinionated about the orientation. + SetNeedsLayout (); + } + + #endregion + private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast; /// @@ -344,7 +367,6 @@ public class Shortcut : View private void Subview_MouseClick (object sender, MouseEventEventArgs e) { // TODO: Remove. This does nothing. - return; } #region Command @@ -434,359 +456,368 @@ public class Shortcut : View SetKeyViewDefaultLayout (); ShowHide (); UpdateKeyBinding (); - - return; } } private void SetCommandViewDefaultLayout () -{ - CommandView.Margin.Thickness = GetMarginThickness (); - CommandView.X = Pos.Align (Alignment.End, AlignmentModes); - CommandView.Y = 0; //Pos.Center (); -} - -private void Shortcut_TitleChanged (object sender, EventArgs e) -{ - // If the Title changes, update the CommandView text. - // This is a helper to make it easier to set the CommandView text. - // CommandView is public and replaceable, but this is a convenience. - _commandView.Text = Title; -} - -#endregion Command - -#region Help - -/// -/// The subview that displays the help text for the command. Internal for unit testing. -/// -internal View HelpView { get; } = new (); - -private void SetHelpViewDefaultLayout () -{ - HelpView.Margin.Thickness = GetMarginThickness (); - HelpView.X = Pos.Align (Alignment.End, AlignmentModes); - HelpView.Y = 0; //Pos.Center (); - HelpView.Width = Dim.Auto (DimAutoStyle.Text); - HelpView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; - - HelpView.Visible = true; - HelpView.VerticalTextAlignment = Alignment.Center; -} - -/// -/// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to -/// . -/// -public override string Text -{ - get => HelpView?.Text; - set { - if (HelpView != null) + CommandView.Margin.Thickness = GetMarginThickness (); + CommandView.X = Pos.Align (Alignment.End, AlignmentModes); + CommandView.Y = 0; //Pos.Center (); + } + + private void Shortcut_TitleChanged (object sender, EventArgs e) + { + // If the Title changes, update the CommandView text. + // This is a helper to make it easier to set the CommandView text. + // CommandView is public and replaceable, but this is a convenience. + _commandView.Text = Title; + } + + #endregion Command + + #region Help + + /// + /// The subview that displays the help text for the command. Internal for unit testing. + /// + internal View HelpView { get; } = new (); + + private void SetHelpViewDefaultLayout () + { + HelpView.Margin.Thickness = GetMarginThickness (); + HelpView.X = Pos.Align (Alignment.End, AlignmentModes); + HelpView.Y = 0; //Pos.Center (); + HelpView.Width = Dim.Auto (DimAutoStyle.Text); + HelpView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; + + HelpView.Visible = true; + HelpView.VerticalTextAlignment = Alignment.Center; + } + + /// + /// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to + /// . + /// + public override string Text + { + get => HelpView?.Text; + set { - HelpView.Text = value; - ShowHide (); - } - } -} - -/// -/// Gets or sets the help text displayed in the middle of the Shortcut. -/// -public string HelpText -{ - get => HelpView?.Text; - set - { - if (HelpView != null) - { - HelpView.Text = value; - ShowHide (); - } - } -} - -#endregion Help - -#region Key - -private Key _key = Key.Empty; - -/// -/// Gets or sets the that will be bound to the command. -/// -public Key Key -{ - get => _key; - set - { - if (value == null) - { - throw new ArgumentNullException (); - } - - _key = value; - - UpdateKeyBinding (); - - KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}"; - ShowHide (); - } -} - -private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey; - -/// -/// Gets or sets the scope for the key binding for how is bound to . -/// -public KeyBindingScope KeyBindingScope -{ - get => _keyBindingScope; - set - { - _keyBindingScope = value; - - UpdateKeyBinding (); - } -} - -/// -/// Gets the subview that displays the key. Internal for unit testing. -/// - -internal View KeyView { get; } = new (); - -private int _minimumKeyTextSize; - -/// -/// Gets or sets the minimum size of the key text. Useful for aligning the key text with other s. -/// -public int MinimumKeyTextSize -{ - get => _minimumKeyTextSize; - set - { - if (value == _minimumKeyTextSize) - { - //return; - } - - _minimumKeyTextSize = value; - SetKeyViewDefaultLayout (); - CommandView.SetNeedsLayout (); - HelpView.SetNeedsLayout (); - KeyView.SetNeedsLayout (); - SetSubViewNeedsDisplay (); - } -} - -private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; } - -private void SetKeyViewDefaultLayout () -{ - KeyView.Margin.Thickness = GetMarginThickness (); - KeyView.X = Pos.Align (Alignment.End, AlignmentModes); - KeyView.Y = 0; //Pos.Center (); - KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize)); - KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; - - KeyView.Visible = true; - - // Right align the text in the keyview - KeyView.TextAlignment = Alignment.End; - KeyView.VerticalTextAlignment = Alignment.Center; - KeyView.KeyBindings.Clear (); -} - -private void UpdateKeyBinding () -{ - if (Key != null) - { - // Disable the command view key bindings - CommandView.KeyBindings.Remove (Key); - CommandView.KeyBindings.Remove (CommandView.HotKey); - KeyBindings.Remove (Key); - KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.Accept); - //KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.Accept); - } -} - -#endregion Key - -#region Accept Handling - -/// -/// Called when the command is received. This -/// occurs -/// - if the user clicks anywhere on the shortcut with the mouse -/// - if the user presses Key -/// - if the user presses the HotKey specified by CommandView -/// - if HasFocus and the user presses Space or Enter (or any other key bound to Command.Accept). -/// -protected bool? OnAccept (CommandContext ctx) -{ - var cancel = false; - - switch (ctx.KeyBinding?.Scope) - { - case KeyBindingScope.Application: - cancel = base.OnAccept () == true; - - break; - - case KeyBindingScope.Focused: - base.OnAccept (); - - // cancel if we're focused - cancel = true; - - break; - - case KeyBindingScope.HotKey: - cancel = base.OnAccept () == true; - - if (CanFocus) + if (HelpView != null) { - SetFocus (); - cancel = true; + HelpView.Text = value; + ShowHide (); + } + } + } + + /// + /// Gets or sets the help text displayed in the middle of the Shortcut. + /// + public string HelpText + { + get => HelpView?.Text; + set + { + if (HelpView != null) + { + HelpView.Text = value; + ShowHide (); + } + } + } + + #endregion Help + + #region Key + + private Key _key = Key.Empty; + + /// + /// Gets or sets the that will be bound to the command. + /// + public Key Key + { + get => _key; + set + { + if (value == null) + { + throw new ArgumentNullException (); } - break; + _key = value; - default: - // Mouse - cancel = base.OnAccept () == true; + UpdateKeyBinding (); - break; + KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}"; + ShowHide (); + } } - CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding); + private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey; - if (Action is { }) + /// + /// Gets or sets the scope for the key binding for how is bound to . + /// + public KeyBindingScope KeyBindingScope { - Action.Invoke (); - // Assume if there's a subscriber to Action, it's handled. - cancel = true; + get => _keyBindingScope; + set + { + _keyBindingScope = value; + + UpdateKeyBinding (); + } } - return cancel; -} + /// + /// Gets the subview that displays the key. Internal for unit testing. + /// -/// -/// 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. -/// -[CanBeNull] -public Action Action { get; set; } + internal View KeyView { get; } = new (); -#endregion Accept Handling + private int _minimumKeyTextSize; -private bool? OnSelect (CommandContext ctx) -{ - if (CommandView.GetSupportedCommands ().Contains (Command.Select)) + /// + /// Gets or sets the minimum size of the key text. Useful for aligning the key text with other s. + /// + public int MinimumKeyTextSize { - return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding); + get => _minimumKeyTextSize; + set + { + if (value == _minimumKeyTextSize) + { + //return; + } + + _minimumKeyTextSize = value; + SetKeyViewDefaultLayout (); + CommandView.SetNeedsLayout (); + HelpView.SetNeedsLayout (); + KeyView.SetNeedsLayout (); + SetSubViewNeedsDisplay (); + } } - return false; -} + private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; } - -#region Focus - -/// -public override ColorScheme ColorScheme -{ - get => base.ColorScheme; - set + private void SetKeyViewDefaultLayout () + { + KeyView.Margin.Thickness = GetMarginThickness (); + KeyView.X = Pos.Align (Alignment.End, AlignmentModes); + KeyView.Y = 0; //Pos.Center (); + KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize)); + KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1; + + KeyView.Visible = true; + + // Right align the text in the keyview + KeyView.TextAlignment = Alignment.End; + KeyView.VerticalTextAlignment = Alignment.Center; + KeyView.KeyBindings.Clear (); + } + + private void UpdateKeyBinding () + { + if (Key != null) + { + // Disable the command view key bindings + CommandView.KeyBindings.Remove (Key); + CommandView.KeyBindings.Remove (CommandView.HotKey); + KeyBindings.Remove (Key); + KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.Accept); + + //KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.Accept); + } + } + + #endregion Key + + #region Accept Handling + + /// + /// Called when the command is received. This + /// occurs + /// - if the user clicks anywhere on the shortcut with the mouse + /// - if the user presses Key + /// - if the user presses the HotKey specified by CommandView + /// - if HasFocus and the user presses Space or Enter (or any other key bound to Command.Accept). + /// + protected bool? OnAccept (CommandContext ctx) + { + var cancel = false; + + switch (ctx.KeyBinding?.Scope) + { + case KeyBindingScope.Application: + cancel = base.OnAccept () == true; + + break; + + case KeyBindingScope.Focused: + base.OnAccept (); + + // cancel if we're focused + cancel = true; + + break; + + case KeyBindingScope.HotKey: + cancel = base.OnAccept () == true; + + if (CanFocus) + { + SetFocus (); + cancel = true; + } + + break; + + default: + // Mouse + cancel = base.OnAccept () == true; + + break; + } + + CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding); + + if (Action is { }) + { + Action.Invoke (); + + // Assume if there's a subscriber to Action, it's handled. + cancel = true; + } + + return cancel; + } + + /// + /// 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. + /// + [CanBeNull] + public Action Action { get; set; } + + #endregion Accept Handling + + private bool? OnSelect (CommandContext ctx) + { + if (CommandView.GetSupportedCommands ().Contains (Command.Select)) + { + return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding); + } + + return false; + } + + #region Focus + + /// + public override ColorScheme ColorScheme + { + get => base.ColorScheme; + set + { + base.ColorScheme = value; + SetColors (); + } + } + + /// + /// + internal void SetColors () + { + // Border should match superview. + Border.ColorScheme = SuperView?.ColorScheme; + + if (HasFocus) + { + // When we have focus, we invert the colors + base.ColorScheme = new (base.ColorScheme) + { + Normal = base.ColorScheme.Focus, + HotNormal = base.ColorScheme.HotFocus, + HotFocus = base.ColorScheme.HotNormal, + Focus = base.ColorScheme.Normal + }; + } + else + { + base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme; + } + + // Set KeyView's colors to show "hot" + if (IsInitialized && base.ColorScheme is { }) + { + var cs = new ColorScheme (base.ColorScheme) + { + Normal = base.ColorScheme.HotNormal, + HotNormal = base.ColorScheme.Normal + }; + KeyView.ColorScheme = cs; + } + } + + private View _lastFocusedView; + + /// + public override bool OnEnter (View view) { - base.ColorScheme = value; SetColors (); + _lastFocusedView = view; + + return base.OnEnter (view); } -} -/// -/// -internal void SetColors () -{ - // Border should match superview. - Border.ColorScheme = SuperView?.ColorScheme; - - if (HasFocus) + /// + public override bool OnLeave (View view) { - // When we have focus, we invert the colors - base.ColorScheme = new (base.ColorScheme) + SetColors (); + _lastFocusedView = this; + + return base.OnLeave (view); + } + + #endregion Focus + + /// + public bool EnableForDesign () + { + Title = "_Shortcut"; + HelpText = "Shortcut help"; + Key = Key.F1; + return true; + } + + /// + protected override void Dispose (bool disposing) + { + if (disposing) { - Normal = base.ColorScheme.Focus, - HotNormal = base.ColorScheme.HotFocus, - HotFocus = base.ColorScheme.HotNormal, - Focus = base.ColorScheme.Normal - }; - } - else - { - base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme; - } + if (CommandView?.IsAdded == false) + { + CommandView.Dispose (); + } - // Set KeyView's colors to show "hot" - if (IsInitialized && base.ColorScheme is { }) - { - var cs = new ColorScheme (base.ColorScheme) - { - Normal = base.ColorScheme.HotNormal, - HotNormal = base.ColorScheme.Normal - }; - KeyView.ColorScheme = cs; - } -} + if (HelpView?.IsAdded == false) + { + HelpView.Dispose (); + } -View _lastFocusedView; -/// -public override bool OnEnter (View view) -{ - SetColors (); - _lastFocusedView = view; - - return base.OnEnter (view); -} - -/// -public override bool OnLeave (View view) -{ - SetColors (); - _lastFocusedView = this; - - return base.OnLeave (view); -} - -#endregion Focus - -/// -protected override void Dispose (bool disposing) -{ - if (disposing) - { - if (CommandView?.IsAdded == false) - { - CommandView.Dispose (); + if (KeyView?.IsAdded == false) + { + KeyView.Dispose (); + } } - if (HelpView?.IsAdded == false) - { - HelpView.Dispose (); - } - - if (KeyView?.IsAdded == false) - { - KeyView.Dispose (); - } + base.Dispose (disposing); } - - base.Dispose (disposing); -} } From 2097a172134787d6a690735a35962fd37277a539 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 13:24:24 -0600 Subject: [PATCH 16/49] Default orientation is Horiz --- .../View/Orientation/OrientationHelper.cs | 2 +- Terminal.Gui/Views/RadioGroup.cs | 1 + .../View/Orientation/OrientationHelperTests.cs | 18 +++++++++--------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/View/Orientation/OrientationHelper.cs b/Terminal.Gui/View/Orientation/OrientationHelper.cs index 3ff93d53e..72ccef0fa 100644 --- a/Terminal.Gui/View/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/View/Orientation/OrientationHelper.cs @@ -46,7 +46,7 @@ /// public class OrientationHelper { - private Orientation _orientation = Orientation.Vertical; + private Orientation _orientation; private readonly IOrientation _owner; /// diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 783aa81bc..e8d845161 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -108,6 +108,7 @@ public class RadioGroup : View, IDesignable, IOrientation }); _orientationHelper = new (this); + _orientationHelper.Orientation = Orientation.Vertical; _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); diff --git a/UnitTests/View/Orientation/OrientationHelperTests.cs b/UnitTests/View/Orientation/OrientationHelperTests.cs index 5294c1726..bd7a884ae 100644 --- a/UnitTests/View/Orientation/OrientationHelperTests.cs +++ b/UnitTests/View/Orientation/OrientationHelperTests.cs @@ -17,7 +17,7 @@ public class OrientationHelperTests orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked = true; }; // Act - orientationHelper.Orientation = Orientation.Horizontal; + orientationHelper.Orientation = Orientation.Vertical; // Assert Assert.True (changingEventInvoked, "OrientationChanging event was not invoked."); @@ -42,7 +42,7 @@ public class OrientationHelperTests var orientationHelper = new OrientationHelper (mockIOrientation.Object); // Act - orientationHelper.Orientation = Orientation.Horizontal; + orientationHelper.Orientation = Orientation.Vertical; // Assert Assert.True (onChangingOverrideCalled, "OnOrientationChanging override was not called."); @@ -55,7 +55,7 @@ public class OrientationHelperTests // Arrange Mock mockIOrientation = new Mock (); var orientationHelper = new OrientationHelper (mockIOrientation.Object); - orientationHelper.Orientation = Orientation.Vertical; // Set initial orientation + orientationHelper.Orientation = Orientation.Horizontal; // Set initial orientation var changingEventInvoked = false; var changedEventInvoked = false; @@ -63,7 +63,7 @@ public class OrientationHelperTests orientationHelper.OrientationChanged += (sender, e) => { changedEventInvoked = true; }; // Act - orientationHelper.Orientation = Orientation.Vertical; // Set to the same value + orientationHelper.Orientation = Orientation.Horizontal; // Set to the same value // Assert Assert.False (changingEventInvoked, "OrientationChanging event was invoked."); @@ -79,10 +79,10 @@ public class OrientationHelperTests orientationHelper.OrientationChanging += (sender, e) => { e.Cancel = true; }; // Cancel the change // Act - orientationHelper.Orientation = Orientation.Horizontal; + orientationHelper.Orientation = Orientation.Vertical; // Assert - Assert.Equal (Orientation.Vertical, orientationHelper.Orientation); // Initial orientation is Vertical + Assert.Equal (Orientation.Horizontal, orientationHelper.Orientation); // Initial orientation is Horizontal } [Fact] @@ -97,11 +97,11 @@ public class OrientationHelperTests var orientationHelper = new OrientationHelper (mockIOrientation.Object); // Act - orientationHelper.Orientation = Orientation.Horizontal; + orientationHelper.Orientation = Orientation.Vertical; // Assert Assert.Equal ( - Orientation.Vertical, - orientationHelper.Orientation); // Initial orientation is Vertical, and it should remain unchanged due to cancellation + Orientation.Horizontal, + orientationHelper.Orientation); // Initial orientation is Horizontal, and it should remain unchanged due to cancellation } } From 6220863b9c4540d895659e68b12e919c1751f9a5 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 22 Jul 2024 14:26:59 -0600 Subject: [PATCH 17/49] Fixed issues @bdisp noted: --- Terminal.Gui/View/Orientation/IOrientation.cs | 7 +++---- Terminal.Gui/View/Orientation/OrientationHelper.cs | 11 +++++------ Terminal.Gui/Views/Bar.cs | 5 ++--- Terminal.Gui/Views/Line.cs | 7 +++---- Terminal.Gui/Views/RadioGroup.cs | 5 ++--- Terminal.Gui/Views/Shortcut.cs | 5 ++--- Terminal.Gui/Views/Slider.cs | 4 ++-- UnitTests/View/Orientation/OrientationHelperTests.cs | 2 +- UnitTests/View/Orientation/OrientationTests.cs | 4 ++-- 9 files changed, 22 insertions(+), 28 deletions(-) diff --git a/Terminal.Gui/View/Orientation/IOrientation.cs b/Terminal.Gui/View/Orientation/IOrientation.cs index 80551f119..34470878e 100644 --- a/Terminal.Gui/View/Orientation/IOrientation.cs +++ b/Terminal.Gui/View/Orientation/IOrientation.cs @@ -29,15 +29,14 @@ public interface IOrientation public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) { return false; } /// - /// + /// Raised when has changed. /// - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; /// /// Called when has been changed. /// - /// /// /// - public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) { return; } + public void OnOrientationChanged (Orientation newOrientation) { return; } } \ No newline at end of file diff --git a/Terminal.Gui/View/Orientation/OrientationHelper.cs b/Terminal.Gui/View/Orientation/OrientationHelper.cs index 72ccef0fa..2227494dc 100644 --- a/Terminal.Gui/View/Orientation/OrientationHelper.cs +++ b/Terminal.Gui/View/Orientation/OrientationHelper.cs @@ -29,7 +29,7 @@ /// } /// /// public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging; -/// public event EventHandler<CancelEventArgs<Orientation>> OrientationChanged; +/// public event EventHandler<EventArgs<Orientation>> OrientationChanged; /// /// public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) /// { @@ -37,7 +37,7 @@ /// return false; // Return true to cancel the change /// } /// -/// public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) +/// public void OnOrientationChanged (Orientation newOrientation) /// { /// // Custom logic after orientation has changed /// } @@ -98,11 +98,10 @@ public class OrientationHelper } // Best practice is to invoke the virtual method first. - _owner?.OnOrientationChanged (old, _orientation); + _owner?.OnOrientationChanged (_orientation); // Even though Changed is not cancelable, it is still a good practice to raise the event after. - args = new (in old, ref _orientation); - OrientationChanged?.Invoke (_owner, args); + OrientationChanged?.Invoke (_owner, new (in _orientation)); } } @@ -135,5 +134,5 @@ public class OrientationHelper /// This event will be raised after the method is called. /// /// - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; } diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index 16d533cd9..1141b5de1 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -75,12 +75,11 @@ public class Bar : View, IOrientation, IDesignable public event EventHandler> OrientationChanging; /// - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; /// Called when has changed. - /// /// - public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + public void OnOrientationChanged (Orientation newOrientation) { SetNeedsLayout (); } diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs index a42f58ebb..730d5a1aa 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -34,15 +34,14 @@ public class Line : View, IOrientation public event EventHandler> OrientationChanging; /// - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; /// Called when has changed. - /// /// - public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + public void OnOrientationChanged (Orientation newOrientation) { - switch (Orientation) + switch (newOrientation) { case Orientation.Horizontal: Height = 1; diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index e8d845161..5ce71c7a4 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -338,12 +338,11 @@ public class RadioGroup : View, IDesignable, IOrientation public event EventHandler> OrientationChanging; /// - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; /// Called when has changed. - /// /// - public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + public void OnOrientationChanged (Orientation newOrientation) { SetupKeyBindings (); SetContentSize (); diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 245308564..9ffb5e72a 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -167,12 +167,11 @@ public class Shortcut : View, IOrientation, IDesignable public event EventHandler> OrientationChanging; /// - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; /// Called when has changed. - /// /// - public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + public void OnOrientationChanged (Orientation newOrientation) { // TODO: Determine what, if anything, is opinionated about the orientation. SetNeedsLayout (); diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index ee0f2f4d0..69a9add71 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -242,10 +242,10 @@ public class Slider : View, IOrientation public event EventHandler> OrientationChanging; /// - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; /// - public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + public void OnOrientationChanged (Orientation newOrientation) { _config._sliderOrientation = newOrientation; diff --git a/UnitTests/View/Orientation/OrientationHelperTests.cs b/UnitTests/View/Orientation/OrientationHelperTests.cs index bd7a884ae..cc6a1f240 100644 --- a/UnitTests/View/Orientation/OrientationHelperTests.cs +++ b/UnitTests/View/Orientation/OrientationHelperTests.cs @@ -36,7 +36,7 @@ public class OrientationHelperTests .Callback (() => onChangingOverrideCalled = true) .Returns (false); // Ensure it doesn't cancel the change - mockIOrientation.Setup (x => x.OnOrientationChanged (It.IsAny (), It.IsAny ())) + mockIOrientation.Setup (x => x.OnOrientationChanged (It.IsAny ())) .Callback (() => onChangedOverrideCalled = true); var orientationHelper = new OrientationHelper (mockIOrientation.Object); diff --git a/UnitTests/View/Orientation/OrientationTests.cs b/UnitTests/View/Orientation/OrientationTests.cs index 01eeda71f..d000ca9db 100644 --- a/UnitTests/View/Orientation/OrientationTests.cs +++ b/UnitTests/View/Orientation/OrientationTests.cs @@ -21,7 +21,7 @@ public class OrientationTests } public event EventHandler> OrientationChanging; - public event EventHandler> OrientationChanged; + public event EventHandler> OrientationChanged; public bool CancelOnOrientationChanging { get; set; } @@ -35,7 +35,7 @@ public class OrientationTests return CancelOnOrientationChanging; // Return true to cancel the change } - public void OnOrientationChanged (Orientation oldOrientation, Orientation newOrientation) + public void OnOrientationChanged (Orientation newOrientation) { OnOrientationChangedCalled = true; // Custom logic after orientation has changed From 88954b9ab772fdeb7dd339a9be8c23512a96128a Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 25 Jul 2024 18:04:46 +0100 Subject: [PATCH 18/49] Fix WSL InvariantCulture test. --- SelfContained/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SelfContained/Program.cs b/SelfContained/Program.cs index ff6dd12b9..51f4d7bb1 100644 --- a/SelfContained/Program.cs +++ b/SelfContained/Program.cs @@ -16,7 +16,7 @@ public static class Program #region The code in this region is not intended for use in a self-contained single-file. It's just here to make sure there is no functionality break with localization in Terminal.Gui using single-file - if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture)) + if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture) && Application.SupportedCultures.Count == 0) { Debug.Assert (Application.SupportedCultures.Count == 0); } From 3e9f585348871a0b6fa131279c038dd5274276d7 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 26 Jul 2024 14:49:05 +0100 Subject: [PATCH 19/49] Fixes #3628. SixLabors.ImageSharp prior to version 3.1.5 are vulnerable. --- UICatalog/UICatalog.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index 8988e1ef4..6731e0395 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -31,7 +31,7 @@ - + From a225b421c813bac407e2a4b145971026428bc222 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sun, 28 Jul 2024 12:23:09 +0100 Subject: [PATCH 20/49] Update ReactiveUI Example --- ReactiveExample/FodyWeavers.xml | 3 - ReactiveExample/FodyWeavers.xsd | 26 --- ReactiveExample/LoginView.cs | 300 +++++++++++-------------- ReactiveExample/LoginViewModel.cs | 77 +++---- ReactiveExample/README.md | 2 +- ReactiveExample/ReactiveExample.csproj | 2 +- ReactiveExample/ViewExtensions.cs | 24 ++ 7 files changed, 188 insertions(+), 246 deletions(-) delete mode 100644 ReactiveExample/FodyWeavers.xml delete mode 100644 ReactiveExample/FodyWeavers.xsd create mode 100644 ReactiveExample/ViewExtensions.cs diff --git a/ReactiveExample/FodyWeavers.xml b/ReactiveExample/FodyWeavers.xml deleted file mode 100644 index 63fc14848..000000000 --- a/ReactiveExample/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/ReactiveExample/FodyWeavers.xsd b/ReactiveExample/FodyWeavers.xsd deleted file mode 100644 index f3ac47620..000000000 --- a/ReactiveExample/FodyWeavers.xsd +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. - - - - - A comma-separated list of error codes that can be safely ignored in assembly verification. - - - - - 'false' to turn off automatic generation of the XML Schema file. - - - - - \ No newline at end of file diff --git a/ReactiveExample/LoginView.cs b/ReactiveExample/LoginView.cs index 154bfbece..2a4876399 100644 --- a/ReactiveExample/LoginView.cs +++ b/ReactiveExample/LoginView.cs @@ -8,20 +8,137 @@ namespace ReactiveExample; public class LoginView : Window, IViewFor { - private readonly CompositeDisposable _disposable = new (); + private const string SuccessMessage = "The input is valid!"; + private const string ErrorMessage = "Please enter a valid user name and password."; + private const string ProgressMessage = "Logging in..."; + private const string IdleMessage = "Press 'Login' to log in."; + + private readonly CompositeDisposable _disposable = []; public LoginView (LoginViewModel viewModel) { Title = $"Reactive Extensions Example - {Application.QuitKey} to Exit"; ViewModel = viewModel; - Label usernameLengthLabel = UsernameLengthLabel (TitleLabel ()); - TextField usernameInput = UsernameInput (usernameLengthLabel); - Label passwordLengthLabel = PasswordLengthLabel (usernameLengthLabel); - TextField passwordInput = PasswordInput (passwordLengthLabel); - Label validationLabel = ValidationLabel (passwordInput); - Button loginButton = LoginButton (validationLabel); - Button clearButton = ClearButton (loginButton); - LoginProgressLabel (clearButton); + var title = this.AddControl [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] [JsonConverter (typeof (JsonStringEnumConverter))] - public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; + public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single; // Default is set in config.json + + /// The default for . + /// This property can be set in a Theme. + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + [JsonConverter (typeof (JsonStringEnumConverter))] + public static Alignment DefaultButtonAlignment { get; set; } = Alignment.Center; // Default is set in config.json /// /// Defines the default minimum MessageBox width, as a percentage of the screen width. Can be configured via @@ -365,10 +371,10 @@ public static class MessageBox var d = new Dialog { Title = title, - Buttons = buttonList.ToArray (), - ButtonAlignment = Alignment.Center, + ButtonAlignment = MessageBox.DefaultButtonAlignment, ButtonAlignmentModes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems, BorderStyle = MessageBox.DefaultBorderStyle, + Buttons = buttonList.ToArray (), }; d.Width = Dim.Auto (DimAutoStyle.Auto, From 271a73cba1db6fa669c3bcc9a5130503bcc219dd Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 4 Aug 2024 12:14:13 -0600 Subject: [PATCH 44/49] Made unit tests more resiliant to config changes --- UnitTests/Dialogs/DialogTests.cs | 31 +++++++++++++++++++++++++++- UnitTests/Dialogs/MessageBoxTests.cs | 12 +++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/UnitTests/Dialogs/DialogTests.cs b/UnitTests/Dialogs/DialogTests.cs index d07b627ee..582cf659a 100644 --- a/UnitTests/Dialogs/DialogTests.cs +++ b/UnitTests/Dialogs/DialogTests.cs @@ -26,6 +26,10 @@ public class DialogTests int width = $@"{CM.Glyphs.VLine} {btn1} {btn2} {CM.Glyphs.VLine}".Length; d.SetBufferSize (width, 1); + // Override CM + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + // Default (center) var dlg = new Dialog { @@ -151,6 +155,7 @@ public class DialogTests int width = buttonRow.Length; d.SetBufferSize (buttonRow.Length, 3); + // Default - Center (runstate, Dialog dlg) = RunButtonTestDialog ( title, @@ -874,6 +879,11 @@ public class DialogTests { ((FakeDriver)Driver).SetBufferSize (20, 5); + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var win = new Window (); var iterations = 0; @@ -889,7 +899,6 @@ public class DialogTests win.Loaded += (s, a) => { - Dialog.DefaultButtonAlignment = Alignment.Center; var dlg = new Dialog { Width = 18, Height = 3, Buttons = [new () { Text = "Ok" }] }; dlg.Loaded += (s, a) => @@ -975,7 +984,10 @@ public class DialogTests var win = new Window (); int iterations = -1; + + // Override CM Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; Iteration += (s, a) => { @@ -1012,7 +1024,10 @@ public class DialogTests public void Dialog_Opened_From_Another_Dialog () { ((FakeDriver)Driver).SetBufferSize (30, 10); + + // Override CM Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; var btn1 = new Button { Text = "press me 1" }; Button btn2 = null; @@ -1159,6 +1174,11 @@ public class DialogTests [AutoInitShutdown] public void Location_When_Application_Top_Not_Default () { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var expected = 5; var d = new Dialog { X = expected, Y = expected, Height = 5, Width = 5 }; Begin (d); @@ -1188,6 +1208,11 @@ public class DialogTests int iterations = -1; + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Iteration += (s, a) => { iterations++; @@ -1361,6 +1386,10 @@ public class DialogTests params Button [] btns ) { + // Override CM + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var dlg = new Dialog { Title = title, diff --git a/UnitTests/Dialogs/MessageBoxTests.cs b/UnitTests/Dialogs/MessageBoxTests.cs index 986894efa..566c31270 100644 --- a/UnitTests/Dialogs/MessageBoxTests.cs +++ b/UnitTests/Dialogs/MessageBoxTests.cs @@ -174,6 +174,10 @@ public class MessageBoxTests var btn = $"{CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} btn {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket}"; + // Override CM + MessageBox.DefaultButtonAlignment = Alignment.End; + MessageBox.DefaultBorderStyle = LineStyle.Double; + Application.Iteration += (s, a) => { iterations++; @@ -239,6 +243,10 @@ public class MessageBoxTests var btn = $"{CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} btn {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket}"; + // Override CM + MessageBox.DefaultButtonAlignment = Alignment.End; + MessageBox.DefaultBorderStyle = LineStyle.Double; + Application.Iteration += (s, a) => { iterations++; @@ -415,6 +423,10 @@ public class MessageBoxTests int iterations = -1; ((FakeDriver)Application.Driver).SetBufferSize (70, 15); + // Override CM + MessageBox.DefaultButtonAlignment = Alignment.End; + MessageBox.DefaultBorderStyle = LineStyle.Double; + Application.Iteration += (s, a) => { iterations++; From f5b1984db781a1bf99b0cf6ca6a7d3e2d44bac14 Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 4 Aug 2024 15:08:18 -0600 Subject: [PATCH 45/49] Made unit tests more resiliant to config changes --- UnitTests/FileServices/FileDialogTests.cs | 5 +++++ UnitTests/Views/MenuBarTests.cs | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index 1395543ee..ec72a7add 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -622,6 +622,11 @@ public class FileDialogTests (ITestOutputHelper output) private FileDialog GetWindowsDialog () { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + // Arrange var fileSystem = new MockFileSystem (new Dictionary (), @"c:\"); fileSystem.MockTime (() => new (2010, 01, 01, 11, 12, 43)); diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index f7804dd42..330f8bacf 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -362,6 +362,11 @@ public class MenuBarTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_A_Menu_Over_A_Dialog () { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Toplevel top = new (); var win = new Window (); top.Add (win); @@ -590,6 +595,11 @@ public class MenuBarTests (ITestOutputHelper output) [AutoInitShutdown] public void Draw_A_Menu_Over_A_Top_Dialog () { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + ((FakeDriver)Application.Driver).SetBufferSize (40, 15); Assert.Equal (new (0, 0, 40, 15), Application.Driver.Clip); From a9f63ad138fa04b7a1e6a7b954287850991c5f4a Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 4 Aug 2024 15:27:56 -0600 Subject: [PATCH 46/49] Made unit tests more resiliant to config changes --- UnitTests/FileServices/FileDialogTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index ec72a7add..c50bb986b 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -571,6 +571,11 @@ public class FileDialogTests (ITestOutputHelper output) private FileDialog GetInitializedFileDialog () { + + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + var dlg = new FileDialog (); Begin (dlg); @@ -579,6 +584,10 @@ public class FileDialogTests (ITestOutputHelper output) private FileDialog GetLinuxDialog () { + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + // Arrange var fileSystem = new MockFileSystem (new Dictionary (), "/"); fileSystem.MockTime (() => new (2010, 01, 01, 11, 12, 43)); From e86a2fca2f552d75f8f3d6516cb1a80cc5389c08 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 5 Aug 2024 08:54:05 -0600 Subject: [PATCH 47/49] Simplfiied app scope key setters --- .../Application/Application.Keyboard.cs | 89 +++++++------------ Terminal.Gui/Application/Application.cs | 4 +- Terminal.Gui/Input/KeyBindings.cs | 15 ++-- Terminal.Gui/Views/Shortcut.cs | 2 +- Terminal.Gui/Views/TableView/TableView.cs | 14 +-- UnitTests/Application/ApplicationTests.cs | 10 ++- UnitTests/Application/KeyboardTests.cs | 2 +- UnitTests/Configuration/SettingsScopeTests.cs | 6 +- UnitTests/Input/KeyBindingTests.cs | 28 +++++- 9 files changed, 90 insertions(+), 80 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 0981bbd2d..48170eb22 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; public static partial class Application // Keyboard handling { - private static Key _nextTabKey = Key.Empty; // Defined in config.json + private static Key _nextTabKey = Key.Tab; // Resources/config.json overrrides /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] @@ -17,22 +17,13 @@ public static partial class Application // Keyboard handling { if (_nextTabKey != value) { - Key oldKey = _nextTabKey; + ReplaceKey (_nextTabKey, value); _nextTabKey = value; - - if (_nextTabKey == Key.Empty) - { - KeyBindings.Remove (_nextTabKey); - } - else - { - KeyBindings.ReplaceKey (oldKey, _nextTabKey); - } } } } - private static Key _prevTabKey = Key.Empty; // Defined in config.json + private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrrides /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] @@ -44,22 +35,13 @@ public static partial class Application // Keyboard handling { if (_prevTabKey != value) { - Key oldKey = _prevTabKey; + ReplaceKey (_prevTabKey, value); _prevTabKey = value; - - if (_prevTabKey == Key.Empty) - { - KeyBindings.Remove (_prevTabKey); - } - else - { - KeyBindings.ReplaceKey (oldKey, _prevTabKey); - } } } } - private static Key _nextTabGroupKey = Key.Empty; // Defined in config.json + private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrrides /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] @@ -71,22 +53,13 @@ public static partial class Application // Keyboard handling { if (_nextTabGroupKey != value) { - Key oldKey = _nextTabGroupKey; + ReplaceKey (_nextTabGroupKey, value); _nextTabGroupKey = value; - - if (_nextTabGroupKey == Key.Empty) - { - KeyBindings.Remove (_nextTabGroupKey); - } - else - { - KeyBindings.ReplaceKey (oldKey, _nextTabGroupKey); - } } } } - private static Key _prevTabGroupKey = Key.Empty; // Defined in config.json + private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrrides /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] @@ -98,22 +71,13 @@ public static partial class Application // Keyboard handling { if (_prevTabGroupKey != value) { - Key oldKey = _prevTabGroupKey; + ReplaceKey (_prevTabGroupKey, value); _prevTabGroupKey = value; - - if (_prevTabGroupKey == Key.Empty) - { - KeyBindings.Remove (_prevTabGroupKey); - } - else - { - KeyBindings.ReplaceKey (oldKey, _prevTabGroupKey); - } } } } - private static Key _quitKey = Key.Empty; // Defined in config.json + private static Key _quitKey = Key.Esc; // Resources/config.json overrrides /// Gets or sets the key to quit the application. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] @@ -125,21 +89,29 @@ public static partial class Application // Keyboard handling { if (_quitKey != value) { - Key oldKey = _quitKey; + ReplaceKey (_quitKey, value); _quitKey = value; - - if (_quitKey == Key.Empty) - { - KeyBindings.Remove (_quitKey); - } - else - { - KeyBindings.ReplaceKey (oldKey, _quitKey); - } } } } + private static void ReplaceKey (Key oldKey, Key newKey) + { + if (KeyBindings.Bindings.Count == 0) + { + return; + } + + if (newKey == Key.Empty) + { + KeyBindings.Remove (oldKey); + } + else + { + KeyBindings.ReplaceKey (oldKey, newKey); + } + } + /// /// Event fired when the user presses a key. Fired by . /// @@ -413,6 +385,13 @@ public static partial class Application // Keyboard handling KeyBindings.Clear (); + // Resources/config.json overrrides + NextTabKey = Key.Tab; + PrevTabKey = Key.Tab.WithShift; + NextTabGroupKey = Key.F6; + PrevTabGroupKey = Key.F6.WithShift; + QuitKey = Key.Esc; + KeyBindings.Add (QuitKey, KeyBindingScope.Application, Command.QuitToplevel); KeyBindings.Add (Key.CursorRight, KeyBindingScope.Application, Command.NextView); diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index e5332ee7f..e3912475f 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -142,14 +142,12 @@ public static partial class Application UnGrabbedMouse = null; // Keyboard - PrevTabGroupKey = Key.Empty; - NextTabGroupKey = Key.Empty; - QuitKey = Key.Empty; KeyDown = null; KeyUp = null; SizeChanging = null; Navigation = null; + AddApplicationKeyBindings (); Colors.Reset (); diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs index 89ffde7ad..5dd69a260 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -136,10 +136,9 @@ public class KeyBindings throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); } - if (key is null || !key.IsValid) + if (key == Key.Empty || !key.IsValid) { - //throw new ArgumentException ("Invalid Key", nameof (commands)); - return; + throw new ArgumentException (@"Invalid Key", nameof (commands)); } if (commands.Length == 0) @@ -150,7 +149,6 @@ public class KeyBindings if (TryGet (key, out KeyBinding binding)) { throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); - //Bindings [key] = new (commands, scope, BoundView); } else { @@ -313,12 +311,17 @@ public class KeyBindings /// Replaces a key combination already bound to a set of s. /// /// The key to be replaced. - /// The new key to be used. + /// The new key to be used. If no action will be taken. public void ReplaceKey (Key oldKey, Key newKey) { if (!TryGet (oldKey, out KeyBinding _)) { - return; + throw new InvalidOperationException ($"Key {oldKey} is not bound."); + } + + if (!newKey.IsValid) + { + throw new InvalidOperationException ($"Key {newKey} is is not valid."); } KeyBinding value = Bindings [oldKey]; diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index c5e023ee7..6530d71bc 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -681,7 +681,7 @@ public class Shortcut : View, IOrientation, IDesignable private void UpdateKeyBinding (Key oldKey) { - if (Key != null) + if (Key != null && Key.IsValid) { // Disable the command view key bindings CommandView.KeyBindings.Remove (Key); diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index a1b69cb65..10c53513d 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -324,11 +324,15 @@ public class TableView : View { if (cellActivationKey != value) { - KeyBindings.ReplaceKey (cellActivationKey, value); + if (KeyBindings.TryGet (cellActivationKey, out _)) + { + KeyBindings.ReplaceKey (cellActivationKey, value); + } + else + { + KeyBindings.Add (value, Command.Accept); + } - // of API user is mixing and matching old and new methods of keybinding then they may have lost - // the old binding (e.g. with ClearKeybindings) so KeyBindings.Replace alone will fail - KeyBindings.Add (value, Command.Accept); cellActivationKey = value; } } @@ -792,7 +796,7 @@ public class TableView : View } /// - protected internal override bool OnMouseEvent (MouseEvent me) + protected internal override bool OnMouseEvent (MouseEvent me) { if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index ce03ac11f..b84dc8ae8 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -183,9 +183,11 @@ public class ApplicationTests Assert.Null (Application.Driver); Assert.Null (Application.MainLoop); Assert.False (Application.EndAfterFirstIteration); - Assert.Equal (Key.Empty, Application.PrevTabGroupKey); - Assert.Equal (Key.Empty, Application.NextTabGroupKey); - Assert.Equal (Key.Empty, Application.QuitKey); + Assert.Equal (Key.Tab.WithShift, Application.PrevTabKey); + Assert.Equal (Key.Tab, Application.NextTabKey); + Assert.Equal (Key.F6.WithShift, Application.PrevTabGroupKey); + Assert.Equal (Key.F6, Application.NextTabGroupKey); + Assert.Equal (Key.Esc, Application.QuitKey); Assert.Null (ApplicationOverlapped.OverlappedChildren); Assert.Null (ApplicationOverlapped.OverlappedTop); @@ -236,7 +238,7 @@ public class ApplicationTests Application.PrevTabGroupKey = Key.A; Application.NextTabGroupKey = Key.B; Application.QuitKey = Key.C; - Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Cancel); + Application.KeyBindings.Add (Key.D, KeyBindingScope.Application, Command.Cancel); //ApplicationOverlapped.OverlappedChildren = new List (); //ApplicationOverlapped.OverlappedTop = diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index 899420d94..2f6d85d00 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -65,7 +65,7 @@ public class KeyboardTests { Application.ResetState (true); // Before Init - Assert.Equal (Key.Empty, Application.QuitKey); + Assert.Equal (Key.Esc, Application.QuitKey); Application.Init (new FakeDriver ()); // After Init diff --git a/UnitTests/Configuration/SettingsScopeTests.cs b/UnitTests/Configuration/SettingsScopeTests.cs index 5525e530a..ca4f6b830 100644 --- a/UnitTests/Configuration/SettingsScopeTests.cs +++ b/UnitTests/Configuration/SettingsScopeTests.cs @@ -29,9 +29,9 @@ public class SettingsScopeTests Settings.Apply (); // assert - Assert.Equal (KeyCode.Q, Application.QuitKey.KeyCode); - Assert.Equal (KeyCode.F, Application.NextTabGroupKey.KeyCode); - Assert.Equal (KeyCode.B, Application.PrevTabGroupKey.KeyCode); + Assert.Equal (Key.Q, Application.QuitKey); + Assert.Equal (Key.F, Application.NextTabGroupKey); + Assert.Equal (Key.B, Application.PrevTabGroupKey); } [Fact] diff --git a/UnitTests/Input/KeyBindingTests.cs b/UnitTests/Input/KeyBindingTests.cs index bf3b007fd..077fc38f1 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/KeyBindingTests.cs @@ -8,11 +8,20 @@ public class KeyBindingTests public KeyBindingTests (ITestOutputHelper output) { _output = output; } [Fact] - public void Add_Empty_Throws () + public void Add_No_Commands_Throws () { var keyBindings = new KeyBindings (); List commands = new (); Assert.Throws (() => keyBindings.Add (Key.A, commands.ToArray ())); + + } + + [Fact] + public void Add_Invalid_Key_Throws () + { + var keyBindings = new KeyBindings (); + List commands = new (); + Assert.Throws (() => keyBindings.Add (Key.Empty, KeyBindingScope.HotKey, Command.Accept)); } [Fact] @@ -193,7 +202,7 @@ public class KeyBindingTests } [Fact] - public void Replace_Key () + public void ReplaceKey_Replaces () { var keyBindings = new KeyBindings (); keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); @@ -218,6 +227,21 @@ public class KeyBindingTests Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.H)); } + [Fact] + public void ReplaceKey_Throws_If_DoesNotContain_Old () + { + var keyBindings = new KeyBindings (); + Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.B)); + } + + [Fact] + public void ReplaceKey_Throws_If_New_Is_Empty () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); + } + // Add with scope does the right things [Theory] [InlineData (KeyBindingScope.Focused)] From 331ab51176c54159a1021d575aa59f1c0aeb39e6 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 5 Aug 2024 09:08:34 -0600 Subject: [PATCH 48/49] Updatd keyboard.md --- UnitTests/Input/KeyBindingTests.cs | 16 ++++++++++++ docfx/docs/keyboard.md | 39 +++++++++++++++--------------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/UnitTests/Input/KeyBindingTests.cs b/UnitTests/Input/KeyBindingTests.cs index 077fc38f1..e78ec5730 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/KeyBindingTests.cs @@ -242,6 +242,20 @@ public class KeyBindingTests Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); } + [Fact] + public void ReplaceKey_Replaces_Leaves_Old_Binding () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); + + keyBindings.ReplaceKey (keyBindings.GetKeyFromCommands(Command.Accept), Key.C); + Assert.Empty (keyBindings.GetCommands (Key.A)); + Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C)); + + } + + // Add with scope does the right things [Theory] [InlineData (KeyBindingScope.Focused)] @@ -341,4 +355,6 @@ public class KeyBindingTests Assert.True (result); Assert.Contains (Command.HotKey, bindings.Commands); } + + } diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index c15f3ea30..cbe3f92f1 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -6,9 +6,9 @@ Tenets higher in the list have precedence over tenets lower in the list. * **Users Have Control** - *Terminal.Gui* provides default key bindings consistent with these tenets, but those defaults are configurable by the user. For example, `ConfigurationManager` allows users to redefine key bindings for the system, a user, or an application. -* **More Editor than Command Line** - Once a *Terminal.Gui* app starts, the user is no longer using the command line. Users expect keyboard idioms in TUI apps to be consistent with GUI apps (such as VS Code, Vim, and Emacs). For example, in almost all GUI apps, `Ctrl-V` is `Paste`. But the Linux shells often use `Shift-Insert`. *Terminal.Gui* binds `Ctrl-V` by default. +* **More Editor than Command Line** - Once a *Terminal.Gui* app starts, the user is no longer using the command line. Users expect keyboard idioms in TUI apps to be consistent with GUI apps (such as VS Code, Vim, and Emacs). For example, in almost all GUI apps, `Ctrl+V` is `Paste`. But the Linux shells often use `Shift+Insert`. *Terminal.Gui* binds `Ctrl+V` by default. -* **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 keyboard input in a way that is consistent with the platform. For example, on Windows to erase a word to the left, users press `Ctrl-Backspace`. But on Linux, `Ctrl-W` is used. +* **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 keyboard input in a way that is consistent with the platform. For example, on Windows to erase a word to the left, users press `Ctrl+Backspace`. But on Linux, `Ctrl+W` is used. * **The Source of Truth is Wikipedia** - We use this [Wikipedia article](https://en.wikipedia.org/wiki/Table_of_keyboard_shortcuts) as our guide for default key bindings. @@ -24,33 +24,33 @@ See [Key](~/api/Terminal.Gui.Key.yml) for more details. ### **[Key Bindings](~/api/Terminal.Gui.KeyBindings.yml)** -The default key for activating a button is `Space`. You can change this using -`Keybindings.Clear` and `Keybinding.Add` methods: +The default key for activating a button is `Space`. You can change this using +`KeyBindings.ReplaceKey()`: ```csharp -var btn = new Button ("Press Me"); -btn.Keybinding.Remove (Command.Accept); -btn.KeyBinding.Add (Key.B, Command.Accept); +var btn = new Button () { Title = "Press me" }; +btn.KeyBindings.ReplaceKey (btn.KeyBindings.GetKeyFromCommands (Command.Accept)); ``` The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. For example `Command.Accept` in a `Button` results in the `Clicked` event firing while in `TableView` it is bound to `CellActivated`. Not all commands are implemented by all views (e.g. you cannot scroll in a `Button`). Use the `GetSupportedCommands()` method to determine which commands are implemented by a `View`. -Key Bindings can be added at the Application or View level. For Application-scoped Key Bindings see [ApplicationNavigation](~/api/Terminal.Gui.ApplicationNavigation.yml). For View-scoped Key Bindings see [Key Bindings](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_KeyBinings). +Key Bindings can be added at the `Application` or `View` level. For Application-scoped Key Bindings see [ApplicationNavigation](~/api/Terminal.Gui.ApplicationNavigation.yml). For View-scoped Key Bindings see [Key Bindings](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_KeyBinings). ### **[HotKey](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_HotKey)** -A **HotKey** is a keypress that selects a visible UI item. For selecting items across `View`s (e.g. a `Button` in a `Dialog`) the keypress must have the `Alt` modifier. For selecting items within a `View` that are not `View`s themselves, the keypress can be key without the `Alt` modifier. For example, in a `Dialog`, a `Button` with the text of "_Text" can be selected with `Alt-T`. Or, in a `Menu` with "_File _Edit", `Alt-F` will select (show) the "_File" menu. If the "_File" menu has a sub-menu of "_New" `Alt-N` or `N` will ONLY select the "_New" sub-menu if the "_File" menu is already opened. +A **HotKey** is a key press that selects a visible UI item. For selecting items across `View`s (e.g. a `Button` in a `Dialog`) the key press must have the `Alt` modifier. For selecting items within a `View` that are not `View`s themselves, the key press can be key without the `Alt` modifier. For example, in a `Dialog`, a `Button` with the text of "_Text" can be selected with `Alt+T`. Or, in a `Menu` with "_File _Edit", `Alt+F` will select (show) the "_File" menu. If the "_File" menu has a sub-menu of "_New" `Alt+N` or `N` will ONLY select the "_New" sub-menu if the "_File" menu is already opened. By default, the `Text` of a `View` is used to determine the `HotKey` by looking for the first occurrence of the [HotKeySpecifier](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_HotKeySpecifier) (which is underscore (`_`) by default). The character following the underscore is the `HotKey`. If the `HotKeySpecifier` is not found in `Text`, the first character of `Text` is used as the `HotKey`. The `Text` of a `View` can be changed at runtime, and the `HotKey` will be updated accordingly. [HotKey](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_HotKey) is `virtual` enabling this behavior to be customized. -### **[Shortcut](~/api/Terminal.Gui.Shortcut.yml) - An opinionated (visually & API) View for displaying a command, helptext, key. -** +### **[Shortcut](~/api/Terminal.Gui.Shortcut.yml)** -A **Shortcut** is a keypress that invokes a [Command](~/api/Terminal.Gui.Command.yml) or `View`-defined action even if the `View` that defines them is not focused or visible (but the `View` must be enabled). Shortcuts can be any keypress; `Key.A`, `Key.A | Key.Ctrl`, `Key.A | Key.Ctrl | Key.Alt`, `Key.Del`, and `Key.F1`, are all valid. +A **Shortcut** is an opinionated (visually & API) View for displaying a command, help text, key key press that invokes a [Command](~/api/Terminal.Gui.Command.yml). -`Shortcuts` are used to define application-wide actions (e.g. `Quit`), or actions that are not visible (e.g. `Copy`). +The Command can be invoked even if the `View` that defines them is not focused or visible (but the `View` must be enabled). Shortcuts can be any key press; `Key.A`, `Key.A.WithCtrl`, `Key.A.WithCtrl.WithAlt`, `Key.Del`, and `Key.F1`, are all valid. + +`Shortcuts` are used to define application-wide actions or actions that are not visible (e.g. `Copy`). [MenuBar](~/api/Terminal.Gui.MenuBar.yml), [ContextMenu](~/api/Terminal.Gui.ContextMenu.yml), and [StatusBar](~/api/Terminal.Gui.StatusBar.yml) support `Shortcut`s. @@ -64,14 +64,14 @@ to the [Application](~/api/Terminal.Gui.Application.yml) class by the [Main Loop If the view is enabled, the [NewKeyDownEvent](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_NewKeyDownEvent_Terminal_Gui_Key_) method will do the following: -1) If the view has a subview that has focus, 'ProcessKeyDown' on the focused view will be called. If the focused view handles the keypress, processing stops. -2) If there is no focused sub-view, or the focused sub-view does not handle the keypress, [OnKeyDown](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_OnKeyDown_Terminal_Gui_Key_) will be called. If the view handles the keypress, processing stops. -3) If the view does not handle the keypress, [OnInvokingKeyBindings](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_OnInvokingKeyBindings_Terminal_Gui_Key_) will be called. This method calls[InvokeKeyBindings](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_InvokeKeyBindings_Terminal_Gui_Key_) to invoke any keys bound to commands. If the key is bound and any of it's command handlers return true, processing stops. -4) If the key is not bound, or the bound command handlers do not return true, [OnProcessKeyDow](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_OnProcessKeyDown_Terminal_Gui_Key_) is called. If the view handles the keypress, processing stops. +1) If the view has a subview that has focus, 'ProcessKeyDown' on the focused view will be called. If the focused view handles the key press, processing stops. +2) If there is no focused sub-view, or the focused sub-view does not handle the key press, [OnKeyDown](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_OnKeyDown_Terminal_Gui_Key_) will be called. If the view handles the key press, processing stops. +3) If the view does not handle the key press, [OnInvokingKeyBindings](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_OnInvokingKeyBindings_Terminal_Gui_Key_) will be called. This method calls[InvokeKeyBindings](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_InvokeKeyBindings_Terminal_Gui_Key_) to invoke any keys bound to commands. If the key is bound and any of it's command handlers return true, processing stops. +4) If the key is not bound, or the bound command handlers do not return true, [OnProcessKeyDow](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_OnProcessKeyDown_Terminal_Gui_Key_) is called. If the view handles the key press, processing stops. -## **[Global Key Handling](~/api/Terminal.Gui.Application.yml#Terminal_Gui_Application_OnKeyDown_Terminal_Gui_Key_)** +## **[Application Key Handling](~/api/Terminal.Gui.Application.yml#Terminal_Gui_Application_OnKeyDown_Terminal_Gui_Key_)** -To define global key handling logic for an entire application in cases where the methods listed above are not suitable, use the `Application.OnKeyDown` event. +To define application key handling logic for an entire application in cases where the methods listed above are not suitable, use the `Application.OnKeyDown` event. ## **[Key Down/Up Events](~/api/Terminal.Gui.View.yml#Terminal_Gui_View_KeyDown)** @@ -108,6 +108,7 @@ To define global key handling logic for an entire application in cases where the ## Application * Implements support for `KeyBindingScope.Application`. +* Exposes [Application.KeyBindings](~/api/Terminal.Gui.Application.yml#Terminal_Gui_Application_KeyBindings_). * Exposes cancelable `KeyDown/Up` events (via `Handled = true`). The `OnKey/Down/Up/` methods are public and can be used to simulate keyboard input. ## View From af9887bc9e8e5e4852b2e00954a7581cada259bb Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 5 Aug 2024 09:22:50 -0600 Subject: [PATCH 49/49] Fixed merged issue --- Terminal.Gui/Views/Shortcut.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index ff1d4b21f..6530d71bc 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -74,7 +74,7 @@ public class Shortcut : View, IOrientation, IDesignable CommandView = new () { Width = Dim.Auto (), - Height = Dim.Auto (1) + Height = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: 1) }; HelpView.Id = "_helpView";