Reorganized View source files to get my head straight

This commit is contained in:
Tig
2024-07-25 10:40:28 -06:00
parent 3a40851848
commit d874f56282
22 changed files with 1318 additions and 1244 deletions

View File

@@ -267,34 +267,6 @@ public static partial class Application // Keyboard handling
CommandImplementations [command] = ctx => f ();
}
///// <summary>
///// The <see cref="KeyBindingScope.Application"/> key bindings.
///// </summary>
//private static readonly Dictionary<Key, List<View?>> _keyBindings = new ();
///// <summary>
///// Gets the list of <see cref="KeyBindingScope.Application"/> key bindings.
///// </summary>
//public static Dictionary<Key, List<View?>> GetKeyBindings () { return _keyBindings; }
///// <summary>
///// Adds an <see cref="KeyBindingScope.Application"/> scoped key binding.
///// </summary>
///// <remarks>
///// This is an internal method used by the <see cref="View"/> class to add Application key bindings.
///// </remarks>
///// <param name="key">The key being bound.</param>
///// <param name="view">The view that is bound to the key. If <see langword="null"/>, <see cref="Application.Current"/> will be used.</param>
//internal static void AddKeyBinding (Key key, View? view)
//{
// if (!_keyBindings.ContainsKey (key))
// {
// _keyBindings [key] = [];
// }
// _keyBindings [key].Add (view);
//}
internal static void AddApplicationKeyBindings ()
{
// Things this view knows how to do
@@ -326,7 +298,7 @@ public static partial class Application // Keyboard handling
);
AddCommand (
Command.NextView,
Command.NextView,
() =>
{
// TODO: Move this method to Application.Navigation.cs

View File

@@ -603,48 +603,6 @@ public abstract class ConsoleDriver
#endregion
}
/// <summary>Terminal Cursor Visibility settings.</summary>
/// <remarks>
/// Hex value are set as 0xAABBCCDD where : AA stand for the TERMINFO DECSUSR parameter value to be used under
/// Linux and MacOS BB stand for the NCurses curs_set parameter value to be used under Linux and MacOS CC stand for the
/// CONSOLE_CURSOR_INFO.bVisible parameter value to be used under Windows DD stand for the CONSOLE_CURSOR_INFO.dwSize
/// parameter value to be used under Windows
/// </remarks>
public enum CursorVisibility
{
/// <summary>Cursor caret has default</summary>
/// <remarks>
/// Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/>. This default directly
/// depends on the XTerm user configuration settings, so it could be Block, I-Beam, Underline with possible blinking.
/// </remarks>
Default = 0x00010119,
/// <summary>Cursor caret is hidden</summary>
Invisible = 0x03000019,
/// <summary>Cursor caret is normally shown as a blinking underline bar _</summary>
Underline = 0x03010119,
/// <summary>Cursor caret is normally shown as a underline bar _</summary>
/// <remarks>Under Windows, this is equivalent to <see ref="UnderscoreBlinking"/></remarks>
UnderlineFix = 0x04010119,
/// <summary>Cursor caret is displayed a blinking vertical bar |</summary>
/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
Vertical = 0x05010119,
/// <summary>Cursor caret is displayed a blinking vertical bar |</summary>
/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
VerticalFix = 0x06010119,
/// <summary>Cursor caret is displayed as a blinking block ▉</summary>
Box = 0x01020164,
/// <summary>Cursor caret is displayed a block ▉</summary>
/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Block"/></remarks>
BoxFix = 0x02020164
}
/// <summary>
/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="ConsoleDriver"/>s and provides a
/// consistent way for application code to specify keys and receive key events.

View File

@@ -0,0 +1,44 @@
#nullable enable
namespace Terminal.Gui;
/// <summary>Terminal Cursor Visibility settings.</summary>
/// <remarks>
/// Hex value are set as 0xAABBCCDD where : AA stand for the TERMINFO DECSUSR parameter value to be used under
/// Linux and MacOS BB stand for the NCurses curs_set parameter value to be used under Linux and MacOS CC stand for the
/// CONSOLE_CURSOR_INFO.bVisible parameter value to be used under Windows DD stand for the CONSOLE_CURSOR_INFO.dwSize
/// parameter value to be used under Windows
/// </remarks>
public enum CursorVisibility
{
/// <summary>Cursor caret has default</summary>
/// <remarks>
/// Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/>. This default directly
/// depends on the XTerm user configuration settings, so it could be Block, I-Beam, Underline with possible blinking.
/// </remarks>
Default = 0x00010119,
/// <summary>Cursor caret is hidden</summary>
Invisible = 0x03000019,
/// <summary>Cursor caret is normally shown as a blinking underline bar _</summary>
Underline = 0x03010119,
/// <summary>Cursor caret is normally shown as a underline bar _</summary>
/// <remarks>Under Windows, this is equivalent to <see ref="UnderscoreBlinking"/></remarks>
UnderlineFix = 0x04010119,
/// <summary>Cursor caret is displayed a blinking vertical bar |</summary>
/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
Vertical = 0x05010119,
/// <summary>Cursor caret is displayed a blinking vertical bar |</summary>
/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Underscore"/></remarks>
VerticalFix = 0x06010119,
/// <summary>Cursor caret is displayed as a blinking block ▉</summary>
Box = 0x01020164,
/// <summary>Cursor caret is displayed a block ▉</summary>
/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Block"/></remarks>
BoxFix = 0x02020164
}

View File

@@ -0,0 +1,29 @@
namespace Terminal.Gui;
/// <summary>Event args for draw events</summary>
public class DrawEventArgs : EventArgs
{
/// <summary>Creates a new instance of the <see cref="DrawEventArgs"/> class.</summary>
/// <param name="newViewport">
/// The Content-relative rectangle describing the new visible viewport into the
/// <see cref="View"/>.
/// </param>
/// <param name="oldViewport">
/// The Content-relative rectangle describing the old visible viewport into the
/// <see cref="View"/>.
/// </param>
public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport)
{
NewViewport = newViewport;
OldViewport = oldViewport;
}
/// <summary>If set to true, the draw operation will be canceled, if applicable.</summary>
public bool Cancel { get; set; }
/// <summary>Gets the Content-relative rectangle describing the old visible viewport into the <see cref="View"/>.</summary>
public Rectangle OldViewport { get; }
/// <summary>Gets the Content-relative rectangle describing the currently visible viewport into the <see cref="View"/>.</summary>
public Rectangle NewViewport { get; }
}

View File

@@ -0,0 +1,12 @@
namespace Terminal.Gui;
/// <summary>Event arguments for the <see cref="View.LayoutComplete"/> event.</summary>
public class LayoutEventArgs : EventArgs
{
/// <summary>Creates a new instance of the <see cref="Terminal.Gui.LayoutEventArgs"/> class.</summary>
/// <param name="oldContentSize">The view that the event is about.</param>
public LayoutEventArgs (Size oldContentSize) { OldContentSize = oldContentSize; }
/// <summary>The viewport of the <see cref="View"/> before it was laid out.</summary>
public Size OldContentSize { get; set; }
}

View File

@@ -0,0 +1,27 @@
namespace Terminal.Gui;
/// <summary>Defines the event arguments for <see cref="View.SetFocus()"/></summary>
public class FocusEventArgs : EventArgs
{
/// <summary>Constructs.</summary>
/// <param name="leaving">The view that is losing focus.</param>
/// <param name="entering">The view that is gaining focus.</param>
public FocusEventArgs (View leaving, View entering) {
Leaving = leaving;
Entering = entering;
}
/// <summary>
/// Indicates if the current focus event has already been processed and the driver should stop notifying any other
/// event subscriber. It's important to set this value to true specially when updating any View's layout from inside the
/// subscriber method.
/// </summary>
public bool Handled { get; set; }
/// <summary>Indicates the view that is losing focus.</summary>
public View Leaving { get; set; }
/// <summary>Indicates the view that is gaining focus.</summary>
public View Entering { get; set; }
}

View File

