Pārlūkot izejas kodu

merged with v2_develop

Tig 1 gadu atpakaļ
vecāks
revīzija
bb16e695fa

+ 2 - 2
SelfContained/Program.cs

@@ -16,14 +16,14 @@ 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) && Application.SupportedCultures.Count == 0)
+        if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture) && Application.SupportedCultures?.Count == 0)
         {
             // Only happens if the project has <InvariantGlobalization>true</InvariantGlobalization>
             Debug.Assert (Application.SupportedCultures.Count == 0);
         }
         else
         {
-            Debug.Assert (Application.SupportedCultures.Count > 0);
+            Debug.Assert (Application.SupportedCultures?.Count > 0);
             Debug.Assert (Equals (CultureInfo.CurrentCulture, Thread.CurrentThread.CurrentUICulture));
         }
 

+ 42 - 0
Terminal.Gui/View/Orientation/IOrientation.cs

@@ -0,0 +1,42 @@
+
+namespace Terminal.Gui;
+using System;
+
+/// <summary>
+///     Implement this interface to provide orientation support.
+/// </summary>
+/// <remarks>
+///     See <see cref="OrientationHelper"/> for a helper class that implements this interface.
+/// </remarks>
+public interface IOrientation
+{
+    /// <summary>
+    ///     Gets or sets the orientation of the View.
+    /// </summary>
+    Orientation Orientation { get; set; }
+
+    /// <summary>
+    ///     Raised when <see cref="Orientation"/> is changing. Can be cancelled.
+    /// </summary>
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
+
+    /// <summary>
+    ///     Called when <see cref="Orientation"/> is changing.
+    /// </summary>
+    /// <param name="currentOrientation">The current orientation.</param>
+    /// <param name="newOrientation">The new orientation.</param>
+    /// <returns><see langword="true"/> to cancel the change.</returns>
+    public bool OnOrientationChanging (Orientation currentOrientation, Orientation newOrientation) { return false; }
+
+    /// <summary>
+    ///     Raised when <see cref="Orientation"/> has changed.
+    /// </summary>
+    public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+
+    /// <summary>
+    ///     Called when <see cref="Orientation"/> has been changed.
+    /// </summary>
+    /// <param name="newOrientation"></param>
+    /// <returns></returns>
+    public void OnOrientationChanged (Orientation newOrientation) { return; }
+}

+ 0 - 0
Terminal.Gui/Views/GraphView/Orientation.cs → Terminal.Gui/View/Orientation/Orientation.cs


+ 138 - 0
Terminal.Gui/View/Orientation/OrientationHelper.cs

