Keyboard UI for move/resize POC

This commit is contained in:
Tig
2024-09-10 17:14:40 -06:00
parent 80bae9f135
commit 85a97e6a05
8 changed files with 269 additions and 28 deletions

View File

@@ -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
}
}
/// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
[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);

View File

@@ -267,5 +267,10 @@ public enum Command
New,
/// <summary>Shows context about the item (e.g. a context menu).</summary>
ShowContextMenu
ShowContextMenu,
/// <summary>
/// Invokes a user interface for editing.
/// </summary>
Edit
}

View File

@@ -22,6 +22,7 @@
"Application.NextTabGroupKey": "F6",
"Application.PrevTabGroupKey": "Shift+F6",
"Application.QuitKey": "Esc",
"Application.ArrangeKey": "Ctrl+F5",
"Key.Separator": "+",
"Theme": "Default",

View File

@@ -1,3 +1,6 @@
using System.Diagnostics;
using static Terminal.Gui.SpinnerStyle;
namespace Terminal.Gui;
/// <summary>The Border for a <see cref="View"/>.</summary>
@@ -52,8 +55,9 @@ public class Border : Adornment
/// <inheritdoc/>
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;
/// <inheritdoc/>
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<Color> stops, out List<int> 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;
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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.