@@ -3,7 +3,7 @@ using System.Text.Json.Serialization;
namespace Terminal.Gui;
public partial class View
public partial class View // Adornments
{
/// <summary>
/// Initializes the Adornments of the View. Called by the constructor.

View File

@@ -0,0 +1,15 @@
namespace Terminal.Gui;
public partial class View
{
/// <summary>
/// Gets or sets the user actions that are enabled for the view within it's <see cref="SuperView"/>.
/// </summary>
/// <remarks>
/// <para>
/// Sizing or moving a view is only possible if the <see cref="View"/> is part of a <see cref="SuperView"/> and
/// the relevant position and dimensions of the <see cref="View"/> are independent of other SubViews
/// </para>
/// </remarks>
public ViewArrangement Arrangement { get; set; }
}

View File

@@ -0,0 +1,35 @@
namespace Terminal.Gui;
public partial class View
{
/// <summary>
/// Gets or sets the cursor style to be used when the view is focused. The default is
/// <see cref="CursorVisibility.Invisible"/>.
/// </summary>
public CursorVisibility CursorVisibility { get; set; } = CursorVisibility.Invisible;
/// <summary>
/// Positions the cursor in the right position based on the currently focused view in the chain.
/// </summary>
/// <remarks>
/// <para>
/// Views that are focusable should override <see cref="PositionCursor()"/> to make sure that the cursor is
/// placed in a location that makes sense. Some terminals do not have a way of hiding the cursor, so it can be
/// distracting to have the cursor left at the last focused view. So views should make sure that they place the
/// cursor in a visually sensible place. The default implementation of <see cref="PositionCursor()"/> will place the
/// cursor at either the hotkey (if defined) or <c>0,0</c>.
/// </para>
/// </remarks>
/// <returns>Viewport-relative cursor position. Return <see langword="null"/> to ensure the cursor is not visible.</returns>
public virtual Point? PositionCursor ()
{
if (IsInitialized && CanFocus && HasFocus)
{
// By default, position the cursor at the hotkey (if any) or 0, 0.
Move (TextFormatter.HotKeyPos == -1 ? 0 : TextFormatter.CursorPosition, 0);
}
// Returning null will hide the cursor.
return null;
}
}

View File

@@ -2,7 +2,7 @@
namespace Terminal.Gui;
public partial class View
public partial class View // Drawing APIs
{
private ColorScheme _colorScheme;

View File

@@ -0,0 +1,320 @@
namespace Terminal.Gui;
public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.)
{
private static readonly IList<View> _empty = new List<View> (0).AsReadOnly ();
internal bool _addingView;
private List<View> _subviews; // This is null, and allocated on demand.
private View _superView;
/// <summary>Indicates whether the view was added to <see cref="SuperView"/>.</summary>
public bool IsAdded { get; private set; }
/// <summary>This returns a list of the subviews contained by this view.</summary>
/// <value>The subviews.</value>
public IList<View> Subviews => _subviews?.AsReadOnly () ?? _empty;
/// <summary>Returns the container for this view, or null if this view has not been added to a container.</summary>
/// <value>The super view.</value>
public virtual View SuperView
{
get => _superView;
set => throw new NotImplementedException ();
}
// Internally, we use InternalSubviews rather than subviews, as we do not expect us
// to make the same mistakes our users make when they poke at the Subviews.
internal IList<View> InternalSubviews => _subviews ?? _empty;
/// <summary>Adds a subview (child) to this view.</summary>
/// <remarks>
/// <para>
/// The Views that have been added to this view can be retrieved via the <see cref="Subviews"/> property. See also
/// <seealso cref="Remove(View)"/> <seealso cref="RemoveAll"/>
/// </para>
/// <para>
/// Subviews will be disposed when this View is disposed. In other-words, calling this method causes
/// the lifecycle of the subviews to be transferred to this View.
/// </para>
/// </remarks>
/// <param name="view">The view to add.</param>
/// <returns>The view that was added.</returns>
public virtual View Add (View view)
{
if (view is null)
{
return view;
}
if (_subviews is null)
{
_subviews = new ();
}
if (_tabIndexes is null)
{
_tabIndexes = new ();
}
_subviews.Add (view);
_tabIndexes.Add (view);
view._superView = this;
if (view.CanFocus)
{
_addingView = true;
if (SuperView?.CanFocus == false)
{
SuperView._addingView = true;
SuperView.CanFocus = true;
SuperView._addingView = false;
}
// QUESTION: This automatic behavior of setting CanFocus to true on the SuperView is not documented, and is annoying.
CanFocus = true;
view._tabIndex = _tabIndexes.IndexOf (view);
_addingView = false;
}
if (view.Enabled && !Enabled)
{
view._oldEnabled = true;
view.Enabled = false;
}
OnAdded (new (this, view));
if (IsInitialized && !view.IsInitialized)
{
view.BeginInit ();
view.EndInit ();
}
CheckDimAuto ();
SetNeedsLayout ();
SetNeedsDisplay ();
return view;
}
/// <summary>Adds the specified views (children) to the view.</summary>
/// <param name="views">Array of one or more views (can be optional parameter).</param>
/// <remarks>
/// <para>
/// The Views that have been added to this view can be retrieved via the <see cref="Subviews"/> property. See also
/// <seealso cref="Remove(View)"/> and <seealso cref="RemoveAll"/>.
/// </para>
/// <para>
/// Subviews will be disposed when this View is disposed. In other-words, calling this method causes
/// the lifecycle of the subviews to be transferred to this View.
/// </para>
/// </remarks>
public void Add (params View [] views)
{
if (views is null)
{
return;
}
foreach (View view in views)
{
Add (view);
}
}
/// <summary>Event fired when this view is added to another.</summary>
public event EventHandler<SuperViewChangedEventArgs> Added;
/// <summary>Get the top superview of a given <see cref="View"/>.</summary>
/// <returns>The superview view.</returns>
public View GetTopSuperView (View view = null, View superview = null)
{
View top = superview ?? Application.Top;
for (View v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView)
{
top = v;
if (top == superview)
{
break;
}
}
return top;
}
/// <summary>Method invoked when a subview is being added to this view.</summary>
/// <param name="e">Event where <see cref="ViewEventArgs.View"/> is the subview being added.</param>
public virtual void OnAdded (SuperViewChangedEventArgs e)
{
View view = e.Child;
view.IsAdded = true;
view.OnResizeNeeded ();
view.Added?.Invoke (this, e);
}
/// <summary>Method invoked when a subview is being removed from this view.</summary>
/// <param name="e">Event args describing the subview being removed.</param>
public virtual void OnRemoved (SuperViewChangedEventArgs e)
{
View view = e.Child;
view.IsAdded = false;
view.Removed?.Invoke (this, e);
}
/// <summary>Removes a subview added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
/// <remarks>
/// <para>
/// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
/// Subview's
/// lifecycle to be transferred to the caller; the caller muse call <see cref="Dispose"/>.
/// </para>
/// </remarks>
public virtual View Remove (View view)
{
if (view is null || _subviews is null)
{
return view;
}
Rectangle touched = view.Frame;
_subviews.Remove (view);
_tabIndexes.Remove (view);
view._superView = null;
view._tabIndex = -1;
SetNeedsLayout ();
SetNeedsDisplay ();
foreach (View v in _subviews)
{
if (v.Frame.IntersectsWith (touched))
{
view.SetNeedsDisplay ();
}
}
OnRemoved (new (this, view));
if (Focused == view)
{
Focused = null;
}
return view;
}
/// <summary>
/// Removes all subviews (children) added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
/// </summary>
/// <remarks>
/// <para>
/// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
/// Subview's
/// lifecycle to be transferred to the caller; the caller must call <see cref="Dispose"/> on any Views that were
/// added.
/// </para>
/// </remarks>
public virtual void RemoveAll ()
{
if (_subviews is null)
{
return;
}
while (_subviews.Count > 0)
{
Remove (_subviews [0]);
}
}
/// <summary>Event fired when this view is removed from another.</summary>
public event EventHandler<SuperViewChangedEventArgs> Removed;
/// <summary>Moves <paramref name="subview"/> one position towards the start of the <see cref="Subviews"/> list</summary>
/// <param name="subview">The subview to move forward.</param>
public void BringSubviewForward (View subview)
{
PerformActionForSubview (
subview,
x =>
{
int idx = _subviews.IndexOf (x);
if (idx + 1 < _subviews.Count)
{
_subviews.Remove (x);
_subviews.Insert (idx + 1, x);
}
}
);
}
/// <summary>Moves <paramref name="subview"/> to the start of the <see cref="Subviews"/> list.</summary>
/// <param name="subview">The subview to send to the start.</param>
public void BringSubviewToFront (View subview)
{
PerformActionForSubview (
subview,
x =>
{
_subviews.Remove (x);
_subviews.Add (x);
}
);
}
/// <summary>Moves <paramref name="subview"/> one position towards the end of the <see cref="Subviews"/> list</summary>
/// <param name="subview">The subview to move backwards.</param>
public void SendSubviewBackwards (View subview)
{
PerformActionForSubview (
subview,
x =>
{
int idx = _subviews.IndexOf (x);
if (idx > 0)
{
_subviews.Remove (x);
_subviews.Insert (idx - 1, x);
}
}
);
}
/// <summary>Moves <paramref name="subview"/> to the end of the <see cref="Subviews"/> list.</summary>
/// <param name="subview">The subview to send to the end.</param>
public void SendSubviewToBack (View subview)
{
PerformActionForSubview (
subview,
x =>
{
_subviews.Remove (x);
_subviews.Insert (0, subview);
}
);
}
/// <summary>
/// Internal API that runs <paramref name="action"/> on a subview if it is part of the <see cref="Subviews"/> list.
/// </summary>
/// <param name="subview"></param>
/// <param name="action"></param>
private void PerformActionForSubview (View subview, Action<View> action)
{
if (_subviews.Contains (subview))
{
action (subview);
}
// BUGBUG: this is odd. Why is this needed?
SetNeedsDisplay ();
subview.SetNeedsDisplay ();
}
}

View File

@@ -3,7 +3,7 @@ using System.Diagnostics;
namespace Terminal.Gui;
public partial class View
public partial class View // Keyboard APIs
{
/// <summary>
/// Helper to configure all things keyboard related for a View. Called from the View constructor.
@@ -254,119 +254,6 @@ public partial class View
#endregion HotKey Support
#region Tab/Focus Handling
// This is null, and allocated on demand.
private List<View> _tabIndexes;
/// <summary>Gets a list of the subviews that are <see cref="TabStop"/>s.</summary>
/// <value>The tabIndexes.</value>
public IList<View> TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty;
private int _tabIndex = -1;
private int _oldTabIndex;
/// <summary>
/// Indicates the index of the current <see cref="View"/> from the <see cref="TabIndexes"/> list. See also:
/// <seealso cref="TabStop"/>.
/// </summary>
public int TabIndex
{
get => _tabIndex;
set
{
if (!CanFocus)
{
_tabIndex = -1;
return;
}
if (SuperView?._tabIndexes is null || SuperView?._tabIndexes.Count == 1)
{
_tabIndex = 0;
return;
}
if (_tabIndex == value && TabIndexes.IndexOf (this) == value)
{
return;
}
_tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 :
value < 0 ? 0 : value;
_tabIndex = GetTabIndex (_tabIndex);
if (SuperView._tabIndexes.IndexOf (this) != _tabIndex)
{
SuperView._tabIndexes.Remove (this);
SuperView._tabIndexes.Insert (_tabIndex, this);
SetTabIndex ();
}
}
}
private int GetTabIndex (int idx)
{
var i = 0;
foreach (View v in SuperView._tabIndexes)
{
if (v._tabIndex == -1 || v == this)
{
continue;
}
i++;
}
return Math.Min (i, idx);
}
private void SetTabIndex ()
{
var i = 0;
foreach (View v in SuperView._tabIndexes)
{
if (v._tabIndex == -1)
{
continue;
}
v._tabIndex = i;
i++;
}
}
private bool _tabStop = true;
/// <summary>
/// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be <see langword="true"/>
/// only if the <see cref="CanFocus"/> is also <see langword="true"/>. Set to <see langword="false"/> to prevent the
/// view from being a stop-point for keyboard navigation.
/// </summary>
/// <remarks>
/// The default keyboard navigation keys are <c>Key.Tab</c> and <c>Key>Tab.WithShift</c>. These can be changed by
/// modifying the key bindings (see <see cref="KeyBindings.Add(Key, Command[])"/>) of the SuperView.
/// </remarks>
public bool TabStop
{
get => _tabStop;
set
{
if (_tabStop == value)
{
return;
}
_tabStop = CanFocus && value;
}
}
#endregion Tab/Focus Handling
#region Low-level Key handling
#region Key Down Event

View File

@@ -3,7 +3,7 @@ using System.Diagnostics;
namespace Terminal.Gui;
public partial class View
public partial class View // Layout APIs
{
#region Frame

View File

@@ -2,7 +2,7 @@
namespace Terminal.Gui;
public partial class View
public partial class View // Mouse APIs
{
[CanBeNull]
private ColorScheme _savedHighlightColorScheme;

View File

@@ -0,0 +1,813 @@
namespace Terminal.Gui;
public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...)
{
/// <summary>Returns a value indicating if this View is currently on Top (Active)</summary>
public bool IsCurrentTop => Application.Current == this;
// BUGBUG: This API is poorly defined and implemented. It deeply intertwines the view hierarchy with the tab order.
/// <summary>Exposed as `internal` for unit tests. Indicates focus navigation direction.</summary>
internal enum NavigationDirection
{
/// <summary>Navigate forward.</summary>
Forward,
/// <summary>Navigate backwards.</summary>
Backward
}
/// <summary>Invoked when this view is gaining focus (entering).</summary>
/// <param name="leavingView">The view that is leaving focus.</param>
/// <returns> <see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// <para>
/// Overrides must call the base class method to ensure that the <see cref="Enter"/> event is raised. If the event
/// is handled, the method should return <see langword="true"/>.
/// </para>
/// </remarks>
public virtual bool OnEnter (View leavingView)
{
var args = new FocusEventArgs (leavingView, this);
Enter?.Invoke (this, args);
if (args.Handled)
{
return true;
}
return false;
}
/// <summary>Invoked when this view is losing focus (leaving).</summary>
/// <param name="enteringView">The view that is entering focus.</param>
/// <returns> <see langword="true"/>, if the event was handled, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// <para>
/// Overrides must call the base class method to ensure that the <see cref="Leave"/> event is raised. If the event
/// is handled, the method should return <see langword="true"/>.
/// </para>
/// </remarks>
public virtual bool OnLeave (View enteringView)
{
var args = new FocusEventArgs (this, enteringView);
Leave?.Invoke (this, args);
if (args.Handled)
{
return true;
}
return false;
}
/// <summary>Raised when the view is gaining (entering) focus. Can be cancelled.</summary>
/// <remarks>
/// Raised by the <see cref="OnEnter"/> virtual method.
/// </remarks>
public event EventHandler<FocusEventArgs> Enter;
/// <summary>Raised when the view is losing (leaving) focus. Can be cancelled.</summary>
/// <remarks>
/// Raised by the <see cref="OnLeave"/> virtual method.
/// </remarks>
public event EventHandler<FocusEventArgs> Leave;
private NavigationDirection _focusDirection;
/// <summary>
/// Gets or sets the focus direction for this view and all subviews.
/// Setting this property will set the focus direction for all views up the SuperView hierarchy.
/// </summary>
internal NavigationDirection FocusDirection
{
get => SuperView?.FocusDirection ?? _focusDirection;
set
{
if (SuperView is { })
{
SuperView.FocusDirection = value;
}
else
{
_focusDirection = value;
}
}
}
private bool _hasFocus;
/// <summary>
/// Gets or sets whether this view has focus.
/// </summary>
/// <remarks>
/// <para>
/// Causes the <see cref="OnEnter"/> and <see cref="OnLeave"/> virtual methods (and <see cref="Enter"/> and
/// <see cref="Leave"/> events to be raised) when the value changes.
/// </para>
/// <para>
/// Setting this property to <see langword="false"/> will recursively set <see cref="HasFocus"/> to
/// <see langword="false"/>
/// for any focused subviews.
/// </para>
/// </remarks>
public bool HasFocus
{
// Force the specified view to have focus
set => SetHasFocus (value, this, true);
get => _hasFocus;
}
/// <summary>
/// Internal API that sets <see cref="HasFocus"/>. This method is called by <c>HasFocus_set</c> and other methods that
/// need to set or remove focus from a view.
/// </summary>
/// <param name="newHasFocus">The new setting for <see cref="HasFocus"/>.</param>
/// <param name="view">The view that will be gaining or losing focus.</param>
/// <param name="force">
/// <see langword="true"/> to force Enter/Leave on <paramref name="view"/> regardless of whether it
/// already HasFocus or not.
/// </param>
/// <remarks>
/// If <paramref name="newHasFocus"/> is <see langword="false"/> and there is a focused subview (<see cref="Focused"/>
/// is not <see langword="null"/>),
/// this method will recursively remove focus from any focused subviews of <see cref="Focused"/>.
/// </remarks>
private void SetHasFocus (bool newHasFocus, View view, bool force = false)
{
if (HasFocus != newHasFocus || force)
{
_hasFocus = newHasFocus;
if (newHasFocus)
{
OnEnter (view);
}
else
{
OnLeave (view);
}
SetNeedsDisplay ();
}
// Remove focus down the chain of subviews if focus is removed
if (!newHasFocus && Focused is { })
{
View f = Focused;
f.OnLeave (view);
f.SetHasFocus (false, view);
Focused = null;
}
}
/// <summary>Raised when <see cref="CanFocus"/> has been changed.</summary>
/// <remarks>
/// Raised by the <see cref="OnCanFocusChanged"/> virtual method.
/// </remarks>
public event EventHandler CanFocusChanged;
/// <summary>Invoked when the <see cref="CanFocus"/> property from a view is changed.</summary>
/// <remarks>
/// Raises the <see cref="CanFocusChanged"/> event.
/// </remarks>
public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); }
private bool _oldCanFocus;
private bool _canFocus;
/// <summary>Gets or sets a value indicating whether this <see cref="View"/> can be focused.</summary>
/// <remarks>
/// <para>
/// <see cref="SuperView"/> must also have <see cref="CanFocus"/> set to <see langword="true"/>.
/// </para>
/// </remarks>
public bool CanFocus
{
get => _canFocus;
set
{
if (!_addingView && IsInitialized && SuperView?.CanFocus == false && value)
{
throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!");
}
if (_canFocus == value)
{
return;
}
_canFocus = value;
switch (_canFocus)
{
case false when _tabIndex > -1:
TabIndex = -1;
break;
case true when SuperView?.CanFocus == false && _addingView:
SuperView.CanFocus = true;
break;
}
if (_canFocus && _tabIndex == -1)
{
TabIndex = SuperView is { } ? SuperView._tabIndexes.IndexOf (this) : -1;
}
TabStop = _canFocus;
if (!_canFocus && SuperView?.Focused == this)
{
SuperView.Focused = null;
}
if (!_canFocus && HasFocus)
{
SetHasFocus (false, this);
SuperView?.EnsureFocus ();
if (SuperView is { Focused: null })
{
SuperView.FocusNext ();
if (SuperView.Focused is null && Application.Current is { })
{
Application.Current.FocusNext ();
}
ApplicationOverlapped.BringOverlappedTopToFront ();
}
}
if (_subviews is { } && IsInitialized)
{
foreach (View view in _subviews)
{
if (view.CanFocus != value)
{
if (!value)
{
view._oldCanFocus = view.CanFocus;
view._oldTabIndex = view._tabIndex;
view.CanFocus = false;
view._tabIndex = -1;
}
else
{
if (_addingView)
{
view._addingView = true;
}
view.CanFocus = view._oldCanFocus;
view._tabIndex = view._oldTabIndex;
view._addingView = false;
}
}
}
if (this is Toplevel && Application.Current.Focused != this)
{
ApplicationOverlapped.BringOverlappedTopToFront ();
}
}
OnCanFocusChanged ();
SetNeedsDisplay ();
}
}
/// <summary>Returns the currently focused Subview inside this view, or <see langword="null"/> if nothing is focused.</summary>
/// <value>The currently focused Subview.</value>
public View Focused { get; private set; }
/// <summary>
/// Returns the most focused Subview in the chain of subviews (the leaf view that has the focus), or
/// <see langword="null"/> if nothing is focused.
/// </summary>
/// <value>The most focused Subview.</value>
public View MostFocused
{
get
{
if (Focused is null)
{
return null;
}
View most = Focused.MostFocused;
if (most is { })
{
return most;
}
return Focused;
}
}
/// <summary>Causes subview specified by <paramref name="view"/> to enter focus.</summary>
/// <param name="view">View.</param>
private void SetFocus (View view)
{
if (view is null)
{
return;
}
//Console.WriteLine ($"Request to focus {view}");
if (!view.CanFocus || !view.Visible || !view.Enabled)
{
return;
}
if (Focused?._hasFocus == true && Focused == view)
{
return;
}
if ((Focused?._hasFocus == true && Focused?.SuperView == view) || view == this)
{
if (!view._hasFocus)
{
view._hasFocus = true;
}
return;
}
// Make sure that this view is a subview
View c;
for (c = view._superView; c != null; c = c._superView)
{
if (c == this)
{
break;
}
}
if (c is null)
{
throw new ArgumentException ("the specified view is not part of the hierarchy of this view");
}
if (Focused is { })
{
Focused.SetHasFocus (false, view);
}
View f = Focused;
Focused = view;
Focused.SetHasFocus (true, f);
Focused.EnsureFocus ();
// Send focus upwards
if (SuperView is { })
{
SuperView.SetFocus (this);
}
else
{
SetFocus (this);
}
}
/// <summary>Causes this view to be focused and entire Superview hierarchy to have the focused order updated.</summary>
public void SetFocus ()
{
if (!CanBeVisible (this) || !Enabled)
{
if (HasFocus)
{
SetHasFocus (false, this);
}
return;
}
if (SuperView is { })
{
SuperView.SetFocus (this);
}
else
{
SetFocus (this);
}
}
/// <summary>
/// If there is no focused subview, calls <see cref="FocusFirst"/> or <see cref="FocusLast"/> based on
/// <see cref="FocusDirection"/>.
/// does nothing.
/// </summary>
public void EnsureFocus ()
{
if (Focused is null && _subviews?.Count > 0)
{
if (FocusDirection == NavigationDirection.Forward)
{
FocusFirst ();
}
else
{
FocusLast ();
}
}
}
/// <summary>
/// Focuses the last focusable view in <see cref="View.TabIndexes"/> if one exists. If there are no views in
/// <see cref="View.TabIndexes"/> then the focus is set to the view itself.
/// </summary>
public void FocusFirst (bool overlapped = false)
{
if (!CanBeVisible (this))
{
return;
}
if (_tabIndexes is null)
{
SuperView?.SetFocus (this);
return;
}
foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)))
{
if (view.CanFocus && view._tabStop && view.Visible && view.Enabled)
{
SetFocus (view);
return;
}
}
}
/// <summary>
/// Focuses the last focusable view in <see cref="View.TabIndexes"/> if one exists. If there are no views in
/// <see cref="View.TabIndexes"/> then the focus is set to the view itself.
/// </summary>
public void FocusLast (bool overlapped = false)
{
if (!CanBeVisible (this))
{
return;
}
if (_tabIndexes is null)
{
SuperView?.SetFocus (this);
return;
}
foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ())
{
if (view.CanFocus && view._tabStop && view.Visible && view.Enabled)
{
SetFocus (view);
return;
}
}
}
/// <summary>
/// Focuses the previous view in <see cref="View.TabIndexes"/>. If there is no previous view, the focus is set to the
/// view itself.
/// </summary>
/// <returns><see langword="true"/> if previous was focused, <see langword="false"/> otherwise.</returns>
public bool FocusPrev ()
{
if (!CanBeVisible (this))
{
return false;
}
FocusDirection = NavigationDirection.Backward;
if (TabIndexes is null || TabIndexes.Count == 0)
{
return false;
}
if (Focused is null)
{
FocusLast ();
return Focused != null;
}
int focusedIdx = -1;
for (int i = TabIndexes.Count; i > 0;)
{
i--;
View w = TabIndexes [i];
if (w.HasFocus)
{
if (w.FocusPrev ())
{
return true;
}
focusedIdx = i;
continue;
}
if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled)
{
Focused.SetHasFocus (false, w);
// If the focused view is overlapped don't focus on the next if it's not overlapped.
if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
FocusLast (true);
return true;
}
// If the focused view is not overlapped and the next is, skip it
if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
continue;
}
if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
{
w.FocusLast ();
}
SetFocus (w);
return true;
}
}
// There's no prev view in tab indexes.
if (Focused is { })
{
// Leave Focused
Focused.SetHasFocus (false, this);
if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
FocusLast (true);
return true;
}
// Signal to caller no next view was found
Focused = null;
}
return false;
}
/// <summary>
/// Focuses the next view in <see cref="View.TabIndexes"/>. If there is no next view, the focus is set to the view
/// itself.
/// </summary>
/// <returns><see langword="true"/> if next was focused, <see langword="false"/> otherwise.</returns>
public bool FocusNext ()
{
if (!CanBeVisible (this))
{
return false;
}
FocusDirection = NavigationDirection.Forward;
if (TabIndexes is null || TabIndexes.Count == 0)
{
return false;
}
if (Focused is null)
{
FocusFirst ();
return Focused != null;
}
int focusedIdx = -1;
for (var i = 0; i < TabIndexes.Count; i++)
{
View w = TabIndexes [i];
if (w.HasFocus)
{
if (w.FocusNext ())
{
return true;
}
focusedIdx = i;
continue;
}
if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled)
{
Focused.SetHasFocus (false, w);
//// If the focused view is overlapped don't focus on the next if it's not overlapped.
//if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped)/* && !w.Arrangement.HasFlag (ViewArrangement.Overlapped)*/)
//{
// return false;
//}
//// If the focused view is not overlapped and the next is, skip it
//if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped))
//{
// continue;
//}
if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
{
w.FocusFirst ();
}
SetFocus (w);
return true;
}
}
// There's no next view in tab indexes.
if (Focused is { })
{
// Leave Focused
Focused.SetHasFocus (false, this);
//if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped))
//{
// FocusFirst (true);
// return true;
//}
// Signal to caller no next view was found
Focused = null;
}
return false;
}
private View GetMostFocused (View view)
{
if (view is null)
{
return null;
}
return view.Focused is { } ? GetMostFocused (view.Focused) : view;
}
#region Tab/Focus Handling
private List<View> _tabIndexes;
// TODO: This should be a get-only property?
// BUGBUG: This returns an AsReadOnly list, but isn't declared as such.
/// <summary>Gets a list of the subviews that are a <see cref="TabStop"/>.</summary>
/// <value>The tabIndexes.</value>
public IList<View> TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty;
// TODO: Change this to int? and use null to indicate the view is not in the tab order.
private int _tabIndex = -1;
private int _oldTabIndex;
/// <summary>
/// Indicates the index of the current <see cref="View"/> from the <see cref="TabIndexes"/> list. See also:
/// <seealso cref="TabStop"/>.
/// </summary>
/// <remarks>
/// <para>
/// If the value is -1, the view is not part of the tab order.
/// </para>
/// <para>
/// On set, if <see cref="CanFocus"/> is <see langword="false"/>, <see cref="TabIndex"/> will be set to -1.
/// </para>
/// <para>
/// On set, if <see cref="SuperView"/> is <see langword="null"/> or has not TabStops, <see cref="TabIndex"/> will
/// be set to 0.
/// </para>
/// <para>
/// On set, if <see cref="SuperView"/> has only one TabStop, <see cref="TabIndex"/> will be set to 0.
/// </para>
/// </remarks>
public int TabIndex
{
get => _tabIndex;
set
{
if (!CanFocus)
{
// BUGBUG: Property setters should set the property to the value passed in and not have side effects.
_tabIndex = -1;
return;
}
if (SuperView?._tabIndexes is null || SuperView?._tabIndexes.Count == 1)
{
// BUGBUG: Property setters should set the property to the value passed in and not have side effects.
_tabIndex = 0;
return;
}
if (_tabIndex == value && TabIndexes.IndexOf (this) == value)
{
return;
}
_tabIndex = value > SuperView!.TabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 :
value < 0 ? 0 : value;
_tabIndex = GetGreatestTabIndexInSuperView (_tabIndex);
if (SuperView._tabIndexes.IndexOf (this) != _tabIndex)
{
// BUGBUG: we have to use _tabIndexes and not TabIndexes because TabIndexes returns is a read-only version of _tabIndexes
SuperView._tabIndexes.Remove (this);
SuperView._tabIndexes.Insert (_tabIndex, this);
ReorderSuperViewTabIndexes ();
}
}
}
/// <summary>
/// Gets the greatest <see cref="TabIndex"/> of the <see cref="SuperView"/>'s <see cref="TabIndexes"/> that is less
/// than or equal to <paramref name="idx"/>.
/// </summary>
/// <param name="idx"></param>
/// <returns>The minimum of <paramref name="idx"/> and the <see cref="SuperView"/>'s <see cref="TabIndexes"/>.</returns>
private int GetGreatestTabIndexInSuperView (int idx)
{
var i = 0;
foreach (View superViewTabStop in SuperView._tabIndexes)
{
if (superViewTabStop._tabIndex == -1 || superViewTabStop == this)
{
continue;
}
i++;
}
return Math.Min (i, idx);
}
/// <summary>
/// Re-orders the <see cref="TabIndex"/>s of the views in the <see cref="SuperView"/>'s <see cref="TabIndexes"/>.
/// </summary>
private void ReorderSuperViewTabIndexes ()
{
var i = 0;
foreach (View superViewTabStop in SuperView._tabIndexes)
{
if (superViewTabStop._tabIndex == -1)
{
continue;
}
superViewTabStop._tabIndex = i;
i++;
}
}
private bool _tabStop = true;
/// <summary>
/// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be <see langword="true"/>
/// only if <see cref="CanFocus"/> is <see langword="true"/>. Set to <see langword="false"/> to prevent the
/// view from being a stop-point for keyboard navigation.
/// </summary>
/// <remarks>
/// The default keyboard navigation keys are <c>Key.Tab</c> and <c>Key>Tab.WithShift</c>. These can be changed by
/// modifying the key bindings (see <see cref="KeyBindings.Add(Key, Command[])"/>) of the SuperView.
/// </remarks>
public bool TabStop
{
get => _tabStop;
set
{
if (_tabStop == value)
{
return;
}
_tabStop = CanFocus && value;
}
}
#endregion Tab/Focus Handling
}