@@ -0,0 +1,138 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Helper class for implementing <see cref="IOrientation"/>.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Implements the standard pattern for changing/changed events.
+///     </para>
+/// </remarks>
+/// <example>
+///     <code>
+/// private class OrientedView : View, IOrientation
+/// {
+///     private readonly OrientationHelper _orientationHelper;
+/// 
+///     public OrientedView ()
+///     {
+///         _orientationHelper = new (this);
+///         Orientation = Orientation.Vertical;
+///         _orientationHelper.OrientationChanging += (sender, e) =&gt; OrientationChanging?.Invoke (this, e);
+///         _orientationHelper.OrientationChanged += (sender, e) =&gt; OrientationChanged?.Invoke (this, e);
+///     }
+/// 
+///     public Orientation Orientation
+///     {
+///         get =&gt; _orientationHelper.Orientation;
+///         set =&gt; _orientationHelper.Orientation = value;
+///     }
+/// 
+///     public event EventHandler&lt;CancelEventArgs&lt;Orientation&gt;&gt; OrientationChanging;
+///     public event EventHandler&lt;EventArgs&lt;Orientation&gt;&gt; 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 newOrientation)
+///     {
+///         // Custom logic after orientation has changed
+///     }
+/// }
+/// </code>
+/// </example>
+public class OrientationHelper
+{
+    private Orientation _orientation;
+    private readonly IOrientation _owner;
+
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="OrientationHelper"/> class.
+    /// </summary>
+    /// <param name="owner">Specifies the object that owns this helper instance and implements <see cref="IOrientation"/>.</param>
+    public OrientationHelper (IOrientation owner) { _owner = owner; }
+
+    /// <summary>
+    ///     Gets or sets the orientation of the View.
+    /// </summary>
+    public Orientation Orientation
+    {
+        get => _orientation;
+        set
+        {
+            if (_orientation == value)
+            {
+                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<Orientation> 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;
+
+                if (_owner is { })
+                {
+                    _owner.Orientation = value;
+                }
+            }
+
+            // Best practice is to invoke the virtual method first.
+            _owner?.OnOrientationChanged (_orientation);
+
+            // Even though Changed is not cancelable, it is still a good practice to raise the event after.
+            OrientationChanged?.Invoke (_owner, new (in _orientation));
+        }
+    }
+
+    /// <summary>
+    ///     Raised when the orientation is changing. This is cancelable.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Views that implement <see cref="IOrientation"/> should raise <see cref="IOrientation.OrientationChanging"/>
+    ///         after the orientation has changed
+    ///         (<code>_orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);</code>).
+    ///     </para>
+    ///     <para>
+    ///         This event will be raised after the <see cref="IOrientation.OnOrientationChanging"/> method is called (assuming
+    ///         it was not canceled).
+    ///     </para>
+    /// </remarks>
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
+
+    /// <summary>
+    ///     Raised when the orientation has changed.
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Views that implement <see cref="IOrientation"/> should raise <see cref="IOrientation.OrientationChanged"/>
+    ///         after the orientation has changed
+    ///         (<code>_orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);</code>).
+    ///     </para>
+    ///     <para>
+    ///         This event will be raised after the <see cref="IOrientation.OnOrientationChanged"/> method is called.
+    ///     </para>
+    /// </remarks>
+    public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+}

+ 49 - 8
Terminal.Gui/Views/Bar.cs

@@ -11,8 +11,10 @@ namespace Terminal.Gui;
 ///         align them in a specific order.
 ///     </para>
 /// </remarks>
-public class Bar : View
+public class Bar : View, IOrientation, IDesignable
 {
+    private readonly OrientationHelper _orientationHelper;
+
     /// <inheritdoc/>
     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
 
     /// <summary>
     ///     Gets or sets the <see cref="Orientation"/> for this <see cref="Bar"/>. The default is
@@ -58,15 +64,26 @@ public class Bar : View
     ///         Vertical orientation arranges the command, help, and key parts of each <see cref="Shortcut"/>s from left to right.
     ///     </para>
     /// </remarks>
+
     public Orientation Orientation
     {
-        get => _orientation;
-        set
-        {
-            _orientation = value;
-            SetNeedsLayout ();
-        }
+        get => _orientationHelper.Orientation;
+        set => _orientationHelper.Orientation = value;
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
+
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+
+    /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
+    /// <param name="newOrientation"></param>
+    public void OnOrientationChanged (Orientation newOrientation)
+    {
+        SetNeedsLayout ();
     }
+    #endregion
 
     private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd;
 
@@ -224,4 +241,28 @@ public class Bar : View
                 break;
         }
     }
+
+    /// <inheritdoc />
+    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;
+    }
 }

+ 32 - 15
Terminal.Gui/Views/Line.cs

@@ -1,43 +1,60 @@
 namespace Terminal.Gui;
 
 /// <summary>Draws a single line using the <see cref="LineStyle"/> specified by <see cref="View.BorderStyle"/>.</summary>
