Browse Source

Merge pull request #3625 from tig/v2-IOrientation

Adds `IOrientation` and `OrientationHelper` - opinionated changing/changed event pattern
Tig 1 year ago
parent
commit
94fdcbe413

+ 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; }
-}

+ 47 - 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.
@@ -44,6 +44,7 @@ public class RadioGroup : View, IDesignable
                         {
                             return false;
                         }
+
                         MoveDownRight ();
 
                         return true;
@@ -58,6 +59,7 @@ public class RadioGroup : View, IDesignable
                         {
                             return false;
                         }
+
                         MoveHome ();
 
                         return true;
@@ -72,6 +74,7 @@ public class RadioGroup : View, IDesignable
                         {
                             return false;
                         }
+
                         MoveEnd ();
 
                         return true;
@@ -93,6 +96,7 @@ public class RadioGroup : View, IDesignable
                     ctx =>
                     {
                         SetFocus ();
+
                         if (ctx.KeyBinding?.Context is { } && (int)ctx.KeyBinding?.Context! < _radioLabels.Count)
                         {
                             SelectedItem = (int)ctx.KeyBinding?.Context!;
@@ -103,6 +107,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;
@@ -142,15 +151,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 +182,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 +191,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.
@@ -323,44 +322,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 ()
     {
@@ -374,7 +378,10 @@ public class RadioGroup : View, IDesignable
 
                 break;
             case Orientation.Horizontal:
-                x = _horizontal [_cursor].pos;
+                if (_horizontal.Count > 0)
+                {
+                    x = _horizontal [_cursor].pos;
+                }
 
                 break;
 
@@ -429,7 +436,7 @@ public class RadioGroup : View, IDesignable
 
     private void SetContentSize ()
     {
-        switch (_orientation)
+        switch (Orientation)
         {
             case Orientation.Vertical:
                 var width = 0;
@@ -462,10 +469,11 @@ public class RadioGroup : View, IDesignable
         }
     }
 
-    /// <inheritdoc />
+    /// <inheritdoc/>
     public bool EnableForDesign ()
     {
         RadioLabels = new [] { "Option _1", "Option _2", "Option _3" };
+
         return true;
     }
 }

+ 316 - 286
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,359 +455,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 ();
-}
+    {
+        CommandView.Margin.Thickness = GetMarginThickness ();
+        CommandView.X = Pos.Align (Alignment.End, AlignmentModes);
+        CommandView.Y = 0; //Pos.Center ();
+    }
 
-private void Shortcut_TitleChanged (object sender, EventArgs<string> 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;
-}
+    private void Shortcut_TitleChanged (object sender, EventArgs<string> 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
+    #endregion Command
 
-#region Help
+    #region Help
 
-/// <summary>
-///     The subview that displays the help text for the command. Internal for unit testing.
-/// </summary>
-internal View HelpView { get; } = new ();
+    /// <summary>
+    ///     The subview that displays the help text for the command. Internal for unit testing.
+    /// </summary>
+    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;
-}
+    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;
+    }
 
-/// <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
+    #endregion Help
 
-#region Key
+    #region Key
 
-private Key _key = Key.Empty;
+    private Key _key = Key.Empty;
 
-/// <summary>
-///     Gets or sets the <see cref="Key"/> that will be bound to the <see cref="Command.Accept"/> command.
-/// </summary>
-public Key Key
-{
-    get => _key;
-    set
+    /// <summary>
+    ///     Gets or sets the <see cref="Key"/> that will be bound to the <see cref="Command.Accept"/> command.
+    /// </summary>
+    public Key Key
     {
-        if (value == null)
+        get => _key;
+        set
         {
-            throw new ArgumentNullException ();
-        }
+            if (value == null)
+            {
+                throw new ArgumentNullException ();
+            }
 
-        _key = value;
+            _key = value;
 
-        UpdateKeyBinding ();
+            UpdateKeyBinding ();
 
-        KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}";
-        ShowHide ();
+            KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}";
+            ShowHide ();
+        }
     }
-}
 
-private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey;
+    private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey;
 
-/// <summary>
-///     Gets or sets the scope for the key binding for how <see cref="Key"/> is bound to <see cref="Command"/>.
-/// </summary>
-public KeyBindingScope KeyBindingScope
-{
-    get => _keyBindingScope;
-    set
+    /// <summary>
+    ///     Gets or sets the scope for the key binding for how <see cref="Key"/> is bound to <see cref="Command"/>.
+    /// </summary>
+    public KeyBindingScope KeyBindingScope
     {
-        _keyBindingScope = value;
+        get => _keyBindingScope;
+        set
+        {
+            _keyBindingScope = value;
 
-        UpdateKeyBinding ();
+            UpdateKeyBinding ();
+        }
     }
-}
 
