From d874f5628295f2811f59ba1321be11f5bb385af5 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 25 Jul 2024 10:40:28 -0600 Subject: [PATCH] Reorganized View source files to get my head straight --- .../Application/Application.Keyboard.cs | 30 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 42 - .../ConsoleDrivers/CursorVisibility.cs | 44 + Terminal.Gui/View/DrawEventArgs.cs | 29 + Terminal.Gui/View/Layout/LayoutEventArgs.cs | 12 + .../View/Navigation/FocusEventArgs.cs | 27 + .../{ViewAdornments.cs => View.Adornments.cs} | 2 +- Terminal.Gui/View/View.Arrangement.cs | 15 + .../View/{ViewContent.cs => View.Content.cs} | 0 Terminal.Gui/View/View.Cursor.cs | 35 + ...ViewDiagnostics.cs => View.Diagnostics.cs} | 0 .../View/{ViewDrawing.cs => View.Drawing.cs} | 2 +- Terminal.Gui/View/View.Hierarchy.cs | 320 ++++++ .../{ViewKeyboard.cs => View.Keyboard.cs} | 115 +-- .../{Layout/ViewLayout.cs => View.Layout.cs} | 2 +- .../View/{ViewMouse.cs => View.Mouse.cs} | 2 +- Terminal.Gui/View/View.Navigation.cs | 813 +++++++++++++++ .../View/{ViewText.cs => View.Text.cs} | 2 +- Terminal.Gui/View/ViewArrangement.cs | 30 +- Terminal.Gui/View/ViewEventArgs.cs | 67 +- Terminal.Gui/View/ViewSubViews.cs | 948 ------------------ UICatalog/Scenarios/ViewExperiments.cs | 25 +- 22 files changed, 1318 insertions(+), 1244 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/CursorVisibility.cs create mode 100644 Terminal.Gui/View/DrawEventArgs.cs create mode 100644 Terminal.Gui/View/Layout/LayoutEventArgs.cs create mode 100644 Terminal.Gui/View/Navigation/FocusEventArgs.cs rename Terminal.Gui/View/{ViewAdornments.cs => View.Adornments.cs} (99%) create mode 100644 Terminal.Gui/View/View.Arrangement.cs rename Terminal.Gui/View/{ViewContent.cs => View.Content.cs} (100%) create mode 100644 Terminal.Gui/View/View.Cursor.cs rename Terminal.Gui/View/{ViewDiagnostics.cs => View.Diagnostics.cs} (100%) rename Terminal.Gui/View/{ViewDrawing.cs => View.Drawing.cs} (99%) create mode 100644 Terminal.Gui/View/View.Hierarchy.cs rename Terminal.Gui/View/{ViewKeyboard.cs => View.Keyboard.cs} (91%) rename Terminal.Gui/View/{Layout/ViewLayout.cs => View.Layout.cs} (99%) rename Terminal.Gui/View/{ViewMouse.cs => View.Mouse.cs} (99%) create mode 100644 Terminal.Gui/View/View.Navigation.cs rename Terminal.Gui/View/{ViewText.cs => View.Text.cs} (99%) delete mode 100644 Terminal.Gui/View/ViewSubViews.cs diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index d26dcd432..969d4c31e 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -267,34 +267,6 @@ public static partial class Application // Keyboard handling CommandImplementations [command] = ctx => f (); } - ///// - ///// The key bindings. - ///// - //private static readonly Dictionary> _keyBindings = new (); - - ///// - ///// Gets the list of key bindings. - ///// - //public static Dictionary> GetKeyBindings () { return _keyBindings; } - - ///// - ///// Adds an scoped key binding. - ///// - ///// - ///// This is an internal method used by the class to add Application key bindings. - ///// - ///// The key being bound. - ///// The view that is bound to the key. If , will be used. - //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 diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index e99521d1e..dc5c785a0 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -603,48 +603,6 @@ public abstract class ConsoleDriver #endregion } -/// Terminal Cursor Visibility settings. -/// -/// 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 -/// -public enum CursorVisibility -{ - /// Cursor caret has default - /// - /// Works under Xterm-like terminal otherwise this is equivalent to . This default directly - /// depends on the XTerm user configuration settings, so it could be Block, I-Beam, Underline with possible blinking. - /// - Default = 0x00010119, - - /// Cursor caret is hidden - Invisible = 0x03000019, - - /// Cursor caret is normally shown as a blinking underline bar _ - Underline = 0x03010119, - - /// Cursor caret is normally shown as a underline bar _ - /// Under Windows, this is equivalent to - UnderlineFix = 0x04010119, - - /// Cursor caret is displayed a blinking vertical bar | - /// Works under Xterm-like terminal otherwise this is equivalent to - Vertical = 0x05010119, - - /// Cursor caret is displayed a blinking vertical bar | - /// Works under Xterm-like terminal otherwise this is equivalent to - VerticalFix = 0x06010119, - - /// Cursor caret is displayed as a blinking block ▉ - Box = 0x01020164, - - /// Cursor caret is displayed a block ▉ - /// Works under Xterm-like terminal otherwise this is equivalent to - BoxFix = 0x02020164 -} - /// /// The enumeration encodes key information from s and provides a /// consistent way for application code to specify keys and receive key events. diff --git a/Terminal.Gui/ConsoleDrivers/CursorVisibility.cs b/Terminal.Gui/ConsoleDrivers/CursorVisibility.cs new file mode 100644 index 000000000..b96d31fd4 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/CursorVisibility.cs @@ -0,0 +1,44 @@ +#nullable enable +namespace Terminal.Gui; + +/// Terminal Cursor Visibility settings. +/// +/// 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 +/// +public enum CursorVisibility +{ + /// Cursor caret has default + /// + /// Works under Xterm-like terminal otherwise this is equivalent to . This default directly + /// depends on the XTerm user configuration settings, so it could be Block, I-Beam, Underline with possible blinking. + /// + Default = 0x00010119, + + /// Cursor caret is hidden + Invisible = 0x03000019, + + /// Cursor caret is normally shown as a blinking underline bar _ + Underline = 0x03010119, + + /// Cursor caret is normally shown as a underline bar _ + /// Under Windows, this is equivalent to + UnderlineFix = 0x04010119, + + /// Cursor caret is displayed a blinking vertical bar | + /// Works under Xterm-like terminal otherwise this is equivalent to + Vertical = 0x05010119, + + /// Cursor caret is displayed a blinking vertical bar | + /// Works under Xterm-like terminal otherwise this is equivalent to + VerticalFix = 0x06010119, + + /// Cursor caret is displayed as a blinking block ▉ + Box = 0x01020164, + + /// Cursor caret is displayed a block ▉ + /// Works under Xterm-like terminal otherwise this is equivalent to + BoxFix = 0x02020164 +} diff --git a/Terminal.Gui/View/DrawEventArgs.cs b/Terminal.Gui/View/DrawEventArgs.cs new file mode 100644 index 000000000..32c07c711 --- /dev/null +++ b/Terminal.Gui/View/DrawEventArgs.cs @@ -0,0 +1,29 @@ +namespace Terminal.Gui; + +/// Event args for draw events +public class DrawEventArgs : EventArgs +{ + /// Creates a new instance of the class. + /// + /// The Content-relative rectangle describing the new visible viewport into the + /// . + /// + /// + /// The Content-relative rectangle describing the old visible viewport into the + /// . + /// + public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport) + { + NewViewport = newViewport; + OldViewport = oldViewport; + } + + /// If set to true, the draw operation will be canceled, if applicable. + public bool Cancel { get; set; } + + /// Gets the Content-relative rectangle describing the old visible viewport into the . + public Rectangle OldViewport { get; } + + /// Gets the Content-relative rectangle describing the currently visible viewport into the . + public Rectangle NewViewport { get; } +} diff --git a/Terminal.Gui/View/Layout/LayoutEventArgs.cs b/Terminal.Gui/View/Layout/LayoutEventArgs.cs new file mode 100644 index 000000000..dac959af0 --- /dev/null +++ b/Terminal.Gui/View/Layout/LayoutEventArgs.cs @@ -0,0 +1,12 @@ +namespace Terminal.Gui; + +/// Event arguments for the event. +public class LayoutEventArgs : EventArgs +{ + /// Creates a new instance of the class. + /// The view that the event is about. + public LayoutEventArgs (Size oldContentSize) { OldContentSize = oldContentSize; } + + /// The viewport of the before it was laid out. + public Size OldContentSize { get; set; } +} diff --git a/Terminal.Gui/View/Navigation/FocusEventArgs.cs b/Terminal.Gui/View/Navigation/FocusEventArgs.cs new file mode 100644 index 000000000..6d8d28267 --- /dev/null +++ b/Terminal.Gui/View/Navigation/FocusEventArgs.cs @@ -0,0 +1,27 @@ +namespace Terminal.Gui; + +/// Defines the event arguments for +public class FocusEventArgs : EventArgs +{ + /// Constructs. + /// The view that is losing focus. + /// The view that is gaining focus. + public FocusEventArgs (View leaving, View entering) { + Leaving = leaving; + Entering = entering; + } + + /// + /// 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. + /// + public bool Handled { get; set; } + + /// Indicates the view that is losing focus. + public View Leaving { get; set; } + + /// Indicates the view that is gaining focus. + public View Entering { get; set; } + +} diff --git a/Terminal.Gui/View/ViewAdornments.cs b/Terminal.Gui/View/View.Adornments.cs similarity index 99% rename from Terminal.Gui/View/ViewAdornments.cs rename to Terminal.Gui/View/View.Adornments.cs index accb15aba..2d179079e 100644 --- a/Terminal.Gui/View/ViewAdornments.cs +++ b/Terminal.Gui/View/View.Adornments.cs @@ -3,7 +3,7 @@ using System.Text.Json.Serialization; namespace Terminal.Gui; -public partial class View +public partial class View // Adornments { /// /// Initializes the Adornments of the View. Called by the constructor. diff --git a/Terminal.Gui/View/View.Arrangement.cs b/Terminal.Gui/View/View.Arrangement.cs new file mode 100644 index 000000000..0fea93324 --- /dev/null +++ b/Terminal.Gui/View/View.Arrangement.cs @@ -0,0 +1,15 @@ +namespace Terminal.Gui; + +public partial class View +{ + /// + /// Gets or sets the user actions that are enabled for the view within it's . + /// + /// + /// + /// Sizing or moving a view is only possible if the is part of a and + /// the relevant position and dimensions of the are independent of other SubViews + /// + /// + public ViewArrangement Arrangement { get; set; } +} diff --git a/Terminal.Gui/View/ViewContent.cs b/Terminal.Gui/View/View.Content.cs similarity index 100% rename from Terminal.Gui/View/ViewContent.cs rename to Terminal.Gui/View/View.Content.cs diff --git a/Terminal.Gui/View/View.Cursor.cs b/Terminal.Gui/View/View.Cursor.cs new file mode 100644 index 000000000..bdba7d85f --- /dev/null +++ b/Terminal.Gui/View/View.Cursor.cs @@ -0,0 +1,35 @@ +namespace Terminal.Gui; + +public partial class View +{ + /// + /// Gets or sets the cursor style to be used when the view is focused. The default is + /// . + /// + public CursorVisibility CursorVisibility { get; set; } = CursorVisibility.Invisible; + + /// + /// Positions the cursor in the right position based on the currently focused view in the chain. + /// + /// + /// + /// Views that are focusable should override 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 will place the + /// cursor at either the hotkey (if defined) or 0,0. + /// + /// + /// Viewport-relative cursor position. Return to ensure the cursor is not visible. + 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; + } +} diff --git a/Terminal.Gui/View/ViewDiagnostics.cs b/Terminal.Gui/View/View.Diagnostics.cs similarity index 100% rename from Terminal.Gui/View/ViewDiagnostics.cs rename to Terminal.Gui/View/View.Diagnostics.cs diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/View.Drawing.cs similarity index 99% rename from Terminal.Gui/View/ViewDrawing.cs rename to Terminal.Gui/View/View.Drawing.cs index 73fa5d550..1077f3917 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -public partial class View +public partial class View // Drawing APIs { private ColorScheme _colorScheme; diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs new file mode 100644 index 000000000..125baf33f --- /dev/null +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -0,0 +1,320 @@ +namespace Terminal.Gui; + +public partial class View // SuperView/SubView hierarchy management (SuperView, SubViews, Add, Remove, etc.) +{ + private static readonly IList _empty = new List (0).AsReadOnly (); + internal bool _addingView; + private List _subviews; // This is null, and allocated on demand. + private View _superView; + + /// Indicates whether the view was added to . + public bool IsAdded { get; private set; } + + /// This returns a list of the subviews contained by this view. + /// The subviews. + public IList Subviews => _subviews?.AsReadOnly () ?? _empty; + + /// Returns the container for this view, or null if this view has not been added to a container. + /// The super view. + 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 InternalSubviews => _subviews ?? _empty; + + /// Adds a subview (child) to this view. + /// + /// + /// The Views that have been added to this view can be retrieved via the property. See also + /// + /// + /// + /// 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. + /// + /// + /// The view to add. + /// The view that was added. + 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; + } + + /// Adds the specified views (children) to the view. + /// Array of one or more views (can be optional parameter). + /// + /// + /// The Views that have been added to this view can be retrieved via the property. See also + /// and . + /// + /// + /// 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. + /// + /// + public void Add (params View [] views) + { + if (views is null) + { + return; + } + + foreach (View view in views) + { + Add (view); + } + } + + /// Event fired when this view is added to another. + public event EventHandler Added; + + /// Get the top superview of a given . + /// The superview view. + 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; + } + + /// Method invoked when a subview is being added to this view. + /// Event where is the subview being added. + public virtual void OnAdded (SuperViewChangedEventArgs e) + { + View view = e.Child; + view.IsAdded = true; + view.OnResizeNeeded (); + view.Added?.Invoke (this, e); + } + + /// Method invoked when a subview is being removed from this view. + /// Event args describing the subview being removed. + public virtual void OnRemoved (SuperViewChangedEventArgs e) + { + View view = e.Child; + view.IsAdded = false; + view.Removed?.Invoke (this, e); + } + + /// Removes a subview added via or from this View. + /// + /// + /// 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 . + /// + /// + 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; + } + + /// + /// Removes all subviews (children) added via or from this View. + /// + /// + /// + /// 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 on any Views that were + /// added. + /// + /// + public virtual void RemoveAll () + { + if (_subviews is null) + { + return; + } + + while (_subviews.Count > 0) + { + Remove (_subviews [0]); + } + } + + /// Event fired when this view is removed from another. + public event EventHandler Removed; + + + /// Moves one position towards the start of the list + /// The subview to move forward. + 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); + } + } + ); + } + + /// Moves to the start of the list. + /// The subview to send to the start. + public void BringSubviewToFront (View subview) + { + PerformActionForSubview ( + subview, + x => + { + _subviews.Remove (x); + _subviews.Add (x); + } + ); + } + + + /// Moves one position towards the end of the list + /// The subview to move backwards. + public void SendSubviewBackwards (View subview) + { + PerformActionForSubview ( + subview, + x => + { + int idx = _subviews.IndexOf (x); + + if (idx > 0) + { + _subviews.Remove (x); + _subviews.Insert (idx - 1, x); + } + } + ); + } + + /// Moves to the end of the list. + /// The subview to send to the end. + public void SendSubviewToBack (View subview) + { + PerformActionForSubview ( + subview, + x => + { + _subviews.Remove (x); + _subviews.Insert (0, subview); + } + ); + } + + /// + /// Internal API that runs on a subview if it is part of the list. + /// + /// + /// + private void PerformActionForSubview (View subview, Action action) + { + if (_subviews.Contains (subview)) + { + action (subview); + } + + // BUGBUG: this is odd. Why is this needed? + SetNeedsDisplay (); + subview.SetNeedsDisplay (); + } + +} diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/View.Keyboard.cs similarity index 91% rename from Terminal.Gui/View/ViewKeyboard.cs rename to Terminal.Gui/View/View.Keyboard.cs index 7a905f129..7009ab4c6 100644 --- a/Terminal.Gui/View/ViewKeyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Terminal.Gui; -public partial class View +public partial class View // Keyboard APIs { /// /// 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 _tabIndexes; - - /// Gets a list of the subviews that are s. - /// The tabIndexes. - public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; - - private int _tabIndex = -1; - private int _oldTabIndex; - - /// - /// Indicates the index of the current from the list. See also: - /// . - /// - 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; - - /// - /// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be - /// only if the is also . Set to to prevent the - /// view from being a stop-point for keyboard navigation. - /// - /// - /// The default keyboard navigation keys are Key.Tab and Key>Tab.WithShift. These can be changed by - /// modifying the key bindings (see ) of the SuperView. - /// - 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 diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/View.Layout.cs similarity index 99% rename from Terminal.Gui/View/Layout/ViewLayout.cs rename to Terminal.Gui/View/View.Layout.cs index 72a9bf14c..deb7da682 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/View.Layout.cs @@ -3,7 +3,7 @@ using System.Diagnostics; namespace Terminal.Gui; -public partial class View +public partial class View // Layout APIs { #region Frame diff --git a/Terminal.Gui/View/ViewMouse.cs b/Terminal.Gui/View/View.Mouse.cs similarity index 99% rename from Terminal.Gui/View/ViewMouse.cs rename to Terminal.Gui/View/View.Mouse.cs index 24314f583..5f1318e21 100644 --- a/Terminal.Gui/View/ViewMouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -public partial class View +public partial class View // Mouse APIs { [CanBeNull] private ColorScheme _savedHighlightColorScheme; diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs new file mode 100644 index 000000000..4ff99c3d7 --- /dev/null +++ b/Terminal.Gui/View/View.Navigation.cs @@ -0,0 +1,813 @@ +namespace Terminal.Gui; + +public partial class View // Focus and cross-view navigation management (TabStop, TabIndex, etc...) +{ + /// Returns a value indicating if this View is currently on Top (Active) + public bool IsCurrentTop => Application.Current == this; + + // BUGBUG: This API is poorly defined and implemented. It deeply intertwines the view hierarchy with the tab order. + /// Exposed as `internal` for unit tests. Indicates focus navigation direction. + internal enum NavigationDirection + { + /// Navigate forward. + Forward, + + /// Navigate backwards. + Backward + } + + /// Invoked when this view is gaining focus (entering). + /// The view that is leaving focus. + /// , if the event was handled, otherwise. + /// + /// + /// Overrides must call the base class method to ensure that the event is raised. If the event + /// is handled, the method should return . + /// + /// + public virtual bool OnEnter (View leavingView) + { + var args = new FocusEventArgs (leavingView, this); + Enter?.Invoke (this, args); + + if (args.Handled) + { + return true; + } + + return false; + } + + /// Invoked when this view is losing focus (leaving). + /// The view that is entering focus. + /// , if the event was handled, otherwise. + /// + /// + /// Overrides must call the base class method to ensure that the event is raised. If the event + /// is handled, the method should return . + /// + /// + public virtual bool OnLeave (View enteringView) + { + var args = new FocusEventArgs (this, enteringView); + Leave?.Invoke (this, args); + + if (args.Handled) + { + return true; + } + + return false; + } + + /// Raised when the view is gaining (entering) focus. Can be cancelled. + /// + /// Raised by the virtual method. + /// + public event EventHandler Enter; + + /// Raised when the view is losing (leaving) focus. Can be cancelled. + /// + /// Raised by the virtual method. + /// + public event EventHandler Leave; + + private NavigationDirection _focusDirection; + + /// + /// 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. + /// + internal NavigationDirection FocusDirection + { + get => SuperView?.FocusDirection ?? _focusDirection; + set + { + if (SuperView is { }) + { + SuperView.FocusDirection = value; + } + else + { + _focusDirection = value; + } + } + } + + private bool _hasFocus; + + /// + /// Gets or sets whether this view has focus. + /// + /// + /// + /// Causes the and virtual methods (and and + /// events to be raised) when the value changes. + /// + /// + /// Setting this property to will recursively set to + /// + /// for any focused subviews. + /// + /// + public bool HasFocus + { + // Force the specified view to have focus + set => SetHasFocus (value, this, true); + get => _hasFocus; + } + + /// + /// Internal API that sets . This method is called by HasFocus_set and other methods that + /// need to set or remove focus from a view. + /// + /// The new setting for . + /// The view that will be gaining or losing focus. + /// + /// to force Enter/Leave on regardless of whether it + /// already HasFocus or not. + /// + /// + /// If is and there is a focused subview ( + /// is not ), + /// this method will recursively remove focus from any focused subviews of . + /// + 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; + } + } + + /// Raised when has been changed. + /// + /// Raised by the virtual method. + /// + public event EventHandler CanFocusChanged; + + /// Invoked when the property from a view is changed. + /// + /// Raises the event. + /// + public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } + + private bool _oldCanFocus; + private bool _canFocus; + + /// Gets or sets a value indicating whether this can be focused. + /// + /// + /// must also have set to . + /// + /// + 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 (); + } + } + + /// Returns the currently focused Subview inside this view, or if nothing is focused. + /// The currently focused Subview. + public View Focused { get; private set; } + + /// + /// Returns the most focused Subview in the chain of subviews (the leaf view that has the focus), or + /// if nothing is focused. + /// + /// The most focused Subview. + public View MostFocused + { + get + { + if (Focused is null) + { + return null; + } + + View most = Focused.MostFocused; + + if (most is { }) + { + return most; + } + + return Focused; + } + } + + /// Causes subview specified by to enter focus. + /// View. + 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); + } + } + + /// Causes this view to be focused and entire Superview hierarchy to have the focused order updated. + public void SetFocus () + { + if (!CanBeVisible (this) || !Enabled) + { + if (HasFocus) + { + SetHasFocus (false, this); + } + + return; + } + + if (SuperView is { }) + { + SuperView.SetFocus (this); + } + else + { + SetFocus (this); + } + } + + /// + /// If there is no focused subview, calls or based on + /// . + /// does nothing. + /// + public void EnsureFocus () + { + if (Focused is null && _subviews?.Count > 0) + { + if (FocusDirection == NavigationDirection.Forward) + { + FocusFirst (); + } + else + { + FocusLast (); + } + } + } + + /// + /// Focuses the last focusable view in if one exists. If there are no views in + /// then the focus is set to the view itself. + /// + 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; + } + } + } + + /// + /// Focuses the last focusable view in if one exists. If there are no views in + /// then the focus is set to the view itself. + /// + 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; + } + } + } + + /// + /// Focuses the previous view in . If there is no previous view, the focus is set to the + /// view itself. + /// + /// if previous was focused, otherwise. + 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; + } + + /// + /// Focuses the next view in . If there is no next view, the focus is set to the view + /// itself. + /// + /// if next was focused, otherwise. + 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 _tabIndexes; + + // TODO: This should be a get-only property? + // BUGBUG: This returns an AsReadOnly list, but isn't declared as such. + /// Gets a list of the subviews that are a . + /// The tabIndexes. + public IList 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; + + /// + /// Indicates the index of the current from the list. See also: + /// . + /// + /// + /// + /// If the value is -1, the view is not part of the tab order. + /// + /// + /// On set, if is , will be set to -1. + /// + /// + /// On set, if is or has not TabStops, will + /// be set to 0. + /// + /// + /// On set, if has only one TabStop, will be set to 0. + /// + /// + 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 (); + } + } + } + + /// + /// Gets the greatest of the 's that is less + /// than or equal to . + /// + /// + /// The minimum of and the 's . + 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); + } + + /// + /// Re-orders the s of the views in the 's . + /// + 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; + + /// + /// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be + /// only if is . Set to to prevent the + /// view from being a stop-point for keyboard navigation. + /// + /// + /// The default keyboard navigation keys are Key.Tab and Key>Tab.WithShift. These can be changed by + /// modifying the key bindings (see ) of the SuperView. + /// + public bool TabStop + { + get => _tabStop; + set + { + if (_tabStop == value) + { + return; + } + + _tabStop = CanFocus && value; + } + } + + #endregion Tab/Focus Handling +} diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/View.Text.cs similarity index 99% rename from Terminal.Gui/View/ViewText.cs rename to Terminal.Gui/View/View.Text.cs index 6b9e0cf94..664640730 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/View.Text.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -public partial class View +public partial class View // Text Property APIs { /// /// Initializes the Text of the View. Called by the constructor. diff --git a/Terminal.Gui/View/ViewArrangement.cs b/Terminal.Gui/View/ViewArrangement.cs index df13c4762..0143b082e 100644 --- a/Terminal.Gui/View/ViewArrangement.cs +++ b/Terminal.Gui/View/ViewArrangement.cs @@ -1,14 +1,16 @@ namespace Terminal.Gui; /// -/// Describes what user actions are enabled for arranging a within it's . +/// Describes what user actions are enabled for arranging a within it's +/// . /// See . /// /// -/// -/// Sizing or moving a view is only possible if the is part of a and -/// the relevant position and dimensions of the are independent of other SubViews -/// +/// +/// Sizing or moving a view is only possible if the is part of a +/// and +/// the relevant position and dimensions of the are independent of other SubViews +/// /// [Flags] public enum ViewArrangement @@ -56,26 +58,14 @@ public enum ViewArrangement Resizable = LeftResizable | RightResizable | TopResizable | BottomResizable, /// - /// The view overlap other views. + /// The view overlap other views. /// /// /// - /// 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. /// /// Overlapped = 32 } -public partial class View -{ - /// - /// Gets or sets the user actions that are enabled for the view within it's . - /// - /// - /// - /// Sizing or moving a view is only possible if the is part of a and - /// the relevant position and dimensions of the are independent of other SubViews - /// - /// - public ViewArrangement Arrangement { get; set; } -} diff --git a/Terminal.Gui/View/ViewEventArgs.cs b/Terminal.Gui/View/ViewEventArgs.cs index b17b98afe..cdcbaa009 100644 --- a/Terminal.Gui/View/ViewEventArgs.cs +++ b/Terminal.Gui/View/ViewEventArgs.cs @@ -13,69 +13,4 @@ public class ViewEventArgs : EventArgs /// child then sender may be the parent while is the child being added. /// public View View { get; } -} - -/// Event arguments for the event. -public class LayoutEventArgs : EventArgs -{ - /// Creates a new instance of the class. - /// The view that the event is about. - public LayoutEventArgs (Size oldContentSize) { OldContentSize = oldContentSize; } - - /// The viewport of the before it was laid out. - public Size OldContentSize { get; set; } -} - -/// Event args for draw events -public class DrawEventArgs : EventArgs -{ - /// Creates a new instance of the class. - /// - /// The Content-relative rectangle describing the new visible viewport into the - /// . - /// - /// - /// The Content-relative rectangle describing the old visible viewport into the - /// . - /// - public DrawEventArgs (Rectangle newViewport, Rectangle oldViewport) - { - NewViewport = newViewport; - OldViewport = oldViewport; - } - - /// If set to true, the draw operation will be canceled, if applicable. - public bool Cancel { get; set; } - - /// Gets the Content-relative rectangle describing the old visible viewport into the . - public Rectangle OldViewport { get; } - - /// Gets the Content-relative rectangle describing the currently visible viewport into the . - public Rectangle NewViewport { get; } -} - -/// Defines the event arguments for -public class FocusEventArgs : EventArgs -{ - /// Constructs. - /// The view that is losing focus. - /// The view that is gaining focus. - public FocusEventArgs (View leaving, View entering) { - Leaving = leaving; - Entering = entering; - } - - /// - /// 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. - /// - public bool Handled { get; set; } - - /// Indicates the view that is losing focus. - public View Leaving { get; set; } - - /// Indicates the view that is gaining focus. - public View Entering { get; set; } - -} +} \ No newline at end of file diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs deleted file mode 100644 index 79cca431e..000000000 --- a/Terminal.Gui/View/ViewSubViews.cs +++ /dev/null @@ -1,948 +0,0 @@ -using System.Diagnostics; - -namespace Terminal.Gui; - -public partial class View -{ - private static readonly IList _empty = new List (0).AsReadOnly (); - internal bool _addingView; - private List _subviews; // This is null, and allocated on demand. - private View _superView; - - /// Indicates whether the view was added to . - public bool IsAdded { get; private set; } - - /// Returns a value indicating if this View is currently on Top (Active) - public bool IsCurrentTop => Application.Current == this; - - /// This returns a list of the subviews contained by this view. - /// The subviews. - public IList Subviews => _subviews?.AsReadOnly () ?? _empty; - - /// Returns the container for this view, or null if this view has not been added to a container. - /// The super view. - 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 InternalSubviews => _subviews ?? _empty; - - /// Adds a subview (child) to this view. - /// - /// - /// The Views that have been added to this view can be retrieved via the property. See also - /// - /// - /// - /// 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. - /// - /// - /// The view to add. - /// The view that was added. - 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; - } - - /// Adds the specified views (children) to the view. - /// Array of one or more views (can be optional parameter). - /// - /// - /// The Views that have been added to this view can be retrieved via the property. See also - /// and . - /// - /// - /// 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. - /// - /// - public void Add (params View [] views) - { - if (views is null) - { - return; - } - - foreach (View view in views) - { - Add (view); - } - } - - /// Event fired when this view is added to another. - public event EventHandler Added; - - /// Moves the subview backwards in the hierarchy, only one step - /// The subview to send backwards - /// If you want to send the view all the way to the back use SendSubviewToBack. - 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); - } - } - ); - } - - /// Brings the specified subview to the front so it is drawn on top of any other views. - /// The subview to send to the front - /// . - public void BringSubviewToFront (View subview) - { - PerformActionForSubview ( - subview, - x => - { - _subviews.Remove (x); - _subviews.Add (x); - } - ); - } - - /// Get the top superview of a given . - /// The superview view. - 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; - } - - /// Method invoked when a subview is being added to this view. - /// Event where is the subview being added. - public virtual void OnAdded (SuperViewChangedEventArgs e) - { - View view = e.Child; - view.IsAdded = true; - view.OnResizeNeeded (); - view.Added?.Invoke (this, e); - } - - /// Method invoked when a subview is being removed from this view. - /// Event args describing the subview being removed. - public virtual void OnRemoved (SuperViewChangedEventArgs e) - { - View view = e.Child; - view.IsAdded = false; - view.Removed?.Invoke (this, e); - } - - /// Removes a subview added via or from this View. - /// - /// - /// 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 . - /// - /// - 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; - } - - /// - /// Removes all subviews (children) added via or from this View. - /// - /// - /// - /// 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 on any Views that were - /// added. - /// - /// - public virtual void RemoveAll () - { - if (_subviews is null) - { - return; - } - - while (_subviews.Count > 0) - { - Remove (_subviews [0]); - } - } - - /// Event fired when this view is removed from another. - public event EventHandler Removed; - - /// Moves the subview backwards in the hierarchy, only one step - /// The subview to send backwards - /// If you want to send the view all the way to the back use SendSubviewToBack. - public void SendSubviewBackwards (View subview) - { - PerformActionForSubview ( - subview, - x => - { - int idx = _subviews.IndexOf (x); - - if (idx > 0) - { - _subviews.Remove (x); - _subviews.Insert (idx - 1, x); - } - } - ); - } - - /// Sends the specified subview to the front so it is the first view drawn - /// The subview to send to the front - /// . - public void SendSubviewToBack (View subview) - { - PerformActionForSubview ( - subview, - x => - { - _subviews.Remove (x); - _subviews.Insert (0, subview); - } - ); - } - - private void PerformActionForSubview (View subview, Action action) - { - if (_subviews.Contains (subview)) - { - action (subview); - } - - SetNeedsDisplay (); - subview.SetNeedsDisplay (); - } - - #region Focus - - /// Exposed as `internal` for unit tests. Indicates focus navigation direction. - internal enum NavigationDirection - { - /// Navigate forward. - Forward, - - /// Navigate backwards. - Backward - } - - /// Event fired when the view gets focus. - public event EventHandler Enter; - - /// Event fired when the view looses focus. - public event EventHandler Leave; - - private NavigationDirection _focusDirection; - - internal NavigationDirection FocusDirection - { - get => SuperView?.FocusDirection ?? _focusDirection; - set - { - if (SuperView is { }) - { - SuperView.FocusDirection = value; - } - else - { - _focusDirection = value; - } - } - } - - private bool _hasFocus; - - /// - 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; - } - } - - /// Event fired when the value is being changed. - public event EventHandler CanFocusChanged; - - /// Method invoked when the property from a view is changed. - public virtual void OnCanFocusChanged () { CanFocusChanged?.Invoke (this, EventArgs.Empty); } - - private bool _oldCanFocus; - private bool _canFocus; - - /// Gets or sets a value indicating whether this can focus. - 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 (); - } - } - - /// - /// Called when a view gets focus. - /// - /// The view that is losing focus. - /// true, if the event was handled, false otherwise. - public virtual bool OnEnter (View view) - { - var args = new FocusEventArgs (view, this); - Enter?.Invoke (this, args); - - if (args.Handled) - { - return true; - } - - return false; - } - - /// Method invoked when a view loses focus. - /// The view that is getting focus. - /// true, if the event was handled, false otherwise. - 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. - /// Returns the currently focused Subview inside this view, or null if nothing is focused. - /// The focused. - 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. - /// Returns the most focused Subview in the chain of subviews (the leaf view that has the focus). - /// The most focused View. - public View MostFocused - { - get - { - if (Focused is null) - { - return null; - } - - View most = Focused.MostFocused; - - if (most is { }) - { - return most; - } - - return Focused; - } - } - - /// Causes the specified subview to have focus. - /// View. - 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); - } - } - - /// Causes this view to be focused and entire Superview hierarchy to have the focused order updated. - public void SetFocus () - { - if (!CanBeVisible (this) || !Enabled) - { - if (HasFocus) - { - SetHasFocus (false, this); - } - - return; - } - - if (SuperView is { }) - { - SuperView.SetFocus (this); - } - else - { - SetFocus (this); - } - } - - /// - /// If there is no focused subview, calls or based on . - /// does nothing. - /// - public void EnsureFocus () - { - if (Focused is null && _subviews?.Count > 0) - { - if (FocusDirection == NavigationDirection.Forward) - { - FocusFirst (); - } - else - { - FocusLast (); - } - } - } - - /// - /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. - /// - 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; - } - } - } - - /// - /// Focuses the last focusable view in if one exists. If there are no views in then the focus is set to the view itself. - /// - 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; - } - } - } - - /// - /// Focuses the previous view in . If there is no previous view, the focus is set to the view itself. - /// - /// if previous was focused, otherwise. - 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; - } - - /// - /// Focuses the next view in . If there is no next view, the focus is set to the view itself. - /// - /// if next was focused, otherwise. - 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; - } - - /// - /// Gets or sets the cursor style to be used when the view is focused. The default is . - /// - public CursorVisibility CursorVisibility { get; set; } = CursorVisibility.Invisible; - - /// - /// Positions the cursor in the right position based on the currently focused view in the chain. - /// - /// - /// - /// Views that are focusable should override 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 will place the - /// cursor at either the hotkey (if defined) or 0,0. - /// - /// - /// Viewport-relative cursor position. Return to ensure the cursor is not visible. - 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 -} diff --git a/UICatalog/Scenarios/ViewExperiments.cs b/UICatalog/Scenarios/ViewExperiments.cs index 263a507d9..4c212b8b7 100644 --- a/UICatalog/Scenarios/ViewExperiments.cs +++ b/UICatalog/Scenarios/ViewExperiments.cs @@ -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 () {