-public class Line : View
+public class Line : View, IOrientation
 {
+    private readonly OrientationHelper _orientationHelper;
+
     /// <summary>Constructs a Line object.</summary>
     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
     /// <summary>
     ///     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.
     /// </summary>
     public Orientation Orientation
     {
-        get => _orientation;
-        set
-        {
-            _orientation = value;
+        get => _orientationHelper.Orientation;
+        set => _orientationHelper.Orientation = value;
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
 
-            switch (Orientation)
-            {
-                case Orientation.Horizontal:
-                    Height = 1;
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+
+    /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
+    /// <param name="newOrientation"></param>
+    public void OnOrientationChanged (Orientation newOrientation)
+    {
+
+        switch (newOrientation)
+        {
+            case Orientation.Horizontal:
+                Height = 1;
 
-                    break;
-                case Orientation.Vertical:
-                    Width = 1;
+                break;
+            case Orientation.Vertical:
+                Width = 1;
 
-                    break;
+                break;
 
-            }
         }
     }
+    #endregion
 
     /// <inheritdoc/>
     public override void SetBorderStyle (LineStyle value)

+ 0 - 19
Terminal.Gui/Views/OrientationEventArgs.cs

@@ -1,19 +0,0 @@
-namespace Terminal.Gui;
-
-/// <summary><see cref="EventArgs"/> for <see cref="Orientation"/> events.</summary>
-public class OrientationEventArgs : EventArgs
-{
-    /// <summary>Constructs a new instance.</summary>
-    /// <param name="orientation">the new orientation</param>
-    public OrientationEventArgs (Orientation orientation)
-    {
-        Orientation = orientation;
-        Cancel = false;
-    }
-
-    /// <summary>If set to true, the orientation change operation will be canceled, if applicable.</summary>
-    public bool Cancel { get; set; }
-
-    /// <summary>The new orientation.</summary>
-    public Orientation Orientation { get; set; }
-}

+ 46 - 39
Terminal.Gui/Views/RadioGroup.cs

@@ -1,14 +1,14 @@
 namespace Terminal.Gui;
 
 /// <summary>Displays a group of labels each with a selected indicator. Only one of those can be selected at a given time.</summary>
-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<string> _radioLabels = [];
     private int _selected;
+    private readonly OrientationHelper _orientationHelper;
 
     /// <summary>
     ///     Initializes a new instance of the <see cref="RadioGroup"/> class.
@@ -54,6 +54,7 @@ public class RadioGroup : View, IDesignable
                         {
                             return false;
                         }
+
                         MoveHome ();
 
                         return true;
@@ -68,6 +69,7 @@ public class RadioGroup : View, IDesignable
                         {
                             return false;
                         }
+
                         MoveEnd ();
 
                         return true;
@@ -89,6 +91,7 @@ public class RadioGroup : View, IDesignable
                     ctx =>
                     {
                         SetFocus ();
+
                         if (ctx.KeyBinding?.Context is { } && (int)ctx.KeyBinding?.Context! < _radioLabels.Count)
                         {
                             SelectedItem = (int)ctx.KeyBinding?.Context!;
@@ -99,6 +102,11 @@ public class RadioGroup : View, IDesignable
                         return true;
                     });
 
+        _orientationHelper = new (this);
+        _orientationHelper.Orientation = Orientation.Vertical;
+        _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
+        _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
+
         SetupKeyBindings ();
 
         LayoutStarted += RadioGroup_LayoutStarted;
@@ -138,15 +146,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;
 
@@ -169,7 +177,7 @@ public class RadioGroup : View, IDesignable
         get => _horizontalSpace;
         set
         {
-            if (_horizontalSpace != value && _orientation == Orientation.Horizontal)
+            if (_horizontalSpace != value && Orientation == Orientation.Horizontal)
             {
                 _horizontalSpace = value;
                 UpdateTextFormatterText ();
@@ -178,16 +186,6 @@ public class RadioGroup : View, IDesignable
         }
     }
 
-    /// <summary>
-    ///     Gets or sets the <see cref="Orientation"/> for this <see cref="RadioGroup"/>. The default is
-    ///     <see cref="Orientation.Vertical"/>.
-    /// </summary>
-    public Orientation Orientation
-    {
-        get => _orientation;
-        set => OnOrientationChanged (value);
-    }
-
     /// <summary>
     ///     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 <see cref="View.HotKey"/> for details on how HotKeys work.
@@ -319,44 +317,49 @@ public class RadioGroup : View, IDesignable
         }
     }
 
-    /// <summary>Called when the view orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
-    /// <param name="newOrientation"></param>
-    /// <returns>True of the event was cancelled.</returns>
-    public virtual bool OnOrientationChanged (Orientation newOrientation)
+    /// <summary>
+    ///     Gets or sets the <see cref="Orientation"/> for this <see cref="RadioGroup"/>. The default is
+    ///     <see cref="Orientation.Vertical"/>.
+    /// </summary>
+    public Orientation Orientation
     {
-        var args = new OrientationEventArgs (newOrientation);
-        OrientationChanged?.Invoke (this, args);
+        get => _orientationHelper.Orientation;
+        set => _orientationHelper.Orientation = value;
+    }
 
-        if (!args.Cancel)
-        {
-            _orientation = newOrientation;
-            SetupKeyBindings ();
-            SetContentSize ();
-        }
+    #region IOrientation
+
+    /// <inheritdoc/>
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
+
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<Orientation>> OrientationChanged;
 
-        return args.Cancel;
+    /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
+    /// <param name="newOrientation"></param>
+    public void OnOrientationChanged (Orientation newOrientation)
+    {
+        SetupKeyBindings ();
+        SetContentSize ();
     }
 
+    #endregion IOrientation
+
     // TODO: This should be cancelable
     /// <summary>Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.</summary>
     /// <param name="selectedItem"></param>
     /// <param name="previousSelectedItem"></param>
     public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
-    { 
+    {
         if (_selected == selectedItem)
         {
             return;
         }
+
         _selected = selectedItem;
         SelectedItemChanged?.Invoke (this, new (selectedItem, previousSelectedItem));
     }
 
-    /// <summary>
-    ///     Fired when the view orientation has changed. Can be cancelled by setting
-    ///     <see cref="OrientationEventArgs.Cancel"/> to true.
-    /// </summary>
-    public event EventHandler<OrientationEventArgs> OrientationChanged;
-
     /// <inheritdoc/>
     public override Point? PositionCursor ()
     {
@@ -370,7 +373,10 @@ public class RadioGroup : View, IDesignable
 
                 break;
             case Orientation.Horizontal:
-                x = _horizontal [_cursor].pos;
+                if (_horizontal.Count > 0)
+                {
+                    x = _horizontal [_cursor].pos;
+                }
 
                 break;
 
@@ -424,7 +430,7 @@ public class RadioGroup : View, IDesignable
 
     private void SetContentSize ()
     {
-        switch (_orientation)
+        switch (Orientation)
         {
             case Orientation.Vertical:
                 var width = 0;
@@ -457,10 +463,11 @@ public class RadioGroup : View, IDesignable
         }
     }
 
-    /// <inheritdoc />
+    /// <inheritdoc/>
     public bool EnableForDesign ()
     {
         RadioLabels = new [] { "Option _1", "Option _2", "Option _3" };
+
         return true;
     }
 }

+ 82 - 52
Terminal.Gui/Views/Shortcut.cs

@@ -1,10 +1,8 @@
-using System.ComponentModel;
-using System.Threading.Channels;
-
-namespace Terminal.Gui;
+namespace Terminal.Gui;
 
 /// <summary>
-///     Displays a command, help text, and a key binding. When the key specified by <see cref="Key"/> is pressed, the command will be invoked. Useful for
+///     Displays a command, help text, and a key binding. When the key specified by <see cref="Key"/> is pressed, the
+///     command will be invoked. Useful for
 ///     displaying a command in <see cref="Bar"/> such as a
 ///     menu, toolbar, or status bar.
 /// </summary>
@@ -12,12 +10,13 @@ namespace Terminal.Gui;
 ///     <para>
 ///         The following user actions will invoke the <see cref="Command.Accept"/>, causing the
 ///         <see cref="View.Accept"/> event to be fired:
-/// - Clicking on the <see cref="Shortcut"/>.
-/// - Pressing the key specified by <see cref="Key"/>.
-/// - Pressing the HotKey specified by <see cref="CommandView"/>.
+///         - Clicking on the <see cref="Shortcut"/>.
+///         - Pressing the key specified by <see cref="Key"/>.
+///         - Pressing the HotKey specified by <see cref="CommandView"/>.
 ///     </para>
 ///     <para>
-///         If <see cref="KeyBindingScope"/> is <see cref="KeyBindingScope.Application"/>, <see cref="Key"/> will invoked <see cref="Command.Accept"/>
+///         If <see cref="KeyBindingScope"/> is <see cref="KeyBindingScope.Application"/>, <see cref="Key"/> will invoked
+///         <see cref="Command.Accept"/>
 ///         command regardless of what View has focus, enabling an application-wide keyboard shortcut.
 ///     </para>
 ///     <para>
@@ -37,8 +36,10 @@ namespace Terminal.Gui;
 ///         If the <see cref="Key"/> is <see cref="Key.Empty"/>, the <see cref="Key"/> text is not displayed.
 ///     </para>
 /// </remarks>
-public class Shortcut : View
+public class Shortcut : View, IOrientation, IDesignable
 {
+    private readonly OrientationHelper _orientationHelper;
+
     /// <summary>
     ///     Creates a new instance of <see cref="Shortcut"/>.
     /// </summary>
@@ -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,48 @@ public class Shortcut : View
         }
     }
 
-
     /// <summary>
     ///     Creates a new instance of <see cref="Shortcut"/>.
     /// </summary>
     public Shortcut () : this (Key.Empty, string.Empty, null) { }
 
-    private Orientation _orientation = Orientation.Horizontal;
+    #region IOrientation members
 
     /// <summary>
-    ///     Gets or sets the <see cref="Orientation"/> for this <see cref="Shortcut"/>. The default is
-    ///     <see cref="Orientation.Horizontal"/>, which is ideal for status bar, menu bar, and tool bar items If set to
-    ///     <see cref="Orientation.Vertical"/>,
-    ///     the Shortcut will be configured for vertical layout, which is ideal for menu items.
+    ///     Gets or sets the <see cref="Orientation"/> for this <see cref="Bar"/>. The default is
+    ///     <see cref="Orientation.Horizontal"/>.
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         Horizontal orientation arranges the command, help, and key parts of each <see cref="Shortcut"/>s from right to
+    ///         left
+    ///         Vertical orientation arranges the command, help, and key parts of each <see cref="Shortcut"/>s from left to
+    ///         right.
+    ///     </para>
+    /// </remarks>
+
     public Orientation Orientation
     {
-        get => _orientation;
-        set
-        {
-            _orientation = value;
+        get => _orientationHelper.Orientation;
+        set => _orientationHelper.Orientation = value;
+    }
 
-            // TODO: Determine what, if anything, is opinionated about the orientation.
-        }
+    /// <inheritdoc/>
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
+
+    /// <inheritdoc/>
+    public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+
+    /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
+    /// <param name="newOrientation"></param>
+    public void OnOrientationChanged (Orientation newOrientation)
+    {
+        // TODO: Determine what, if anything, is opinionated about the orientation.
+        SetNeedsLayout ();
     }
 
+    #endregion
+
     private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast;
 
     /// <summary>
@@ -344,7 +366,6 @@ public class Shortcut : View
     private void Subview_MouseClick (object sender, MouseEventEventArgs e)
     {
         // TODO: Remove. This does nothing.
-        return;
     }
 
     #region Command
@@ -434,8 +455,6 @@ public class Shortcut : View
             SetKeyViewDefaultLayout ();
             ShowHide ();
             UpdateKeyBinding ();
-
-            return;
         }
     }
 