-/// <summary>
-///     Gets the subview that displays the key. Internal for unit testing.
-/// </summary>
+    /// <summary>
+    ///     Gets the subview that displays the key. Internal for unit testing.
+    /// </summary>
 
-internal View KeyView { get; } = new ();
+    internal View KeyView { get; } = new ();
 
-private int _minimumKeyTextSize;
+    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.
-/// </summary>
-public int MinimumKeyTextSize
-{
-    get => _minimumKeyTextSize;
-    set
+    /// <summary>
+    ///     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
     {
-        if (value == _minimumKeyTextSize)
+        get => _minimumKeyTextSize;
+        set
         {
-            //return;
-        }
+            if (value == _minimumKeyTextSize)
+            {
+                //return;
+            }
 
-        _minimumKeyTextSize = value;
-        SetKeyViewDefaultLayout ();
-        CommandView.SetNeedsLayout ();
-        HelpView.SetNeedsLayout ();
-        KeyView.SetNeedsLayout ();
-        SetSubViewNeedsDisplay ();
+            _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 int GetMinimumKeyViewSize () { return MinimumKeyTextSize; }
 
-private void UpdateKeyBinding ()
-{
-    if (Key != null)
+    private void SetKeyViewDefaultLayout ()
     {
-        // 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);
+        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 ();
     }
-}
-
-#endregion Key
 
-#region Accept Handling
+    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);
 
-/// <summary>
-///     Called when the <see cref="Command.Accept"/> 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).
-/// </summary>
-protected bool? OnAccept (CommandContext ctx)
-{
-    var cancel = false;
+            //KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.Accept);
+        }
+    }
 
-    switch (ctx.KeyBinding?.Scope)
-    {
-        case KeyBindingScope.Application:
-            cancel = base.OnAccept () == true;
+    #endregion Key
 
-            break;
+    #region Accept Handling
 
-        case KeyBindingScope.Focused:
-            base.OnAccept ();
+    /// <summary>
+    ///     Called when the <see cref="Command.Accept"/> 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).
+    /// </summary>
+    protected bool? OnAccept (CommandContext ctx)
+    {
+        var cancel = false;
 
-            // cancel if we're focused
-            cancel = true;
+        switch (ctx.KeyBinding?.Scope)
+        {
+            case KeyBindingScope.Application:
+                cancel = base.OnAccept () == true;
 
-            break;
+                break;
 
-        case KeyBindingScope.HotKey:
-            cancel = base.OnAccept () == true;
+            case KeyBindingScope.Focused:
+                base.OnAccept ();
 
-            if (CanFocus)
-            {
-                SetFocus ();
+                // cancel if we're focused
                 cancel = true;
-            }
 
-            break;
+                break;
 
-        default:
-            // Mouse
-            cancel = base.OnAccept () == true;
+            case KeyBindingScope.HotKey:
+                cancel = base.OnAccept () == true;
 
-            break;
-    }
+                if (CanFocus)
+                {
+                    SetFocus ();
+                    cancel = true;
+                }
 
-    CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding);
+                break;
 
-    if (Action is { })
-    {
-        Action.Invoke ();
-        // Assume if there's a subscriber to Action, it's handled.
-        cancel = true;
-    }
+            default:
+                // Mouse
+                cancel = base.OnAccept () == true;
 
-    return cancel;
-}
+                break;
+        }
 
-/// <summary>
-///     Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the
-///     mouse.
-/// </summary>
-/// <remarks>
-///     Note, the <see cref="View.Accept"/> event is fired first, and if cancelled, the event will not be invoked.
-/// </remarks>
-[CanBeNull]
-public Action Action { get; set; }
+        CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding);
 
-#endregion Accept Handling
+        if (Action is { })
+        {
+            Action.Invoke ();
 
-private bool? OnSelect (CommandContext ctx)
-{
-    if (CommandView.GetSupportedCommands ().Contains (Command.Select))
-    {
-        return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
-    }
-    return false;
+            // Assume if there's a subscriber to Action, it's handled.
+            cancel = true;
+        }
 
-}
+        return cancel;
+    }
 
