diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index f64285f0f..fdf024307 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -106,6 +106,27 @@ public class GlyphDefinitions /// Identical To (U+226) public Rune IdenticalTo { get; set; } = (Rune)'≡'; + /// Move indicator. Default is Lozenge (U+25CA) - ◊. + public Rune Move { get; set; } = (Rune)'◊'; + + /// Size Horizontally indicator. Default is ┥Left Right Arrow - ↔ U+02194 + public Rune SizeHorizontal { get; set; } = (Rune)'↔'; + + /// Size Vertical indicator. Default Up Down Arrow - ↕ U+02195 + public Rune SizeVertical { get; set; } = (Rune)'↕'; + + /// Size Top Left indicator. North West Arrow - ↖ U+02196 + public Rune SizeTopLeft { get; set; } = (Rune)'↖'; + + /// Size Top Right indicator. North East Arrow - ↗ U+02197 + public Rune SizeTopRight { get; set; } = (Rune)'↗'; + + /// Size Bottom Right indicator. South East Arrow - ↘ U+02198 + public Rune SizeBottomRight { get; set; } = (Rune)'↘'; + + /// Size Bottom Left indicator. South West Arrow - ↙ U+02199 + public Rune SizeBottomLLeft { get; set; } = (Rune)'↙'; + /// Apple (non-BMP). Because snek. And because it's an example of a non-BMP surrogate pair. See Issue #2610. public Rune Apple { get; set; } = "🍎".ToRunes () [0]; // nonBMP diff --git a/Terminal.Gui/View/Adornment/Adornment.cs b/Terminal.Gui/View/Adornment/Adornment.cs index 6c575ad99..791d2e77f 100644 --- a/Terminal.Gui/View/Adornment/Adornment.cs +++ b/Terminal.Gui/View/Adornment/Adornment.cs @@ -26,7 +26,8 @@ public class Adornment : View /// public Adornment (View parent) { - CanFocus = true; + // By default Adornments can't get focus; has to be enabled specifically. + CanFocus = false; Parent = parent; } diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index de7390c9f..838760417 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -1,7 +1,5 @@ #nullable enable using System.Diagnostics; -using Microsoft.CodeAnalysis; -using static Terminal.Gui.SpinnerStyle; namespace Terminal.Gui; @@ -68,7 +66,6 @@ public class Border : Adornment Highlight += Border_Highlight; } - #if SUBVIEW_BASED_BORDER private Line _left; @@ -78,7 +75,6 @@ public class Border : Adornment public Button CloseButton { get; internal set; } #endif - /// public override void BeginInit () { @@ -117,11 +113,8 @@ public class Border : Adornment LayoutStarted += OnLayoutStarted; } #endif - } - - #if SUBVIEW_BASED_BORDER private void OnLayoutStarted (object sender, LayoutEventArgs e) { @@ -164,6 +157,7 @@ public class Border : Adornment internal Rectangle GetBorderRectangle () { Rectangle screenRect = ViewportToScreen (Viewport); + return new ( screenRect.X + Math.Max (0, Thickness.Left - 1), screenRect.Y + Math.Max (0, Thickness.Top - 1), @@ -312,6 +306,11 @@ public class Border : Adornment // Adornment.Contains takes Parent SuperView=relative coords. if (Contains (new (mouseEvent.Position.X + Parent.Frame.X + Frame.X, mouseEvent.Position.Y + Parent.Frame.Y + Frame.Y))) { + if (_arranging != ViewArrangement.Fixed) + { + EndArrangeMode (); + } + // Set the start grab point to the Frame coords _startGrabPoint = new (mouseEvent.Position.X + Frame.X, mouseEvent.Position.Y + Frame.Y); _dragPosition = mouseEvent.Position; @@ -319,23 +318,19 @@ public class Border : Adornment SetHighlight (HighlightStyle); + // If not resizable, but movable: Drag anywhere is move // If resizable and movable: Drag on top is move, other 3 sides are size // If not movable, but resizable: Drag on any side sizes. // Get rectangle representing Thickness.Top // If mouse is in that rectangle, set _arranging to ViewArrangement.Movable - Rectangle sideRect = new (Frame.X + Thickness.Left, Frame.Y, Frame.Width - Thickness.Left - Thickness.Right, Thickness.Top); - if (Parent!.Arrangement.HasFlag (ViewArrangement.Movable) && sideRect.Contains (_startGrabPoint)) - { - EnterArrangeMode (ViewArrangement.Movable); - - return true; - } + Rectangle sideRect; // If mouse is in any other rectangle, set _arranging to ViewArrangement. if (Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable)) { sideRect = new (Frame.X, Frame.Y + Thickness.Top, Thickness.Left, Frame.Height - Thickness.Top - Thickness.Bottom); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.LeftResizable); @@ -346,7 +341,12 @@ public class Border : Adornment if (Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable)) { - sideRect = new (Frame.X + Frame.Width - Thickness.Right, Frame.Y + Thickness.Top, Thickness.Right, Frame.Height - Thickness.Top - Thickness.Bottom); + sideRect = new ( + Frame.X + Frame.Width - Thickness.Right, + Frame.Y + Thickness.Top, + Thickness.Right, + Frame.Height - Thickness.Top - Thickness.Bottom); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.RightResizable); @@ -355,9 +355,10 @@ public class Border : Adornment } } - if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable)) + if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable) && !Parent!.Arrangement.HasFlag (ViewArrangement.Movable)) { sideRect = new (Frame.X + Thickness.Left, Frame.Y, Frame.Width - Thickness.Left - Thickness.Right, Thickness.Top); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.TopResizable); @@ -368,7 +369,12 @@ public class Border : Adornment if (Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable)) { - sideRect = new (Frame.X + Thickness.Left, Frame.Y + Frame.Height - Thickness.Bottom, Frame.Width - Thickness.Left - Thickness.Right, Thickness.Bottom); + sideRect = new ( + Frame.X + Thickness.Left, + Frame.Y + Frame.Height - Thickness.Bottom, + Frame.Width - Thickness.Left - Thickness.Right, + Thickness.Bottom); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.BottomResizable); @@ -377,10 +383,10 @@ public class Border : Adornment } } - if (Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable)) { sideRect = new (Frame.X, Frame.Height - Thickness.Top, Thickness.Left, Thickness.Bottom); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.BottomResizable | ViewArrangement.LeftResizable); @@ -392,6 +398,7 @@ public class Border : Adornment if (Parent!.Arrangement.HasFlag (ViewArrangement.BottomResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable)) { sideRect = new (Frame.X + Frame.Width - Thickness.Right, Frame.Height - Thickness.Top, Thickness.Right, Thickness.Bottom); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.BottomResizable | ViewArrangement.RightResizable); @@ -403,6 +410,7 @@ public class Border : Adornment if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable)) { sideRect = new (Frame.X + Frame.Width - Thickness.Right, Frame.Y, Thickness.Right, Thickness.Top); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.TopResizable | ViewArrangement.RightResizable); @@ -414,6 +422,7 @@ public class Border : Adornment if (Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable) && Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable)) { sideRect = new (Frame.X, Frame.Y, Thickness.Left, Thickness.Top); + if (sideRect.Contains (_startGrabPoint)) { EnterArrangeMode (ViewArrangement.TopResizable | ViewArrangement.LeftResizable); @@ -422,7 +431,17 @@ public class Border : Adornment } } + if (Parent!.Arrangement.HasFlag (ViewArrangement.Movable)) + { + //sideRect = new (Frame.X + Thickness.Left, Frame.Y, Frame.Width - Thickness.Left - Thickness.Right, Thickness.Top); + //if (sideRect.Contains (_startGrabPoint)) + { + EnterArrangeMode (ViewArrangement.Movable); + + return true; + } + } } return true; @@ -449,11 +468,11 @@ public class Border : Adornment int minHeight = Thickness.Vertical + Parent!.Margin.Thickness.Bottom; int minWidth = Thickness.Horizontal + Parent!.Margin.Thickness.Right; + switch (_arranging) { case ViewArrangement.Movable: - GetLocationEnsuringFullVisibility ( Parent, parentLoc.X - _startGrabPoint.X, @@ -484,6 +503,7 @@ public class Border : Adornment case ViewArrangement.BottomResizable: Parent.Height = Math.Max (minHeight, parentLoc.Y - Parent.Frame.Y + Parent!.Margin.Thickness.Bottom + 1); + break; case ViewArrangement.LeftResizable: @@ -497,6 +517,7 @@ public class Border : Adornment Parent.Width = newWidth; Parent.X = parentLoc.X - _startGrabPoint.X; } + break; case ViewArrangement.RightResizable: @@ -521,6 +542,7 @@ public class Border : Adornment } Parent.Height = Math.Max (minHeight, parentLoc.Y - Parent.Frame.Y + Parent!.Margin.Thickness.Bottom + 1); + break; case ViewArrangement.TopResizable | ViewArrangement.RightResizable: @@ -534,6 +556,7 @@ public class Border : Adornment } Parent.Width = Math.Max (minWidth, parentLoc.X - Parent.Frame.X + Parent!.Margin.Thickness.Right + 1); + break; case ViewArrangement.TopResizable | ViewArrangement.LeftResizable: @@ -554,11 +577,10 @@ public class Border : Adornment Parent.Width = newW2; Parent.X = parentLoc.X - _startGrabPoint.X; } + break; - } - return true; } } @@ -569,7 +591,7 @@ public class Border : Adornment Application.UngrabMouse (); SetHighlight (HighlightStyle.None); - EndArrange (); + EndArrangeMode (); return true; } @@ -713,12 +735,12 @@ public class Border : Adornment { // ╔╡╞╗ should be ╔══╗ lc?.AddLine ( - new (borderBounds.Location.X, titleY), - borderBounds.Width, - Orientation.Horizontal, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.Location.X, titleY), + borderBounds.Width, + Orientation.Horizontal, + lineStyle, + Driver.GetAttribute () + ); } else { @@ -728,12 +750,12 @@ public class Border : Adornment if (Thickness.Top == 2) { lc?.AddLine ( - new (borderBounds.X + 1, topTitleLineY), - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), - Orientation.Horizontal, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.X + 1, topTitleLineY), + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), + Orientation.Horizontal, + lineStyle, + Driver.GetAttribute () + ); } // ┌────┐ @@ -742,70 +764,70 @@ public class Border : Adornment if (borderBounds.Width >= 4 && Thickness.Top > 2) { lc?.AddLine ( - new (borderBounds.X + 1, topTitleLineY), - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), - Orientation.Horizontal, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.X + 1, topTitleLineY), + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), + Orientation.Horizontal, + lineStyle, + Driver.GetAttribute () + ); lc?.AddLine ( - new (borderBounds.X + 1, topTitleLineY + 2), - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), - Orientation.Horizontal, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.X + 1, topTitleLineY + 2), + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), + Orientation.Horizontal, + lineStyle, + Driver.GetAttribute () + ); } // ╔╡Title╞═════╗ // Add a short horiz line for ╔╡ lc?.AddLine ( - new (borderBounds.Location.X, titleY), - 2, - Orientation.Horizontal, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.Location.X, titleY), + 2, + Orientation.Horizontal, + lineStyle, + Driver.GetAttribute () + ); // Add a vert line for ╔╡ lc?.AddLine ( - new (borderBounds.X + 1, topTitleLineY), - titleBarsLength, - Orientation.Vertical, - LineStyle.Single, - Driver.GetAttribute () - ); + new (borderBounds.X + 1, topTitleLineY), + titleBarsLength, + Orientation.Vertical, + LineStyle.Single, + Driver.GetAttribute () + ); // Add a vert line for ╞ lc?.AddLine ( - new ( - borderBounds.X - + 1 - + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - - 1, - topTitleLineY - ), - titleBarsLength, - Orientation.Vertical, - LineStyle.Single, - Driver.GetAttribute () - ); + new ( + borderBounds.X + + 1 + + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) + - 1, + topTitleLineY + ), + titleBarsLength, + Orientation.Vertical, + LineStyle.Single, + Driver.GetAttribute () + ); // Add the right hand line for ╞═════╗ lc?.AddLine ( - new ( - borderBounds.X - + 1 - + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - - 1, - titleY - ), - borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), - Orientation.Horizontal, - lineStyle, - Driver.GetAttribute () - ); + new ( + borderBounds.X + + 1 + + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) + - 1, + titleY + ), + borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), + Orientation.Horizontal, + lineStyle, + Driver.GetAttribute () + ); } } @@ -814,35 +836,35 @@ public class Border : Adornment if (drawLeft) { lc?.AddLine ( - new (borderBounds.Location.X, titleY), - sideLineLength, - Orientation.Vertical, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.Location.X, titleY), + sideLineLength, + Orientation.Vertical, + lineStyle, + Driver.GetAttribute () + ); } #endif if (drawBottom) { lc?.AddLine ( - new (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), - borderBounds.Width, - Orientation.Horizontal, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), + borderBounds.Width, + Orientation.Horizontal, + lineStyle, + Driver.GetAttribute () + ); } if (drawRight) { lc?.AddLine ( - new (borderBounds.X + borderBounds.Width - 1, titleY), - sideLineLength, - Orientation.Vertical, - lineStyle, - Driver.GetAttribute () - ); + new (borderBounds.X + borderBounds.Width - 1, titleY), + sideLineLength, + Orientation.Vertical, + lineStyle, + Driver.GetAttribute () + ); } Driver.SetAttribute (prevAttr); @@ -862,9 +884,9 @@ public class Border : Adornment if (drawTop && maxTitleWidth > 0 && Settings.FastHasFlags (BorderSettings.Title)) { Parent!.TitleTextFormatter.Draw ( - new (borderBounds.X + 2, titleY, maxTitleWidth, 1), - Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor (), - Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor ()); + new (borderBounds.X + 2, titleY, maxTitleWidth, 1), + Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor (), + Parent.HasFocus ? Parent.GetFocusColor () : Parent.GetNormalColor ()); } //Left @@ -943,7 +965,8 @@ public class Border : Adornment /// or keyboard. If is keyboard mode is enabled. /// /// - /// Arrange Mode is exited by the user pressing , , or by clicking + /// Arrange Mode is exited by the user pressing , , or by + /// clicking /// the mouse out of the 's Frame. /// /// @@ -956,7 +979,7 @@ public class Border : Adornment && !Parent!.Arrangement.HasFlag (ViewArrangement.TopResizable) && !Parent!.Arrangement.HasFlag (ViewArrangement.LeftResizable) && !Parent!.Arrangement.HasFlag (ViewArrangement.RightResizable) - ) + ) { return false; } @@ -971,7 +994,7 @@ public class Border : Adornment { Debug.Assert (_moveButton is null); - _moveButton = new Button + _moveButton = new () { Id = "moveButton", CanFocus = true, @@ -980,8 +1003,9 @@ public class Border : Adornment NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None, - Text = $"{Glyphs.Diamond}", + Text = $"{Glyphs.Move}", Visible = false, + Data = ViewArrangement.Movable }; Add (_moveButton); } @@ -990,7 +1014,7 @@ public class Border : Adornment { Debug.Assert (_allSizeButton is null); - _allSizeButton = new Button + _allSizeButton = new () { Id = "allSizeButton", CanFocus = true, @@ -999,10 +1023,11 @@ public class Border : Adornment NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None, - Text = $"{Glyphs.Diamond}", + Text = $"{Glyphs.SizeBottomRight}", X = Pos.AnchorEnd (), Y = Pos.AnchorEnd (), Visible = false, + Data = ViewArrangement.Resizable }; Add (_allSizeButton); } @@ -1011,7 +1036,7 @@ public class Border : Adornment { Debug.Assert (_topSizeButton is null); - _topSizeButton = new Button + _topSizeButton = new () { Id = "topSizeButton", CanFocus = true, @@ -1020,10 +1045,11 @@ public class Border : Adornment NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None, - Text = $"{Glyphs.Diamond}", + Text = $"{Glyphs.SizeVertical}", X = Pos.Center () + Parent!.Margin.Thickness.Horizontal, Y = 0, Visible = false, + Data = ViewArrangement.TopResizable }; Add (_topSizeButton); } @@ -1032,7 +1058,7 @@ public class Border : Adornment { Debug.Assert (_rightSizeButton is null); - _rightSizeButton = new Button + _rightSizeButton = new () { Id = "rightSizeButton", CanFocus = true, @@ -1041,10 +1067,11 @@ public class Border : Adornment NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None, - Text = $"{Glyphs.Diamond}", + Text = $"{Glyphs.SizeHorizontal}", X = Pos.AnchorEnd (), - Y = Pos.Center () + Parent!.Margin.Thickness.Vertical, + Y = Pos.Center () + Parent!.Margin.Thickness.Vertical / 2, Visible = false, + Data = ViewArrangement.RightResizable }; Add (_rightSizeButton); } @@ -1053,7 +1080,7 @@ public class Border : Adornment { Debug.Assert (_leftSizeButton is null); - _leftSizeButton = new Button + _leftSizeButton = new () { Id = "leftSizeButton", CanFocus = true, @@ -1062,10 +1089,11 @@ public class Border : Adornment NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None, - Text = $"{Glyphs.Diamond}", + Text = $"{Glyphs.SizeHorizontal}", X = 0, - Y = Pos.Center () + Parent!.Margin.Thickness.Vertical, + Y = Pos.Center () + Parent!.Margin.Thickness.Vertical / 2, Visible = false, + Data = ViewArrangement.LeftResizable }; Add (_leftSizeButton); } @@ -1074,7 +1102,7 @@ public class Border : Adornment { Debug.Assert (_bottomSizeButton is null); - _bottomSizeButton = new Button + _bottomSizeButton = new () { Id = "bottomSizeButton", CanFocus = true, @@ -1083,29 +1111,31 @@ public class Border : Adornment NoDecorations = true, NoPadding = true, ShadowStyle = ShadowStyle.None, - Text = $"{Glyphs.Diamond}", - X = Pos.Center () + Parent!.Margin.Thickness.Horizontal, + Text = $"{Glyphs.SizeVertical}", + X = Pos.Center () + Parent!.Margin.Thickness.Horizontal / 2, Y = Pos.AnchorEnd (), Visible = false, + Data = ViewArrangement.BottomResizable }; Add (_bottomSizeButton); } - if (arrangement == ViewArrangement.Fixed) { // Keyboard mode if (Parent!.Arrangement.HasFlag (ViewArrangement.Movable)) { - _arranging = ViewArrangement.Movable; _moveButton!.Visible = true; } if (Parent!.Arrangement.HasFlag (ViewArrangement.Resizable)) { - _arranging = ViewArrangement.Resizable; _allSizeButton!.Visible = true; } + + _arranging = ViewArrangement.Movable; + CanFocus = true; + SetFocus (); } else { @@ -1116,55 +1146,74 @@ public class Border : Adornment { case ViewArrangement.Movable: _moveButton!.Visible = true; + break; case ViewArrangement.RightResizable | ViewArrangement.BottomResizable: case ViewArrangement.Resizable: _rightSizeButton!.Visible = true; _bottomSizeButton!.Visible = true; - _allSizeButton!.X = Pos.AnchorEnd (); - _allSizeButton!.Y = Pos.AnchorEnd (); - _allSizeButton!.Visible = true; + + if (_allSizeButton is { }) + { + _allSizeButton!.X = Pos.AnchorEnd (); + _allSizeButton!.Y = Pos.AnchorEnd (); + _allSizeButton!.Visible = true; + } + break; case ViewArrangement.LeftResizable: _leftSizeButton!.Visible = true; + break; case ViewArrangement.RightResizable: _rightSizeButton!.Visible = true; + break; case ViewArrangement.TopResizable: _topSizeButton!.Visible = true; + break; case ViewArrangement.BottomResizable: _bottomSizeButton!.Visible = true; + break; case ViewArrangement.LeftResizable | ViewArrangement.BottomResizable: _rightSizeButton!.Visible = true; _bottomSizeButton!.Visible = true; - _allSizeButton!.X = 0; - _allSizeButton!.Y = Pos.AnchorEnd (); - _allSizeButton!.Visible = true; + + if (_allSizeButton is { }) + { + _allSizeButton.X = 0; + _allSizeButton.Y = Pos.AnchorEnd (); + _allSizeButton.Visible = true; + } + break; case ViewArrangement.LeftResizable | ViewArrangement.TopResizable: _leftSizeButton!.Visible = true; _topSizeButton!.Visible = true; + break; case ViewArrangement.RightResizable | ViewArrangement.TopResizable: _rightSizeButton!.Visible = true; _topSizeButton!.Visible = true; - _allSizeButton!.X = Pos.AnchorEnd (); - _allSizeButton!.Y = 0; - _allSizeButton!.Visible = true; + + if (_allSizeButton is { }) + { + _allSizeButton.X = Pos.AnchorEnd (); + _allSizeButton.Y = 0; + _allSizeButton.Visible = true; + } break; - } } @@ -1173,22 +1222,24 @@ public class Border : Adornment if (arrangement == ViewArrangement.Fixed) { // Keyboard mode - enable nav - CanFocus = true; - SetFocus (); + } + return true; } // Hack for now - EndArrange (); + EndArrangeMode (); + return false; } private void AddArrangeModeKeyBindings () { - AddCommand (Command.Quit, EndArrange); + AddCommand (Command.Quit, EndArrangeMode); - AddCommand (Command.Up, + AddCommand ( + Command.Up, () => { if (Parent is null) @@ -1212,7 +1263,8 @@ public class Border : Adornment return true; }); - AddCommand (Command.Down, + AddCommand ( + Command.Down, () => { if (Parent is null) @@ -1232,7 +1284,9 @@ public class Border : Adornment return true; }); - AddCommand (Command.Left, + + AddCommand ( + Command.Left, () => { if (Parent is null) @@ -1252,10 +1306,12 @@ public class Border : Adornment Parent!.Width = Parent.Width! - 1; } } + return true; }); - AddCommand (Command.Right, + AddCommand ( + Command.Right, () => { if (Parent is null) @@ -1276,18 +1332,25 @@ public class Border : Adornment return true; }); - AddCommand (Command.Tab, () => - { - AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + AddCommand ( + Command.Tab, + () => + { + AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + _arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed); - return true; // Always eat - }); - AddCommand (Command.BackTab, () => - { - AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop); + return true; // Always eat + }); - return true; // Always eat - }); + AddCommand ( + Command.BackTab, + () => + { + AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop); + _arranging = (ViewArrangement)(Focused?.Data ?? ViewArrangement.Fixed); + + return true; // Always eat + }); KeyBindings.Add (Key.Esc, KeyBindingScope.HotKey, Command.Quit); KeyBindings.Add (Application.ArrangeKey, KeyBindingScope.HotKey, Command.Quit); @@ -1313,11 +1376,11 @@ public class Border : Adornment if (!Thickness.Contains (Frame, framePos)) { - EndArrange (); + EndArrangeMode (); } } - private bool? EndArrange () + private bool? EndArrangeMode () { // Debug.Assert (_arranging != ViewArrangement.Fixed); _arranging = ViewArrangement.Fixed; @@ -1390,5 +1453,4 @@ public class Border : Adornment _dragPosition = null; base.Dispose (disposing); } - } diff --git a/Terminal.Gui/View/View.Hierarchy.cs b/Terminal.Gui/View/View.Hierarchy.cs index 4ef00a5b7..24215eb44 100644 --- a/Terminal.Gui/View/View.Hierarchy.cs +++ b/Terminal.Gui/View/View.Hierarchy.cs @@ -159,14 +159,10 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, Rectangle touched = view.Frame; - // If a view being removed is focused, it should lose focus. - if (view.HasFocus) - { - view.HasFocus = false; - } + bool hadFocus = view.HasFocus; _subviews.Remove (view); - view._superView = null; // Null this AFTER removing focus + view._superView = null; SetNeedsLayout (); SetNeedsDisplay (); @@ -179,11 +175,20 @@ public partial class View // SuperView/SubView hierarchy management (SuperView, } } - if (HasFocus) + if (_previouslyFocused == view) { - FocusDeepest (NavigationDirection.Forward, TabStop); + _previouslyFocused = null; } + if (hadFocus) + { + // Access _hasFocus directly; don't use HasFocus because it will try to find the focused view + view._hasFocus = false; + AdvanceFocus (NavigationDirection.Forward, null); + } + + Debug.Assert (!view.HasFocus); + OnRemoved (new (this, view)); return view; diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index d38d5c4f3..205183fe9 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -1,5 +1,6 @@ #nullable enable using System.Diagnostics; +using System.Reflection.PortableExecutable; namespace Terminal.Gui; @@ -39,7 +40,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // AdvanceFocus did not advance - do we wrap, or move up to the superview? - View [] focusChain = GetSubviewFocusChain (direction, behavior); + View [] focusChain = GetFocusChain (direction, behavior); if (focusChain.Length == 0) { @@ -52,11 +53,11 @@ public partial class View // Focus and cross-view navigation management (TabStop if (direction == NavigationDirection.Forward && focused == focusChain [^1] && SuperView is null) { // We're at the top of the focus chain. Go back down the focus chain and focus the first TabGroup - View [] views = GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); + View [] views = GetFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); if (views.Length > 0) { - View [] subViews = views [0].GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); + View [] subViews = views [0].GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); if (subViews.Length > 0) { @@ -71,11 +72,11 @@ public partial class View // Focus and cross-view navigation management (TabStop if (direction == NavigationDirection.Backward && focused == focusChain [0]) { // We're at the bottom of the focus chain - View [] views = GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); + View [] views = GetFocusChain (NavigationDirection.Forward, TabBehavior.TabGroup); if (views.Length > 0) { - View [] subViews = views [^1].GetSubviewFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); + View [] subViews = views [^1].GetFocusChain (NavigationDirection.Forward, TabBehavior.TabStop); if (subViews.Length > 0) { @@ -102,7 +103,7 @@ public partial class View // Focus and cross-view navigation management (TabStop if (SuperView is { }) { // If we are TabStop, and we have at least one other focusable peer, move to the SuperView's chain - if (TabStop == TabBehavior.TabStop && SuperView is { } && SuperView.GetSubviewFocusChain (direction, behavior).Length > 1) + if (TabStop == TabBehavior.TabStop && SuperView is { } && SuperView.GetFocusChain (direction, behavior).Length > 1) { return false; } @@ -110,7 +111,7 @@ public partial class View // Focus and cross-view navigation management (TabStop // TabGroup is special-cased. if (focused?.TabStop == TabBehavior.TabGroup) { - if (SuperView?.GetSubviewFocusChain (direction, TabBehavior.TabGroup)?.Length > 0) + if (SuperView?.GetFocusChain (direction, TabBehavior.TabGroup)?.Length > 0) { // Our superview has a TabGroup subview; signal we couldn't move so we nav out to it return false; @@ -176,7 +177,7 @@ public partial class View // Focus and cross-view navigation management (TabStop HasFocus = false; } - if (_canFocus && !HasFocus && Visible && SuperView is { } && SuperView.Focused is null) + if (_canFocus && !HasFocus && Visible && SuperView is { Focused: null }) { // If CanFocus is set to true and this view does not have focus, make it enter focus SetFocus (); @@ -211,7 +212,7 @@ public partial class View // Focus and cross-view navigation management (TabStop return SetFocus (); } - /// Gets the currently focused Subview of this view, or if nothing is focused. + /// Gets the currently focused Subview or Adornment of this view, or if nothing is focused. public View? Focused { get @@ -224,7 +225,7 @@ public partial class View // Focus and cross-view navigation management (TabStop } // How about in Adornments? - if (Margin is { HasFocus:true }) + if (Margin is { HasFocus: true }) { return Margin; } @@ -287,9 +288,9 @@ public partial class View // Focus and cross-view navigation management (TabStop { if (Focused is null && _subviews?.Count > 0) { - if (_previouslyMostFocused is { }) + if (_previouslyFocused is { }) { - return _previouslyMostFocused.SetFocus (); + return _previouslyFocused.SetFocus (); } return false; @@ -300,7 +301,7 @@ public partial class View // Focus and cross-view navigation management (TabStop private View? FindDeepestFocusableView (NavigationDirection direction, TabBehavior? behavior) { - View [] indicies = GetSubviewFocusChain (direction, behavior); + View [] indicies = GetFocusChain (direction, behavior); foreach (View v in indicies) { @@ -384,9 +385,9 @@ public partial class View // Focus and cross-view navigation management (TabStop } /// - /// Caches the most focused subview when this view is losing focus. This is used by . + /// A cache of the subview that was focused when this view last lost focus. This is used by . /// - private View? _previouslyMostFocused; + private View? _previouslyFocused; /// /// INTERNAL: Called when focus is going to change to this view. This method is called by and @@ -469,14 +470,15 @@ public partial class View // Focus and cross-view navigation management (TabStop if (!traversingUp) { - // Restore focus to the previously most focused subview in the subview-hierarchy + // Restore focus to the previously focused subview if (!RestoreFocus ()) { // Couldn't restore focus, so use Advance to navigate to the next focusable subview if (!AdvanceFocus (NavigationDirection.Forward, null)) { // Couldn't advance, so we're the most focused view in the application - _previouslyMostFocused = null; + _previouslyFocused = null; + Application.Navigation?.SetFocused (this); } } @@ -590,17 +592,16 @@ public partial class View // Focus and cross-view navigation management (TabStop // If newFocusedVew is null, we need to find the view that should get focus, and SetFocus on it. if (!traversingDown && newFocusedView is null) { - if (superViewOrParent?._previouslyMostFocused is { }) + if (superViewOrParent?._previouslyFocused is { }) { - if (superViewOrParent?._previouslyMostFocused != this) + Debug.Assert (!superViewOrParent._previouslyFocused.WasDisposed); + if (superViewOrParent._previouslyFocused != this) { - superViewOrParent?._previouslyMostFocused?.SetFocus (); + superViewOrParent?._previouslyFocused?.SetFocus (); // The above will cause SetHasFocusFalse, so we can return return; } - - newFocusedView = superViewOrParent?._previouslyMostFocused; } if (superViewOrParent is { }) @@ -655,9 +656,11 @@ public partial class View // Focus and cross-view navigation management (TabStop a.SetHasFocusFalse (newFocusedView, true); } - Debug.Assert (!mostFocused.WasDisposed); + } - _previouslyMostFocused = mostFocused; + if (superViewOrParent is { }) + { + superViewOrParent._previouslyFocused = this; } bool previousValue = HasFocus; @@ -665,8 +668,10 @@ public partial class View // Focus and cross-view navigation management (TabStop // Note, can't be cancelled. NotifyFocusChanging (HasFocus, !HasFocus, newFocusedView, this); - // Get whatever peer has focus, if any + // Get whatever peer has focus, if any so we can update our superview's _previouslyMostFocused View? focusedPeer = superViewOrParent?.Focused; + + // Set HasFocus false _hasFocus = false; if (Application.Navigation is { }) @@ -687,12 +692,6 @@ public partial class View // Focus and cross-view navigation management (TabStop return; } - if (superViewOrParent is { }) - { - Debug.Assert(!focusedPeer.WasDisposed); - superViewOrParent._previouslyMostFocused = focusedPeer; - } - // Post-conditions - prove correctness if (HasFocus == previousValue) { @@ -739,32 +738,48 @@ public partial class View // Focus and cross-view navigation management (TabStop #region Tab/Focus Handling /// - /// Gets TabIndexes that are scoped to the specified behavior and direction. If behavior is null, all TabIndexes are - /// returned. + /// Gets the subviews and Adornments of this view that are scoped to the specified behavior and direction. If behavior is null, all focusable subviews and + /// Adornments are returned. /// /// /// /// - /// GetScopedTabIndexes - internal View [] GetSubviewFocusChain (NavigationDirection direction, TabBehavior? behavior) + internal View [] GetFocusChain (NavigationDirection direction, TabBehavior? behavior) { - IEnumerable? fitleredSubviews; + IEnumerable? filteredSubviews; if (behavior.HasValue) { - fitleredSubviews = _subviews?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true }); + filteredSubviews = _subviews?.Where (v => v.TabStop == behavior && v is { CanFocus: true, Visible: true, Enabled: true }); } else { - fitleredSubviews = _subviews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); + filteredSubviews = _subviews?.Where (v => v is { CanFocus: true, Visible: true, Enabled: true }); + } + + + // How about in Adornments? + if (Padding is { CanFocus: true, Visible: true, Enabled: true } && Padding.TabStop == behavior) + { + filteredSubviews = filteredSubviews?.Append (Padding); + } + + if (Border is { CanFocus: true, Visible: true, Enabled: true } && Border.TabStop == behavior) + { + filteredSubviews = filteredSubviews?.Append (Border); + } + + if (Margin is { CanFocus: true, Visible: true, Enabled: true } && Margin.TabStop == behavior) + { + filteredSubviews = filteredSubviews?.Append (Margin); } if (direction == NavigationDirection.Backward) { - fitleredSubviews = fitleredSubviews?.Reverse (); + filteredSubviews = filteredSubviews?.Reverse (); } - return fitleredSubviews?.ToArray () ?? Array.Empty (); + return filteredSubviews?.ToArray () ?? Array.Empty (); } private TabBehavior? _tabStop; diff --git a/UnitTests/View/Navigation/SetFocusTests.cs b/UnitTests/View/Navigation/SetFocusTests.cs index 8dfd6375a..832c88ac3 100644 --- a/UnitTests/View/Navigation/SetFocusTests.cs +++ b/UnitTests/View/Navigation/SetFocusTests.cs @@ -260,6 +260,14 @@ public class SetFocusTests () : TestsAllViews Assert.False (subViewSubView3.HasFocus); view.Border.CanFocus = true; + Assert.True (view.HasFocus); + Assert.True (subView.HasFocus); + Assert.False (view.Border.HasFocus); + Assert.False (borderSubView.HasFocus); + Assert.False (subViewSubView1.HasFocus); + Assert.False (subViewSubView2.HasFocus); + Assert.False (subViewSubView3.HasFocus); + view.Border.SetFocus (); Assert.True (view.HasFocus); Assert.True (view.Border.HasFocus);