@@ -475,38 +494,38 @@ public class Shortcut : View
         HelpView.VerticalTextAlignment = Alignment.Center;
     }
 
-/// <summary>
-///     Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to <see cref="HelpText"/>
-///     .
-/// </summary>
-public override string Text
-{
-    get => HelpView?.Text;
-    set
+    /// <summary>
+    ///     Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to <see cref="HelpText"/>
+    ///     .
+    /// </summary>
+    public override string Text
     {
-        if (HelpView is {})
+        get => HelpView?.Text;
+        set
         {
-            HelpView.Text = value;
-            ShowHide ();
+            if (HelpView is { })
+            {
+                HelpView.Text = value;
+                ShowHide ();
+            }
         }
     }
-}
 
-/// <summary>
-///     Gets or sets the help text displayed in the middle of the Shortcut.
-/// </summary>
-public string HelpText
-{
-    get => HelpView?.Text;
-    set
+    /// <summary>
+    ///     Gets or sets the help text displayed in the middle of the Shortcut.
+    /// </summary>
+    public string HelpText
     {
-        if (HelpView is {})
+        get => HelpView?.Text;
+        set
         {
-            HelpView.Text = value;
-            ShowHide ();
+            if (HelpView is { })
+            {
+                HelpView.Text = value;
+                ShowHide ();
+            }
         }
     }
-}
 
     #endregion Help
 