+    /// <summary>
+    ///     Gets or sets the action to be invoked when the shortcut key is pressed or the shortcut is clicked on with the
+    ///     mouse.
+    /// </summary>
+    /// <remarks>
+    ///     Note, the <see cref="View.Accept"/> event is fired first, and if cancelled, the event will not be invoked.
+    /// </remarks>
+    [CanBeNull]
+    public Action Action { get; set; }
 
-#region Focus
+    #endregion Accept Handling
 
-/// <inheritdoc/>
-public override ColorScheme ColorScheme
-{
-    get => base.ColorScheme;
-    set
+    private bool? OnSelect (CommandContext ctx)
     {
-        base.ColorScheme = value;
-        SetColors ();
+        if (CommandView.GetSupportedCommands ().Contains (Command.Select))
+        {
+            return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
+        }
+
+        return false;
     }
-}
 
-/// <summary>
-/// </summary>
-internal void SetColors ()
-{
-    // Border should match superview.
-    Border.ColorScheme = SuperView?.ColorScheme;
+    #region Focus
 
-    if (HasFocus)
+    /// <inheritdoc/>
+    public override ColorScheme ColorScheme
     {
-        // When we have focus, we invert the colors
-        base.ColorScheme = new (base.ColorScheme)
+        get => base.ColorScheme;
+        set
         {
-            Normal = base.ColorScheme.Focus,
-            HotNormal = base.ColorScheme.HotFocus,
-            HotFocus = base.ColorScheme.HotNormal,
-            Focus = base.ColorScheme.Normal
-        };
-    }
-    else
-    {
-        base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme;
+            base.ColorScheme = value;
+            SetColors ();
+        }
     }
 
-    // Set KeyView's colors to show "hot"
-    if (IsInitialized && base.ColorScheme is { })
+    /// <summary>
+    /// </summary>
+    internal void SetColors ()
     {
-        var cs = new ColorScheme (base.ColorScheme)
+        // Border should match superview.
+        Border.ColorScheme = SuperView?.ColorScheme;
+
+        if (HasFocus)
         {
-            Normal = base.ColorScheme.HotNormal,
-            HotNormal = base.ColorScheme.Normal
-        };
-        KeyView.ColorScheme = cs;
+            // 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;
+        }
     }
-}
 
-View _lastFocusedView;
-/// <inheritdoc/>
-public override bool OnEnter (View view)
-{
-    SetColors ();
-    _lastFocusedView = view;
+    private View _lastFocusedView;
 
-    return base.OnEnter (view);
-}
+    /// <inheritdoc/>
+    public override bool OnEnter (View view)
+    {
+        SetColors ();
+        _lastFocusedView = view;
 
-/// <inheritdoc/>
-public override bool OnLeave (View view)
-{
-    SetColors ();
-    _lastFocusedView = this;
+        return base.OnEnter (view);
+    }
 
-    return base.OnLeave (view);
-}
+    /// <inheritdoc/>
+    public override bool OnLeave (View view)
+    {
+        SetColors ();
+        _lastFocusedView = this;
 
-#endregion Focus
+        return base.OnLeave (view);
+    }
 
-/// <inheritdoc/>
-protected override void Dispose (bool disposing)
-{
-    if (disposing)
+    #endregion Focus
+
+    /// <inheritdoc/>
+    public bool EnableForDesign ()
     {
-        if (CommandView?.IsAdded == false)
-        {
-            CommandView.Dispose ();
-        }
+        Title = "_Shortcut";
+        HelpText = "Shortcut help";
+        Key = Key.F1;
+        return true;
+    }
 
-        if (HelpView?.IsAdded == false)
+    /// <inheritdoc/>
+    protected override void Dispose (bool disposing)
+    {
+        if (disposing)
         {
-            HelpView.Dispose ();
-        }
+            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);
+    }
 }

+ 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;
 
@@ -1738,7 +1738,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 ([]) { }
@@ -74,4 +74,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.

+ 7 - 3
UnitTests/UnitTests.csproj

@@ -30,9 +30,10 @@
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="JetBrains.Annotations" Version="[2024.2.0,)" PrivateAssets="all" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="[17.10,18)" />
-    <PackageReference Include="ReportGenerator" Version="[5.3.7,6)" />
-    <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="[21.0.22,22)" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.0-release-24352-06" />
+    <PackageReference Include="Moq" Version="4.20.70" />
+    <PackageReference Include="ReportGenerator" Version="5.3.8" />
+    <PackageReference Include="TestableIO.System.IO.Abstractions.TestingHelpers" Version="21.0.26" />
     <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)">
@@ -58,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.");
+    }
+}