diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 259af8fba..74411b09b 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -5,12 +5,10 @@ public static partial class Application // Keyboard handling { private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrrides private static Key _nextTabKey = Key.Tab; // Resources/config.json overrrides - private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrrides - private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrrides - private static Key _quitKey = Key.Esc; // Resources/config.json overrrides + private static Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrrides static Application () { AddApplicationKeyBindings (); } @@ -262,6 +260,22 @@ public static partial class Application // Keyboard handling } } + + /// Gets or sets the key to activate arranging views using the keyboard. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key ArrangeKey + { + get => _arrangeKey; + set + { + if (_arrangeKey != value) + { + ReplaceKey (_arrangeKey, value); + _arrangeKey = value; + } + } + } + internal static void AddApplicationKeyBindings () { CommandImplementations = new (); @@ -344,6 +358,24 @@ public static partial class Application // Keyboard handling } ); + AddCommand (Command.Edit, static () => + { + View? viewToArrange = Navigation?.GetFocused (); + + // Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed + while (viewToArrange?.SuperView is { } && viewToArrange.Arrangement == ViewArrangement.Fixed) + { + viewToArrange = viewToArrange.SuperView; + } + + if (viewToArrange is { }) + { + return viewToArrange.Border?.Arrange (); + } + + return false; + }); + KeyBindings.Clear (); // Resources/config.json overrrides @@ -352,6 +384,7 @@ public static partial class Application // Keyboard handling NextTabGroupKey = Key.F6; PrevTabGroupKey = Key.F6.WithShift; QuitKey = Key.Esc; + ArrangeKey = Key.F5.WithCtrl; KeyBindings.Add (QuitKey, KeyBindingScope.Application, Command.QuitToplevel); @@ -365,6 +398,8 @@ public static partial class Application // Keyboard handling KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextViewOrTop); KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousViewOrTop); + KeyBindings.Add (ArrangeKey, KeyBindingScope.Application, Command.Edit); + // TODO: Refresh Key should be configurable KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh); diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index 42dcb16e6..2eaba7c0d 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -267,5 +267,10 @@ public enum Command New, /// Shows context about the item (e.g. a context menu). - ShowContextMenu + ShowContextMenu, + + /// + /// Invokes a user interface for editing. + /// + Edit } diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index adba1e584..9be0b0212 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -22,6 +22,7 @@ "Application.NextTabGroupKey": "F6", "Application.PrevTabGroupKey": "Shift+F6", "Application.QuitKey": "Esc", + "Application.ArrangeKey": "Ctrl+F5", "Key.Separator": "+", "Theme": "Default", diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index de6ebd4cc..05070d887 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -1,3 +1,6 @@ +using System.Diagnostics; +using static Terminal.Gui.SpinnerStyle; + namespace Terminal.Gui; /// The Border for a . @@ -52,8 +55,9 @@ public class Border : Adornment /// public Border (View parent) : base (parent) { - /* Do nothing; View.CreateAdornment requires a constructor that takes a parent */ Parent = parent; + CanFocus = false; + Application.GrabbingMouse += Application_GrabbingMouse; Application.UnGrabbingMouse += Application_UnGrabbingMouse; @@ -70,6 +74,10 @@ public class Border : Adornment public Button CloseButton { get; internal set; } #endif + + [CanBeNull] + private Button _arrangeButton; + /// public override void BeginInit () { @@ -108,8 +116,11 @@ public class Border : Adornment LayoutStarted += OnLayoutStarted; } #endif + } + + #if SUBVIEW_BASED_BORDER private void OnLayoutStarted (object sender, LayoutEventArgs e) { @@ -427,6 +438,8 @@ public class Border : Adornment int sideLineLength = borderBounds.Height; bool canDrawBorder = borderBounds is { Width: > 0, Height: > 0 }; + LineStyle lineStyle = LineStyle; + if (Settings.FastHasFlags (BorderSettings.Title)) { if (Thickness.Top == 2) @@ -506,7 +519,7 @@ public class Border : Adornment new (borderBounds.Location.X, titleY), borderBounds.Width, Orientation.Horizontal, - LineStyle, + lineStyle, Driver.GetAttribute () ); } @@ -521,7 +534,7 @@ public class Border : Adornment new (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, - LineStyle, + lineStyle, Driver.GetAttribute () ); } @@ -535,7 +548,7 @@ public class Border : Adornment new (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, - LineStyle, + lineStyle, Driver.GetAttribute () ); @@ -543,7 +556,7 @@ public class Border : Adornment new (borderBounds.X + 1, topTitleLineY + 2), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, - LineStyle, + lineStyle, Driver.GetAttribute () ); } @@ -554,7 +567,7 @@ public class Border : Adornment new (borderBounds.Location.X, titleY), 2, Orientation.Horizontal, - LineStyle, + lineStyle, Driver.GetAttribute () ); @@ -593,7 +606,7 @@ public class Border : Adornment ), borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, - LineStyle, + lineStyle, Driver.GetAttribute () ); } @@ -607,7 +620,7 @@ public class Border : Adornment new (borderBounds.Location.X, titleY), sideLineLength, Orientation.Vertical, - LineStyle, + lineStyle, Driver.GetAttribute () ); } @@ -619,7 +632,7 @@ public class Border : Adornment new (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), borderBounds.Width, Orientation.Horizontal, - LineStyle, + lineStyle, Driver.GetAttribute () ); } @@ -630,7 +643,7 @@ public class Border : Adornment new (borderBounds.X + borderBounds.Width - 1, titleY), sideLineLength, Orientation.Vertical, - LineStyle, + lineStyle, Driver.GetAttribute () ); } @@ -705,7 +718,7 @@ public class Border : Adornment private static void GetAppealingGradientColors (out List stops, out List steps) { // Define the colors of the gradient stops with more appealing colors - stops = new() + stops = new () { new (0, 128, 255), // Bright Blue new (0, 255, 128), // Bright Green @@ -716,6 +729,174 @@ public class Border : Adornment // Define the number of steps between each color for smoother transitions // If we pass only a single value then it will assume equal steps between all pairs - steps = new() { 15 }; + steps = new () { 15 }; + } + + private ViewArrangement _arranging; + + public bool? Arrange () + { + Debug.Assert (_arranging == ViewArrangement.Fixed); + + CanFocus = true; + SetFocus (); + + Debug.Assert (_arrangeButton is null); + _arrangeButton = new Button + { + CanFocus = true, + Width = 1, + Height = 1, + NoDecorations = true, + NoPadding = true, + ShadowStyle = ShadowStyle.None, + Text = $"{Glyphs.Diamond}", + }; + Add (_arrangeButton); + + AddCommand (Command.QuitToplevel, EndArrange); + + AddCommand (Command.LineUp, + () => + { + if (_arranging == ViewArrangement.Movable) + { + Parent!.Y = Parent.Y - 1; + } + + if (_arranging == ViewArrangement.Resizable) + { + if (Parent!.Viewport.Height > 0) + { + Parent!.Height = Parent.Height - 1; + } + } + + return true; + }); + + AddCommand (Command.LineDown, + () => + { + if (_arranging == ViewArrangement.Movable) + { + Parent!.Y = Parent.Y + 1; + } + + if (_arranging == ViewArrangement.Resizable) + { + Parent!.Height = Parent.Height + 1; + } + + return true; + }); + AddCommand (Command.Left, + () => + { + if (_arranging == ViewArrangement.Movable) + { + Parent!.X = Parent.X - 1; + } + + if (_arranging == ViewArrangement.Resizable) + { + if (Parent!.Viewport.Width > 0) + { + Parent!.Width = Parent.Width - 1; + } + } + return true; + }); + + AddCommand (Command.Right, + () => + { + if (_arranging == ViewArrangement.Movable) + { + Parent!.X = Parent.X + 1; + } + + if (_arranging == ViewArrangement.Resizable) + { + Parent!.Width = Parent.Width + 1; + } + + return true; + }); + + AddCommand (Command.Tab, + () => + { + // TODO: Move arrangement focus to next side + if (Parent!.Arrangement.HasFlag (ViewArrangement.Resizable)) + { + _arranging = ViewArrangement.Resizable; + _arrangeButton.X = Pos.AnchorEnd (); + _arrangeButton.Y = Pos.AnchorEnd (); + + return true; + } + return true; + }); + + AddCommand (Command.BackTab, + () => + { + // TODO: Move arrangement focus to prev side + if (Parent!.Arrangement.HasFlag (ViewArrangement.Movable)) + { + _arranging = ViewArrangement.Movable; + _arrangeButton.X = 0; + _arrangeButton.Y = 0; + return true; + } + return true; + }); + + KeyBindings.Add (Key.Esc, KeyBindingScope.HotKey, Command.QuitToplevel); + KeyBindings.Add (Application.ArrangeKey, KeyBindingScope.HotKey, Command.QuitToplevel); + KeyBindings.Add (Key.CursorUp, KeyBindingScope.HotKey, Command.LineUp); + KeyBindings.Add (Key.CursorDown, KeyBindingScope.HotKey, Command.LineDown); + KeyBindings.Add (Key.CursorLeft, KeyBindingScope.HotKey, Command.Left); + KeyBindings.Add (Key.CursorRight, KeyBindingScope.HotKey, Command.Right); + + KeyBindings.Add (Key.Tab, KeyBindingScope.HotKey, Command.Tab); + KeyBindings.Add (Key.Tab.WithShift, KeyBindingScope.HotKey, Command.BackTab); + + if (Parent!.Arrangement.HasFlag (ViewArrangement.Movable)) + { + _arranging = ViewArrangement.Movable; + _arrangeButton.X = 0; + _arrangeButton.Y = 0; + return true; + } + else + { + if (Parent!.Arrangement.HasFlag (ViewArrangement.Resizable)) + { + _arranging = ViewArrangement.Resizable; + _arrangeButton.X = Pos.AnchorEnd (); + _arrangeButton.Y = Pos.AnchorEnd (); + + return true; + } + } + + // Hack for now + EndArrange (); + return false; + } + private bool? EndArrange () + { + _arranging = ViewArrangement.Fixed; + CanFocus = false; + + KeyBindings.Clear (); + + Remove (_arrangeButton); + _arrangeButton.Dispose (); + _arrangeButton = null; + + return true; } } diff --git a/Terminal.Gui/View/Adornment/ShadowView.cs b/Terminal.Gui/View/Adornment/ShadowView.cs index c9893c65c..4e1647149 100644 --- a/Terminal.Gui/View/Adornment/ShadowView.cs +++ b/Terminal.Gui/View/Adornment/ShadowView.cs @@ -137,7 +137,7 @@ internal class ShadowView : View Rectangle screen = ViewportToScreen (viewport); // Fill the rest of the rectangle - for (int i = screen.Y; i < screen.Y + viewport.Height; i++) + for (int i = Math.Max (0, screen.Y); i < screen.Y + viewport.Height; i++) { Driver.Move (screen.X, i); diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index ce72d068a..9561b6a5c 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -579,6 +579,13 @@ public partial class View // Keyboard APIs private bool ProcessAdornmentKeyBindings (Adornment adornment, Key keyEvent, KeyBindingScope scope, ref bool? handled) { + bool? adornmentHandled = adornment.OnInvokingKeyBindings (keyEvent, scope); + + if (adornmentHandled is true) + { + return true; + } + if (adornment?.Subviews is null) { return false; diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index 852b4b130..77b9b8cbc 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -422,6 +422,20 @@ public partial class View // Focus and cross-view navigation management (TabStop } } + // Are we an Adornment? + if (this is Adornment adornment) + { + if (adornment.Parent is { HasFocus: false } parent) + { + (bool focusSet, bool parentCancelled) = parent.SetHasFocusTrue (previousFocusedView, true); + + if (!focusSet) + { + return (false, parentCancelled); + } + } + } + if (_hasFocus) { // Something else beat us to the change (likely a FocusChanged handler). @@ -433,6 +447,15 @@ public partial class View // Focus and cross-view navigation management (TabStop // Get whatever peer has focus, if any View? focusedPeer = SuperView?.Focused; + if (focusedPeer is null) + { + // Are we an Adornment? + if (this is Adornment ad) + { + focusedPeer = ad.Parent?.Focused; + } + } + _hasFocus = true; // Ensure that the peer loses focus diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index d6626659d..577c60d2b 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -80,17 +80,6 @@ public class Dialog : Window Modal = true; ButtonAlignment = DefaultButtonAlignment; ButtonAlignmentModes = DefaultButtonAlignmentModes; - - AddCommand ( - Command.QuitToplevel, - () => - { - Canceled = true; - RequestStop (); - - return true; - }); - KeyBindings.Add (Key.Esc, Command.QuitToplevel); } // BUGBUG: We override GetNormal/FocusColor because "Dialog" ColorScheme is goofy.