View File

@@ -2,7 +2,7 @@
namespace Terminal.Gui;
public partial class View
public partial class View // Text Property APIs
{
/// <summary>
/// Initializes the Text of the View. Called by the constructor.

View File

@@ -1,14 +1,16 @@
namespace Terminal.Gui;
/// <summary>
/// Describes what user actions are enabled for arranging a <see cref="View"/> within it's <see cref="View.SuperView"/>.
/// Describes what user actions are enabled for arranging a <see cref="View"/> within it's <see cref="View.SuperView"/>
/// .
/// See <see cref="View.Arrangement"/>.
/// </summary>
/// <remarks>
/// <para>
/// Sizing or moving a view is only possible if the <see cref="View"/> is part of a <see cref="View.SuperView"/> and
/// the relevant position and dimensions of the <see cref="View"/> are independent of other SubViews
/// </para>
/// <para>
/// Sizing or moving a view is only possible if the <see cref="View"/> is part of a <see cref="View.SuperView"/>
/// and
/// the relevant position and dimensions of the <see cref="View"/> are independent of other SubViews
/// </para>
/// </remarks>
[Flags]
public enum ViewArrangement
@@ -56,26 +58,14 @@ public enum ViewArrangement
Resizable = LeftResizable | RightResizable | TopResizable | BottomResizable,
/// <summary>
/// The view overlap other views.
/// The view overlap other views.
/// </summary>
/// <remarks>
/// <para>
/// When set, Tab and Shift-Tab will be constrained to the subviews of the view (normally, they will navigate to the next/prev view in the next/prev Tabindex).
/// When set, Tab and Shift-Tab will be constrained to the subviews of the view (normally, they will navigate to
/// the next/prev view in the next/prev Tabindex).
/// Use Ctrl-Tab (Ctrl-PageDown) / Ctrl-Shift-Tab (Ctrl-PageUp) to move between overlapped views.
/// </para>
/// </remarks>
Overlapped = 32
}
public partial class View
{
/// <summary>
/// Gets or sets the user actions that are enabled for the view within it's <see cref="SuperView"/>.
/// </summary>
/// <remarks>
/// <para>
/// Sizing or moving a view is only possible if the <see cref="View"/> is part of a <see cref="SuperView"/> and
/// the relevant position and dimensions of the <see cref="View"/> are independent of other SubViews
/// </para>
/// </remarks>
public ViewArrangement Arrangement { get; set; }
}

