diff --git a/Terminal.Gui/Application.MainLoopSyncContext.cs b/Terminal.Gui/Application.MainLoopSyncContext.cs deleted file mode 100644 index 513608e8c..000000000 --- a/Terminal.Gui/Application.MainLoopSyncContext.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Terminal.Gui; - -public static partial class Application -{ - /// - /// provides the sync context set while executing code in Terminal.Gui, to let - /// users use async/await on their code - /// - private sealed class MainLoopSyncContext : SynchronizationContext - { - public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); } - - public override void Post (SendOrPostCallback d, object state) - { - MainLoop?.AddIdle ( - () => - { - d (state); - - return false; - } - ); - } - - //_mainLoop.Driver.Wakeup (); - public override void Send (SendOrPostCallback d, object state) - { - if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) - { - d (state); - } - else - { - var wasExecuted = false; - - Invoke ( - () => - { - d (state); - wasExecuted = true; - } - ); - - while (!wasExecuted) - { - Thread.Sleep (15); - } - } - } - } -} diff --git a/Terminal.Gui/Application.cs b/Terminal.Gui/Application/Application.cs similarity index 75% rename from Terminal.Gui/Application.cs rename to Terminal.Gui/Application/Application.cs index d825fb872..f015065fe 100644 --- a/Terminal.Gui/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -99,7 +99,6 @@ public static partial class Application // Don't dispose the Top. It's up to caller dispose it if (Top is { }) { - Debug.Assert (Top.WasDisposed); // If End wasn't called _cachedRunStateToplevel may be null @@ -158,6 +157,7 @@ public static partial class Application KeyDown = null; KeyUp = null; SizeChanging = null; + ClearKeyBindings (); Colors.Reset (); @@ -526,12 +526,8 @@ public static partial class Application MoveCurrent (Current); } - //if (Toplevel.LayoutStyle == LayoutStyle.Computed) { toplevel.SetRelativeLayout (Driver.Screen.Size); - //} - - // BUGBUG: This call is likely not needed. toplevel.LayoutSubviews (); toplevel.PositionToplevels (); toplevel.FocusFirst (); @@ -543,6 +539,7 @@ public static partial class Application toplevel.SetNeedsDisplay (); toplevel.Draw (); Driver.UpdateScreen (); + if (PositionCursor (toplevel)) { Driver.UpdateCursor (); @@ -555,13 +552,14 @@ public static partial class Application } /// - /// Calls on the most focused view in the view starting with . + /// Calls on the most focused view in the view starting with . /// /// - /// Does nothing if is or if the most focused view is not visible or enabled. - /// - /// If the most focused view is not visible within it's superview, the cursor will be hidden. - /// + /// Does nothing if is or if the most focused view is not visible or + /// enabled. + /// + /// If the most focused view is not visible within it's superview, the cursor will be hidden. + /// /// /// if a view positioned the cursor and the position is visible. internal static bool PositionCursor (View view) @@ -585,6 +583,7 @@ public static partial class Application if (!mostFocused.Visible || !mostFocused.Enabled) { Driver.GetCursorVisibility (out CursorVisibility current); + if (current != CursorVisibility.Invisible) { Driver.SetCursorVisibility (CursorVisibility.Invisible); @@ -596,6 +595,7 @@ public static partial class Application // If the view is not visible within it's superview, don't position the cursor Rectangle mostFocusedViewport = mostFocused.ViewportToScreen (mostFocused.Viewport with { Location = Point.Empty }); Rectangle superViewViewport = mostFocused.SuperView?.ViewportToScreen (mostFocused.SuperView.Viewport with { Location = Point.Empty }) ?? Driver.Screen; + if (!superViewViewport.IntersectsWith (mostFocusedViewport)) { return false; @@ -677,7 +677,7 @@ public static partial class Application /// /// The created T object. The caller is responsible for disposing this object. public static T Run (Func errorHandler = null, ConsoleDriver driver = null) - where T : Toplevel, new() + where T : Toplevel, new () { var top = new T (); @@ -964,6 +964,7 @@ public static partial class Application { state.Toplevel.Draw (); Driver.UpdateScreen (); + //Driver.UpdateCursor (); } @@ -1424,516 +1425,4 @@ public static partial class Application } #endregion Toplevel handling - - #region Mouse handling - - /// Disable or enable the mouse. The mouse is enabled by default. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static bool IsMouseDisabled { get; set; } - - /// The current object that wants continuous mouse button pressed events. - public static View WantContinuousButtonPressedView { get; private set; } - - /// - /// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to - /// this view until the view calls or the mouse is released. - /// - public static View MouseGrabView { get; private set; } - - /// Invoked when a view wants to grab the mouse; can be canceled. - public static event EventHandler GrabbingMouse; - - /// Invoked when a view wants un-grab the mouse; can be canceled. - public static event EventHandler UnGrabbingMouse; - - /// Invoked after a view has grabbed the mouse. - public static event EventHandler GrabbedMouse; - - /// Invoked after a view has un-grabbed the mouse. - public static event EventHandler UnGrabbedMouse; - - /// - /// Grabs the mouse, forcing all mouse events to be routed to the specified view until - /// is called. - /// - /// View that will receive all mouse events until is invoked. - public static void GrabMouse (View view) - { - if (view is null) - { - return; - } - - if (!OnGrabbingMouse (view)) - { - OnGrabbedMouse (view); - MouseGrabView = view; - } - } - - /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is. - public static void UngrabMouse () - { - if (MouseGrabView is null) - { - return; - } - - if (!OnUnGrabbingMouse (MouseGrabView)) - { - View view = MouseGrabView; - MouseGrabView = null; - OnUnGrabbedMouse (view); - } - } - - private static bool OnGrabbingMouse (View view) - { - if (view is null) - { - return false; - } - - var evArgs = new GrabMouseEventArgs (view); - GrabbingMouse?.Invoke (view, evArgs); - - return evArgs.Cancel; - } - - private static bool OnUnGrabbingMouse (View view) - { - if (view is null) - { - return false; - } - - var evArgs = new GrabMouseEventArgs (view); - UnGrabbingMouse?.Invoke (view, evArgs); - - return evArgs.Cancel; - } - - private static void OnGrabbedMouse (View view) - { - if (view is null) - { - return; - } - - GrabbedMouse?.Invoke (view, new (view)); - } - - private static void OnUnGrabbedMouse (View view) - { - if (view is null) - { - return; - } - - UnGrabbedMouse?.Invoke (view, new (view)); - } - -#nullable enable - - // Used by OnMouseEvent to track the last view that was clicked on. - internal static View? _mouseEnteredView; - - /// Event fired when a mouse move or click occurs. Coordinates are screen relative. - /// - /// - /// Use this event to receive mouse events in screen coordinates. Use to - /// receive mouse events relative to a . - /// - /// The will contain the that contains the mouse coordinates. - /// - public static event EventHandler? MouseEvent; - - /// Called when a mouse event occurs. Raises the event. - /// This method can be used to simulate a mouse event, e.g. in unit tests. - /// The mouse event with coordinates relative to the screen. - internal static void OnMouseEvent (MouseEvent mouseEvent) - { - if (IsMouseDisabled) - { - return; - } - - var view = View.FindDeepestView (Current, mouseEvent.Position); - - if (view is { }) - { - mouseEvent.View = view; - } - - MouseEvent?.Invoke (null, mouseEvent); - - if (mouseEvent.Handled) - { - return; - } - - if (MouseGrabView is { }) - { - // If the mouse is grabbed, send the event to the view that grabbed it. - // The coordinates are relative to the Bounds of the view that grabbed the mouse. - Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.Position); - - var viewRelativeMouseEvent = new MouseEvent - { - Position = frameLoc, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.Position, - View = MouseGrabView - }; - - if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false) - { - // The mouse has moved outside the bounds of the view that grabbed the mouse - _mouseEnteredView?.NewMouseLeaveEvent (mouseEvent); - } - - //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); - if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true) - { - return; - } - } - - if (view is { WantContinuousButtonPressed: true }) - { - WantContinuousButtonPressedView = view; - } - else - { - WantContinuousButtonPressedView = null; - } - - - if (view is not Adornment) - { - if ((view is null || view == OverlappedTop) - && Current is { Modal: false } - && OverlappedTop != null - && mouseEvent.Flags != MouseFlags.ReportMousePosition - && mouseEvent.Flags != 0) - { - // This occurs when there are multiple overlapped "tops" - // E.g. "Mdi" - in the Background Worker Scenario - View? top = FindDeepestTop (Top, mouseEvent.Position); - view = View.FindDeepestView (top, mouseEvent.Position); - - if (view is { } && view != OverlappedTop && top != Current && top is { }) - { - MoveCurrent ((Toplevel)top); - } - } - } - - if (view is null) - { - return; - } - - MouseEvent? me = null; - - if (view is Adornment adornment) - { - Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position); - - me = new () - { - Position = frameLoc, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.Position, - View = view - }; - } - else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position)) - { - Point viewportLocation = view.ScreenToViewport (mouseEvent.Position); - - me = new () - { - Position = viewportLocation, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.Position, - View = view - }; - } - - if (me is null) - { - return; - } - - if (_mouseEnteredView is null) - { - _mouseEnteredView = view; - view.NewMouseEnterEvent (me); - } - else if (_mouseEnteredView != view) - { - _mouseEnteredView.NewMouseLeaveEvent (me); - view.NewMouseEnterEvent (me); - _mouseEnteredView = view; - } - - if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition) - { - return; - } - - WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null; - - //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}"); - - while (view.NewMouseEvent (me) != true) - { - if (MouseGrabView is { }) - { - break; - } - - if (view is Adornment adornmentView) - { - view = adornmentView.Parent.SuperView; - } - else - { - view = view.SuperView; - } - - if (view is null) - { - break; - } - - Point boundsPoint = view.ScreenToViewport (mouseEvent.Position); - - me = new () - { - Position = boundsPoint, - Flags = mouseEvent.Flags, - ScreenPosition = mouseEvent.Position, - View = view - }; - } - - BringOverlappedTopToFront (); - } -#nullable restore - - #endregion Mouse handling - - #region Keyboard handling - - private static Key _alternateForwardKey = Key.Empty; // Defined in config.json - - /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - [JsonConverter (typeof (KeyJsonConverter))] - public static Key AlternateForwardKey - { - get => _alternateForwardKey; - set - { - if (_alternateForwardKey != value) - { - Key oldKey = _alternateForwardKey; - _alternateForwardKey = value; - OnAlternateForwardKeyChanged (new (oldKey, value)); - } - } - } - - private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) - { - foreach (Toplevel top in _topLevels.ToArray ()) - { - top.OnAlternateForwardKeyChanged (e); - } - } - - private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json - - /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - [JsonConverter (typeof (KeyJsonConverter))] - public static Key AlternateBackwardKey - { - get => _alternateBackwardKey; - set - { - if (_alternateBackwardKey != value) - { - Key oldKey = _alternateBackwardKey; - _alternateBackwardKey = value; - OnAlternateBackwardKeyChanged (new (oldKey, value)); - } - } - } - - private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey) - { - foreach (Toplevel top in _topLevels.ToArray ()) - { - top.OnAlternateBackwardKeyChanged (oldKey); - } - } - - private static Key _quitKey = Key.Empty; // Defined in config.json - - /// Gets or sets the key to quit the application. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - [JsonConverter (typeof (KeyJsonConverter))] - public static Key QuitKey - { - get => _quitKey; - set - { - if (_quitKey != value) - { - Key oldKey = _quitKey; - _quitKey = value; - OnQuitKeyChanged (new (oldKey, value)); - } - } - } - - private static void OnQuitKeyChanged (KeyChangedEventArgs e) - { - // Duplicate the list so if it changes during enumeration we're safe - foreach (Toplevel top in _topLevels.ToArray ()) - { - top.OnQuitKeyChanged (e); - } - } - - /// - /// Event fired when the user presses a key. Fired by . - /// - /// Set to to indicate the key was handled and to prevent - /// additional processing. - /// - /// - /// - /// All drivers support firing the event. Some drivers (Curses) do not support firing the - /// and events. - /// Fired after and before . - /// - public static event EventHandler KeyDown; - - /// - /// Called by the when the user presses a key. Fires the event - /// then calls on all top level views. Called after and - /// before . - /// - /// Can be used to simulate key press events. - /// - /// if the key was handled. - public static bool OnKeyDown (Key keyEvent) - { - if (!_initialized) - { - return true; - } - - KeyDown?.Invoke (null, keyEvent); - - if (keyEvent.Handled) - { - return true; - } - - foreach (Toplevel topLevel in _topLevels.ToList ()) - { - if (topLevel.NewKeyDownEvent (keyEvent)) - { - return true; - } - - if (topLevel.Modal) - { - break; - } - } - - // Invoke any Global KeyBindings - foreach (Toplevel topLevel in _topLevels.ToList ()) - { - foreach (View view in topLevel.Subviews.Where ( - v => v.KeyBindings.TryGet ( - keyEvent, - KeyBindingScope.Application, - out KeyBinding _ - ) - )) - { - if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out KeyBinding _)) - { - bool? handled = view.OnInvokingKeyBindings (keyEvent); - - if (handled is { } && (bool)handled) - { - return true; - } - } - } - } - - return false; - } - - /// - /// Event fired when the user releases a key. Fired by . - /// - /// Set to to indicate the key was handled and to prevent - /// additional processing. - /// - /// - /// - /// All drivers support firing the event. Some drivers (Curses) do not support firing the - /// and events. - /// Fired after . - /// - public static event EventHandler KeyUp; - - /// - /// Called by the when the user releases a key. Fires the event - /// then calls on all top level views. Called after . - /// - /// Can be used to simulate key press events. - /// - /// if the key was handled. - public static bool OnKeyUp (Key a) - { - if (!_initialized) - { - return true; - } - - KeyUp?.Invoke (null, a); - - if (a.Handled) - { - return true; - } - - foreach (Toplevel topLevel in _topLevels.ToList ()) - { - if (topLevel.NewKeyUpEvent (a)) - { - return true; - } - - if (topLevel.Modal) - { - break; - } - } - - return false; - } - - #endregion Keyboard handling } diff --git a/Terminal.Gui/Application/ApplicationKeyboard.cs b/Terminal.Gui/Application/ApplicationKeyboard.cs new file mode 100644 index 000000000..0a56ce712 --- /dev/null +++ b/Terminal.Gui/Application/ApplicationKeyboard.cs @@ -0,0 +1,298 @@ +using System.Text.Json.Serialization; + +namespace Terminal.Gui; + +partial class Application +{ + private static Key _alternateForwardKey = Key.Empty; // Defined in config.json + + /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + [JsonConverter (typeof (KeyJsonConverter))] + public static Key AlternateForwardKey + { + get => _alternateForwardKey; + set + { + if (_alternateForwardKey != value) + { + Key oldKey = _alternateForwardKey; + _alternateForwardKey = value; + OnAlternateForwardKeyChanged (new (oldKey, value)); + } + } + } + + private static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) + { + foreach (Toplevel top in _topLevels.ToArray ()) + { + top.OnAlternateForwardKeyChanged (e); + } + } + + private static Key _alternateBackwardKey = Key.Empty; // Defined in config.json + + /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + [JsonConverter (typeof (KeyJsonConverter))] + public static Key AlternateBackwardKey + { + get => _alternateBackwardKey; + set + { + if (_alternateBackwardKey != value) + { + Key oldKey = _alternateBackwardKey; + _alternateBackwardKey = value; + OnAlternateBackwardKeyChanged (new (oldKey, value)); + } + } + } + + private static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey) + { + foreach (Toplevel top in _topLevels.ToArray ()) + { + top.OnAlternateBackwardKeyChanged (oldKey); + } + } + + private static Key _quitKey = Key.Empty; // Defined in config.json + + /// Gets or sets the key to quit the application. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + [JsonConverter (typeof (KeyJsonConverter))] + public static Key QuitKey + { + get => _quitKey; + set + { + if (_quitKey != value) + { + Key oldKey = _quitKey; + _quitKey = value; + OnQuitKeyChanged (new (oldKey, value)); + } + } + } + + private static void OnQuitKeyChanged (KeyChangedEventArgs e) + { + // Duplicate the list so if it changes during enumeration we're safe + foreach (Toplevel top in _topLevels.ToArray ()) + { + top.OnQuitKeyChanged (e); + } + } + + /// + /// Event fired when the user presses a key. Fired by . + /// + /// Set to to indicate the key was handled and to prevent + /// additional processing. + /// + /// + /// + /// All drivers support firing the event. Some drivers (Curses) do not support firing the + /// and events. + /// Fired after and before . + /// + public static event EventHandler KeyDown; + + /// + /// Called by the when the user presses a key. Fires the event + /// then calls on all top level views. Called after and + /// before . + /// + /// Can be used to simulate key press events. + /// + /// if the key was handled. + public static bool OnKeyDown (Key keyEvent) + { + if (!_initialized) + { + return true; + } + + KeyDown?.Invoke (null, keyEvent); + + if (keyEvent.Handled) + { + return true; + } + + foreach (Toplevel topLevel in _topLevels.ToList ()) + { + if (topLevel.NewKeyDownEvent (keyEvent)) + { + return true; + } + + if (topLevel.Modal) + { + break; + } + } + + // Invoke any global (Application-scoped) KeyBindings. + // The first view that handles the key will stop the loop. + foreach (KeyValuePair> binding in _keyBindings.Where (b => b.Key == keyEvent.KeyCode)) + { + foreach (View view in binding.Value) + { + bool? handled = view?.OnInvokingKeyBindings (keyEvent); + + if (handled != null && (bool)handled) + { + return true; + } + } + } + + return false; + } + + /// + /// Event fired when the user releases a key. Fired by . + /// + /// Set to to indicate the key was handled and to prevent + /// additional processing. + /// + /// + /// + /// All drivers support firing the event. Some drivers (Curses) do not support firing the + /// and events. + /// Fired after . + /// + public static event EventHandler KeyUp; + + /// + /// Called by the when the user releases a key. Fires the event + /// then calls on all top level views. Called after . + /// + /// Can be used to simulate key press events. + /// + /// if the key was handled. + public static bool OnKeyUp (Key a) + { + if (!_initialized) + { + return true; + } + + KeyUp?.Invoke (null, a); + + if (a.Handled) + { + return true; + } + + foreach (Toplevel topLevel in _topLevels.ToList ()) + { + if (topLevel.NewKeyUpEvent (a)) + { + return true; + } + + if (topLevel.Modal) + { + break; + } + } + + return false; + } + + /// + /// 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. + internal static void AddKeyBinding (Key key, View view) + { + if (!_keyBindings.ContainsKey (key)) + { + _keyBindings [key] = []; + } + + _keyBindings [key].Add (view); + } + + /// + /// Gets the list of Views that have key bindings. + /// + /// + /// This is an internal method used by the class to add Application key bindings. + /// + /// The list of Views that have Application-scoped key bindings. + internal static List GetViewsWithKeyBindings () { return _keyBindings.Values.SelectMany (v => v).ToList (); } + + /// + /// Gets the list of Views that have key bindings for the specified key. + /// + /// + /// This is an internal method used by the class to add Application key bindings. + /// + /// The key to check. + /// Outputs the list of views bound to + /// if successful. + internal static bool TryGetKeyBindings (Key key, out List views) { return _keyBindings.TryGetValue (key, out views); } + + /// + /// Removes an scoped key binding. + /// + /// + /// This is an internal method used by the class to remove Application key bindings. + /// + /// The key that was bound. + /// The view that is bound to the key. + internal static void RemoveKeyBinding (Key key, View view) + { + if (_keyBindings.TryGetValue (key, out List views)) + { + views.Remove (view); + + if (views.Count == 0) + { + _keyBindings.Remove (key); + } + } + } + + /// + /// Removes all scoped key bindings for the specified view. + /// + /// + /// This is an internal method used by the class to remove Application key bindings. + /// + /// The view that is bound to the key. + internal static void ClearKeyBindings (View view) + { + foreach (Key key in _keyBindings.Keys) + { + _keyBindings [key].Remove (view); + } + } + + /// + /// Removes all scoped key bindings for the specified view. + /// + /// + /// This is an internal method used by the class to remove Application key bindings. + /// + internal static void ClearKeyBindings () { _keyBindings.Clear (); } +} diff --git a/Terminal.Gui/Application/ApplicationMouse.cs b/Terminal.Gui/Application/ApplicationMouse.cs new file mode 100644 index 000000000..9f2a95339 --- /dev/null +++ b/Terminal.Gui/Application/ApplicationMouse.cs @@ -0,0 +1,302 @@ +namespace Terminal.Gui; + +partial class Application +{ + #region Mouse handling + + /// Disable or enable the mouse. The mouse is enabled by default. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static bool IsMouseDisabled { get; set; } + + /// The current object that wants continuous mouse button pressed events. + public static View WantContinuousButtonPressedView { get; private set; } + + /// + /// Gets the view that grabbed the mouse (e.g. for dragging). When this is set, all mouse events will be routed to + /// this view until the view calls or the mouse is released. + /// + public static View MouseGrabView { get; private set; } + + /// Invoked when a view wants to grab the mouse; can be canceled. + public static event EventHandler GrabbingMouse; + + /// Invoked when a view wants un-grab the mouse; can be canceled. + public static event EventHandler UnGrabbingMouse; + + /// Invoked after a view has grabbed the mouse. + public static event EventHandler GrabbedMouse; + + /// Invoked after a view has un-grabbed the mouse. + public static event EventHandler UnGrabbedMouse; + + /// + /// Grabs the mouse, forcing all mouse events to be routed to the specified view until + /// is called. + /// + /// View that will receive all mouse events until is invoked. + public static void GrabMouse (View view) + { + if (view is null) + { + return; + } + + if (!OnGrabbingMouse (view)) + { + OnGrabbedMouse (view); + MouseGrabView = view; + } + } + + /// Releases the mouse grab, so mouse events will be routed to the view on which the mouse is. + public static void UngrabMouse () + { + if (MouseGrabView is null) + { + return; + } + + if (!OnUnGrabbingMouse (MouseGrabView)) + { + View view = MouseGrabView; + MouseGrabView = null; + OnUnGrabbedMouse (view); + } + } + + private static bool OnGrabbingMouse (View view) + { + if (view is null) + { + return false; + } + + var evArgs = new GrabMouseEventArgs (view); + GrabbingMouse?.Invoke (view, evArgs); + + return evArgs.Cancel; + } + + private static bool OnUnGrabbingMouse (View view) + { + if (view is null) + { + return false; + } + + var evArgs = new GrabMouseEventArgs (view); + UnGrabbingMouse?.Invoke (view, evArgs); + + return evArgs.Cancel; + } + + private static void OnGrabbedMouse (View view) + { + if (view is null) + { + return; + } + + GrabbedMouse?.Invoke (view, new (view)); + } + + private static void OnUnGrabbedMouse (View view) + { + if (view is null) + { + return; + } + + UnGrabbedMouse?.Invoke (view, new (view)); + } + +#nullable enable + + // Used by OnMouseEvent to track the last view that was clicked on. + internal static View? _mouseEnteredView; + + /// Event fired when a mouse move or click occurs. Coordinates are screen relative. + /// + /// + /// Use this event to receive mouse events in screen coordinates. Use to + /// receive mouse events relative to a . + /// + /// The will contain the that contains the mouse coordinates. + /// + public static event EventHandler? MouseEvent; + + /// Called when a mouse event occurs. Raises the event. + /// This method can be used to simulate a mouse event, e.g. in unit tests. + /// The mouse event with coordinates relative to the screen. + internal static void OnMouseEvent (MouseEvent mouseEvent) + { + if (IsMouseDisabled) + { + return; + } + + var view = View.FindDeepestView (Current, mouseEvent.Position); + + if (view is { }) + { + mouseEvent.View = view; + } + + MouseEvent?.Invoke (null, mouseEvent); + + if (mouseEvent.Handled) + { + return; + } + + if (MouseGrabView is { }) + { + // If the mouse is grabbed, send the event to the view that grabbed it. + // The coordinates are relative to the Bounds of the view that grabbed the mouse. + Point frameLoc = MouseGrabView.ScreenToViewport (mouseEvent.Position); + + var viewRelativeMouseEvent = new MouseEvent + { + Position = frameLoc, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.Position, + View = MouseGrabView + }; + + if ((MouseGrabView.Viewport with { Location = Point.Empty }).Contains (viewRelativeMouseEvent.Position) is false) + { + // The mouse has moved outside the bounds of the view that grabbed the mouse + _mouseEnteredView?.NewMouseLeaveEvent (mouseEvent); + } + + //System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}"); + if (MouseGrabView?.NewMouseEvent (viewRelativeMouseEvent) == true) + { + return; + } + } + + if (view is { WantContinuousButtonPressed: true }) + { + WantContinuousButtonPressedView = view; + } + else + { + WantContinuousButtonPressedView = null; + } + + if (view is not Adornment) + { + if ((view is null || view == OverlappedTop) + && Current is { Modal: false } + && OverlappedTop != null + && mouseEvent.Flags != MouseFlags.ReportMousePosition + && mouseEvent.Flags != 0) + { + // This occurs when there are multiple overlapped "tops" + // E.g. "Mdi" - in the Background Worker Scenario + View? top = FindDeepestTop (Top, mouseEvent.Position); + view = View.FindDeepestView (top, mouseEvent.Position); + + if (view is { } && view != OverlappedTop && top != Current && top is { }) + { + MoveCurrent ((Toplevel)top); + } + } + } + + if (view is null) + { + return; + } + + MouseEvent? me = null; + + if (view is Adornment adornment) + { + Point frameLoc = adornment.ScreenToFrame (mouseEvent.Position); + + me = new () + { + Position = frameLoc, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.Position, + View = view + }; + } + else if (view.ViewportToScreen (Rectangle.Empty with { Size = view.Viewport.Size }).Contains (mouseEvent.Position)) + { + Point viewportLocation = view.ScreenToViewport (mouseEvent.Position); + + me = new () + { + Position = viewportLocation, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.Position, + View = view + }; + } + + if (me is null) + { + return; + } + + if (_mouseEnteredView is null) + { + _mouseEnteredView = view; + view.NewMouseEnterEvent (me); + } + else if (_mouseEnteredView != view) + { + _mouseEnteredView.NewMouseLeaveEvent (me); + view.NewMouseEnterEvent (me); + _mouseEnteredView = view; + } + + if (!view.WantMousePositionReports && mouseEvent.Flags == MouseFlags.ReportMousePosition) + { + return; + } + + WantContinuousButtonPressedView = view.WantContinuousButtonPressed ? view : null; + + //Debug.WriteLine ($"OnMouseEvent: ({a.MouseEvent.X},{a.MouseEvent.Y}) - {a.MouseEvent.Flags}"); + + while (view.NewMouseEvent (me) != true) + { + if (MouseGrabView is { }) + { + break; + } + + if (view is Adornment adornmentView) + { + view = adornmentView.Parent.SuperView; + } + else + { + view = view.SuperView; + } + + if (view is null) + { + break; + } + + Point boundsPoint = view.ScreenToViewport (mouseEvent.Position); + + me = new () + { + Position = boundsPoint, + Flags = mouseEvent.Flags, + ScreenPosition = mouseEvent.Position, + View = view + }; + } + + BringOverlappedTopToFront (); + } + + #endregion Mouse handling +} diff --git a/Terminal.Gui/IterationEventArgs.cs b/Terminal.Gui/Application/IterationEventArgs.cs similarity index 100% rename from Terminal.Gui/IterationEventArgs.cs rename to Terminal.Gui/Application/IterationEventArgs.cs diff --git a/Terminal.Gui/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs similarity index 100% rename from Terminal.Gui/MainLoop.cs rename to Terminal.Gui/Application/MainLoop.cs diff --git a/Terminal.Gui/Application/MainLoopSyncContext.cs b/Terminal.Gui/Application/MainLoopSyncContext.cs new file mode 100644 index 000000000..5290a2076 --- /dev/null +++ b/Terminal.Gui/Application/MainLoopSyncContext.cs @@ -0,0 +1,48 @@ +namespace Terminal.Gui; + +/// +/// provides the sync context set while executing code in Terminal.Gui, to let +/// users use async/await on their code +/// +internal sealed class MainLoopSyncContext : SynchronizationContext +{ + public override SynchronizationContext CreateCopy () { return new MainLoopSyncContext (); } + + public override void Post (SendOrPostCallback d, object state) + { + Application.MainLoop?.AddIdle ( + () => + { + d (state); + + return false; + } + ); + } + + //_mainLoop.Driver.Wakeup (); + public override void Send (SendOrPostCallback d, object state) + { + if (Thread.CurrentThread.ManagedThreadId == Application._mainThreadId) + { + d (state); + } + else + { + var wasExecuted = false; + + Application.Invoke ( + () => + { + d (state); + wasExecuted = true; + } + ); + + while (!wasExecuted) + { + Thread.Sleep (15); + } + } + } +} diff --git a/Terminal.Gui/RunState.cs b/Terminal.Gui/Application/RunState.cs similarity index 100% rename from Terminal.Gui/RunState.cs rename to Terminal.Gui/Application/RunState.cs diff --git a/Terminal.Gui/RunStateEventArgs.cs b/Terminal.Gui/Application/RunStateEventArgs.cs similarity index 100% rename from Terminal.Gui/RunStateEventArgs.cs rename to Terminal.Gui/Application/RunStateEventArgs.cs diff --git a/Terminal.Gui/Timeout.cs b/Terminal.Gui/Application/Timeout.cs similarity index 100% rename from Terminal.Gui/Timeout.cs rename to Terminal.Gui/Application/Timeout.cs diff --git a/Terminal.Gui/TimeoutEventArgs.cs b/Terminal.Gui/Application/TimeoutEventArgs.cs similarity index 100% rename from Terminal.Gui/TimeoutEventArgs.cs rename to Terminal.Gui/Application/TimeoutEventArgs.cs diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 7187d7191..b5ab6fa9c 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -315,12 +315,31 @@ public abstract class ConsoleDriver { Contents [row, c] = new Cell { - Rune = (Rune)' ', - Attribute = new Attribute (Color.White, Color.Black), + Rune = (Rune)' ', + Attribute = new Attribute (Color.White, Color.Black), IsDirty = true }; - _dirtyLines [row] = true; } + _dirtyLines [row] = true; + } + } + } + + /// + /// Sets as dirty for situations where views + /// don't need layout and redrawing, but just refresh the screen. + /// + public void SetContentsAsDirty () + { + lock (Contents) + { + for (var row = 0; row < Rows; row++) + { + for (var c = 0; c < Cols; c++) + { + Contents [row, c].IsDirty = true; + } + _dirtyLines [row] = true; } } } diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs index 2d5e28acf..b62ef17a4 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqReq.cs @@ -76,7 +76,7 @@ public class EscSeqRequests return false; } - if (found is { } && found.NumOutstanding > 0) + if (found is { NumOutstanding: > 0 }) { return true; } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index f7f6df8b0..0dd0e9038 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -208,7 +208,7 @@ internal class NetEvents : IDisposable while (!cancellationToken.IsCancellationRequested) { - Task.Delay (100); + Task.Delay (100, cancellationToken).Wait (cancellationToken); if (Console.KeyAvailable) { @@ -223,7 +223,7 @@ internal class NetEvents : IDisposable private void ProcessInputQueue () { - while (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested) + while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) { try { @@ -242,13 +242,8 @@ internal class NetEvents : IDisposable ConsoleModifiers mod = 0; ConsoleKeyInfo newConsoleKeyInfo = default; - while (true) + while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) { - if (_inputReadyCancellationTokenSource.Token.IsCancellationRequested) - { - return; - } - ConsoleKeyInfo consoleKeyInfo; try @@ -338,7 +333,7 @@ internal class NetEvents : IDisposable while (!cancellationToken.IsCancellationRequested) { // Wait for a while then check if screen has changed sizes - Task.Delay (500, cancellationToken); + Task.Delay (500, cancellationToken).Wait (cancellationToken); int buffHeight, buffWidth; @@ -367,13 +362,8 @@ internal class NetEvents : IDisposable cancellationToken.ThrowIfCancellationRequested (); } - while (true) + while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false }) { - if (_inputReadyCancellationTokenSource.IsCancellationRequested) - { - return; - } - try { _winChange.Wait (_inputReadyCancellationTokenSource.Token); @@ -852,11 +842,37 @@ internal class NetDriver : ConsoleDriver { } } - #region Not Implemented + public override void Suspend () + { + if (Environment.OSVersion.Platform != PlatformID.Unix) + { + return; + } - public override void Suspend () { throw new NotImplementedException (); } + StopReportingMouseMoves (); - #endregion + if (!RunningUnitTests) + { + Console.ResetColor (); + Console.Clear (); + + //Disable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + + //Set cursor key to cursor. + Console.Out.Write (EscSeqUtils.CSI_ShowCursor); + + Platform.Suspend (); + + //Enable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + + SetContentsAsDirty (); + Refresh (); + } + + StartReportingMouseMoves (); + } public override void UpdateScreen () { @@ -877,7 +893,7 @@ internal class NetDriver : ConsoleDriver Attribute? redrawAttr = null; int lastCol = -1; - CursorVisibility? savedVisibitity = _cachedCursorVisibility; + CursorVisibility? savedVisibility = _cachedCursorVisibility; SetCursorVisibility (CursorVisibility.Invisible); for (int row = top; row < rows; row++) @@ -1006,7 +1022,7 @@ internal class NetDriver : ConsoleDriver SetCursorPosition (0, 0); - _cachedCursorVisibility = savedVisibitity; + _cachedCursorVisibility = savedVisibility; void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) { @@ -1333,12 +1349,9 @@ internal class NetDriver : ConsoleDriver { _cachedCursorVisibility = visibility; - bool isVisible = RunningUnitTests - ? visibility == CursorVisibility.Default - : Console.CursorVisible = visibility == CursorVisibility.Default; - Console.Out.Write (isVisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); + Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); - return isVisible; + return visibility == CursorVisibility.Default; } public override bool EnsureCursorVisibility () @@ -1667,7 +1680,7 @@ internal class NetMainLoop : IMainLoopDriver private readonly CancellationTokenSource _inputHandlerTokenSource = new (); private readonly Queue _resultQueue = new (); private readonly ManualResetEventSlim _waitForProbe = new (false); - private CancellationTokenSource _eventReadyTokenSource = new (); + private readonly CancellationTokenSource _eventReadyTokenSource = new (); private MainLoop _mainLoop; /// Initializes the class with the console driver. @@ -1719,14 +1732,13 @@ internal class NetMainLoop : IMainLoopDriver _eventReady.Reset (); } + _eventReadyTokenSource.Token.ThrowIfCancellationRequested (); + if (!_eventReadyTokenSource.IsCancellationRequested) { return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); } - _eventReadyTokenSource.Dispose (); - _eventReadyTokenSource = new CancellationTokenSource (); - return true; } @@ -1783,26 +1795,21 @@ internal class NetMainLoop : IMainLoopDriver return; } + _inputHandlerTokenSource.Token.ThrowIfCancellationRequested (); + if (_resultQueue.Count == 0) { _resultQueue.Enqueue (_netEvents.DequeueInput ()); } - try + while (_resultQueue.Count > 0 && _resultQueue.Peek () is null) { - while (_resultQueue.Peek () is null) - { - _resultQueue.Dequeue (); - } - - if (_resultQueue.Count > 0) - { - _eventReady.Set (); - } + _resultQueue.Dequeue (); } - catch (InvalidOperationException) + + if (_resultQueue.Count > 0) { - // Ignore + _eventReady.Set (); } } } diff --git a/Terminal.Gui/Drawing/Aligner.cs b/Terminal.Gui/Drawing/Aligner.cs new file mode 100644 index 000000000..1c96a2ac0 --- /dev/null +++ b/Terminal.Gui/Drawing/Aligner.cs @@ -0,0 +1,369 @@ +using System.ComponentModel; + +namespace Terminal.Gui; + +/// +/// Aligns items within a container based on the specified . Both horizontal and vertical +/// alignments are supported. +/// +public class Aligner : INotifyPropertyChanged +{ + private Alignment _alignment; + + /// + /// Gets or sets how the aligns items within a container. + /// + /// + /// + /// provides additional options for aligning items in a container. + /// + /// + public Alignment Alignment + { + get => _alignment; + set + { + _alignment = value; + PropertyChanged?.Invoke (this, new (nameof (Alignment))); + } + } + + private AlignmentModes _alignmentMode = AlignmentModes.StartToEnd; + + /// + /// Gets or sets the modes controlling . + /// + public AlignmentModes AlignmentModes + { + get => _alignmentMode; + set + { + _alignmentMode = value; + PropertyChanged?.Invoke (this, new (nameof (AlignmentModes))); + } + } + + private int _containerSize; + + /// + /// The size of the container. + /// + public int ContainerSize + { + get => _containerSize; + set + { + _containerSize = value; + PropertyChanged?.Invoke (this, new (nameof (ContainerSize))); + } + } + + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Takes a list of item sizes and returns a list of the positions of those items when aligned within + /// + /// using the and settings. + /// + /// The sizes of the items to align. + /// The locations of the items, from left/top to right/bottom. + public int [] Align (int [] sizes) { return Align (Alignment, AlignmentModes, ContainerSize, sizes); } + + /// + /// Takes a list of item sizes and returns a list of the positions of those items when aligned within + /// + /// using specified parameters. + /// + /// Specifies how the items will be aligned. + /// + /// The size of the container. + /// The sizes of the items to align. + /// The positions of the items, from left/top to right/bottom. + public static int [] Align (in Alignment alignment, in AlignmentModes alignmentMode, in int containerSize, in int [] sizes) + { + if (sizes.Length == 0) + { + return []; + } + + var sizesCopy = sizes; + if (alignmentMode.FastHasFlags (AlignmentModes.EndToStart)) + { + sizesCopy = sizes.Reverse ().ToArray (); + } + + int maxSpaceBetweenItems = alignmentMode.FastHasFlags (AlignmentModes.AddSpaceBetweenItems) ? 1 : 0; + int totalItemsSize = sizes.Sum (); + int totalGaps = sizes.Length - 1; // total gaps between items + int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spacesToGive if we had enough room + int spacesToGive = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out + + if (totalItemsSize >= containerSize) + { + spacesToGive = 0; + } + else if (totalItemsAndSpaces > containerSize) + { + spacesToGive = containerSize - totalItemsSize; + } + + switch (alignment) + { + case Alignment.Start: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast: + return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.EndToStart: + return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); + + case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast: + return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ; + } + + break; + + case Alignment.End: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast: + return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.EndToStart: + return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); + + case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast: + return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ; + } + + break; + + case Alignment.Center: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.EndToStart: + return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); + } + + break; + + case Alignment.Fill: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return Fill (in sizesCopy, containerSize, totalItemsSize); + + case AlignmentModes.EndToStart: + return Fill (in sizesCopy, containerSize, totalItemsSize).Reverse ().ToArray (); + } + + break; + + default: + throw new ArgumentOutOfRangeException (nameof (alignment), alignment, null); + } + + return []; + } + + internal static int [] Start (ref readonly int [] sizes, int maxSpaceBetweenItems, int spacesToGive) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i == 0) + { + positions [0] = 0; // first item position + + continue; + } + + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + // subsequent items are placed one space after the previous item + positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; + } + + return positions; + } + + internal static int [] IgnoreFirst ( + ref readonly int [] sizes, + int containerSize, + int totalItemsSize, + int maxSpaceBetweenItems, + int spacesToGive + ) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + if (sizes.Length > 1) + { + var currentPosition = 0; + positions [0] = currentPosition; // first item is flush left + + for (int i = sizes.Length - 1; i >= 0; i--) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i == sizes.Length - 1) + { + // start at right + currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i]; + positions [i] = currentPosition; + } + + if (i < sizes.Length - 1 && i > 0) + { + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + positions [i] = currentPosition - sizes [i] - spaceBefore; + currentPosition = positions [i]; + } + } + } + else if (sizes.Length == 1) + { + CheckSizeCannotBeNegative (0, in sizes); + positions [0] = 0; // single item is flush left + } + + return positions; + } + + internal static int [] IgnoreLast ( + ref readonly int [] sizes, + int containerSize, + int totalItemsSize, + int maxSpaceBetweenItems, + int spacesToGive + ) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + if (sizes.Length > 1) + { + var currentPosition = 0; + if (totalItemsSize > containerSize) + { + currentPosition = containerSize - totalItemsSize - spacesToGive; + } + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i < sizes.Length - 1) + { + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + positions [i] = currentPosition; + currentPosition += sizes [i] + spaceBefore; + } + } + + positions [sizes.Length - 1] = containerSize - sizes [^1]; + } + else if (sizes.Length == 1) + { + CheckSizeCannotBeNegative (0, in sizes); + + positions [0] = containerSize - sizes [0]; // single item is flush right + } + + return positions; + } + + internal static int [] Fill (ref readonly int [] sizes, int containerSize, int totalItemsSize) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0; + int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0; + var currentPosition = 0; + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + positions [i] = currentPosition; + int extraSpace = i < remainder ? 1 : 0; + currentPosition += sizes [i] + spaceBetween + extraSpace; + } + + return positions; + } + + internal static int [] Center (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + if (sizes.Length > 1) + { + // remaining space to be distributed before first and after the items + int remainingSpace = containerSize - totalItemsSize - spacesToGive; + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i == 0) + { + positions [i] = remainingSpace / 2; // first item position + + continue; + } + + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + // subsequent items are placed one space after the previous item + positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; + } + } + else if (sizes.Length == 1) + { + CheckSizeCannotBeNegative (0, in sizes); + positions [0] = (containerSize - sizes [0]) / 2; // single item is centered + } + + return positions; + } + + internal static int [] End (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + int currentPosition = containerSize - totalItemsSize - spacesToGive; + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + positions [i] = currentPosition; + currentPosition += sizes [i] + spaceBefore; + } + + return positions; + } + + private static void CheckSizeCannotBeNegative (int i, ref readonly int [] sizes) + { + if (sizes [i] < 0) + { + throw new ArgumentException ("The size of an item cannot be negative."); + } + } +} diff --git a/Terminal.Gui/Drawing/Alignment.cs b/Terminal.Gui/Drawing/Alignment.cs new file mode 100644 index 000000000..40061a8c1 --- /dev/null +++ b/Terminal.Gui/Drawing/Alignment.cs @@ -0,0 +1,82 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Determines the position of items when arranged in a container. +/// +[GenerateEnumExtensionMethods (FastHasFlags = true)] + +public enum Alignment +{ + /// + /// The items will be aligned to the start (left or top) of the container. + /// + /// + /// + /// If the container is smaller than the total size of the items, the end items will be clipped (their locations + /// will be greater than the container size). + /// + /// + /// The enumeration provides additional options for aligning items in a container. + /// + /// + /// + /// + /// |111 2222 33333 | + /// + /// + Start = 0, + + /// + /// The items will be aligned to the end (right or bottom) of the container. + /// + /// + /// + /// If the container is smaller than the total size of the items, the start items will be clipped (their locations + /// will be negative). + /// + /// + /// The enumeration provides additional options for aligning items in a container. + /// + /// + /// + /// + /// | 111 2222 33333| + /// + /// + End, + + /// + /// Center in the available space. + /// + /// + /// + /// If centering is not possible, the group will be left-aligned. + /// + /// + /// Extra space will be distributed between the items, biased towards the left. + /// + /// + /// + /// + /// | 111 2222 33333 | + /// + /// + Center, + + /// + /// The items will fill the available space. + /// + /// + /// + /// Extra space will be distributed between the items, biased towards the end. + /// + /// + /// + /// + /// |111 2222 33333| + /// + /// + Fill, +} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/AlignmentModes.cs b/Terminal.Gui/Drawing/AlignmentModes.cs new file mode 100644 index 000000000..4de4d5c98 --- /dev/null +++ b/Terminal.Gui/Drawing/AlignmentModes.cs @@ -0,0 +1,52 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Determines alignment modes for . +/// +[Flags] +[GenerateEnumExtensionMethods (FastHasFlags = true)] +public enum AlignmentModes +{ + /// + /// The items will be arranged from start (left/top) to end (right/bottom). + /// + StartToEnd = 0, + + /// + /// The items will be arranged from end (right/bottom) to start (left/top). + /// + /// + /// Not implemented. + /// + EndToStart = 1, + + /// + /// At least one space will be added between items. Useful for justifying text where at least one space is needed. + /// + /// + /// + /// If the total size of the items is greater than the container size, the space between items will be ignored + /// starting from the end. + /// + /// + AddSpaceBetweenItems = 2, + + /// + /// When aligning via or , the item opposite to the alignment (the first or last item) will be ignored. + /// + /// + /// + /// If the container is smaller than the total size of the items, the end items will be clipped (their locations + /// will be greater than the container size). + /// + /// + /// + /// + /// Start: |111 2222 33333| + /// End: |111 2222 33333| + /// + /// + IgnoreFirstOrLast = 4, +} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/Justification.cs b/Terminal.Gui/Drawing/Justification.cs deleted file mode 100644 index f1fba56a8..000000000 --- a/Terminal.Gui/Drawing/Justification.cs +++ /dev/null @@ -1,333 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Controls how the justifies items within a container. -/// -public enum Justification -{ - /// - /// The items will be aligned to the left. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Left, - - /// - /// The items will be aligned to the right. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Right, - - /// - /// The group will be centered in the container. - /// If centering is not possible, the group will be left-justified. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Centered, - - /// - /// The items will be justified. Space will be added between the items such that the first item - /// is at the start and the right side of the last item against the end. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Justified, - - /// - /// The first item will be aligned to the left and the remaining will aligned to the right. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - FirstLeftRestRight, - - /// - /// The last item will be aligned to the right and the remaining will aligned to the left. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - LastRightRestLeft -} - -/// -/// Justifies items within a container based on the specified . -/// -public class Justifier -{ - /// - /// Gets or sets how the justifies items within a container. - /// - public Justification Justification { get; set; } - - /// - /// The size of the container. - /// - public int ContainerSize { get; set; } - - /// - /// Gets or sets whether puts a space is placed between items. Default is . If , a space will be - /// placed between each item, which is useful for justifying text. - /// - public bool PutSpaceBetweenItems { get; set; } - - /// - /// Takes a list of items and returns their positions when justified within a container wide based on the specified - /// . - /// - /// The sizes of the items to justify. - /// The locations of the items, from left to right. - public int [] Justify (int [] sizes) - { - return Justify (Justification, PutSpaceBetweenItems, ContainerSize, sizes); - } - - /// - /// Takes a list of items and returns their positions when justified within a container wide based on the specified - /// . - /// - /// The sizes of the items to justify. - /// The justification style. - /// - /// The size of the container. - /// The locations of the items, from left to right. - public static int [] Justify (Justification justification, bool putSpaceBetweenItems, int containerSize, int [] sizes) - { - if (sizes.Length == 0) - { - return new int [] { }; - } - - int maxSpaceBetweenItems = putSpaceBetweenItems ? 1 : 0; - - var positions = new int [sizes.Length]; // positions of the items. the return value. - int totalItemsSize = sizes.Sum (); - int totalGaps = sizes.Length - 1; // total gaps between items - int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spaces if we had enough room - - int spaces = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out - if (totalItemsSize >= containerSize) - { - spaces = 0; - } - else if (totalItemsAndSpaces > containerSize) - { - spaces = containerSize - totalItemsSize; - } - - switch (justification) - { - case Justification.Left: - var currentPosition = 0; - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i == 0) - { - positions [0] = 0; // first item position - - continue; - } - - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - // subsequent items are placed one space after the previous item - positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; - } - - break; - case Justification.Right: - currentPosition = Math.Max (0, containerSize - totalItemsSize - spaces); - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - positions [i] = currentPosition; - currentPosition += sizes [i] + spaceBefore; - } - - break; - - case Justification.Centered: - if (sizes.Length > 1) - { - // remaining space to be distributed before first and after the items - int remainingSpace = Math.Max (0, containerSize - totalItemsSize - spaces); - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i == 0) - { - positions [i] = remainingSpace / 2; // first item position - - continue; - } - - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - // subsequent items are placed one space after the previous item - positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; - } - } - else if (sizes.Length == 1) - { - if (sizes [0] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [0] = (containerSize - sizes [0]) / 2; // single item is centered - } - - break; - - case Justification.Justified: - int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0; - int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0; - currentPosition = 0; - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [i] = currentPosition; - int extraSpace = i < remainder ? 1 : 0; - currentPosition += sizes [i] + spaceBetween + extraSpace; - } - - break; - - // 111 2222 33333 - case Justification.LastRightRestLeft: - if (sizes.Length > 1) - { - currentPosition = 0; - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i < sizes.Length - 1) - { - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - positions [i] = currentPosition; - currentPosition += sizes [i] + spaceBefore; - } - } - - positions [sizes.Length - 1] = containerSize - sizes [sizes.Length - 1]; - } - else if (sizes.Length == 1) - { - if (sizes [0] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [0] = containerSize - sizes [0]; // single item is flush right - } - - break; - - // 111 2222 33333 - case Justification.FirstLeftRestRight: - if (sizes.Length > 1) - { - currentPosition = 0; - positions [0] = currentPosition; // first item is flush left - - for (int i = sizes.Length - 1; i >= 0; i--) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i == sizes.Length - 1) - { - // start at right - currentPosition = containerSize - sizes [i]; - positions [i] = currentPosition; - } - - if (i < sizes.Length - 1 && i > 0) - { - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - positions [i] = currentPosition - sizes [i] - spaceBefore; - currentPosition = positions [i]; - } - } - } - else if (sizes.Length == 1) - { - if (sizes [0] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [0] = 0; // single item is flush left - } - - break; - - default: - throw new ArgumentOutOfRangeException (nameof (justification), justification, null); - } - - return positions; - } -} diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index 6070e6cbe..ad684470b 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -230,8 +230,8 @@ public class Thickness : IEquatable var tf = new TextFormatter { Text = label is null ? string.Empty : $"{label} {this}", - Alignment = TextAlignment.Centered, - VerticalAlignment = VerticalTextAlignment.Bottom, + Alignment = Alignment.Center, + VerticalAlignment = Alignment.End, AutoSize = true }; tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect); diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs new file mode 100644 index 000000000..5d3269226 --- /dev/null +++ b/Terminal.Gui/Input/CommandContext.cs @@ -0,0 +1,41 @@ +#nullable enable +namespace Terminal.Gui; +/// +/// Provides context for a that is being invoked. +/// +/// +/// To define a that is invoked with context, +/// use +/// +/// +public record struct CommandContext +{ + /// + /// Initializes a new instance of with the specified , + /// + /// + /// + /// + public CommandContext (Command command, Key? key, KeyBinding? keyBinding = null) + { + Command = command; + Key = key; + KeyBinding = keyBinding; + } + + /// + /// The that is being invoked. + /// + public Command Command { get; set; } + + /// + /// The that is being invoked. This is the key that was pressed to invoke the . + /// + public Key? Key { get; set; } + + /// + /// The KeyBinding that was used to invoke the , if any. + /// + public KeyBinding? KeyBinding { get; set; } +} diff --git a/Terminal.Gui/Input/KeyBinding.cs b/Terminal.Gui/Input/KeyBinding.cs index b92eebc74..baac07384 100644 --- a/Terminal.Gui/Input/KeyBinding.cs +++ b/Terminal.Gui/Input/KeyBinding.cs @@ -1,260 +1,34 @@ -// These classes use a key binding system based on the design implemented in Scintilla.Net which is an +#nullable enable + +// These classes use a key binding system based on the design implemented in Scintilla.Net which is an // MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs namespace Terminal.Gui; /// -/// Defines the scope of a that has been bound to a key with -/// . +/// Provides a collection of objects that are scoped to . /// -/// -/// Key bindings are scoped to the most-focused view () by default. -/// -[Flags] -public enum KeyBindingScope -{ - /// The key binding is scoped to just the view that has focus. - Focused = 1, - - /// - /// The key binding is scoped to the View's SuperView and will be triggered even when the View does not have focus, as - /// long as the SuperView does have focus. This is typically used for s. - /// - /// - /// Use for Views such as MenuBar and StatusBar which provide commands (shortcuts etc...) that trigger even - /// when not focused. - /// - /// - /// HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or - /// any of its subviews. - /// - /// - /// - HotKey = 2, - - /// - /// The key binding will be triggered regardless of which view has focus. This is typically used for global - /// commands. - /// - /// - /// Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or - /// any of its subviews, and if the key down event was not bound to a . - /// - Application = 4 -} - -/// Provides a collection of objects that are scoped to . -public class KeyBinding +public record struct KeyBinding { /// Initializes a new instance. - /// - /// - public KeyBinding (Command [] commands, KeyBindingScope scope) + /// The commands this key binding will invoke. + /// The scope of the . + /// Arbitrary context that can be associated with this key binding. + public KeyBinding (Command [] commands, KeyBindingScope scope, object? context = null) { Commands = commands; Scope = scope; + Context = context; } - /// The actions which can be performed by the application or bound to keys in a control. + /// The commands this key binding will invoke. public Command [] Commands { get; set; } - /// The scope of the bound to a key. + /// The scope of the . public KeyBindingScope Scope { get; set; } -} - -/// A class that provides a collection of objects bound to a . -public class KeyBindings -{ - // TODO: Add a dictionary comparer that ignores Scope - /// The collection of objects. - public Dictionary Bindings { get; } = new (); - - /// Adds a to the collection. - /// - /// - public void Add (Key key, KeyBinding binding) { Bindings.Add (key, binding); } - - /// - /// Adds a new key combination that will trigger the commands in . - /// - /// If the key is already bound to a different array of s it will be rebound - /// . - /// - /// - /// - /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch - /// focus to another view and perform multiple commands there). - /// - /// The key to check. - /// The scope for the command. - /// - /// The command to invoked on the when is pressed. When - /// multiple commands are provided,they will be applied in sequence. The bound strike will be - /// consumed if any took effect. - /// - public void Add (Key key, KeyBindingScope scope, params Command [] commands) - { - if (key is null || !key.IsValid) - { - //throw new ArgumentException ("Invalid Key", nameof (commands)); - return; - } - - if (commands.Length == 0) - { - throw new ArgumentException (@"At least one command must be specified", nameof (commands)); - } - - if (TryGet (key, out KeyBinding _)) - { - Bindings [key] = new KeyBinding (commands, scope); - } - else - { - Bindings.Add (key, new KeyBinding (commands, scope)); - } - } - - /// - /// - /// Adds a new key combination that will trigger the commands in (if supported by the - /// View - see ). - /// - /// - /// This is a helper function for for - /// scoped commands. - /// - /// - /// If the key is already bound to a different array of s it will be rebound - /// . - /// - /// - /// - /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch - /// focus to another view and perform multiple commands there). - /// - /// The key to check. - /// - /// The command to invoked on the when is pressed. When - /// multiple commands are provided,they will be applied in sequence. The bound strike will be - /// consumed if any took effect. - /// - public void Add (Key key, params Command [] commands) { Add (key, KeyBindingScope.Focused, commands); } - - /// Removes all objects from the collection. - public void Clear () { Bindings.Clear (); } - - /// - /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to - /// the same command sets and this method will clear all of them. - /// - /// - public void Clear (params Command [] command) - { - var kvps = Bindings - .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) - .ToArray (); - foreach (KeyValuePair kvp in kvps) - { - Bindings.Remove (kvp.Key); - } - } - - /// Gets the for the specified . - /// - /// - public KeyBinding Get (Key key) { return TryGet (key, out KeyBinding binding) ? binding : null; } - - /// Gets the for the specified . - /// - /// - /// - public KeyBinding Get (Key key, KeyBindingScope scope) { return TryGet (key, scope, out KeyBinding binding) ? binding : null; } - - /// Gets the array of s bound to if it exists. - /// The key to check. - /// - /// The array of s if is bound. An empty array - /// if not. - /// - public Command [] GetCommands (Key key) - { - if (TryGet (key, out KeyBinding bindings)) - { - return bindings.Commands; - } - - return Array.Empty (); - } - - /// Gets the Key used by a set of commands. - /// - /// The set of commands to search. - /// The used by a - /// If no matching set of commands was found. - public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; } - - /// Removes a from the collection. - /// - public void Remove (Key key) { Bindings.Remove (key); } - - /// Replaces a key combination already bound to a set of s. - /// - /// The key to be replaced. - /// The new key to be used. - public void Replace (Key fromKey, Key toKey) - { - if (!TryGet (fromKey, out KeyBinding _)) - { - return; - } - - KeyBinding value = Bindings [fromKey]; - Bindings.Remove (fromKey); - Bindings [toKey] = value; - } - - /// Gets the commands bound with the specified Key. - /// - /// The key to check. - /// - /// When this method returns, contains the commands bound with the specified Key, if the Key is - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the Key is bound; otherwise . - public bool TryGet (Key key, out KeyBinding binding) - { - if (key.IsValid) - { - return Bindings.TryGetValue (key, out binding); - } - - binding = new KeyBinding (Array.Empty (), KeyBindingScope.Focused); - - return false; - } - - /// Gets the commands bound with the specified Key that are scoped to a particular scope. - /// - /// The key to check. - /// the scope to filter on - /// - /// When this method returns, contains the commands bound with the specified Key, if the Key is - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the Key is bound; otherwise . - public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) - { - if (key.IsValid && Bindings.TryGetValue (key, out binding)) - { - if (scope.HasFlag (binding.Scope)) - { - return true; - } - } - - binding = new KeyBinding (Array.Empty (), KeyBindingScope.Focused); - - return false; - } + + /// + /// Arbitrary context that can be associated with this key binding. + /// + public object? Context { get; set; } } diff --git a/Terminal.Gui/Input/KeyBindingScope.cs b/Terminal.Gui/Input/KeyBindingScope.cs new file mode 100644 index 000000000..9799fc831 --- /dev/null +++ b/Terminal.Gui/Input/KeyBindingScope.cs @@ -0,0 +1,46 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Defines the scope of a that has been bound to a key with +/// . +/// +/// +/// Key bindings are scoped to the most-focused view () by default. +/// +[Flags] +[GenerateEnumExtensionMethods (FastHasFlags = true)] +public enum KeyBindingScope +{ + /// The key binding is scoped to just the view that has focus. + Focused = 1, + + /// + /// The key binding is scoped to the View's Superview hierarchy and will be triggered even when the View does not have + /// focus, as + /// long as the SuperView does have focus. This is typically used for s. + /// + /// + /// The View must be visible. + /// + /// + /// HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or + /// any of its subviews. + /// + /// + /// + HotKey = 2, + + /// + /// The key binding will be triggered regardless of which view has focus. This is typically used for global + /// commands, which are called Shortcuts. + /// + /// + /// + /// Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or + /// any of its subviews, and if the key was not bound to a . + /// + /// + Application = 4 +} diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs new file mode 100644 index 000000000..8ec38329e --- /dev/null +++ b/Terminal.Gui/Input/KeyBindings.cs @@ -0,0 +1,258 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Provides a collection of objects bound to a . +/// +public class KeyBindings +{ + /// + /// Initializes a new instance. This constructor is used when the are not bound to a + /// , such as in unit tests. + /// + public KeyBindings () { } + + /// Initializes a new instance bound to . + public KeyBindings (View boundView) { BoundView = boundView; } + + /// + /// The view that the are bound to. + /// + public View? BoundView { get; } + + // TODO: Add a dictionary comparer that ignores Scope + // TODO: This should not be public! + /// The collection of objects. + public Dictionary Bindings { get; } = new (); + + /// Adds a to the collection. + /// + /// + public void Add (Key key, KeyBinding binding) + { + if (TryGet (key, out KeyBinding _)) + { + Bindings [key] = binding; + } + else + { + Bindings.Add (key, binding); + if (binding.Scope.FastHasFlags (KeyBindingScope.Application)) + { + Application.AddKeyBinding (key, BoundView); + } + } + } + + /// + /// Adds a new key combination that will trigger the commands in . + /// + /// If the key is already bound to a different array of s it will be rebound + /// . + /// + /// + /// + /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch + /// focus to another view and perform multiple commands there). + /// + /// The key to check. + /// The scope for the command. + /// + /// The command to invoked on the when is pressed. When + /// multiple commands are provided,they will be applied in sequence. The bound strike will be + /// consumed if any took effect. + /// + public void Add (Key key, KeyBindingScope scope, params Command [] commands) + { + if (key is null || !key.IsValid) + { + //throw new ArgumentException ("Invalid Key", nameof (commands)); + return; + } + + if (commands.Length == 0) + { + throw new ArgumentException (@"At least one command must be specified", nameof (commands)); + } + + if (TryGet (key, out KeyBinding _)) + { + Bindings [key] = new (commands, scope); + } + else + { + Add (key, new KeyBinding (commands, scope)); + } + } + + /// + /// + /// Adds a new key combination that will trigger the commands in (if supported by the + /// View - see ). + /// + /// + /// This is a helper function for for + /// scoped commands. + /// + /// + /// If the key is already bound to a different array of s it will be rebound + /// . + /// + /// + /// + /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch + /// focus to another view and perform multiple commands there). + /// + /// The key to check. + /// + /// The command to invoked on the when is pressed. When + /// multiple commands are provided,they will be applied in sequence. The bound strike will be + /// consumed if any took effect. + /// + public void Add (Key key, params Command [] commands) + { + Add (key, KeyBindingScope.Focused, commands); + } + + /// Removes all objects from the collection. + public void Clear () + { + Application.ClearKeyBindings (BoundView); + + Bindings.Clear (); + } + + /// + /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to + /// the same command sets and this method will clear all of them. + /// + /// + public void Clear (params Command [] command) + { + KeyValuePair [] kvps = Bindings + .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) + .ToArray (); + + foreach (KeyValuePair kvp in kvps) + { + Remove (kvp.Key); + } + } + + /// Gets the for the specified . + /// + /// + public KeyBinding Get (Key key) + { + if (TryGet (key, out KeyBinding binding)) + { + return binding; + } + throw new InvalidOperationException ($"Key {key} is not bound."); + } + + /// Gets the for the specified . + /// + /// + /// + public KeyBinding Get (Key key, KeyBindingScope scope) + { + if (TryGet (key, scope, out KeyBinding binding)) + { + return binding; + } + throw new InvalidOperationException ($"Key {key}/{scope} is not bound."); + } + + /// Gets the array of s bound to if it exists. + /// The key to check. + /// + /// The array of s if is bound. An empty array + /// if not. + /// + public Command [] GetCommands (Key key) + { + if (TryGet (key, out KeyBinding bindings)) + { + return bindings.Commands; + } + + return Array.Empty (); + } + + /// Gets the Key used by a set of commands. + /// + /// The set of commands to search. + /// The used by a + /// If no matching set of commands was found. + public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; } + + /// Removes a from the collection. + /// + public void Remove (Key key) + { + Bindings.Remove (key); + Application.RemoveKeyBinding (key, BoundView); + } + + /// Replaces a key combination already bound to a set of s. + /// + /// The key to be replaced. + /// The new key to be used. + public void Replace (Key oldKey, Key newKey) + { + if (!TryGet (oldKey, out KeyBinding _)) + { + return; + } + + KeyBinding value = Bindings [oldKey]; + Remove (oldKey); + Add (newKey, value); + } + + /// Gets the commands bound with the specified Key. + /// + /// The key to check. + /// + /// When this method returns, contains the commands bound with the specified Key, if the Key is + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the Key is bound; otherwise . + public bool TryGet (Key key, out KeyBinding binding) + { + if (key.IsValid) + { + return Bindings.TryGetValue (key, out binding); + } + + binding = new (Array.Empty (), KeyBindingScope.Focused); + + return false; + } + + /// Gets the commands bound with the specified Key that are scoped to a particular scope. + /// + /// The key to check. + /// the scope to filter on + /// + /// When this method returns, contains the commands bound with the specified Key, if the Key is + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the Key is bound; otherwise . + public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) + { + if (key.IsValid && Bindings.TryGetValue (key, out binding)) + { + if (scope.HasFlag (binding.Scope)) + { + return true; + } + } + + binding = new (Array.Empty (), KeyBindingScope.Focused); + + return false; + } +} diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 368ccd8bf..8380a14f5 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -24,7 +24,8 @@ "Themes": [ { "Default": { - "Dialog.DefaultButtonAlignment": "Center", + "Dialog.DefaultButtonAlignment": "End", + "Dialog.DefaultButtonAlignmentModes": "AddSpaceBetweenItems", "FrameView.DefaultBorderStyle": "Single", "Window.DefaultBorderStyle": "Single", "ColorSchemes": [ diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index f0840c041..a8b1f64ba 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -136,5 +136,6 @@ true true Miguel de Icaza, Tig Kindel (@tig), @BDisp + true \ No newline at end of file diff --git a/Terminal.Gui/Text/TextAlignment.cs b/Terminal.Gui/Text/TextAlignment.cs deleted file mode 100644 index 44950cfd5..000000000 --- a/Terminal.Gui/Text/TextAlignment.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Terminal.Gui; - -/// Text alignment enumeration, controls how text is displayed. -public enum TextAlignment -{ - /// The text will be left-aligned. - Left, - - /// The text will be right-aligned. - Right, - - /// The text will be centered horizontally. - Centered, - - /// - /// The text will be justified (spaces will be added to existing spaces such that the text fills the container - /// horizontally). - /// - Justified -} \ No newline at end of file diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index af4a7b97b..bee37de67 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Terminal.Gui; /// @@ -15,14 +17,14 @@ public class TextFormatter private Size _size; private int _tabWidth = 4; private string _text; - private TextAlignment _textAlignment; + private Alignment _textAlignment = Alignment.Start; private TextDirection _textDirection; - private VerticalTextAlignment _textVerticalAlignment; + private Alignment _textVerticalAlignment = Alignment.Start; private bool _wordWrap = true; - /// Controls the horizontal text-alignment property. + /// Get or sets the horizontal text alignment. /// The text alignment. - public TextAlignment Alignment + public Alignment Alignment { get => _textAlignment; set => _textAlignment = EnableNeedsFormat (value); @@ -32,8 +34,7 @@ public class TextFormatter /// /// Used when is using to resize the view's to fit . /// - /// AutoSize is ignored if and - /// are used. + /// AutoSize is ignored if is used. /// /// public bool AutoSize @@ -68,9 +69,8 @@ public class TextFormatter /// Only the first HotKey specifier found in is supported. /// /// - /// If (the default) the width required for the HotKey specifier is returned. Otherwise the - /// height - /// is returned. + /// If (the default) the width required for the HotKey specifier is returned. Otherwise, the + /// height is returned. /// /// /// The number of characters required for the . If the text @@ -97,8 +97,8 @@ public class TextFormatter /// public int CursorPosition { get; internal set; } - /// Controls the text-direction property. - /// The text vertical alignment. + /// Gets or sets the text-direction. + /// The text direction. public TextDirection Direction { get => _textDirection; @@ -112,8 +112,7 @@ public class TextFormatter } } } - - + /// /// Determines if the viewport width will be used or only the text width will be used, /// If all the viewport area will be filled with whitespaces and the same background color @@ -223,9 +222,9 @@ public class TextFormatter } } - /// Controls the vertical text-alignment property. + /// Gets or sets the vertical text-alignment. /// The text vertical alignment. - public VerticalTextAlignment VerticalAlignment + public Alignment VerticalAlignment { get => _textVerticalAlignment; set => _textVerticalAlignment = EnableNeedsFormat (value); @@ -318,10 +317,10 @@ public class TextFormatter // When text is justified, we lost left or right, so we use the direction to align. - int x, y; + int x = 0, y = 0; // Horizontal Alignment - if (Alignment is TextAlignment.Right) + if (Alignment is Alignment.End) { if (isVertical) { @@ -336,7 +335,7 @@ public class TextFormatter CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); } } - else if (Alignment is TextAlignment.Left) + else if (Alignment is Alignment.Start) { if (isVertical) { @@ -352,7 +351,7 @@ public class TextFormatter CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; } - else if (Alignment is TextAlignment.Justified) + else if (Alignment is Alignment.Fill) { if (isVertical) { @@ -375,7 +374,7 @@ public class TextFormatter CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; } - else if (Alignment is TextAlignment.Centered) + else if (Alignment is Alignment.Center) { if (isVertical) { @@ -395,11 +394,13 @@ public class TextFormatter } else { - throw new ArgumentOutOfRangeException ($"{nameof (Alignment)}"); + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + + return; } // Vertical Alignment - if (VerticalAlignment is VerticalTextAlignment.Bottom) + if (VerticalAlignment is Alignment.End) { if (isVertical) { @@ -410,7 +411,7 @@ public class TextFormatter y = screen.Bottom - linesFormatted.Count + line; } } - else if (VerticalAlignment is VerticalTextAlignment.Top) + else if (VerticalAlignment is Alignment.Start) { if (isVertical) { @@ -421,7 +422,7 @@ public class TextFormatter y = screen.Top + line; } } - else if (VerticalAlignment is VerticalTextAlignment.Justified) + else if (VerticalAlignment is Alignment.Fill) { if (isVertical) { @@ -435,7 +436,7 @@ public class TextFormatter line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1; } } - else if (VerticalAlignment is VerticalTextAlignment.Middle) + else if (VerticalAlignment is Alignment.Center) { if (isVertical) { @@ -450,7 +451,9 @@ public class TextFormatter } else { - throw new ArgumentOutOfRangeException ($"{nameof (VerticalAlignment)}"); + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + + return; } int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0; @@ -471,8 +474,8 @@ public class TextFormatter { if (idx < 0 || (isVertical - ? VerticalAlignment != VerticalTextAlignment.Bottom && current < 0 - : Alignment != TextAlignment.Right && x + current + colOffset < 0)) + ? VerticalAlignment != Alignment.End && current < 0 + : Alignment != Alignment.End && x + current + colOffset < 0)) { current++; @@ -561,7 +564,7 @@ public class TextFormatter if (HotKeyPos > -1 && idx == HotKeyPos) { - if ((isVertical && VerticalAlignment == VerticalTextAlignment.Justified) || (!isVertical && Alignment == TextAlignment.Justified)) + if ((isVertical && VerticalAlignment == Alignment.Fill) || (!isVertical && Alignment == Alignment.Fill)) { CursorPosition = idx - start; } @@ -699,7 +702,7 @@ public class TextFormatter _lines = Format ( text, Size.Height, - VerticalAlignment == VerticalTextAlignment.Justified, + VerticalAlignment == Alignment.Fill, Size.Width > colsWidth && WordWrap, PreserveTrailingSpaces, TabWidth, @@ -723,7 +726,7 @@ public class TextFormatter _lines = Format ( text, Size.Width, - Alignment == TextAlignment.Justified, + Alignment == Alignment.Fill, Size.Height > 1 && WordWrap, PreserveTrailingSpaces, TabWidth, @@ -977,7 +980,7 @@ public class TextFormatter // if value is not wide enough if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width) { - // pad it out with spaces to the given alignment + // pad it out with spaces to the given Alignment int toPad = width - text.EnumerateRunes ().Sum (c => c.GetColumns ()); return text + new string (' ', toPad); @@ -999,7 +1002,7 @@ public class TextFormatter /// instance to access any of his objects. /// A list of word wrapped lines. /// - /// This method does not do any justification. + /// This method does not do any alignment. /// This method strips Newline ('\n' and '\r\n') sequences before processing. /// /// If is at most one space will be preserved @@ -1031,7 +1034,7 @@ public class TextFormatter List runes = StripCRLF (text).ToRuneList (); int start = Math.Max ( - !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom } && IsVerticalDirection (textDirection) + !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: Alignment.End } && IsVerticalDirection (textDirection) ? runes.Count - width : 0, 0); @@ -1249,7 +1252,7 @@ public class TextFormatter /// The number of columns to clip the text to. Text longer than will be /// clipped. /// - /// Alignment. + /// Alignment. /// The text direction. /// The number of columns used for a tab. /// instance to access any of his objects. @@ -1257,13 +1260,13 @@ public class TextFormatter public static string ClipAndJustify ( string text, int width, - TextAlignment talign, + Alignment textAlignment, TextDirection textDirection = TextDirection.LeftRight_TopBottom, int tabWidth = 0, TextFormatter textFormatter = null ) { - return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection, tabWidth, textFormatter); + return ClipAndJustify (text, width, textAlignment == Alignment.Fill, textDirection, tabWidth, textFormatter); } /// Justifies text within a specified width. @@ -1304,12 +1307,12 @@ public class TextFormatter { if (IsHorizontalDirection (textDirection)) { - if (textFormatter is { Alignment: TextAlignment.Right }) + if (textFormatter is { Alignment: Alignment.End }) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } - if (textFormatter is { Alignment: TextAlignment.Centered }) + if (textFormatter is { Alignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1319,12 +1322,12 @@ public class TextFormatter if (IsVerticalDirection (textDirection)) { - if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom }) + if (textFormatter is { VerticalAlignment: Alignment.End }) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } - if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle }) + if (textFormatter is { VerticalAlignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1342,14 +1345,14 @@ public class TextFormatter if (IsHorizontalDirection (textDirection)) { - if (textFormatter is { Alignment: TextAlignment.Right }) + if (textFormatter is { Alignment: Alignment.End }) { if (GetRuneWidth (text, tabWidth, textDirection) > width) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } } - else if (textFormatter is { Alignment: TextAlignment.Centered }) + else if (textFormatter is { Alignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1361,14 +1364,14 @@ public class TextFormatter if (IsVerticalDirection (textDirection)) { - if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom }) + if (textFormatter is { VerticalAlignment: Alignment.End }) { if (runes.Count - zeroLength > width) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } } - else if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle }) + else if (textFormatter is { VerticalAlignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1475,7 +1478,7 @@ public class TextFormatter /// Formats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries. /// /// The number of columns to constrain the text to for word wrapping and clipping. - /// Specifies how the text will be aligned horizontally. + /// Specifies how the text will be aligned horizontally. /// /// If , the text will be wrapped to new lines no longer than /// . If , forces text to fit a single line. Line breaks are converted @@ -1498,7 +1501,7 @@ public class TextFormatter public static List Format ( string text, int width, - TextAlignment talign, + Alignment textAlignment, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0, @@ -1510,7 +1513,7 @@ public class TextFormatter return Format ( text, width, - talign == TextAlignment.Justified, + textAlignment == Alignment.Fill, wordWrap, preserveTrailingSpaces, tabWidth, @@ -1884,7 +1887,7 @@ public class TextFormatter return lineIdx; } - /// Calculates the rectangle required to hold text, assuming no word wrapping or justification. + /// Calculates the rectangle required to hold text, assuming no word wrapping or alignment. /// /// This API will return incorrect results if the text includes glyphs who's width is dependent on surrounding /// glyphs (e.g. Arabic). diff --git a/Terminal.Gui/Text/VerticalTextAlignment.cs b/Terminal.Gui/Text/VerticalTextAlignment.cs deleted file mode 100644 index ef7788577..000000000 --- a/Terminal.Gui/Text/VerticalTextAlignment.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Terminal.Gui; - -/// Vertical text alignment enumeration, controls how text is displayed. -public enum VerticalTextAlignment -{ - /// The text will be top-aligned. - Top, - - /// The text will be bottom-aligned. - Bottom, - - /// The text will centered vertically. - Middle, - - /// - /// The text will be justified (spaces will be added to existing spaces such that the text fills the container - /// vertically). - /// - Justified -} \ No newline at end of file diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index 293c27e88..901852e22 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -196,6 +196,26 @@ public class Border : Adornment set => _lineStyle = value; } + private bool _showTitle = true; + + /// + /// Gets or sets whether the title should be shown. The default is . + /// + public bool ShowTitle + { + get => _showTitle; + set + { + if (value == _showTitle) + { + return; + } + _showTitle = value; + + Parent?.SetNeedsDisplay (); + } + } + #region Mouse Support private Color? _savedForeColor; @@ -358,7 +378,7 @@ public class Border : Adornment } } -#endregion Mouse Support + #endregion Mouse Support /// public override void OnDrawContent (Rectangle viewport) @@ -394,12 +414,13 @@ public class Border : Adornment Math.Min (screenBounds.Width - 4, borderBounds.Width - 4) ) ); + Parent.TitleTextFormatter.Size = new (maxTitleWidth, 1); int sideLineLength = borderBounds.Height; bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 }; - if (!string.IsNullOrEmpty (Parent?.Title)) + if (ShowTitle) { if (Thickness.Top == 2) { @@ -431,13 +452,13 @@ public class Border : Adornment } } - if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) + if (canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && ShowTitle && !string.IsNullOrEmpty (Parent?.Title)) { - var focus = Parent.GetNormalColor(); + var focus = Parent.GetNormalColor (); if (Parent.SuperView is { } && Parent.SuperView?.Subviews!.Count (s => s.CanFocus) > 1) { // Only use focus color if there are multiple focusable views - focus = Parent.GetFocusColor() ; + focus = Parent.GetFocusColor (); } Parent.TitleTextFormatter.Draw ( @@ -450,9 +471,9 @@ public class Border : Adornment { LineCanvas lc = Parent?.LineCanvas; - bool drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height > 1; + bool drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height >= 1; bool drawLeft = Thickness.Left > 0 && (Frame.Height > 1 || Thickness.Top == 0); - bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1; + bool drawBottom = Thickness.Bottom > 0 && Frame.Width > 1 && Frame.Height > 1; bool drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0); Attribute prevAttr = Driver.GetAttribute (); @@ -470,7 +491,7 @@ public class Border : Adornment { // ╔╡Title╞═════╗ // ╔╡╞═════╗ - if (borderBounds.Width < 4 || string.IsNullOrEmpty (Parent?.Title)) + if (borderBounds.Width < 4 || !ShowTitle || string.IsNullOrEmpty (Parent?.Title)) { // ╔╡╞╗ should be ╔══╗ lc.AddLine ( @@ -620,7 +641,7 @@ public class Border : Adornment } // Redraw title - if (drawTop && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) + if (drawTop && maxTitleWidth > 0 && ShowTitle) { Parent.TitleTextFormatter.Draw ( new (borderBounds.X + 2, titleY, maxTitleWidth, 1), diff --git a/Terminal.Gui/View/Layout/Dim.cs b/Terminal.Gui/View/Layout/Dim.cs index 1fc16c9c2..259eed499 100644 --- a/Terminal.Gui/View/Layout/Dim.cs +++ b/Terminal.Gui/View/Layout/Dim.cs @@ -6,8 +6,7 @@ namespace Terminal.Gui; /// /// /// A Dim object describes the dimensions of a . Dim is the type of the -/// and properties of . Dim objects enable -/// Computed Layout (see ) to automatically manage the dimensions of a view. +/// and properties of . /// /// /// Integer values are implicitly convertible to an absolute . These objects are created using @@ -150,7 +149,7 @@ public abstract class Dim /// Creates a percentage object that is a percentage of the width or height of the SuperView. /// The percent object. /// A value between 0 and 100 representing the percentage. - /// + /// the mode. Defaults to . /// /// This initializes a that will be centered horizontally, is 50% of the way down, is 30% the /// height, @@ -187,7 +186,7 @@ public abstract class Dim /// Gets a dimension that is anchored to a certain point in the layout. /// This method is typically used internally by the layout system to determine the size of a View. /// - /// The width of the area where the View is being sized (Superview.ContentSize). + /// The width of the area where the View is being sized (Superview.GetContentSize ()). /// /// An integer representing the calculated dimension. The way this dimension is calculated depends on the specific /// subclass of Dim that is used. For example, DimAbsolute returns a fixed dimension, DimFactor returns a diff --git a/Terminal.Gui/View/Layout/DimAuto.cs b/Terminal.Gui/View/Layout/DimAuto.cs index b8dc4204f..0538c6f08 100644 --- a/Terminal.Gui/View/Layout/DimAuto.cs +++ b/Terminal.Gui/View/Layout/DimAuto.cs @@ -60,7 +60,8 @@ public class DimAuto () : Dim var subviewsSize = 0; int autoMin = MinimumContentDim?.GetAnchor (superviewContentSize) ?? 0; - + int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? int.MaxValue; + if (Style.FastHasFlags (DimAutoStyle.Text)) { textSize = int.Max (autoMin, dimension == Dimension.Width ? us.TextFormatter.Size.Width : us.TextFormatter.Size.Height); @@ -68,24 +69,46 @@ public class DimAuto () : Dim if (Style.FastHasFlags (DimAutoStyle.Content)) { - if (us._contentSize is { }) + if (!us.ContentSizeTracksViewport) { - subviewsSize = dimension == Dimension.Width ? us.ContentSize.Width : us.ContentSize.Height; + // ContentSize was explicitly set. Ignore subviews. + subviewsSize = dimension == Dimension.Width ? us.GetContentSize ().Width : us.GetContentSize ().Height; } else { + // ContentSize was NOT explicitly set. Use subviews to determine size. + // TODO: This whole body of code is a WIP (for https://github.com/gui-cs/Terminal.Gui/pull/3451). subviewsSize = 0; + List includedSubviews = us.Subviews.ToList();//.Where (v => !v.ExcludeFromLayout).ToList (); List subviews; + #region Not Anchored and Are Not Dependent + // Start with subviews that are not anchored to the end, aligned, or dependent on content size + // [x] PosAnchorEnd + // [x] PosAlign + // [ ] PosCenter + // [ ] PosPercent + // [ ] PosView + // [ ] PosFunc + // [x] DimFill + // [ ] DimPercent + // [ ] DimFunc + // [ ] DimView if (dimension == Dimension.Width) { - subviews = us.Subviews.Where (v => v.X is not PosAnchorEnd && v.Width is not DimFill).ToList (); + subviews = includedSubviews.Where (v => v.X is not PosAnchorEnd + && v.X is not PosAlign + // && v.X is not PosCenter + && v.Width is not DimFill).ToList (); } else { - subviews = us.Subviews.Where (v => v.Y is not PosAnchorEnd && v.Height is not DimFill).ToList (); + subviews = includedSubviews.Where (v => v.Y is not PosAnchorEnd + && v.Y is not PosAlign + // && v.Y is not PosCenter + && v.Height is not DimFill).ToList (); } for (var i = 0; i < subviews.Count; i++) @@ -96,17 +119,22 @@ public class DimAuto () : Dim if (size > subviewsSize) { + // BUGBUG: Should we break here? Or choose min/max? subviewsSize = size; } } + #endregion Not Anchored and Are Not Dependent + #region Anchored + // Now, handle subviews that are anchored to the end + // [x] PosAnchorEnd if (dimension == Dimension.Width) { - subviews = us.Subviews.Where (v => v.X is PosAnchorEnd).ToList (); + subviews = includedSubviews.Where (v => v.X is PosAnchorEnd).ToList (); } else { - subviews = us.Subviews.Where (v => v.Y is PosAnchorEnd).ToList (); + subviews = includedSubviews.Where (v => v.Y is PosAnchorEnd).ToList (); } int maxAnchorEnd = 0; @@ -117,31 +145,64 @@ public class DimAuto () : Dim } subviewsSize += maxAnchorEnd; + #endregion Anchored + //#region Center + //// Now, handle subviews that are Centered + //if (dimension == Dimension.Width) + //{ + // subviews = us.Subviews.Where (v => v.X is PosCenter).ToList (); + //} + //else + //{ + // subviews = us.Subviews.Where (v => v.Y is PosCenter).ToList (); + //} + //int maxCenter = 0; + //for (var i = 0; i < subviews.Count; i++) + //{ + // View v = subviews [i]; + // maxCenter = dimension == Dimension.Width ? v.Frame.Width : v.Frame.Height; + //} + + //subviewsSize += maxCenter; + //#endregion Center + + #region Are Dependent + // Now, go back to those that are dependent on content size + // [x] DimFill + // [ ] DimPercent if (dimension == Dimension.Width) { - subviews = us.Subviews.Where (v => v.Width is DimFill).ToList (); + subviews = includedSubviews.Where (v => v.Width is DimFill + // || v.X is PosCenter + ).ToList (); } else { - subviews = us.Subviews.Where (v => v.Height is DimFill).ToList (); + subviews = includedSubviews.Where (v => v.Height is DimFill + //|| v.Y is PosCenter + ).ToList (); } + int maxFill = 0; for (var i = 0; i < subviews.Count; i++) { View v = subviews [i]; if (dimension == Dimension.Width) { - v.SetRelativeLayout (new Size (autoMin - subviewsSize, 0)); + v.SetRelativeLayout (new Size (autoMax - subviewsSize, 0)); } else { - v.SetRelativeLayout (new Size (0, autoMin - subviewsSize)); + v.SetRelativeLayout (new Size (0, autoMax - subviewsSize)); } + maxFill = dimension == Dimension.Width ? v.Frame.Width : v.Frame.Height; } + subviewsSize += maxFill; + #endregion Are Dependent } } @@ -156,14 +217,14 @@ public class DimAuto () : Dim Thickness thickness = us.GetAdornmentsThickness (); max += dimension switch - { - Dimension.Width => thickness.Horizontal, - Dimension.Height => thickness.Vertical, - Dimension.None => 0, - _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null) - }; + { + Dimension.Width => thickness.Horizontal, + Dimension.Height => thickness.Vertical, + Dimension.None => 0, + _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null) + }; - return int.Min (max, MaximumContentDim?.GetAnchor (superviewContentSize) ?? max); + return int.Min (max, autoMax); } internal override bool ReferencesOtherViews () diff --git a/Terminal.Gui/View/Layout/DimAutoStyle.cs b/Terminal.Gui/View/Layout/DimAutoStyle.cs index 85b162569..f350e8045 100644 --- a/Terminal.Gui/View/Layout/DimAutoStyle.cs +++ b/Terminal.Gui/View/Layout/DimAutoStyle.cs @@ -10,11 +10,9 @@ namespace Terminal.Gui; public enum DimAutoStyle { /// - /// The dimensions will be computed based on the View's non-Text content. + /// The dimensions will be computed based on the View's and/or . /// - /// If is explicitly set (is not ) then - /// - /// will be used to determine the dimension. + /// If is , will be used to determine the dimension. /// /// /// Otherwise, the Subview in with the largest corresponding position plus dimension @@ -24,7 +22,7 @@ public enum DimAutoStyle /// The corresponding dimension of the view's will be ignored. /// /// - Content = 0, + Content = 1, /// /// @@ -33,14 +31,14 @@ public enum DimAutoStyle /// will be used to determine the dimension. /// /// - /// The corresponding dimensions of the will be ignored. + /// The corresponding dimensions of and/or will be ignored. /// /// - Text = 1, + Text = 2, /// - /// The dimension will be computed using both the view's and - /// (whichever is larger). + /// The dimension will be computed using the largest of the view's , , and + /// corresponding dimension /// Auto = Content | Text, } \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimPercent.cs b/Terminal.Gui/View/Layout/DimPercent.cs index af849c53f..2ae81302a 100644 --- a/Terminal.Gui/View/Layout/DimPercent.cs +++ b/Terminal.Gui/View/Layout/DimPercent.cs @@ -11,7 +11,7 @@ namespace Terminal.Gui; /// The percentage. /// /// If the dimension is computed using the View's position ( or -/// ); otherwise, the dimension is computed using the View's . +/// ); otherwise, the dimension is computed using the View's . /// public class DimPercent (int percent, DimPercentMode mode = DimPercentMode.ContentSize) : Dim { @@ -32,7 +32,7 @@ public class DimPercent (int percent, DimPercentMode mode = DimPercentMode.Conte public override string ToString () { return $"Percent({Percent},{Mode})"; } /// - /// Gets whether the dimension is computed using the View's position or ContentSize. + /// Gets whether the dimension is computed using the View's position or GetContentSize (). /// public DimPercentMode Mode { get; } = mode; diff --git a/Terminal.Gui/View/Layout/DimPercentMode.cs b/Terminal.Gui/View/Layout/DimPercentMode.cs index 74d64b77c..60a7da056 100644 --- a/Terminal.Gui/View/Layout/DimPercentMode.cs +++ b/Terminal.Gui/View/Layout/DimPercentMode.cs @@ -15,7 +15,7 @@ public enum DimPercentMode Position = 0, /// - /// The dimension is computed using the View's . + /// The dimension is computed using the View's . /// ContentSize = 1 } \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/LayoutStyle.cs b/Terminal.Gui/View/Layout/LayoutStyle.cs deleted file mode 100644 index 81883bfcc..000000000 --- a/Terminal.Gui/View/Layout/LayoutStyle.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Terminal.Gui.Analyzers.Internal.Attributes; - -namespace Terminal.Gui; - -/// -/// Indicates the LayoutStyle for the . -/// -/// If Absolute, the , , , and -/// objects are all absolute values and are not relative. The position and size of the -/// view is described by . -/// -/// -/// If Computed, one or more of the , , , or -/// objects are relative to the and are computed at layout -/// time. -/// -/// -[GenerateEnumExtensionMethods] -public enum LayoutStyle -{ - /// - /// Indicates the , , , and - /// objects are all absolute values and are not relative. The position and size of the view - /// is described by . - /// - Absolute, - - /// - /// Indicates one or more of the , , , or - /// - /// objects are relative to the and are computed at layout time. The position and size of - /// the - /// view - /// will be computed based on these objects at layout time. will provide the absolute computed - /// values. - /// - Computed -} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/Pos.cs b/Terminal.Gui/View/Layout/Pos.cs index e27fabb3c..a5cf52249 100644 --- a/Terminal.Gui/View/Layout/Pos.cs +++ b/Terminal.Gui/View/Layout/Pos.cs @@ -26,6 +26,14 @@ namespace Terminal.Gui; /// /// /// +/// +/// +/// +/// Creates a object that aligns a set of views. +/// +/// +/// +/// /// /// /// @@ -132,6 +140,30 @@ public abstract class Pos /// The value to convert to the . public static Pos Absolute (int position) { return new PosAbsolute (position); } + /// + /// Creates a object that aligns a set of views according to the specified + /// and . + /// + /// The alignment. The default includes . + /// The optional alignment modes. + /// + /// The optional identifier of a set of views that should be aligned together. When only a single + /// set of views in a SuperView is aligned, this parameter is optional. + /// + /// + public static Pos Align (Alignment alignment, AlignmentModes modes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems, int groupId = 0) + { + return new PosAlign + { + Aligner = new () + { + Alignment = alignment, + AlignmentModes = modes + }, + GroupId = groupId + }; + } + /// /// Creates a object that is anchored to the end (right side or /// bottom) of the SuperView's Content Area, minus the respective size of the View. This is equivalent to using @@ -264,12 +296,12 @@ public abstract class Pos /// /// Gets the starting point of an element based on the size of the parent element (typically - /// Superview.ContentSize). + /// Superview.GetContentSize ()). /// This method is meant to be overridden by subclasses to provide different ways of calculating the starting point. /// This method is used /// internally by the layout system to determine where a View should be positioned. /// - /// The size of the parent element (typically Superview.ContentSize). + /// The size of the parent element (typically Superview.GetContentSize ()). /// /// An integer representing the calculated position. The way this position is calculated depends on the specific /// subclass of Pos that is used. For example, PosAbsolute returns a fixed position, PosAnchorEnd returns a @@ -359,5 +391,4 @@ public abstract class Pos } #endregion operators - -} \ No newline at end of file +} diff --git a/Terminal.Gui/View/Layout/PosAlign.cs b/Terminal.Gui/View/Layout/PosAlign.cs new file mode 100644 index 000000000..31bb81a26 --- /dev/null +++ b/Terminal.Gui/View/Layout/PosAlign.cs @@ -0,0 +1,222 @@ +#nullable enable + +using System.ComponentModel; +using System.Drawing; + +namespace Terminal.Gui; + +/// +/// Enables alignment of a set of views. +/// +/// +/// +/// Updating the properties of is supported, but will not automatically cause re-layout to +/// happen. +/// must be called on the SuperView. +/// +/// +/// Views that should be aligned together must have a distinct . When only a single +/// set of views is aligned within a SuperView, setting is optional because it defaults to 0. +/// +/// +/// The first view added to the Superview with a given is used to determine the alignment of +/// the group. +/// The alignment is applied to all views with the same . +/// +/// +public class PosAlign : Pos +{ + /// + /// The cached location. Used to store the calculated location to minimize recalculating it. + /// + private int? _cachedLocation; + + /// + /// Gets the identifier of a set of views that should be aligned together. When only a single + /// set of views in a SuperView is aligned, setting is not needed because it defaults to 0. + /// + public int GroupId { get; init; } + + private readonly Aligner? _aligner; + + /// + /// Gets the alignment settings. + /// + public required Aligner Aligner + { + get => _aligner!; + init + { + if (_aligner is { }) + { + _aligner.PropertyChanged -= Aligner_PropertyChanged; + } + + _aligner = value; + _aligner.PropertyChanged += Aligner_PropertyChanged; + } + } + + /// + /// Aligns the views in that have the same group ID as . + /// Updates each view's cached _location. + /// + /// + /// + /// + /// + private static void AlignAndUpdateGroup (int groupId, IList views, Dimension dimension, int size) + { + List dimensionsList = new (); + + // PERF: If this proves a perf issue, consider caching a ref to this list in each item + List viewsInGroup = views.Where ( + v => + { + return dimension switch + { + Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId, + Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId, + _ => false + }; + }) + .ToList (); + + if (viewsInGroup.Count == 0) + { + return; + } + + // PERF: We iterate over viewsInGroup multiple times here. + + Aligner? firstInGroup = null; + + // Update the dimensionList with the sizes of the views + for (var index = 0; index < viewsInGroup.Count; index++) + { + View view = viewsInGroup [index]; + PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign; + + if (posAlign is { }) + { + if (index == 0) + { + firstInGroup = posAlign.Aligner; + } + + dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height); + } + } + + // Update the first item in the group with the new container size. + firstInGroup!.ContainerSize = size; + + // Align + int [] locations = firstInGroup.Align (dimensionsList.ToArray ()); + + // Update the cached location for each item + for (var index = 0; index < viewsInGroup.Count; index++) + { + View view = viewsInGroup [index]; + PosAlign? align = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign; + + if (align is { }) + { + align._cachedLocation = locations [index]; + } + } + } + + private void Aligner_PropertyChanged (object? sender, PropertyChangedEventArgs e) { _cachedLocation = null; } + + /// + public override bool Equals (object? other) + { + return other is PosAlign align + && GroupId == align.GroupId + && align.Aligner.Alignment == Aligner.Alignment + && align.Aligner.AlignmentModes == Aligner.AlignmentModes; + } + + /// + public override int GetHashCode () { return HashCode.Combine (Aligner, GroupId); } + + /// + public override string ToString () { return $"Align(alignment={Aligner.Alignment},modes={Aligner.AlignmentModes},groupId={GroupId})"; } + + internal override int GetAnchor (int width) { return _cachedLocation ?? 0 - width; } + + internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension) + { + if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension) + { + return _cachedLocation.Value; + } + + if (us?.SuperView is null) + { + return 0; + } + + AlignAndUpdateGroup (GroupId, us.SuperView.Subviews, dimension, superviewDimension); + + if (_cachedLocation.HasValue) + { + return _cachedLocation.Value; + } + + return 0; + } + + internal int CalculateMinDimension (int groupId, IList views, Dimension dimension) + { + List dimensionsList = new (); + + // PERF: If this proves a perf issue, consider caching a ref to this list in each item + List viewsInGroup = views.Where ( + v => + { + return dimension switch + { + Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId, + Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId, + _ => false + }; + }) + .ToList (); + + if (viewsInGroup.Count == 0) + { + return 0; + } + + // PERF: We iterate over viewsInGroup multiple times here. + + Aligner? firstInGroup = null; + + // Update the dimensionList with the sizes of the views + for (var index = 0; index < viewsInGroup.Count; index++) + { + View view = viewsInGroup [index]; + + PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign; + + if (posAlign is { }) + { + if (index == 0) + { + firstInGroup = posAlign.Aligner; + } + + dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height); + } + } + + // Align + var aligner = firstInGroup; + aligner.ContainerSize = dimensionsList.Sum(); + int [] locations = aligner.Align (dimensionsList.ToArray ()); + + return locations.Sum (); + } +} diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs index 94348fc75..58b0d23ce 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -1,6 +1,5 @@ #nullable enable using System.Diagnostics; -using Microsoft.CodeAnalysis; namespace Terminal.Gui; @@ -13,14 +12,13 @@ public partial class View /// Gets or sets the absolute location and dimension of the view. /// /// The rectangle describing absolute location and dimension of the view, in coordinates relative to the - /// 's Content, which is bound by . + /// 's Content, which is bound by . /// /// - /// Frame is relative to the 's Content, which is bound by . + /// Frame is relative to the 's Content, which is bound by . /// /// Setting Frame will set , , , and to the /// values of the corresponding properties of the parameter. - /// This causes to be . /// /// /// Altering the Frame will eventually (when the view hierarchy is next laid out via see @@ -41,8 +39,7 @@ public partial class View SetFrame (value with { Width = Math.Max (value.Width, 0), Height = Math.Max (value.Height, 0) }); - // If Frame gets set, by definition, the View is now LayoutStyle.Absolute, so - // set all Pos/Dim to Absolute values. + // If Frame gets set, set all Pos/Dim to Absolute values. _x = _frame.X; _y = _frame.Y; _width = _frame.Width; @@ -136,7 +133,7 @@ public partial class View /// The object representing the X position. /// /// - /// The position is relative to the 's Content, which is bound by . + /// The position is relative to the 's Content, which is bound by . /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the view has been @@ -148,8 +145,7 @@ public partial class View /// and methods to be called. /// /// - /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// Changing this property will cause to be updated. /// /// The default value is Pos.At (0). /// @@ -175,7 +171,7 @@ public partial class View /// The object representing the Y position. /// /// - /// The position is relative to the 's Content, which is bound by . + /// The position is relative to the 's Content, which is bound by . /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the view has been @@ -187,8 +183,7 @@ public partial class View /// and methods to be called. /// /// - /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// Changing this property will cause to be updated. /// /// The default value is Pos.At (0). /// @@ -213,7 +208,7 @@ public partial class View /// The object representing the height of the view (the number of rows). /// /// - /// The dimension is relative to the 's Content, which is bound by + /// The dimension is relative to the 's Content, which is bound by /// . /// /// @@ -226,8 +221,7 @@ public partial class View /// and methods to be called. /// /// - /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// Changing this property will cause to be updated. /// /// The default value is Dim.Sized (0). /// @@ -259,7 +253,7 @@ public partial class View /// The object representing the width of the view (the number of columns). /// /// - /// The dimension is relative to the 's Content, which is bound by + /// The dimension is relative to the 's Content, which is bound by /// . /// /// @@ -272,8 +266,7 @@ public partial class View /// and methods to be called. /// /// - /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// Changing this property will cause to be updated. /// /// The default value is Dim.Sized (0). /// @@ -303,55 +296,6 @@ public partial class View #region Layout Engine - - // @tig Notes on layout flow. Ignore for now. - // BeginLayout - // If !LayoutNeeded return - // If !SizeNeeded return - // Call OnLayoutStarted - // Views and subviews can update things - // - - - // EndLayout - - /// - /// Controls how the View's is computed during . If the style is - /// set to , LayoutSubviews does not change the . If the style is - /// the is updated using the , , - /// , and properties. - /// - /// - /// - /// Setting this property to will cause to determine the - /// size and position of the view. and will be set to - /// using . - /// - /// - /// Setting this property to will cause the view to use the - /// method to size and position of the view. If either of the and - /// properties are `null` they will be set to using the current value - /// of . If either of the and properties are `null` - /// they will be set to using . - /// - /// - /// The layout style. - public LayoutStyle LayoutStyle - { - get - { - if (_x is PosAbsolute - && _y is PosAbsolute - && _width is DimAbsolute - && _height is DimAbsolute) - { - return LayoutStyle.Absolute; - } - - return LayoutStyle.Computed; - } - } - #endregion Layout Engine /// @@ -626,7 +570,7 @@ public partial class View CheckDimAuto (); - var contentSize = ContentSize; + var contentSize = GetContentSize (); OnLayoutStarted (new (contentSize)); LayoutAdornments (); @@ -650,7 +594,7 @@ public partial class View { foreach ((View from, View to) in edges) { - LayoutSubview (to, from.ContentSize); + LayoutSubview (to, from.GetContentSize ()); } } @@ -703,8 +647,8 @@ public partial class View // Determine our container's ContentSize - // First try SuperView.Viewport, then Application.Top, then Driver.Viewport. // Finally, if none of those are valid, use int.MaxValue (for Unit tests). - Size superViewContentSize = SuperView is { IsInitialized: true } ? SuperView.ContentSize : - Application.Top is { } && Application.Top != this && Application.Top.IsInitialized ? Application.Top.ContentSize : + Size superViewContentSize = SuperView is { IsInitialized: true } ? SuperView.GetContentSize () : + Application.Top is { } && Application.Top != this && Application.Top.IsInitialized ? Application.Top.GetContentSize () : Application.Driver?.Screen.Size ?? new (int.MaxValue, int.MaxValue); SetTextFormatterSize (); @@ -746,7 +690,7 @@ public partial class View /// /// Adjusts given the SuperView's ContentSize (nominally the same as - /// this.SuperView.ContentSize) + /// this.SuperView.GetContentSize ()) /// and the position (, ) and dimension (, and /// ). /// @@ -758,7 +702,7 @@ public partial class View /// /// /// - /// The size of the SuperView's content (nominally the same as this.SuperView.ContentSize). + /// The size of the SuperView's content (nominally the same as this.SuperView.GetContentSize ()). /// internal void SetRelativeLayout (Size superviewContentSize) { @@ -796,8 +740,7 @@ public partial class View if (Frame != newFrame) { - // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height, making - // the view LayoutStyle.Absolute. + // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height SetFrame (newFrame); if (_x is PosAbsolute) @@ -835,12 +778,6 @@ public partial class View foreach (View? v in from.InternalSubviews) { nNodes.Add (v); - - if (v.LayoutStyle != LayoutStyle.Computed) - { - continue; - } - CollectPos (v.X, v, ref nNodes, ref nEdges); CollectPos (v.Y, v, ref nNodes, ref nEdges); CollectDim (v.Width, v, ref nNodes, ref nEdges); @@ -1049,13 +986,13 @@ public partial class View // Verify none of the subviews are using Dim objects that depend on the SuperView's dimensions. foreach (View view in Subviews) { - if (widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Content) && _contentSize is null) + if (widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Content) && ContentSizeTracksViewport) { ThrowInvalid (view, view.Width, nameof (view.Width)); ThrowInvalid (view, view.X, nameof (view.X)); } - if (heightAuto is { } && heightAuto.Style.FastHasFlags (DimAutoStyle.Content) && _contentSize is null) + if (heightAuto is { } && heightAuto.Style.FastHasFlags (DimAutoStyle.Content) && ContentSizeTracksViewport) { ThrowInvalid (view, view.Height, nameof (view.Height)); ThrowInvalid (view, view.Y, nameof (view.Y)); diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 1632eca2f..441b0a3ac 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -43,13 +43,6 @@ namespace Terminal.Gui; /// more subviews, can respond to user input and render themselves on the screen. /// /// -/// View supports two layout styles: or . -/// The style is determined by the values of , , , and -/// . If any of these is set to non-absolute or object, -/// then the layout style is . Otherwise it is -/// . -/// -/// /// To create a View using Absolute layout, call a constructor that takes a Rect parameter to specify the /// absolute position and size or simply set ). To create a View using Computed layout use /// a constructor that does not take a Rect parameter and set the X, Y, Width and Height properties on the view to @@ -79,9 +72,7 @@ namespace Terminal.Gui; /// To flag the entire view for redraw call . /// /// -/// The method is invoked when the size or layout of a view has changed. The default -/// processing system will keep the size and dimensions for views that use the , -/// and will recompute the Adornments for the views that use . +/// The method is invoked when the size or layout of a view has changed. /// /// /// Views have a property that defines the default colors that subviews should use for @@ -127,38 +118,25 @@ public partial class View : Responder, ISupportInitializeNotification /// /// /// Use , , , and properties to dynamically - /// control the size and location of the view. The will be created using - /// coordinates. The initial size ( ) will be adjusted - /// to fit the contents of , including newlines ('\n') for multiple lines. - /// - /// If is greater than one, word wrapping is provided. - /// - /// This constructor initialize a View with a of . - /// Use , , , and properties to dynamically - /// control the size and location of the view, changing it to . + /// control the size and location of the view. /// /// public View () { - CreateAdornments (); - - HotKeySpecifier = (Rune)'_'; - TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged; - - TextDirection = TextDirection.LeftRight_TopBottom; - Text = string.Empty; + SetupAdornments (); + SetupKeyboard (); + //SetupMouse (); + SetupText (); CanFocus = false; TabIndex = -1; TabStop = false; - - AddCommands (); } /// /// Event called only once when the is being initialized for the first time. Allows - /// configurations and assignments to be performed before the being shown. This derived from - /// to allow notify all the views that are being initialized. + /// configurations and assignments to be performed before the being shown. + /// View implements to allow for more sophisticated initialization. /// public event EventHandler Initialized; @@ -525,6 +503,7 @@ public partial class View : Responder, ISupportInitializeNotification { LineCanvas.Dispose (); + DisposeKeyboard (); DisposeAdornments (); for (int i = InternalSubviews.Count - 1; i >= 0; i--) diff --git a/Terminal.Gui/View/ViewAdornments.cs b/Terminal.Gui/View/ViewAdornments.cs index 54b4609b1..0ebf5499f 100644 --- a/Terminal.Gui/View/ViewAdornments.cs +++ b/Terminal.Gui/View/ViewAdornments.cs @@ -2,7 +2,10 @@ public partial class View { - private void CreateAdornments () + /// + /// Initializes the Adornments of the View. Called by the constructor. + /// + private void SetupAdornments () { //// TODO: Move this to Adornment as a static factory method if (this is not Adornment) diff --git a/Terminal.Gui/View/ViewContent.cs b/Terminal.Gui/View/ViewContent.cs index 7fa48ed38..08443a851 100644 --- a/Terminal.Gui/View/ViewContent.cs +++ b/Terminal.Gui/View/ViewContent.cs @@ -1,6 +1,4 @@ -using System.Diagnostics; - -namespace Terminal.Gui; +namespace Terminal.Gui; public partial class View { @@ -13,33 +11,31 @@ public partial class View /// /// /// - /// By default, the content size is set to . - /// - /// - /// - /// - /// If , and the View has no visible subviews, will track the size of . - /// - /// - /// If , and the View has visible subviews, will track the maximum position plus size of any - /// visible Subviews - /// and Viewport.Location will track the minimum position and size of any visible Subviews. - /// - /// - /// If not , is set to the passed value and describes the portion of the content currently visible - /// to the user. This enables virtual scrolling. - /// - /// - /// If not , is set to the passed value and the behavior of will be to use the ContentSize - /// to determine the size of the view. - /// - /// /// Negative sizes are not supported. /// - /// + /// + /// If not explicitly set, and the View has no visible subviews, will return the + /// size of + /// . + /// + /// + /// If not explicitly set, and the View has visible subviews, will return the + /// maximum + /// position + dimension of the Subviews, supporting with the + /// flag set. + /// + /// + /// If set describes the portion of the content currently visible to the user. This enables + /// virtual scrolling. + /// + /// + /// If set the behavior of will be to use the ContentSize to determine the size + /// of the view. + /// + /// public void SetContentSize (Size? contentSize) { - if (ContentSize.Width < 0 || ContentSize.Height < 0) + if (contentSize is { } && (contentSize.Value.Width < 0 || contentSize.Value.Height < 0)) { throw new ArgumentException (@"ContentSize cannot be negative.", nameof (contentSize)); } @@ -56,19 +52,86 @@ public partial class View /// /// Gets the size of the View's content. /// - /// + /// a> /// - /// Use to change to change the content size. - /// - /// - /// If the content size has not been explicitly set with , the value tracks + /// If the content size was not explicitly set by , and the View has no visible subviews, will return the + /// size of /// . /// + /// + /// If the content size was not explicitly set by , and the View has visible subviews, will return the + /// maximum + /// position + dimension of the Subviews, supporting with the + /// flag set. + /// + /// + /// If set describes the portion of the content currently visible to the user. This enables + /// virtual scrolling. + /// + /// + /// If set the behavior of will be to use the ContentSize to determine the size + /// of the view. + /// /// - public Size ContentSize => _contentSize ?? Viewport.Size; + /// + /// If the content size was not explicitly set by , will + /// return the size of the and will be . + /// + public Size GetContentSize () { return _contentSize ?? Viewport.Size; } /// - /// Called when has changed. + /// Gets or sets a value indicating whether the view's content size tracks the 's + /// size or not. + /// + /// + /// + /// + /// Value Result + /// + /// + /// + /// + /// + /// + /// + /// will return the 's size. Content scrolling + /// will be + /// disabled. + /// + /// + /// The behavior of will be to use position and size of the Subviews + /// to + /// determine the size of the view, ignoring . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// The return value of is independent of and + /// describes the portion of the content currently visible to the user enabling content scrolling. + /// + /// + /// The behavior of will be to use + /// to + /// determine the + /// size of the view, ignoring the position and size of the Subviews. + /// + /// + /// + /// + /// + public bool ContentSizeTracksViewport + { + get => _contentSize is null; + set => _contentSize = value ? null : _contentSize; + } + + /// + /// Called when has changed. /// /// /// @@ -79,6 +142,7 @@ public partial class View if (e.Cancel != true) { OnResizeNeeded (); + //SetNeedsLayout (); //SetNeedsDisplay (); } @@ -87,7 +151,7 @@ public partial class View } /// - /// Event raised when the changes. + /// Event raised when the changes. /// public event EventHandler ContentSizeChanged; @@ -155,37 +219,35 @@ public partial class View /// /// The location of the viewport into the view's content (0,0) is the top-left corner of the content. The Content /// area's size - /// is . + /// is . /// private Point _viewportLocation; /// /// Gets or sets the rectangle describing the portion of the View's content that is visible to the user. /// The viewport Location is relative to the top-left corner of the inner rectangle of . - /// If the viewport Size is the same as , or is + /// If the viewport Size is the same as , or is /// the Location will be 0, 0. /// /// /// The rectangle describing the location and size of the viewport into the View's virtual content, described by - /// . + /// . /// /// /// /// Positive values for the location indicate the visible area is offset into (down-and-right) the View's virtual - /// . This enables scrolling down and to the right (e.g. in a . + /// . This enables scrolling down and to the right (e.g. in a + /// . /// /// /// Negative values for the location indicate the visible area is offset above (up-and-left) the View's virtual - /// . This enables scrolling up and to the left (e.g. in an image viewer that supports zoom + /// . This enables scrolling up and to the left (e.g. in an image viewer that + /// supports + /// zoom /// where the image stays centered). /// /// - /// The property controls how scrolling is handled. - /// - /// - /// If is the value of Viewport is indeterminate until - /// the view has been initialized ( is true) and has been - /// called. + /// The property controls how scrolling is handled. /// /// /// Updates to the Viewport Size updates , and has the same impact as updating the @@ -207,6 +269,7 @@ public partial class View } Thickness thickness = GetAdornmentsThickness (); + return new ( _viewportLocation, new ( @@ -239,6 +302,7 @@ public partial class View } OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); + return; } @@ -254,9 +318,9 @@ public partial class View { if (!ViewportSettings.HasFlag (ViewportSettings.AllowXGreaterThanContentWidth)) { - if (newViewport.X >= ContentSize.Width) + if (newViewport.X >= GetContentSize ().Width) { - newViewport.X = ContentSize.Width - 1; + newViewport.X = GetContentSize ().Width - 1; } } @@ -271,9 +335,9 @@ public partial class View if (!ViewportSettings.HasFlag (ViewportSettings.AllowYGreaterThanContentHeight)) { - if (newViewport.Y >= ContentSize.Height) + if (newViewport.Y >= GetContentSize ().Height) { - newViewport.Y = ContentSize.Height - 1; + newViewport.Y = GetContentSize ().Height - 1; } } @@ -289,7 +353,8 @@ public partial class View } /// - /// Fired when the changes. This event is fired after the has been updated. + /// Fired when the changes. This event is fired after the has been + /// updated. /// [CanBeNull] public event EventHandler ViewportChanged; @@ -298,10 +363,7 @@ public partial class View /// Called when the changes. Invokes the event. /// /// - protected virtual void OnViewportChanged (DrawEventArgs e) - { - ViewportChanged?.Invoke (this, e); - } + protected virtual void OnViewportChanged (DrawEventArgs e) { ViewportChanged?.Invoke (this, e); } /// /// Converts a -relative location and size to a screen-relative location and size. @@ -311,10 +373,7 @@ public partial class View /// /// Viewport-relative location and size. /// Screen-relative location and size. - public Rectangle ViewportToScreen (in Rectangle viewport) - { - return viewport with { Location = ViewportToScreen (viewport.Location) }; - } + public Rectangle ViewportToScreen (in Rectangle viewport) { return viewport with { Location = ViewportToScreen (viewport.Location) }; } /// /// Converts a -relative location to a screen-relative location. @@ -367,7 +426,7 @@ public partial class View /// if the was changed. public bool? ScrollVertical (int rows) { - if (ContentSize == Size.Empty || ContentSize == Viewport.Size) + if (GetContentSize () == Size.Empty || GetContentSize () == Viewport.Size) { return false; } @@ -388,7 +447,7 @@ public partial class View /// if the was changed. public bool? ScrollHorizontal (int cols) { - if (ContentSize == Size.Empty || ContentSize == Viewport.Size) + if (GetContentSize () == Size.Empty || GetContentSize () == Viewport.Size) { return false; } diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs index 95232fa8c..74beffd3f 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/ViewDrawing.cs @@ -106,7 +106,7 @@ public partial class View if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly)) { - Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize)); + Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); toClear = Rectangle.Intersect (toClear, visibleContent); } @@ -172,7 +172,7 @@ public partial class View if (ViewportSettings.HasFlag (ViewportSettings.ClipContentOnly)) { // Clamp the Clip to the just content area that is within the viewport - Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize)); + Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); clip = Rectangle.Intersect (clip, visibleContent); } @@ -236,6 +236,16 @@ public partial class View OnRenderLineCanvas (); + // TODO: This is a hack to force the border subviews to draw. + if (Border?.Subviews is { }) + { + foreach (View view in Border.Subviews) + { + view.SetNeedsDisplay (); + view.Draw (); + } + } + // Invoke DrawContentCompleteEvent OnDrawContentComplete (Viewport); @@ -329,7 +339,7 @@ public partial class View public virtual Attribute GetFocusColor () { ColorScheme cs = ColorScheme; - if (ColorScheme is null) + if (cs is null) { cs = new (); } @@ -347,7 +357,7 @@ public partial class View { ColorScheme cs = ColorScheme; - if (ColorScheme is null) + if (cs is null) { cs = new (); } @@ -365,7 +375,7 @@ public partial class View { ColorScheme cs = ColorScheme; - if (ColorScheme is null) + if (cs is null) { cs = new (); } @@ -435,12 +445,12 @@ public partial class View /// /// /// The Location and Size indicate what part of the View's content, defined - /// by , is visible and should be drawn. The coordinates taken by and + /// by , is visible and should be drawn. The coordinates taken by and /// are relative to , thus if ViewPort.Location.Y is 5 /// the 6th row of the content should be drawn using MoveTo (x, 5). /// /// - /// If is larger than ViewPort.Size drawing code should use + /// If is larger than ViewPort.Size drawing code should use /// to constrain drawing for better performance. /// /// @@ -475,7 +485,7 @@ public partial class View // This should NOT clear // TODO: If the output is not in the Viewport, do nothing - var drawRect = new Rectangle (ContentToScreen (Point.Empty), ContentSize); + var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ()); TextFormatter?.Draw ( drawRect, @@ -584,7 +594,7 @@ public partial class View /// /// /// The location of is relative to the View's content, bound by Size.Empty and - /// . + /// . /// /// /// If the view has not been initialized ( is ), the area to be diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/ViewKeyboard.cs index 687491997..7863c6799 100644 --- a/Terminal.Gui/View/ViewKeyboard.cs +++ b/Terminal.Gui/View/ViewKeyboard.cs @@ -4,8 +4,15 @@ namespace Terminal.Gui; public partial class View { - private void AddCommands () + /// + /// Helper to configure all things keyboard related for a View. Called from the View constructor. + /// + private void SetupKeyboard () { + KeyBindings = new (this); + HotKeySpecifier = (Rune)'_'; + TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged; + // By default, the HotKey command sets the focus AddCommand (Command.HotKey, OnHotKey); @@ -13,6 +20,15 @@ public partial class View AddCommand (Command.Accept, OnAccept); } + /// + /// Helper to dispose all things keyboard related for a View. Called from the View Dispose method. + /// + private void DisposeKeyboard () + { + TitleTextFormatter.HotKeyChanged -= TitleTextFormatter_HotKeyChanged; + KeyBindings.Clear (); + } + #region HotKey Support /// @@ -113,9 +129,10 @@ public partial class View /// /// The HotKey is replacing. Key bindings for this key will be removed. /// The new HotKey. If bindings will be removed. + /// Arbitrary context that can be associated with this key binding. /// if the HotKey bindings were added. /// - public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey) + public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, [CanBeNull] object context = null) { if (_hotKey == hotKey) { @@ -178,15 +195,16 @@ public partial class View // Add the new if (newKey != Key.Empty) { + KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context); // Add the base and Alt key - KeyBindings.Add (newKey, KeyBindingScope.HotKey, Command.HotKey); - KeyBindings.Add (newKey.WithAlt, KeyBindingScope.HotKey, Command.HotKey); + KeyBindings.Add (newKey, keyBinding); + KeyBindings.Add (newKey.WithAlt, keyBinding); // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask if (newKey.IsKeyCodeAtoZ) { - KeyBindings.Add (newKey.WithShift, KeyBindingScope.HotKey, Command.HotKey); - KeyBindings.Add (newKey.WithShift.WithAlt, KeyBindingScope.HotKey, Command.HotKey); + KeyBindings.Add (newKey.WithShift, keyBinding); + KeyBindings.Add (newKey.WithShift.WithAlt, keyBinding); } } @@ -601,9 +619,9 @@ public partial class View #region Key Bindings /// Gets the key bindings for this view. - public KeyBindings KeyBindings { get; } = new (); + public KeyBindings KeyBindings { get; internal set; } - private Dictionary> CommandImplementations { get; } = new (); + private Dictionary> CommandImplementations { get; } = new (); /// /// Low-level API called when a user presses a key; invokes any key bindings set on the view. This is called @@ -646,17 +664,17 @@ public partial class View return true; } - if (Margin is {} && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled)) + if (Margin is { } && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled)) { return true; } - if (Padding is {} && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled)) + if (Padding is { } && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled)) { return true; } - if (Border is {} && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled)) + if (Border is { } && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled)) { return true; } @@ -739,7 +757,7 @@ public partial class View } // each command has its own return value - bool? thisReturn = InvokeCommand (command); + bool? thisReturn = InvokeCommand (command, key, binding); // if we haven't got anything yet, the current command result should be used toReturn ??= thisReturn; @@ -758,12 +776,13 @@ public partial class View /// Invokes the specified commands. /// /// + /// The key that caused the commands to be invoked, if any. /// /// if no command was found. /// if the command was invoked and it handled the command. /// if the command was invoked and it did not handle the command. /// - public bool? InvokeCommands (Command [] commands) + public bool? InvokeCommands (Command [] commands, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null) { bool? toReturn = null; @@ -775,7 +794,7 @@ public partial class View } // each command has its own return value - bool? thisReturn = InvokeCommand (command); + bool? thisReturn = InvokeCommand (command, key, keyBinding); // if we haven't got anything yet, the current command result should be used toReturn ??= thisReturn; @@ -791,42 +810,68 @@ public partial class View } /// Invokes the specified command. - /// + /// The command to invoke. + /// The key that caused the command to be invoked, if any. + /// /// - /// if no command was found. if the command was invoked and it - /// handled the command. if the command was invoked and it did not handle the command. + /// if no command was found. if the command was invoked, and it + /// handled the command. if the command was invoked, and it did not handle the command. /// - public bool? InvokeCommand (Command command) + public bool? InvokeCommand (Command command, [CanBeNull] Key key = null, [CanBeNull] KeyBinding? keyBinding = null) { - if (!CommandImplementations.ContainsKey (command)) + if (CommandImplementations.TryGetValue (command, out Func implementation)) { - return null; + var context = new CommandContext (command, key, keyBinding); // Create the context here + return implementation (context); } - return CommandImplementations [command] (); + return null; } /// /// /// Sets the function that will be invoked for a . Views should call - /// for each command they support. + /// AddCommand for each command they support. /// /// - /// If has already been called for will + /// If AddCommand has already been called for will /// replace the old one. /// /// + /// + /// + /// This version of AddCommand is for commands that require . Use + /// in cases where the command does not require a . + /// + /// + /// The command. + /// The function. + protected void AddCommand (Command command, Func f) + { + CommandImplementations [command] = f; + } + + /// + /// + /// Sets the function that will be invoked for a . Views should call + /// AddCommand for each command they support. + /// + /// + /// If AddCommand has already been called for will + /// replace the old one. + /// + /// + /// + /// + /// This version of AddCommand is for commands that do not require a . + /// If the command requires context, use + /// + /// /// The command. /// The function. protected void AddCommand (Command command, Func f) { - // if there is already an implementation of this command - // replace that implementation - // else record how to perform the action (this should be the normal case) - if (CommandImplementations is { }) - { - CommandImplementations [command] = f; - } + CommandImplementations [command] = ctx => f (); ; } /// Returns all commands that are supported by this . diff --git a/Terminal.Gui/View/ViewMouse.cs b/Terminal.Gui/View/ViewMouse.cs index bf743efe7..f9352ab7d 100644 --- a/Terminal.Gui/View/ViewMouse.cs +++ b/Terminal.Gui/View/ViewMouse.cs @@ -447,6 +447,10 @@ public partial class View internal bool SetHighlight (HighlightStyle style) { // TODO: Make the highlight colors configurable + if (!CanFocus) + { + return false; + } // Enable override via virtual method and/or event if (OnHighlight (style) == true) diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs index 2ee3a51a0..12def9323 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -4,6 +4,15 @@ namespace Terminal.Gui; public partial class View { + /// + /// Initializes the Text of the View. Called by the constructor. + /// + private void SetupText () + { + Text = string.Empty; + TextDirection = TextDirection.LeftRight_TopBottom; + } + private string _text; /// @@ -37,11 +46,11 @@ public partial class View /// to and . /// /// - /// The text will word-wrap to additional lines if it does not fit horizontally. If 's height + /// The text will word-wrap to additional lines if it does not fit horizontally. If 's height /// is 1, the text will be clipped. /// /// If or are using , - /// the will be adjusted to fit the text. + /// the will be adjusted to fit the text. /// When the text changes, the is fired. /// public virtual string Text @@ -84,10 +93,10 @@ public partial class View /// redisplay the . /// /// - /// or are using , the will be adjusted to fit the text. + /// or are using , the will be adjusted to fit the text. /// /// The text alignment. - public virtual TextAlignment TextAlignment + public virtual Alignment TextAlignment { get => TextFormatter.Alignment; set @@ -103,9 +112,9 @@ public partial class View /// . /// /// - /// or are using , the will be adjusted to fit the text. + /// or are using , the will be adjusted to fit the text. /// - /// The text alignment. + /// The text direction. public virtual TextDirection TextDirection { get => TextFormatter.Direction; @@ -127,10 +136,10 @@ public partial class View /// the . /// /// - /// or are using , the will be adjusted to fit the text. + /// or are using , the will be adjusted to fit the text. /// - /// The text alignment. - public virtual VerticalTextAlignment VerticalTextAlignment + /// The vertical text alignment. + public virtual Alignment VerticalTextAlignment { get => TextFormatter.VerticalAlignment; set @@ -179,8 +188,8 @@ public partial class View // We need to ensure TextFormatter is accurate by calling it here. UpdateTextFormatterText (); - // Default is to use ContentSize. - var size = ContentSize; + // Default is to use GetContentSize (). + var size = GetContentSize (); // TODO: This is a hack. Figure out how to move this into DimDimAuto // Use _width & _height instead of Width & Height to avoid debug spew @@ -193,12 +202,12 @@ public partial class View if (widthAuto is null || !widthAuto.Style.FastHasFlags (DimAutoStyle.Text)) { - size.Width = ContentSize.Width; + size.Width = GetContentSize ().Width; } if (heightAuto is null || !heightAuto.Style.FastHasFlags (DimAutoStyle.Text)) { - size.Height = ContentSize.Height; + size.Height = GetContentSize ().Height; } } diff --git a/Terminal.Gui/View/ViewportSettings.cs b/Terminal.Gui/View/ViewportSettings.cs index 443d1b0ca..f0e2af75e 100644 --- a/Terminal.Gui/View/ViewportSettings.cs +++ b/Terminal.Gui/View/ViewportSettings.cs @@ -47,13 +47,13 @@ public enum ViewportSettings AllowNegativeLocation = AllowNegativeX | AllowNegativeY, /// - /// If set, .X can be set values greater than + /// If set, .X can be set values greater than /// .Width enabling scrolling beyond the right /// of the content area. /// /// /// - /// When not set, .X is constrained to + /// When not set, .X is constrained to /// .Width - 1. /// This means the last column of the content will remain visible even if there is an attempt to scroll the /// Viewport past the last column. @@ -65,13 +65,13 @@ public enum ViewportSettings AllowXGreaterThanContentWidth = 4, /// - /// If set, .Y can be set values greater than + /// If set, .Y can be set values greater than /// .Height enabling scrolling beyond the right /// of the content area. /// /// /// - /// When not set, .Y is constrained to + /// When not set, .Y is constrained to /// .Height - 1. /// This means the last row of the content will remain visible even if there is an attempt to scroll the Viewport /// past the last row. @@ -83,13 +83,13 @@ public enum ViewportSettings AllowYGreaterThanContentHeight = 8, /// - /// If set, .Size can be set values greater than + /// If set, .Size can be set values greater than /// enabling scrolling beyond the bottom-right /// of the content area. /// /// /// - /// When not set, is constrained to -1. + /// When not set, is constrained to -1. /// This means the last column and row of the content will remain visible even if there is an attempt to /// scroll the Viewport past the last column or row. /// diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 52c72ab5f..2d922a79f 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -33,12 +33,11 @@ public class Button : View private readonly Rune _rightDefault; private bool _isDefault; - /// Initializes a new instance of using layout. - /// The width of the is computed based on the text length. The height will always be 1. + /// Initializes a new instance of . public Button () { - TextAlignment = TextAlignment.Centered; - VerticalTextAlignment = VerticalTextAlignment.Middle; + TextAlignment = Alignment.Center; + VerticalTextAlignment = Alignment.Center; _leftBracket = Glyphs.LeftBracket; _rightBracket = Glyphs.RightBracket; diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 5971c02ed..471ab5203 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -11,8 +11,7 @@ public class CheckBox : View private bool? _checked = false; /// - /// Initializes a new instance of based on the given text, using - /// layout. + /// Initializes a new instance of . /// public CheckBox () { @@ -155,13 +154,13 @@ public class CheckBox : View { switch (TextAlignment) { - case TextAlignment.Left: - case TextAlignment.Centered: - case TextAlignment.Justified: + case Alignment.Start: + case Alignment.Center: + case Alignment.Fill: TextFormatter.Text = $"{GetCheckedState ()} {Text}"; break; - case TextAlignment.Right: + case Alignment.End: TextFormatter.Text = $"{Text} {GetCheckedState ()}"; break; diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index c61fdc5f3..addf7a1a4 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -39,7 +39,7 @@ public class ColorPicker : View Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); - SetContentSize(new (_boxWidth * _cols, _boxHeight * _rows)); + SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); MouseClick += ColorPicker_MouseClick; } @@ -178,9 +178,9 @@ public class ColorPicker : View Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ()); var colorIndex = 0; - for (var y = 0; y < Math.Max(2, viewport.Height / BoxHeight); y++) + for (var y = 0; y < Math.Max (2, viewport.Height / BoxHeight); y++) { - for (var x = 0; x < Math.Max(8, viewport.Width / BoxWidth); x++) + for (var x = 0; x < Math.Max (8, viewport.Width / BoxWidth); x++) { int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols; Driver.SetAttribute (new Attribute ((ColorName)foregroundColorIndex, (ColorName)colorIndex)); diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 3eb630e64..36af3853e 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -5,7 +5,7 @@ // Ross Ferguson (ross.c.ferguson@btinternet.com) // -using System.Collections; +using System.Collections.ObjectModel; using System.ComponentModel; namespace Terminal.Gui; @@ -16,7 +16,7 @@ public class ComboBox : View private readonly ComboListView _listview; private readonly int _minimumHeight = 2; private readonly TextField _search; - private readonly IList _searchset = new List (); + private readonly ObservableCollection _searchSet = []; private bool _autoHide = true; private bool _hideDropdownListOnClick; private int _lastSelectedItem = -1; @@ -47,9 +47,9 @@ public class ComboBox : View _listview.SelectedItemChanged += (sender, e) => { - if (!HideDropdownListOnClick && _searchset.Count > 0) + if (!HideDropdownListOnClick && _searchSet.Count > 0) { - SetValue (_searchset [_listview.SelectedItem]); + SetValue (_searchSet [_listview.SelectedItem]); } }; @@ -174,7 +174,7 @@ public class ComboBox : View /// Gets or sets the backing this , enabling custom rendering. /// The source. - /// Use to set a new source. + /// Use to set a new source. public IListDataSource Source { get => _source; @@ -366,13 +366,13 @@ public class ComboBox : View /// This event is raised when the selected item in the has changed. public event EventHandler SelectedItemChanged; - /// Sets the source of the to an . - /// An object implementing the IList interface. + /// Sets the source of the to an . + /// An object implementing the INotifyCollectionChanged and INotifyPropertyChanged interface. /// - /// Use the property to set a new source and use custome + /// Use the property to set a new source and use custom /// rendering. /// - public void SetSource (IList source) + public void SetSource (ObservableCollection source) { if (source is null) { @@ -380,7 +380,7 @@ public class ComboBox : View } else { - _listview.SetSource (source); + _listview.SetSource (source); Source = _listview.Source; } } @@ -408,7 +408,7 @@ public class ComboBox : View return Math.Min ( Math.Max (Viewport.Height - 1, _minimumHeight - 1), - _searchset?.Count > 0 ? _searchset.Count : + _searchSet?.Count > 0 ? _searchSet.Count : IsShow ? Math.Max (Viewport.Height - 1, _minimumHeight - 1) : 0 ); } @@ -468,9 +468,9 @@ public class ComboBox : View return -1; } - for (var i = 0; i < _searchset.Count; i++) + for (var i = 0; i < _searchSet.Count; i++) { - if (_searchset [i].ToString () == searchText) + if (_searchSet [i].ToString () == searchText) { return i; } @@ -504,14 +504,14 @@ public class ComboBox : View if (_search.HasFocus) { // jump to list - if (_searchset?.Count > 0) + if (_searchSet?.Count > 0) { _listview.TabStop = true; _listview.SetFocus (); if (_listview.SelectedItem > -1) { - SetValue (_searchset [_listview.SelectedItem]); + SetValue (_searchSet [_listview.SelectedItem]); } else { @@ -572,7 +572,7 @@ public class ComboBox : View private bool? MoveUpList () { - if (_listview.HasFocus && _listview.SelectedItem == 0 && _searchset?.Count > 0) // jump back to search + if (_listview.HasFocus && _listview.SelectedItem == 0 && _searchSet?.Count > 0) // jump back to search { _search.CursorPosition = _search.Text.GetRuneCount (); _search.SetFocus (); @@ -619,8 +619,8 @@ public class ComboBox : View { _search.Width = _listview.Width = _autoHide ? Viewport.Width - 1 : Viewport.Width; _listview.Height = CalculatetHeight (); - _search.SetRelativeLayout (ContentSize); - _listview.SetRelativeLayout (ContentSize); + _search.SetRelativeLayout (GetContentSize ()); + _listview.SetRelativeLayout (GetContentSize ()); } } @@ -634,7 +634,7 @@ public class ComboBox : View ResetSearchSet (); - _listview.SetSource (_searchset); + _listview.SetSource (_searchSet); _listview.Height = CalculatetHeight (); if (Subviews.Count > 0 && HasFocus) @@ -645,7 +645,7 @@ public class ComboBox : View private void ResetSearchSet (bool noCopy = false) { - _searchset.Clear (); + _searchSet.Clear (); if (_autoHide || noCopy) { @@ -680,16 +680,19 @@ public class ComboBox : View IsShow = true; ResetSearchSet (true); - foreach (object item in _source.ToList ()) + if (!string.IsNullOrEmpty (_search.Text)) { - // Iterate to preserver object type and force deep copy - if (item.ToString () - .StartsWith ( - _search.Text, - StringComparison.CurrentCultureIgnoreCase - )) + foreach (object item in _source.ToList ()) { - _searchset.Add (item); + // Iterate to preserver object type and force deep copy + if (item.ToString () + .StartsWith ( + _search.Text, + StringComparison.CurrentCultureIgnoreCase + )) + { + _searchSet.Add (item); + } } } } @@ -710,7 +713,7 @@ public class ComboBox : View IsShow = false; _listview.TabStop = false; - if (_listview.Source.Count == 0 || (_searchset?.Count ?? 0) == 0) + if (_listview.Source.Count == 0 || (_searchSet?.Count ?? 0) == 0) { _text = ""; HideList (); @@ -719,7 +722,7 @@ public class ComboBox : View return; } - SetValue (_listview.SelectedItem > -1 ? _searchset [_listview.SelectedItem] : _text); + SetValue (_listview.SelectedItem > -1 ? _searchSet [_listview.SelectedItem] : _text); _search.CursorPosition = _search.Text.GetColumns (); Search_Changed (this, new StateEventArgs (_search.Text, _search.Text)); OnOpenSelectedItem (); @@ -738,7 +741,7 @@ public class ComboBox : View // force deep copy foreach (object item in Source.ToList ()) { - _searchset.Add (item); + _searchSet.Add (item); } } @@ -762,7 +765,7 @@ public class ComboBox : View /// Consider making public private void ShowList () { - _listview.SetSource (_searchset); + _listview.SetSource (_searchSet); _listview.Clear (); _listview.Height = CalculatetHeight (); SuperView?.BringSubviewToFront (this); @@ -784,9 +787,9 @@ public class ComboBox : View private bool _isFocusing; public ComboListView (ComboBox container, bool hideDropdownListOnClick) { SetInitialProperties (container, hideDropdownListOnClick); } - public ComboListView (ComboBox container, IList source, bool hideDropdownListOnClick) + public ComboListView (ComboBox container, ObservableCollection source, bool hideDropdownListOnClick) { - Source = new ListWrapper (source); + Source = new ListWrapper (source); SetInitialProperties (container, hideDropdownListOnClick); } diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs index 5640e5b87..7a4797fb6 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -21,10 +21,10 @@ public class DateField : TextField private string _format; private string _separator; - /// Initializes a new instance of using layout. + /// Initializes a new instance of . public DateField () : this (DateTime.MinValue) { } - /// Initializes a new instance of using layout. + /// Initializes a new instance of . /// public DateField (DateTime date) { diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index 613051fbe..d0cb41330 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -215,7 +215,6 @@ public class DatePicker : View { X = Pos.Center () - 2, Y = Pos.Bottom (_calendar) - 1, - Height = 1, Width = 2, Text = GetBackButtonText (), WantContinuousButtonPressed = true, @@ -234,7 +233,6 @@ public class DatePicker : View { X = Pos.Right (_previousMonthButton) + 2, Y = Pos.Bottom (_calendar) - 1, - Height = 1, Width = 2, Text = GetForwardButtonText (), WantContinuousButtonPressed = true, @@ -273,8 +271,8 @@ public class DatePicker : View Text = _date.ToString (Format); }; - Height = Dim.Auto (); - Width = Dim.Auto (); + Width = Dim.Auto (DimAutoStyle.Content); + Height = Dim.Auto (DimAutoStyle.Content); // BUGBUG: Remove when Dim.Auto(subviews) fully works SetContentSize (new (_calendar.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 7, _calendar.Frame.Height + 1)); diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 9a964d86c..96a55f229 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -9,27 +9,38 @@ namespace Terminal.Gui; /// /// /// To run the modally, create the , and pass it to -/// . This will execute the dialog until it terminates via the +/// . This will execute the dialog until +/// it terminates via the /// [ESC] or [CTRL-Q] key, or when one of the views or buttons added to the dialog calls /// . /// public class Dialog : Window { - /// Determines the horizontal alignment of the Dialog buttons. - public enum ButtonAlignments - { - /// Center-aligns the buttons (the default). - Center = 0, + /// The default for . + /// This property can be set in a Theme. + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + [JsonConverter (typeof (JsonStringEnumConverter))] + public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; - /// Justifies the buttons - Justify, + /// The default for . + /// This property can be set in a Theme. + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + [JsonConverter (typeof (JsonStringEnumConverter))] + public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; - /// Left-aligns the buttons - Left, + /// + /// Defines the default minimum Dialog width, as a percentage of the container width. Can be configured via + /// . + /// + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + public static int DefaultMinimumWidth { get; set; } = 25; - /// Right-aligns the buttons - Right - } + /// + /// Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via + /// . + /// + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + public static int DefaultMinimumHeight { get; set; } = 25; // TODO: Reenable once border/borderframe design is settled /// @@ -44,42 +55,37 @@ public class Dialog : Window private readonly List