Merge pull request #3625 from tig/v2-IOrientation

Adds `IOrientation` and `OrientationHelper` - opinionated changing/changed event pattern
This commit is contained in:
Tig
2024-08-02 13:35:12 -04:00
committed by GitHub
16 changed files with 1057 additions and 486 deletions

View File

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

View File

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

View File

@@ -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,16 +64,27 @@ 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;
/// <summary>
@@ -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;
}
}

View File

@@ -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
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)
{
switch (newOrientation)
{
_orientation = value;
case Orientation.Horizontal:
Height = 1;
switch (Orientation)
{
case Orientation.Horizontal:
Height = 1;
break;
case Orientation.Vertical:
Width = 1;
break;
case Orientation.Vertical:
Width = 1;
break;
break;
}
}
}
#endregion
/// <inheritdoc/>
public override void SetBorderStyle (LineStyle value)

View File

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

View File

@@ -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);
if (!args.Cancel)
{
_orientation = newOrientation;
SetupKeyBindings ();
SetContentSize ();
}
return args.Cancel;
get => _orientationHelper.Orientation;
set => _orientationHelper.Orientation = value;
}
#region IOrientation
/// <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)
{
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;
}
}

View File

@@ -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;
// TODO: Determine what, if anything, is opinionated about the orientation.
}
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)
{
// 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 ();
}
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
#region Help
/// <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;
}
/// <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
{
if (HelpView is {})
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;
}
#endregion Command
#region Help
/// <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;
}
/// <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
{
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
{
if (HelpView is {})
{
HelpView.Text = value;
ShowHide ();
}
}
}
#endregion Help
#region Key
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
{
if (value == null)
{
throw new ArgumentNullException ();
}
_key = value;
UpdateKeyBinding ();
KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}";
ShowHide ();
}
}
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
{
_keyBindingScope = value;
UpdateKeyBinding ();
}
}
/// <summary>
/// Gets the subview that displays the key. Internal for unit testing.
/// </summary>
internal View KeyView { get; } = new ();
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
{
if (value == _minimumKeyTextSize)
{
//return;
}
_minimumKeyTextSize = value;
SetKeyViewDefaultLayout ();
CommandView.SetNeedsLayout ();
HelpView.SetNeedsLayout ();
KeyView.SetNeedsLayout ();
SetSubViewNeedsDisplay ();
}
}
private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; }
private void SetKeyViewDefaultLayout ()
{
KeyView.Margin.Thickness = GetMarginThickness ();
KeyView.X = Pos.Align (Alignment.End, AlignmentModes);
KeyView.Y = 0; //Pos.Center ();
KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize));
KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1;
KeyView.Visible = true;
// Right align the text in the keyview
KeyView.TextAlignment = Alignment.End;
KeyView.VerticalTextAlignment = Alignment.Center;
KeyView.KeyBindings.Clear ();
}
private void UpdateKeyBinding ()
{
if (Key != null)
{
// Disable the command view key bindings
CommandView.KeyBindings.Remove (Key);
CommandView.KeyBindings.Remove (CommandView.HotKey);
KeyBindings.Remove (Key);
KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.Accept);
//KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.Accept);
}
}
#endregion Key
#region Accept Handling
/// <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;
switch (ctx.KeyBinding?.Scope)
{
case KeyBindingScope.Application:
cancel = base.OnAccept () == true;
break;
case KeyBindingScope.Focused:
base.OnAccept ();
// cancel if we're focused
cancel = true;
break;
case KeyBindingScope.HotKey:
cancel = base.OnAccept () == true;
if (CanFocus)
if (HelpView is {})
{
SetFocus ();
cancel = true;
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
{
if (HelpView is {})
{
HelpView.Text = value;
ShowHide ();
}
}
}
#endregion Help
#region Key
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
{
if (value == null)
{
throw new ArgumentNullException ();
}
break;
_key = value;
default:
// Mouse
cancel = base.OnAccept () == true;
UpdateKeyBinding ();
break;
KeyView.Text = Key == Key.Empty ? string.Empty : $"{Key}";
ShowHide ();
}
}
CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding);
private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey;
if (Action is { })
/// <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
{
Action.Invoke ();
// Assume if there's a subscriber to Action, it's handled.
cancel = true;
get => _keyBindingScope;
set
{
_keyBindingScope = value;
UpdateKeyBinding ();
}
}
return cancel;
}
/// <summary>
/// Gets the subview that displays the key. Internal for unit testing.
/// </summary>
/// <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; }
internal View KeyView { get; } = new ();
#endregion Accept Handling
private int _minimumKeyTextSize;
private bool? OnSelect (CommandContext ctx)
{
if (CommandView.GetSupportedCommands ().Contains (Command.Select))
/// <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
{
return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
get => _minimumKeyTextSize;
set
{
if (value == _minimumKeyTextSize)
{
//return;
}
_minimumKeyTextSize = value;
SetKeyViewDefaultLayout ();
CommandView.SetNeedsLayout ();
HelpView.SetNeedsLayout ();
KeyView.SetNeedsLayout ();
SetSubViewNeedsDisplay ();
}
}
return false;
}
private int GetMinimumKeyViewSize () { return MinimumKeyTextSize; }
#region Focus
/// <inheritdoc/>
public override ColorScheme ColorScheme
{
get => base.ColorScheme;
set
private void SetKeyViewDefaultLayout ()
{
KeyView.Margin.Thickness = GetMarginThickness ();
KeyView.X = Pos.Align (Alignment.End, AlignmentModes);
KeyView.Y = 0; //Pos.Center ();
KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (GetMinimumKeyViewSize));
KeyView.Height = CommandView?.Visible == true ? Dim.Height (CommandView) : 1;
KeyView.Visible = true;
// Right align the text in the keyview
KeyView.TextAlignment = Alignment.End;
KeyView.VerticalTextAlignment = Alignment.Center;
KeyView.KeyBindings.Clear ();
}
private void UpdateKeyBinding ()
{
if (Key != null)
{
// Disable the command view key bindings
CommandView.KeyBindings.Remove (Key);
CommandView.KeyBindings.Remove (CommandView.HotKey);
KeyBindings.Remove (Key);
KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.Accept);
//KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.Accept);
}
}
#endregion Key
#region Accept Handling
/// <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;
switch (ctx.KeyBinding?.Scope)
{
case KeyBindingScope.Application:
cancel = base.OnAccept () == true;
break;
case KeyBindingScope.Focused:
base.OnAccept ();
// cancel if we're focused
cancel = true;
break;
case KeyBindingScope.HotKey:
cancel = base.OnAccept () == true;
if (CanFocus)
{
SetFocus ();
cancel = true;
}
break;
default:
// Mouse
cancel = base.OnAccept () == true;
break;
}
CommandView.InvokeCommand (Command.Accept, ctx.Key, ctx.KeyBinding);
if (Action is { })
{
Action.Invoke ();
// Assume if there's a subscriber to Action, it's handled.
cancel = true;
}
return cancel;
}
/// <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; }
#endregion Accept Handling
private bool? OnSelect (CommandContext ctx)
{
if (CommandView.GetSupportedCommands ().Contains (Command.Select))
{
return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
}
return false;
}
#region Focus
/// <inheritdoc/>
public override ColorScheme ColorScheme
{
get => base.ColorScheme;
set
{
base.ColorScheme = value;
SetColors ();
}
}
/// <summary>
/// </summary>
internal void SetColors ()
{
// Border should match superview.
Border.ColorScheme = SuperView?.ColorScheme;
if (HasFocus)
{
// When we have focus, we invert the colors
base.ColorScheme = new (base.ColorScheme)
{
Normal = base.ColorScheme.Focus,
HotNormal = base.ColorScheme.HotFocus,
HotFocus = base.ColorScheme.HotNormal,
Focus = base.ColorScheme.Normal
};
}
else
{
base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme;
}
// Set KeyView's colors to show "hot"
if (IsInitialized && base.ColorScheme is { })
{
var cs = new ColorScheme (base.ColorScheme)
{
Normal = base.ColorScheme.HotNormal,
HotNormal = base.ColorScheme.Normal
};
KeyView.ColorScheme = cs;
}
}
private View _lastFocusedView;
/// <inheritdoc/>
public override bool OnEnter (View view)
{
base.ColorScheme = value;
SetColors ();
_lastFocusedView = view;
return base.OnEnter (view);
}
}
/// <summary>
/// </summary>
internal void SetColors ()
{
// Border should match superview.
Border.ColorScheme = SuperView?.ColorScheme;
if (HasFocus)
/// <inheritdoc/>
public override bool OnLeave (View view)
{
// When we have focus, we invert the colors
base.ColorScheme = new (base.ColorScheme)
SetColors ();
_lastFocusedView = this;
return base.OnLeave (view);
}
#endregion Focus
/// <inheritdoc/>
public bool EnableForDesign ()
{
Title = "_Shortcut";
HelpText = "Shortcut help";
Key = Key.F1;
return true;
}
/// <inheritdoc/>
protected override void Dispose (bool disposing)
{
if (disposing)
{
Normal = base.ColorScheme.Focus,
HotNormal = base.ColorScheme.HotFocus,
HotFocus = base.ColorScheme.HotNormal,
Focus = base.ColorScheme.Normal
};
}
else
{
base.ColorScheme = SuperView?.ColorScheme ?? base.ColorScheme;
}
if (CommandView?.IsAdded == false)
{
CommandView.Dispose ();
}
// Set KeyView's colors to show "hot"
if (IsInitialized && base.ColorScheme is { })
{
var cs = new ColorScheme (base.ColorScheme)
{
Normal = base.ColorScheme.HotNormal,
HotNormal = base.ColorScheme.Normal
};
KeyView.ColorScheme = cs;
}
}
if (HelpView?.IsAdded == false)
{
HelpView.Dispose ();
}
View _lastFocusedView;
/// <inheritdoc/>
public override bool OnEnter (View view)
{
SetColors ();
_lastFocusedView = view;
return base.OnEnter (view);
}
/// <inheritdoc/>
public override bool OnLeave (View view)
{
SetColors ();
_lastFocusedView = this;
return base.OnLeave (view);
}
#endregion Focus
/// <inheritdoc/>
protected override void Dispose (bool disposing)
{
if (disposing)
{
if (CommandView?.IsAdded == false)
{
CommandView.Dispose ();
if (KeyView?.IsAdded == false)
{
KeyView.Dispose ();
}
}
if (HelpView?.IsAdded == false)
{
HelpView.Dispose ();
}
if (KeyView?.IsAdded == false)
{
KeyView.Dispose ();
}
base.Dispose (disposing);
}
base.Dispose (disposing);
}
}

View File

@@ -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,13 +226,46 @@ 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;
}

View File

@@ -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}"); }
}
}

View File

@@ -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

View File

@@ -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
{

View File

@@ -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.

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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.");
}
}