View File

@@ -13,69 +13,4 @@ public class ViewEventArgs : EventArgs
/// child then sender may be the parent while <see cref="View"/> is the child being added.
/// </remarks>
public View View { get; }
}
/// <summary>Event arguments for the <see cref="View.LayoutComplete"/> event.</summary>
public class LayoutEventArgs : EventArgs
{
/// <summary>Creates a new instance of the <see cref="Terminal.Gui.LayoutEventArgs"/> class.</summary>
/// <param name="oldContentSize">The view that the event is about.</param>
public LayoutEventArgs (Size oldContentSize) { OldContentSize = oldContentSize; }
/// <summary>The viewport of the <see cref="View"/> before it was laid out.</summary>
public Size OldContentSize { get; set; }
}
/// <summary>Event args for draw events</summary>
public class DrawEventArgs : EventArgs
{
/// <summary>Creates a new instance of the <see cref="DrawEventArgs"/> class.</summary>
/// <param name="newViewport">
/// The Content-relative rectangle describing the new visible viewport into the
/// <see cref="View"/>.
/// </param>
/// <param name="oldViewport">
/// The Content-relative rectangle describing the old visible viewport into the
/// <see cref="View"/>.
/// </param>
public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport)
{
NewViewport = newViewport;
OldViewport = oldViewport;
}
/// <summary>If set to true, the draw operation will be canceled, if applicable.</summary>
public bool Cancel { get; set; }
/// <summary>Gets the Content-relative rectangle describing the old visible viewport into the <see cref="View"/>.</summary>
public Rectangle OldViewport { get; }
/// <summary>Gets the Content-relative rectangle describing the currently visible viewport into the <see cref="View"/>.</summary>
public Rectangle NewViewport { get; }
}
/// <summary>Defines the event arguments for <see cref="View.SetFocus()"/></summary>
public class FocusEventArgs : EventArgs
{
/// <summary>Constructs.</summary>
/// <param name="leaving">The view that is losing focus.</param>
/// <param name="entering">The view that is gaining focus.</param>
public FocusEventArgs (View leaving, View entering) {
Leaving = leaving;
Entering = entering;
}
/// <summary>
/// Indicates if the current focus event has already been processed and the driver should stop notifying any other
/// event subscriber. It's important to set this value to true specially when updating any View's layout from inside the
/// subscriber method.
/// </summary>
public bool Handled { get; set; }
/// <summary>Indicates the view that is losing focus.</summary>
public View Leaving { get; set; }
/// <summary>Indicates the view that is gaining focus.</summary>
public View Entering { get; set; }
}
}