@@ -561,7 +580,7 @@ public string HelpText
     private int _minimumKeyTextSize;
 
     /// <summary>
-    /// Gets or sets the minimum size of the key text. Useful for aligning the key text with other <see cref="Shortcut"/>s.
+    ///     Gets or sets the minimum size of the key text. Useful for aligning the key text with other <see cref="Shortcut"/>s.
     /// </summary>
     public int MinimumKeyTextSize
     {
@@ -675,6 +694,7 @@ public string HelpText
         if (Action is { })
         {
             Action.Invoke ();
+
             // Assume if there's a subscriber to Action, it's handled.
             cancel = true;
         }
@@ -700,11 +720,10 @@ public string HelpText
         {
             return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
         }
-        return false;
 
+        return false;
     }
 
-
     #region Focus
 
     /// <inheritdoc/>
@@ -753,7 +772,8 @@ public string HelpText
         }
     }
 
-    View _lastFocusedView;
+    private View _lastFocusedView;
+
     /// <inheritdoc/>
     public override bool OnEnter (View view)
     {
@@ -774,6 +794,16 @@ public string HelpText
 
     #endregion Focus
 
+    /// <inheritdoc/>
+    public bool EnableForDesign ()
+    {
+        Title = "_Shortcut";
+        HelpText = "Shortcut help";
+        Key = Key.F1;
+
+        return true;
+    }
+
     /// <inheritdoc/>
     protected override void Dispose (bool disposing)
     {

+ 44 - 44
Terminal.Gui/Views/Slider.cs

@@ -21,7 +21,7 @@ public class Slider : Slider<object>
 ///     keyboard or mouse.
 /// </summary>
 /// <typeparam name="T"></typeparam>
-public class Slider<T> : View
+public class Slider<T> : View, IOrientation
 {
     private readonly SliderConfiguration _config = new ();
 
@@ -31,6 +31,8 @@ public class Slider<T> : View
     // Options
     private List<SliderOption<T>> _options;
 
+    private OrientationHelper _orientationHelper;
+
     #region Initialize
 
     private void SetInitialProperties (
@@ -45,11 +47,13 @@ public class Slider<T> : View
 
         _options = options ?? new List<SliderOption<T>> ();
 
-        _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,12 +226,45 @@ public class Slider<T> : View
         }
     }
 
-    /// <summary>Slider Orientation. <see cref="Gui.Orientation"></see></summary>
+
+    /// <summary>
+    ///     Gets or sets the <see cref="Orientation"/>. The default is <see cref="Orientation.Horizontal"/>.
+    /// </summary>
     public Orientation Orientation
     {
-        get => _config._sliderOrientation;
-        set => OnOrientationChanged (value);
+        get => _orientationHelper.Orientation;
+        set => _orientationHelper.Orientation = value;
+    }
+
+    #region IOrientation members
+
+    /// <inheritdoc />
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
+
+    /// <inheritdoc />
+    public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+
+    /// <inheritdoc />
+    public void OnOrientationChanged (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
 
     /// <summary>Legends Orientation. <see cref="Gui.Orientation"></see></summary>
     public Orientation LegendsOrientation
@@ -309,43 +346,6 @@ public class Slider<T> : View
 
     #region Events
 
-    /// <summary>
-    ///     Fired when the slider orientation has changed. Can be cancelled by setting
-    ///     <see cref="OrientationEventArgs.Cancel"/> to true.
-    /// </summary>
-    public event EventHandler<OrientationEventArgs> OrientationChanged;
-
-    /// <summary>Called when the slider orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
-    /// <param name="newOrientation"></param>
-    /// <returns>True of the event was cancelled.</returns>
-    public virtual bool OnOrientationChanged (Orientation newOrientation)
-    {
-        var args = new OrientationEventArgs (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;
-    }
-
     /// <summary>Event raised when the slider option/s changed. The dictionary contains: key = option index, value = T</summary>
     public event EventHandler<SliderEventArgs<T>> OptionsChanged;
 
@@ -1742,7 +1742,7 @@ public class Slider<T> : View
 
     internal bool Select ()
     {
-        SetFocusedOption();
+        SetFocusedOption ();
 
         return true;
     }

+ 71 - 1
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.
 /// </summary>
-public class StatusBar : Bar
+public class StatusBar : Bar, IDesignable
 {
     /// <inheritdoc/>
     public StatusBar () : this ([]) { }
@@ -73,4 +73,74 @@ public class StatusBar : Bar
 
         return view;
     }
+
+    /// <inheritdoc />
+    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}"); }
+    }
+
 }

+ 4 - 6
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

+ 1 - 1
UICatalog/Scenarios/Bars.cs

@@ -402,7 +402,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
         {

+ 3 - 4
UICatalog/Scenarios/ExpanderButton.cs

@@ -74,7 +74,7 @@ public class ExpanderButton : Button
     /// <returns>True of the event was cancelled.</returns>
     protected virtual bool OnOrientationChanging (Orientation newOrientation)
     {
-        var args = new OrientationEventArgs (newOrientation);
+        var args = new CancelEventArgs<Orientation> (in _orientation, ref newOrientation);
         OrientationChanging?.Invoke (this, args);
 
         if (!args.Cancel)
@@ -103,10 +103,9 @@ public class ExpanderButton : Button
     }
 
     /// <summary>
-    ///     Fired when the orientation has changed. Can be cancelled by setting
-    ///     <see cref="OrientationEventArgs.Cancel"/> to true.
+    ///     Fired when the orientation has changed. Can be cancelled.
     /// </summary>
-    public event EventHandler<OrientationEventArgs> OrientationChanging;
+    public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
 
     /// <summary>
     ///     The glyph to display when the view is collapsed.

+ 6 - 3
UnitTests/UnitTests.csproj

@@ -31,9 +31,9 @@
   <ItemGroup>
     <PackageReference Include="JetBrains.Annotations" Version="[2024.2.0,)" PrivateAssets="all" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="[17.10,18)" />
-    <PackageReference Include="Moq" Version="4.20.70" />
-    <PackageReference Include="ReportGenerator" Version="5.3.8" />
-    <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="21.0.29" />
+    <PackageReference Include="Moq" Version="[4.20.70,5)" />
+    <PackageReference Include="ReportGenerator" Version="[5.3.8,6)" />
+    <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[21.0.29,22)" />
     <PackageReference Include="xunit" Version="[2.9.0,3)" />
     <PackageReference Include="Xunit.Combinatorial" Version="[1.6.24,2)" />
     <PackageReference Include="xunit.runner.visualstudio" Version="[2.8.2,3)">
@@ -59,6 +59,9 @@
     <Using Include="Terminal.Gui" />
     <Using Include="Xunit" />
   </ItemGroup>
+  <ItemGroup>
+    <Folder Include="View\Orientation\" />
+  </ItemGroup>
   <PropertyGroup Label="FineCodeCoverage">
     <Enabled>
       False

+ 107 - 0
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<IOrientation> mockIOrientation = new Mock<IOrientation> ();
+        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.Vertical;
+
+        // 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<IOrientation> mockIOrientation = new Mock<IOrientation> ();
+        var onChangingOverrideCalled = false;
+        var onChangedOverrideCalled = false;
+
+        mockIOrientation.Setup (x => x.OnOrientationChanging (It.IsAny<Orientation> (), It.IsAny<Orientation> ()))
+                        .Callback (() => onChangingOverrideCalled = true)
+                        .Returns (false); // Ensure it doesn't cancel the change
+
+        mockIOrientation.Setup (x => x.OnOrientationChanged (It.IsAny<Orientation> ()))
+                        .Callback (() => onChangedOverrideCalled = true);
+
+        var orientationHelper = new OrientationHelper (mockIOrientation.Object);
+
+        // Act
+        orientationHelper.Orientation = Orientation.Vertical;
+
+        // 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<IOrientation> mockIOrientation = new Mock<IOrientation> ();
+        var orientationHelper = new OrientationHelper (mockIOrientation.Object);
+        orientationHelper.Orientation = Orientation.Horizontal; // 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.Horizontal; // 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<IOrientation> mockIOrientation = new Mock<IOrientation> ();
+        var orientationHelper = new OrientationHelper (mockIOrientation.Object);
+        orientationHelper.OrientationChanging += (sender, e) => { e.Cancel = true; }; // Cancel the change
+
+        // Act
+        orientationHelper.Orientation = Orientation.Vertical;
+
+        // Assert
+        Assert.Equal (Orientation.Horizontal, orientationHelper.Orientation); // Initial orientation is Horizontal
+    }
+
+    [Fact]
+    public void Orientation_Set_NewValue_OnOrientationChanging_CancelsChange ()
+    {
+        // Arrange
+        Mock<IOrientation> mockIOrientation = new Mock<IOrientation> ();
+
+        mockIOrientation.Setup (x => x.OnOrientationChanging (It.IsAny<Orientation> (), It.IsAny<Orientation> ()))
+                        .Returns (true); // Override to return true, cancelling the change
+
+        var orientationHelper = new OrientationHelper (mockIOrientation.Object);
+
+        // Act
+        orientationHelper.Orientation = Orientation.Vertical;
+
+        // Assert
+        Assert.Equal (
+                      Orientation.Horizontal,
+                      orientationHelper.Orientation); // Initial orientation is Horizontal, and it should remain unchanged due to cancellation
+    }
+}

+ 136 - 0
UnitTests/View/Orientation/OrientationTests.cs

@@ -0,0 +1,136 @@
+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<CancelEventArgs<Orientation>> OrientationChanging;
+        public event EventHandler<EventArgs<Orientation>> OrientationChanged;
+
+        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 newOrientation)
+        {
+            OnOrientationChangedCalled = true;
+            // 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
+    }
+
+
+    [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.");
+    }
+}