merged with v2_develop

This commit is contained in:
Tig
2024-08-02 11:56:45 -06:00
17 changed files with 766 additions and 197 deletions

View File

@@ -16,14 +16,14 @@ public static class Program
#region The code in this region is not intended for use in a self-contained single-file. It's just here to make sure there is no functionality break with localization in Terminal.Gui using single-file
if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture) && Application.SupportedCultures.Count == 0)
if (Equals (Thread.CurrentThread.CurrentUICulture, CultureInfo.InvariantCulture) && Application.SupportedCultures?.Count == 0)
{
// Only happens if the project has <InvariantGlobalization>true</InvariantGlobalization>
Debug.Assert (Application.SupportedCultures.Count == 0);
}
else
{
Debug.Assert (Application.SupportedCultures.Count > 0);
Debug.Assert (Application.SupportedCultures?.Count > 0);
Debug.Assert (Equals (CultureInfo.CurrentCulture, Thread.CurrentThread.CurrentUICulture));
}

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.
@@ -54,6 +54,7 @@ public class RadioGroup : View, IDesignable
{
return false;
}
MoveHome ();
return true;
@@ -68,6 +69,7 @@ public class RadioGroup : View, IDesignable
{
return false;
}
MoveEnd ();
return true;
@@ -89,6 +91,7 @@ public class RadioGroup : View, IDesignable
ctx =>
{
SetFocus ();
if (ctx.KeyBinding?.Context is { } && (int)ctx.KeyBinding?.Context! < _radioLabels.Count)
{
SelectedItem = (int)ctx.KeyBinding?.Context!;
@@ -99,6 +102,11 @@ public class RadioGroup : View, IDesignable
return true;
});
_orientationHelper = new (this);
_orientationHelper.Orientation = Orientation.Vertical;
_orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
_orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
SetupKeyBindings ();
LayoutStarted += RadioGroup_LayoutStarted;
@@ -138,15 +146,15 @@ public class RadioGroup : View, IDesignable
int viewportX = e.MouseEvent.Position.X;
int viewportY = e.MouseEvent.Position.Y;
int pos = _orientation == Orientation.Horizontal ? viewportX : viewportY;
int pos = Orientation == Orientation.Horizontal ? viewportX : viewportY;
int rCount = _orientation == Orientation.Horizontal
int rCount = Orientation == Orientation.Horizontal
? _horizontal.Last ().pos + _horizontal.Last ().length
: _radioLabels.Count;
if (pos < rCount)
{
int c = _orientation == Orientation.Horizontal
int c = Orientation == Orientation.Horizontal
? _horizontal.FindIndex (x => x.pos <= viewportX && x.pos + x.length - 2 >= viewportX)
: viewportY;
@@ -169,7 +177,7 @@ public class RadioGroup : View, IDesignable
get => _horizontalSpace;
set
{
if (_horizontalSpace != value && _orientation == Orientation.Horizontal)
if (_horizontalSpace != value && Orientation == Orientation.Horizontal)
{
_horizontalSpace = value;
UpdateTextFormatterText ();
@@ -178,16 +186,6 @@ public class RadioGroup : View, IDesignable
}
}
/// <summary>
/// Gets or sets the <see cref="Orientation"/> for this <see cref="RadioGroup"/>. The default is
/// <see cref="Orientation.Vertical"/>.
/// </summary>
public Orientation Orientation
{
get => _orientation;
set => OnOrientationChanged (value);
}
/// <summary>
/// The radio labels to display. A key binding will be added for each radio enabling the user to select
/// and/or focus the radio label using the keyboard. See <see cref="View.HotKey"/> for details on how HotKeys work.
@@ -319,44 +317,49 @@ public class RadioGroup : View, IDesignable
}
}
/// <summary>Called when the view orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
/// <param name="newOrientation"></param>
/// <returns>True of the event was cancelled.</returns>
public virtual bool OnOrientationChanged (Orientation newOrientation)
/// <summary>
/// Gets or sets the <see cref="Orientation"/> for this <see cref="RadioGroup"/>. The default is
/// <see cref="Orientation.Vertical"/>.
/// </summary>
public Orientation Orientation
{
var args = new OrientationEventArgs (newOrientation);
OrientationChanged?.Invoke (this, args);
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 ()
{
@@ -370,7 +373,10 @@ public class RadioGroup : View, IDesignable
break;
case Orientation.Horizontal:
x = _horizontal [_cursor].pos;
if (_horizontal.Count > 0)
{
x = _horizontal [_cursor].pos;
}
break;
@@ -424,7 +430,7 @@ public class RadioGroup : View, IDesignable
private void SetContentSize ()
{
switch (_orientation)
switch (Orientation)
{
case Orientation.Vertical:
var width = 0;
@@ -457,10 +463,11 @@ public class RadioGroup : View, IDesignable
}
}
/// <inheritdoc />
/// <inheritdoc/>
public bool EnableForDesign ()
{
RadioLabels = new [] { "Option _1", "Option _2", "Option _3" };
return true;
}
}

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,8 +455,6 @@ public class Shortcut : View
SetKeyViewDefaultLayout ();
ShowHide ();
UpdateKeyBinding ();
return;
}
}
@@ -475,38 +494,38 @@ public class Shortcut : View
HelpView.VerticalTextAlignment = Alignment.Center;
}
/// <summary>
/// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to <see cref="HelpText"/>
/// .
/// </summary>
public override string Text
{
get => HelpView?.Text;
set
/// <summary>
/// Gets or sets the help text displayed in the middle of the Shortcut. Identical in function to <see cref="HelpText"/>
/// .
/// </summary>
public override string Text
{
if (HelpView is {})
get => HelpView?.Text;
set
{
HelpView.Text = value;
ShowHide ();
if (HelpView is { })
{
HelpView.Text = value;
ShowHide ();
}
}
}
}
/// <summary>
/// Gets or sets the help text displayed in the middle of the Shortcut.
/// </summary>
public string HelpText
{
get => HelpView?.Text;
set
/// <summary>
/// Gets or sets the help text displayed in the middle of the Shortcut.
/// </summary>
public string HelpText
{
if (HelpView is {})
get => HelpView?.Text;
set
{
HelpView.Text = value;
ShowHide ();
if (HelpView is { })
{
HelpView.Text = value;
ShowHide ();
}
}
}
}
#endregion Help
@@ -561,7 +580,7 @@ public string HelpText
private int _minimumKeyTextSize;
/// <summary>
/// Gets or sets the minimum size of the key text. Useful for aligning the key text with other <see cref="Shortcut"/>s.
/// Gets or sets the minimum size of the key text. Useful for aligning the key text with other <see cref="Shortcut"/>s.
/// </summary>
public int MinimumKeyTextSize
{
@@ -675,6 +694,7 @@ public string HelpText
if (Action is { })
{
Action.Invoke ();
// Assume if there's a subscriber to Action, it's handled.
cancel = true;
}
@@ -700,11 +720,10 @@ public string HelpText
{
return CommandView.InvokeCommand (Command.Select, ctx.Key, ctx.KeyBinding);
}
return false;
}
#region Focus
/// <inheritdoc/>
@@ -753,7 +772,8 @@ public string HelpText
}
}
View _lastFocusedView;
private View _lastFocusedView;
/// <inheritdoc/>
public override bool OnEnter (View view)
{
@@ -774,6 +794,16 @@ public string HelpText
#endregion Focus
/// <inheritdoc/>
public bool EnableForDesign ()
{
Title = "_Shortcut";
HelpText = "Shortcut help";
Key = Key.F1;
return true;
}
/// <inheritdoc/>
protected override void Dispose (bool disposing)
{

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;
@@ -1742,7 +1742,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 ([]) { }
@@ -73,4 +73,74 @@ public class StatusBar : Bar
return view;
}
/// <inheritdoc />
bool IDesignable.EnableForDesign ()
{
var shortcut = new Shortcut
{
Text = "Quit",
Title = "Q_uit",
Key = Key.Z.WithCtrl,
};
Add (shortcut);
shortcut = new Shortcut
{
Text = "Help Text",
Title = "Help",
Key = Key.F1,
};
Add (shortcut);
shortcut = new Shortcut
{
Title = "_Show/Hide",
Key = Key.F10,
CommandView = new CheckBox
{
CanFocus = false,
Text = "_Show/Hide"
},
};
Add (shortcut);
var button1 = new Button
{
Text = "I'll Hide",
// Visible = false
};
button1.Accept += Button_Clicked;
Add (button1);
shortcut.Accept += (s, e) =>
{
button1.Visible = !button1.Visible;
button1.Enabled = button1.Visible;
e.Handled = false;
};
Add (new Label
{
HotKeySpecifier = new Rune ('_'),
Text = "Fo_cusLabel",
CanFocus = true
});
var button2 = new Button
{
Text = "Or me!",
};
button2.Accept += (s, e) => Application.RequestStop ();
Add (button2);
return true;
void Button_Clicked (object sender, EventArgs e) { MessageBox.Query ("Hi", $"You clicked {sender}"); }
}
}

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

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

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