View File

@@ -1,948 +0,0 @@
using System.Diagnostics;
namespace Terminal.Gui;
public partial class View
{
private static readonly IList<View> _empty = new List<View> (0).AsReadOnly ();
internal bool _addingView;
private List<View> _subviews; // This is null, and allocated on demand.
private View _superView;
/// <summary>Indicates whether the view was added to <see cref="SuperView"/>.</summary>
public bool IsAdded { get; private set; }
/// <summary>Returns a value indicating if this View is currently on Top (Active)</summary>
public bool IsCurrentTop => Application.Current == this;
/// <summary>This returns a list of the subviews contained by this view.</summary>
/// <value>The subviews.</value>
public IList<View> Subviews => _subviews?.AsReadOnly () ?? _empty;
/// <summary>Returns the container for this view, or null if this view has not been added to a container.</summary>
/// <value>The super view.</value>
public virtual View SuperView
{
get => _superView;
set => throw new NotImplementedException ();
}
// Internally, we use InternalSubviews rather than subviews, as we do not expect us
// to make the same mistakes our users make when they poke at the Subviews.
internal IList<View> InternalSubviews => _subviews ?? _empty;
/// <summary>Adds a subview (child) to this view.</summary>
/// <remarks>
/// <para>
/// The Views that have been added to this view can be retrieved via the <see cref="Subviews"/> property. See also
/// <seealso cref="Remove(View)"/> <seealso cref="RemoveAll"/>
/// </para>
/// <para>
/// Subviews will be disposed when this View is disposed. In other-words, calling this method causes
/// the lifecycle of the subviews to be transferred to this View.
/// </para>
/// </remarks>
/// <param name="view">The view to add.</param>
/// <returns>The view that was added.</returns>
public virtual View Add (View view)
{
if (view is null)
{
return view;
}
if (_subviews is null)
{
_subviews = new ();
}
if (_tabIndexes is null)
{
_tabIndexes = new ();
}
_subviews.Add (view);
_tabIndexes.Add (view);
view._superView = this;
if (view.CanFocus)
{
_addingView = true;
if (SuperView?.CanFocus == false)
{
SuperView._addingView = true;
SuperView.CanFocus = true;
SuperView._addingView = false;
}
// QUESTION: This automatic behavior of setting CanFocus to true on the SuperView is not documented, and is annoying.
CanFocus = true;
view._tabIndex = _tabIndexes.IndexOf (view);
_addingView = false;
}
if (view.Enabled && !Enabled)
{
view._oldEnabled = true;
view.Enabled = false;
}
OnAdded (new (this, view));
if (IsInitialized && !view.IsInitialized)
{
view.BeginInit ();
view.EndInit ();
}
CheckDimAuto ();
SetNeedsLayout ();
SetNeedsDisplay ();
return view;
}
/// <summary>Adds the specified views (children) to the view.</summary>
/// <param name="views">Array of one or more views (can be optional parameter).</param>
/// <remarks>
/// <para>
/// The Views that have been added to this view can be retrieved via the <see cref="Subviews"/> property. See also
/// <seealso cref="Remove(View)"/> and <seealso cref="RemoveAll"/>.
/// </para>
/// <para>
/// Subviews will be disposed when this View is disposed. In other-words, calling this method causes
/// the lifecycle of the subviews to be transferred to this View.
/// </para>
/// </remarks>
public void Add (params View [] views)
{
if (views is null)
{
return;
}
foreach (View view in views)
{
Add (view);
}
}
/// <summary>Event fired when this view is added to another.</summary>
public event EventHandler<SuperViewChangedEventArgs> Added;
/// <summary>Moves the subview backwards in the hierarchy, only one step</summary>
/// <param name="subview">The subview to send backwards</param>
/// <remarks>If you want to send the view all the way to the back use SendSubviewToBack.</remarks>
public void BringSubviewForward (View subview)
{
PerformActionForSubview (
subview,
x =>
{
int idx = _subviews.IndexOf (x);
if (idx + 1 < _subviews.Count)
{
_subviews.Remove (x);
_subviews.Insert (idx + 1, x);
}
}
);
}
/// <summary>Brings the specified subview to the front so it is drawn on top of any other views.</summary>
/// <param name="subview">The subview to send to the front</param>
/// <remarks><seealso cref="SendSubviewToBack"/>.</remarks>
public void BringSubviewToFront (View subview)
{
PerformActionForSubview (
subview,
x =>
{
_subviews.Remove (x);
_subviews.Add (x);
}
);
}
/// <summary>Get the top superview of a given <see cref="View"/>.</summary>
/// <returns>The superview view.</returns>
public View GetTopSuperView (View view = null, View superview = null)
{
View top = superview ?? Application.Top;
for (View v = view?.SuperView ?? this?.SuperView; v != null; v = v.SuperView)
{
top = v;
if (top == superview)
{
break;
}
}
return top;
}
/// <summary>Method invoked when a subview is being added to this view.</summary>
/// <param name="e">Event where <see cref="ViewEventArgs.View"/> is the subview being added.</param>
public virtual void OnAdded (SuperViewChangedEventArgs e)
{
View view = e.Child;
view.IsAdded = true;
view.OnResizeNeeded ();
view.Added?.Invoke (this, e);
}
/// <summary>Method invoked when a subview is being removed from this view.</summary>
/// <param name="e">Event args describing the subview being removed.</param>
public virtual void OnRemoved (SuperViewChangedEventArgs e)
{
View view = e.Child;
view.IsAdded = false;
view.Removed?.Invoke (this, e);
}
/// <summary>Removes a subview added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.</summary>
/// <remarks>
/// <para>
/// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
/// Subview's
/// lifecycle to be transferred to the caller; the caller muse call <see cref="Dispose"/>.
/// </para>
/// </remarks>
public virtual View Remove (View view)
{
if (view is null || _subviews is null)
{
return view;
}
Rectangle touched = view.Frame;
_subviews.Remove (view);
_tabIndexes.Remove (view);
view._superView = null;
view._tabIndex = -1;
SetNeedsLayout ();
SetNeedsDisplay ();
foreach (View v in _subviews)
{
if (v.Frame.IntersectsWith (touched))
{
view.SetNeedsDisplay ();
}
}
OnRemoved (new (this, view));
if (Focused == view)
{
Focused = null;
}
return view;
}
/// <summary>
/// Removes all subviews (children) added via <see cref="Add(View)"/> or <see cref="Add(View[])"/> from this View.
/// </summary>
/// <remarks>
/// <para>
/// Normally Subviews will be disposed when this View is disposed. Removing a Subview causes ownership of the
/// Subview's
/// lifecycle to be transferred to the caller; the caller must call <see cref="Dispose"/> on any Views that were
/// added.
/// </para>
/// </remarks>
public virtual void RemoveAll ()
{
if (_subviews is null)
{
return;
}
while (_subviews.Count > 0)
{
Remove (_subviews [0]);
}
}
/// <summary>Event fired when this view is removed from another.</summary>
public event EventHandler<SuperViewChangedEventArgs> Removed;
/// <summary>Moves the subview backwards in the hierarchy, only one step</summary>
/// <param name="subview">The subview to send backwards</param>
/// <remarks>If you want to send the view all the way to the back use SendSubviewToBack.</remarks>
public void SendSubviewBackwards (View subview)
{
PerformActionForSubview (
subview,
x =>
{
int idx = _subviews.IndexOf (x);
if (idx > 0)
{
_subviews.Remove (x);
_subviews.Insert (idx - 1, x);
}
}
);
}
/// <summary>Sends the specified subview to the front so it is the first view drawn</summary>
/// <param name="subview">The subview to send to the front</param>
/// <remarks><seealso cref="BringSubviewToFront(View)"/>.</remarks>
public void SendSubviewToBack (View subview)
{
PerformActionForSubview (
subview,
x =>
{
_subviews.Remove (x);
_subviews.Insert (0, subview);
}
);
}
private void PerformActionForSubview (View subview, Action<View> action)
{
if (_subviews.Contains (subview))
{
action (subview);
}
SetNeedsDisplay ();
subview.SetNeedsDisplay ();
}
#region Focus
/// <summary>Exposed as `internal` for unit tests. Indicates focus navigation direction.</summary>
internal enum NavigationDirection
{
/// <summary>Navigate forward.</summary>
Forward,
/// <summary>Navigate backwards.</summary>
Backward
}
/// <summary>Event fired when the view gets focus.</summary>
public event EventHandler<FocusEventArgs> Enter;
/// <summary>Event fired when the view looses focus.</summary>
public event EventHandler<FocusEventArgs> Leave;
private NavigationDirection _focusDirection;
internal NavigationDirection FocusDirection
{
get => SuperView?.FocusDirection ?? _focusDirection;
set
{
if (SuperView is { })
{
SuperView.FocusDirection = value;
}
else
{
_focusDirection = value;
}
}
}
private bool _hasFocus;
/// <inheritdoc/>
public bool HasFocus
{
set => SetHasFocus (value, this, true);
get => _hasFocus;
}
private void SetHasFocus (bool value, View view, bool force = false)
{
if (HasFocus != value || force)
{
_hasFocus = value;
if (value)
{
OnEnter (view);
}
else
{
OnLeave (view);
}
SetNeedsDisplay ();
}
// Remove focus down the chain of subviews if focus is removed
if (!value && Focused is { })
{
View f = Focused;
f.OnLeave (view);
f.SetHasFocus (false, view);
Focused = null;
}
}
/// <summary>Event fired when the <see cref="CanFocus"/> value is being changed.</summary>
public event EventHandler CanFocusChanged;
/// <summary>Method invoked when the <see cref="CanFocus"/> property from a view is changed.</summary>
public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); }
private bool _oldCanFocus;
private bool _canFocus;
/// <summary>Gets or sets a value indicating whether this <see cref="View"/> can focus.</summary>
public bool CanFocus
{
get => _canFocus;
set
{
if (!_addingView && IsInitialized && SuperView?.CanFocus == false && value)
{
throw new InvalidOperationException ("Cannot set CanFocus to true if the SuperView CanFocus is false!");
}
if (_canFocus == value)
{
return;
}
_canFocus = value;
switch (_canFocus)
{
case false when _tabIndex > -1:
TabIndex = -1;
break;
case true when SuperView?.CanFocus == false && _addingView:
SuperView.CanFocus = true;
break;
}
if (_canFocus && _tabIndex == -1)
{
TabIndex = SuperView is { } ? SuperView._tabIndexes.IndexOf (this) : -1;
}
TabStop = _canFocus;
if (!_canFocus && SuperView?.Focused == this)
{
SuperView.Focused = null;
}
if (!_canFocus && HasFocus)
{
SetHasFocus (false, this);
SuperView?.EnsureFocus ();
if (SuperView is { Focused: null })
{
SuperView.FocusNext ();
if (SuperView.Focused is null && Application.Current is { })
{
Application.Current.FocusNext ();
}
ApplicationOverlapped.BringOverlappedTopToFront ();
}
}
if (_subviews is { } && IsInitialized)
{
foreach (View view in _subviews)
{
if (view.CanFocus != value)
{
if (!value)
{
view._oldCanFocus = view.CanFocus;
view._oldTabIndex = view._tabIndex;
view.CanFocus = false;
view._tabIndex = -1;
}
else
{
if (_addingView)
{
view._addingView = true;
}
view.CanFocus = view._oldCanFocus;
view._tabIndex = view._oldTabIndex;
view._addingView = false;
}
}
}
if (this is Toplevel && Application.Current.Focused != this)
{
ApplicationOverlapped.BringOverlappedTopToFront ();
}
}
OnCanFocusChanged ();
SetNeedsDisplay ();
}
}
/// <summary>
/// Called when a view gets focus.
/// </summary>
/// <param name="view">The view that is losing focus.</param>
/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
public virtual bool OnEnter (View view)
{
var args = new FocusEventArgs (view, this);
Enter?.Invoke (this, args);
if (args.Handled)
{
return true;
}
return false;
}
/// <summary>Method invoked when a view loses focus.</summary>
/// <param name="view">The view that is getting focus.</param>
/// <returns><c>true</c>, if the event was handled, <c>false</c> otherwise.</returns>
public virtual bool OnLeave (View view)
{
var args = new FocusEventArgs (this, view);
Leave?.Invoke (this, args);
if (args.Handled)
{
return true;
}
return false;
}
// BUGBUG: This API is poorly defined and implemented. It does not specify what it means if THIS view is focused and has no subviews.
/// <summary>Returns the currently focused Subview inside this view, or null if nothing is focused.</summary>
/// <value>The focused.</value>
public View Focused { get; private set; }
// BUGBUG: This API is poorly defined and implemented. It does not specify what it means if THIS view is focused and has no subviews.
/// <summary>Returns the most focused Subview in the chain of subviews (the leaf view that has the focus).</summary>
/// <value>The most focused View.</value>
public View MostFocused
{
get
{
if (Focused is null)
{
return null;
}
View most = Focused.MostFocused;
if (most is { })
{
return most;
}
return Focused;
}
}
/// <summary>Causes the specified subview to have focus.</summary>
/// <param name="view">View.</param>
private void SetFocus (View view)
{
if (view is null)
{
return;
}
//Console.WriteLine ($"Request to focus {view}");
if (!view.CanFocus || !view.Visible || !view.Enabled)
{
return;
}
if (Focused?._hasFocus == true && Focused == view)
{
return;
}
if ((Focused?._hasFocus == true && Focused?.SuperView == view) || view == this)
{
if (!view._hasFocus)
{
view._hasFocus = true;
}
return;
}
// Make sure that this view is a subview
View c;
for (c = view._superView; c != null; c = c._superView)
{
if (c == this)
{
break;
}
}
if (c is null)
{
throw new ArgumentException ("the specified view is not part of the hierarchy of this view");
}
if (Focused is { })
{
Focused.SetHasFocus (false, view);
}
View f = Focused;
Focused = view;
Focused.SetHasFocus (true, f);
Focused.EnsureFocus ();
// Send focus upwards
if (SuperView is { })
{
SuperView.SetFocus (this);
}
else
{
SetFocus (this);
}
}
/// <summary>Causes this view to be focused and entire Superview hierarchy to have the focused order updated.</summary>
public void SetFocus ()
{
if (!CanBeVisible (this) || !Enabled)
{
if (HasFocus)
{
SetHasFocus (false, this);
}
return;
}
if (SuperView is { })
{
SuperView.SetFocus (this);
}
else
{
SetFocus (this);
}
}
/// <summary>
/// If there is no focused subview, calls <see cref="FocusFirst"/> or <see cref="FocusLast"/> based on <see cref="FocusDirection"/>.
/// does nothing.
/// </summary>
public void EnsureFocus ()
{
if (Focused is null && _subviews?.Count > 0)
{
if (FocusDirection == NavigationDirection.Forward)
{
FocusFirst ();
}
else
{
FocusLast ();
}
}
}
/// <summary>
/// Focuses the last focusable view in <see cref="View.TabIndexes"/> if one exists. If there are no views in <see cref="View.TabIndexes"/> then the focus is set to the view itself.
/// </summary>
public void FocusFirst (bool overlapped = false)
{
if (!CanBeVisible (this))
{
return;
}
if (_tabIndexes is null)
{
SuperView?.SetFocus (this);
return;
}
foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)))
{
if (view.CanFocus && view._tabStop && view.Visible && view.Enabled)
{
SetFocus (view);
return;
}
}
}
/// <summary>
/// Focuses the last focusable view in <see cref="View.TabIndexes"/> if one exists. If there are no views in <see cref="View.TabIndexes"/> then the focus is set to the view itself.
/// </summary>
public void FocusLast (bool overlapped = false)
{
if (!CanBeVisible (this))
{
return;
}
if (_tabIndexes is null)
{
SuperView?.SetFocus (this);
return;
}
foreach (View view in _tabIndexes.Where (v => !overlapped || v.Arrangement.HasFlag (ViewArrangement.Overlapped)).Reverse ())
{
if (view.CanFocus && view._tabStop && view.Visible && view.Enabled)
{
SetFocus (view);
return;
}
}
}
/// <summary>
/// Focuses the previous view in <see cref="View.TabIndexes"/>. If there is no previous view, the focus is set to the view itself.
/// </summary>
/// <returns><see langword="true"/> if previous was focused, <see langword="false"/> otherwise.</returns>
public bool FocusPrev ()
{
if (!CanBeVisible (this))
{
return false;
}
FocusDirection = NavigationDirection.Backward;
if (TabIndexes is null || TabIndexes.Count == 0)
{
return false;
}
if (Focused is null)
{
FocusLast ();
return Focused != null;
}
int focusedIdx = -1;
for (int i = TabIndexes.Count; i > 0;)
{
i--;
View w = TabIndexes [i];
if (w.HasFocus)
{
if (w.FocusPrev ())
{
return true;
}
focusedIdx = i;
continue;
}
if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled)
{
Focused.SetHasFocus (false, w);
// If the focused view is overlapped don't focus on the next if it's not overlapped.
if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
return false;
}
// If the focused view is not overlapped and the next is, skip it
if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
continue;
}
if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
{
w.FocusLast ();
}
SetFocus (w);
return true;
}
}
// There's no prev view in tab indexes.
if (Focused is { })
{
// Leave Focused
Focused.SetHasFocus (false, this);
if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
FocusLast (true);
return true;
}
// Signal to caller no next view was found
Focused = null;
}
return false;
}
/// <summary>
/// Focuses the next view in <see cref="View.TabIndexes"/>. If there is no next view, the focus is set to the view itself.
/// </summary>
/// <returns><see langword="true"/> if next was focused, <see langword="false"/> otherwise.</returns>
public bool FocusNext ()
{
if (!CanBeVisible (this))
{
return false;
}
FocusDirection = NavigationDirection.Forward;
if (TabIndexes is null || TabIndexes.Count == 0)
{
return false;
}
if (Focused is null)
{
FocusFirst ();
return Focused != null;
}
int focusedIdx = -1;
for (var i = 0; i < TabIndexes.Count; i++)
{
View w = TabIndexes [i];
if (w.HasFocus)
{
if (w.FocusNext ())
{
return true;
}
focusedIdx = i;
continue;
}
if (w.CanFocus && focusedIdx != -1 && w._tabStop && w.Visible && w.Enabled)
{
Focused.SetHasFocus (false, w);
// If the focused view is overlapped don't focus on the next if it's not overlapped.
if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && !w.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
return false;
}
// If the focused view is not overlapped and the next is, skip it
if (!Focused.Arrangement.HasFlag (ViewArrangement.Overlapped) && w.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
continue;
}
if (w.CanFocus && w._tabStop && w.Visible && w.Enabled)
{
w.FocusFirst ();
}
SetFocus (w);
return true;
}
}
// There's no next view in tab indexes.
if (Focused is { })
{
// Leave Focused
Focused.SetHasFocus (false, this);
if (Focused.Arrangement.HasFlag (ViewArrangement.Overlapped))
{
FocusFirst (true);
return true;
}
// Signal to caller no next view was found
Focused = null;
}
return false;
}
private View GetMostFocused (View view)
{
if (view is null)
{
return null;
}
return view.Focused is { } ? GetMostFocused (view.Focused) : view;
}
/// <summary>
/// Gets or sets the cursor style to be used when the view is focused. The default is <see cref="CursorVisibility.Invisible"/>.
/// </summary>
public CursorVisibility CursorVisibility { get; set; } = CursorVisibility.Invisible;
/// <summary>
/// Positions the cursor in the right position based on the currently focused view in the chain.
/// </summary>
/// <remarks>
/// <para>
/// Views that are focusable should override <see cref="PositionCursor"/> to make sure that the cursor is
/// placed in a location that makes sense. Some terminals do not have a way of hiding the cursor, so it can be
/// distracting to have the cursor left at the last focused view. So views should make sure that they place the
/// cursor in a visually sensible place. The default implementation of <see cref="PositionCursor"/> will place the
/// cursor at either the hotkey (if defined) or <c>0,0</c>.
/// </para>
/// </remarks>
/// <returns>Viewport-relative cursor position. Return <see langword="null"/> to ensure the cursor is not visible.</returns>
public virtual Point? PositionCursor ()
{
if (IsInitialized && CanFocus && HasFocus)
{
// By default, position the cursor at the hotkey (if any) or 0, 0.
Move (TextFormatter.HotKeyPos == -1 ? 0 : TextFormatter.CursorPosition, 0);
}
// Returning null will hide the cursor.
return null;
}
#endregion Focus
}

View File

@@ -27,6 +27,8 @@ public class ViewExperiments : Scenario
Title = "View1",
ColorScheme = Colors.ColorSchemes ["Base"],
Id = "View1",
ShadowStyle = ShadowStyle.Transparent,
BorderStyle = LineStyle.Double,
CanFocus = true, // Can't drag without this? BUGBUG
Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped
};
@@ -46,16 +48,7 @@ public class ViewExperiments : Scenario
//app.Add (view);
view.Margin.Thickness = new (0);
view.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"];
view.Margin.Data = "Margin";
view.Border.Thickness = new (1);
view.Border.LineStyle = LineStyle.Double;
view.Border.ColorScheme = view.ColorScheme;
view.Border.Data = "Border";
view.Padding.Thickness = new (0);
view.Padding.ColorScheme = Colors.ColorSchemes ["Error"];
view.Padding.Data = "Padding";
view.BorderStyle = LineStyle.Double;
var view2 = new View
{
@@ -66,6 +59,8 @@ public class ViewExperiments : Scenario
Title = "View2",
ColorScheme = Colors.ColorSchemes ["Base"],
Id = "View2",
ShadowStyle = ShadowStyle.Transparent,
BorderStyle = LineStyle.Double,
CanFocus = true, // Can't drag without this? BUGBUG
Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped
};
@@ -85,16 +80,6 @@ public class ViewExperiments : Scenario
view2.Add (button);
view2.Add (button);
view2.Margin.Thickness = new (0);
view2.Margin.ColorScheme = Colors.ColorSchemes ["Toplevel"];
view2.Margin.Data = "Margin";
view2.Border.Thickness = new (1);
view2.Border.LineStyle = LineStyle.Double;
view2.Border.ColorScheme = view2.ColorScheme;
view2.Border.Data = "Border";
view2.Padding.Thickness = new (0);
view2.Padding.ColorScheme = Colors.ColorSchemes ["Error"];
view2.Padding.Data = "Padding";
button = new ()
{