Addressed feedback #1

This commit is contained in:
Tig
2024-08-27 06:47:07 -07:00
parent ca73f23ac1
commit 7556892c02
8 changed files with 435 additions and 227 deletions

View File

@@ -35,7 +35,7 @@ dotnet run
* [API Documentation](https://gui-cs.github.io/Terminal.GuiV2Docs/api/Terminal.Gui.html)
* [Documentation Home](https://gui-cs.github.io/Terminal.GuiV2Docs)
The above documentation matches the most recent Nuget release from the `v2_develop` branch. Get the [v1 documentation here]( (This is the v2 API documentation. For v1 go here: https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.html)
The above documentation matches the most recent Nuget release from the `v2_develop` branch. Get the [v1 documentation here](This is the v2 API documentation. For v1 go here: https://gui-cs.github.io/Terminal.Gui/api/Terminal.Gui.html)
See the [`Terminal.Gui/` README](https://github.com/gui-cs/Terminal.Gui/tree/master/Terminal.Gui) for an overview of how the library is structured.

View File

@@ -1,45 +1,49 @@
#nullable enable
using System.Text.Json.Serialization;
namespace Terminal.Gui;
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
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key NextTabKey
{
get => _nextTabKey;
set
{
if (_nextTabKey != value)
{
ReplaceKey (_nextTabKey, value);
_nextTabKey = value;
}
}
}
private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrrides
private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrrides
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key PrevTabKey
{
get => _prevTabKey;
set
{
if (_prevTabKey != value)
{
ReplaceKey (_prevTabKey, value);
_prevTabKey = value;
}
}
}
private static Key _quitKey = Key.Esc; // Resources/config.json overrrides
private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrrides
static Application () { AddApplicationKeyBindings (); }
/// <summary>Gets the key bindings for this view.</summary>
public static KeyBindings KeyBindings { get; internal set; } = new ();
/// <summary>
/// Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>.
/// <para>
/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
/// additional processing.
/// </para>
/// </summary>
/// <remarks>
/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
/// </remarks>
public static event EventHandler<Key>? KeyDown;
/// <summary>
/// Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
/// <para>
/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
/// additional processing.
/// </para>
/// </summary>
/// <remarks>
/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/>.</para>
/// </remarks>
public static event EventHandler<Key>? KeyUp;
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
@@ -56,71 +60,21 @@ public static partial class Application // Keyboard handling
}
}
private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrrides
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key PrevTabGroupKey
public static Key NextTabKey
{
get => _prevTabGroupKey;
get => _nextTabKey;
set
{
if (_prevTabGroupKey != value)
if (_nextTabKey != value)
{
ReplaceKey (_prevTabGroupKey, value);
_prevTabGroupKey = value;
ReplaceKey (_nextTabKey, value);
_nextTabKey = value;
}
}
}
private static Key _quitKey = Key.Esc; // Resources/config.json overrrides
/// <summary>Gets or sets the key to quit the application.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key QuitKey
{
get => _quitKey;
set
{
if (_quitKey != value)
{
ReplaceKey (_quitKey, value);
_quitKey = value;
}
}
}
private static void ReplaceKey (Key oldKey, Key newKey)
{
if (KeyBindings.Bindings.Count == 0)
{
return;
}
if (newKey == Key.Empty)
{
KeyBindings.Remove (oldKey);
}
else
{
KeyBindings.ReplaceKey (oldKey, newKey);
}
}
/// <summary>
/// Event fired when the user presses a key. Fired by <see cref="OnKeyDown"/>.
/// <para>
/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
/// additional processing.
/// </para>
/// </summary>
/// <remarks>
/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
/// </remarks>
public static event EventHandler<Key>? KeyDown;
/// <summary>
/// Called by the <see cref="ConsoleDriver"/> when the user presses a key. Fires the <see cref="KeyDown"/> event
/// then calls <see cref="View.NewKeyDownEvent"/> on all top level views. Called after <see cref="OnKeyDown"/> and
@@ -217,20 +171,6 @@ public static partial class Application // Keyboard handling
return false;
}
/// <summary>
/// Event fired when the user releases a key. Fired by <see cref="OnKeyUp"/>.
/// <para>
/// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
/// additional processing.
/// </para>
/// </summary>
/// <remarks>
/// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
/// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
/// <para>Fired after <see cref="KeyDown"/>.</para>
/// </remarks>
public static event EventHandler<Key>? KeyUp;
/// <summary>
/// Called by the <see cref="ConsoleDriver"/> when the user releases a key. Fires the <see cref="KeyUp"/> event
/// then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="OnKeyDown"/>.
@@ -268,33 +208,50 @@ public static partial class Application // Keyboard handling
return false;
}
/// <summary>Gets the key bindings for this view.</summary>
public static KeyBindings KeyBindings { get; internal set; } = new ();
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key PrevTabGroupKey
{
get => _prevTabGroupKey;
set
{
if (_prevTabGroupKey != value)
{
ReplaceKey (_prevTabGroupKey, value);
_prevTabGroupKey = value;
}
}
}
/// <summary>
/// Commands for Application.
/// </summary>
private static Dictionary<Command, Func<CommandContext, bool?>>? CommandImplementations { get; set; }
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key PrevTabKey
{
get => _prevTabKey;
set
{
if (_prevTabKey != value)
{
ReplaceKey (_prevTabKey, value);
_prevTabKey = value;
}
}
}
/// <summary>
/// <para>
/// Sets the function that will be invoked for a <see cref="Command"/>.
/// </para>
/// <para>
/// If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
/// replace the old one.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
/// </para>
/// </remarks>
/// <param name="command">The command.</param>
/// <param name="f">The function.</param>
private static void AddCommand (Command command, Func<bool?> f) { CommandImplementations! [command] = ctx => f (); }
static Application () { AddApplicationKeyBindings (); }
/// <summary>Gets or sets the key to quit the application.</summary>
[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
public static Key QuitKey
{
get => _quitKey;
set
{
if (_quitKey != value)
{
ReplaceKey (_quitKey, value);
_quitKey = value;
}
}
}
internal static void AddApplicationKeyBindings ()
{
@@ -303,7 +260,7 @@ public static partial class Application // Keyboard handling
// Things this view knows how to do
AddCommand (
Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic.
() =>
static () =>
{
if (ApplicationOverlapped.OverlappedTop is { })
{
@@ -320,7 +277,7 @@ public static partial class Application // Keyboard handling
AddCommand (
Command.Suspend,
() =>
static () =>
{
Driver?.Suspend ();
@@ -330,40 +287,22 @@ public static partial class Application // Keyboard handling
AddCommand (
Command.NextView,
() =>
{
View? current = Application.Current;
if (current is { })
{
return current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop);
}
return false;
}
);
static () => Current?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
AddCommand (
Command.PreviousView,
() =>
{
View? current = Application.Current;
if (current is { })
{
return current.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop);
}
return false;
}
);
static () => Current?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
AddCommand (
Command.NextViewOrTop,
() =>
static () =>
{
// TODO: This OverlapppedTop tomfoolery goes away in addressing #2491
if (ApplicationOverlapped.OverlappedTop is null)
{
View? current = Application.Current;
if (current is { })
if ((View?)Current is { })
{
return current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup);
return Current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup);
}
}
else
@@ -379,19 +318,19 @@ public static partial class Application // Keyboard handling
AddCommand (
Command.PreviousViewOrTop,
() =>
static () =>
{
// TODO: This OverlapppedTop tomfoolery goes away in addressing #2491
if (ApplicationOverlapped.OverlappedTop is null)
{
View? current = Application.Current;
if (current is { })
if ((View?)Current is { })
{
return current.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup);
return Current.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup);
}
}
else
{
ApplicationOverlapped.OverlappedMovePrevious();
ApplicationOverlapped.OverlappedMovePrevious ();
return true;
}
@@ -402,7 +341,7 @@ public static partial class Application // Keyboard handling
AddCommand (
Command.Refresh,
() =>
static () =>
{
Refresh ();
@@ -464,4 +403,44 @@ public static partial class Application // Keyboard handling
.Distinct ()
.ToList ();
}
/// <summary>
/// <para>
/// Sets the function that will be invoked for a <see cref="Command"/>.
/// </para>
/// <para>
/// If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
/// replace the old one.
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
/// </para>
/// </remarks>
/// <param name="command">The command.</param>
/// <param name="f">The function.</param>
private static void AddCommand (Command command, Func<bool?> f) { CommandImplementations! [command] = ctx => f (); }
/// <summary>
/// Commands for Application.
/// </summary>
private static Dictionary<Command, Func<CommandContext, bool?>>? CommandImplementations { get; set; }
private static void ReplaceKey (Key oldKey, Key newKey)
{
if (KeyBindings.Bindings.Count == 0)
{
return;
}
if (newKey == Key.Empty)
{
KeyBindings.Remove (oldKey);
}
else
{
KeyBindings.ReplaceKey (oldKey, newKey);
}
}
}

View File

@@ -19,11 +19,13 @@ public class Adornments : Scenario
var editor = new AdornmentsEditor
{
AutoSelectViewToEdit = true,
// This is for giggles, to show that the editor can be moved around.
Arrangement = ViewArrangement.Movable,
X = Pos.AnchorEnd(),
X = Pos.AnchorEnd ()
};
editor.Border.Thickness = new Thickness (1, 2, 1, 1);
editor.Border.Thickness = new (1, 2, 1, 1);
app.Add (editor);
@@ -31,7 +33,8 @@ public class Adornments : Scenario
{
Title = "The _Window",
Arrangement = ViewArrangement.Movable,
// X = Pos.Center (),
// X = Pos.Center (),
Width = Dim.Percent (60),
Height = Dim.Percent (80)
};
@@ -127,9 +130,36 @@ public class Adornments : Scenario
#endif
};
Application.MouseEvent += ApplicationOnMouseEvent;
Application.Run (app);
app.Dispose ();
Application.Shutdown ();
return;
void ApplicationOnMouseEvent (object sender, MouseEvent e)
{
if (!editor.AutoSelectViewToEdit || editor.FrameToScreen ().Contains (e.Position))
{
return;
}
// TODO: Add a setting (property) so only subviews of a specified view are considered.
View view = e.View;
if (view is { } && e.Flags == MouseFlags.Button1Clicked)
{
if (view is Adornment adornment)
{
editor.ViewToEdit = adornment.Parent;
}
else
{
editor.ViewToEdit = view;
}
}
}
}
}

View File

@@ -21,8 +21,6 @@ public class AdornmentsEditor : View
TabStop = TabBehavior.TabGroup;
//Application.MouseEvent += Application_MouseEvent;
Application.Navigation!.FocusedChanged += ApplicationNavigationOnFocusedChanged;
Initialized += AdornmentsEditor_Initialized;
}
@@ -155,35 +153,4 @@ public class AdornmentsEditor : View
Add (_diagRulerCheckBox);
_diagRulerCheckBox.Y = Pos.Bottom (_diagPaddingCheckBox);
}
private void Application_MouseEvent (object sender, MouseEvent e)
{
if (!AutoSelectViewToEdit || FrameToScreen ().Contains (e.Position))
{
return;
}
// TODO: Add a setting (property) so only subviews of a specified view are considered.
View view = e.View;
if (view is { } && e.Flags == MouseFlags.Button1Clicked)
{
if (view is Adornment adornment)
{
ViewToEdit = adornment.Parent;
}
else
{
ViewToEdit = view;
}
}
}
private void ApplicationNavigationOnFocusedChanged (object sender, EventArgs e)
{
if (ApplicationNavigation.IsInHierarchy (this, Application.Navigation!.GetFocused ()))
{
return;
}
}
}

View File

@@ -407,8 +407,35 @@ public class ContentScrolling : Scenario
app.Closed += (s, e) => View.Diagnostics = _diagnosticFlags;
Application.MouseEvent += ApplicationOnMouseEvent;
Application.Run (app);
app.Dispose ();
Application.Shutdown ();
return;
void ApplicationOnMouseEvent (object sender, MouseEvent e)
{
if (!editor.AutoSelectViewToEdit || editor.FrameToScreen ().Contains (e.Position))
{
return;
}
// TODO: Add a setting (property) so only subviews of a specified view are considered.
View view = e.View;
if (view is { } && e.Flags == MouseFlags.Button1Clicked)
{
if (view is Adornment adornment)
{
editor.ViewToEdit = adornment.Parent;
}
else
{
editor.ViewToEdit = view;
}
}
}
}
}

View File

@@ -0,0 +1,165 @@
using Terminal.Gui;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("Navigation", "Navigation Tester")]
[ScenarioCategory ("Mouse and Keyboard")]
[ScenarioCategory ("Layout")]
public class Navigation : Scenario
{
public override void Main ()
{
Application.Init ();
Window app = new ()
{
Title = GetQuitKeyAndName (),
TabStop = TabBehavior.TabGroup
};
var editor = new AdornmentsEditor
{
X = 0,
Y = 0,
AutoSelectViewToEdit = true,
TabStop = TabBehavior.NoStop
};
app.Add (editor);
FrameView testFrame = new ()
{
Title = "_1 Test Frame",
X = Pos.Right (editor),
Width = Dim.Fill (),
Height = Dim.Fill (),
};
app.Add (testFrame);
Button button = new ()
{
X = 0,
Y = 0,
Title = $"TopButton _{GetNextHotKey ()}",
};
testFrame.Add (button);
var tiledView1 = CreateTiledView (0, 2, 2);
var tiledView2 = CreateTiledView (1, Pos.Right (tiledView1), Pos.Top (tiledView1));
testFrame.Add (tiledView1);
testFrame.Add (tiledView2);
var tiledView3 = CreateTiledView (1, Pos.Right (tiledView2), Pos.Top (tiledView2));
tiledView3.TabStop = TabBehavior.TabGroup;
tiledView3.BorderStyle = LineStyle.Double;
testFrame.Add (tiledView3);
var overlappedView1 = CreateOverlappedView (2, Pos.Center () - 5, Pos.Center ());
var tiledSubView = CreateTiledView (4, 0, 2);
overlappedView1.Add (tiledSubView);
var overlappedView2 = CreateOverlappedView (3, Pos.Center () + 10, Pos.Center () + 5);
// BUGBUG: F6 through nested tab groups doesn't work yet.
#if NESTED_TABGROUPS
var overlappedInOverlapped1 = CreateOverlappedView (4, 1, 4);
overlappedView2.Add (overlappedInOverlapped1);
var overlappedInOverlapped2 = CreateOverlappedView (5, 10, 7);
overlappedView2.Add (overlappedInOverlapped2);
#endif
testFrame.Add (overlappedView1);
testFrame.Add (overlappedView2);
button = new ()
{
X = Pos.AnchorEnd (),
Y = Pos.AnchorEnd (),
Title = $"TopButton _{GetNextHotKey ()}",
};
testFrame.Add (button);
Application.Run (app);
app.Dispose ();
Application.Shutdown ();
}
private int _hotkeyCount;
private char GetNextHotKey ()
{
return (char)((int)'A' + _hotkeyCount++);
}
private View CreateTiledView (int id, Pos x, Pos y)
{
View overlapped = new View
{
X = x,
Y = y,
Height = Dim.Auto (),
Width = Dim.Auto (),
Title = $"Tiled{id} _{GetNextHotKey ()}",
Id = $"Tiled{id}",
BorderStyle = LineStyle.Single,
CanFocus = true, // Can't drag without this? BUGBUG
TabStop = TabBehavior.TabStop,
Arrangement = ViewArrangement.Fixed
};
Button button = new ()
{
Title = $"Tiled Button{id} _{GetNextHotKey ()}"
};
overlapped.Add (button);
button = new ()
{
Y = Pos.Bottom (button),
Title = $"Tiled Button{id} _{GetNextHotKey ()}"
};
overlapped.Add (button);
return overlapped;
}
private View CreateOverlappedView (int id, Pos x, Pos y)
{
View overlapped = new View
{
X = x,
Y = y,
Height = Dim.Auto (),
Width = Dim.Auto (),
Title = $"Overlapped{id} _{GetNextHotKey ()}",
ColorScheme = Colors.ColorSchemes ["Toplevel"],
Id = $"Overlapped{id}",
ShadowStyle = ShadowStyle.Transparent,
BorderStyle = LineStyle.Double,
CanFocus = true, // Can't drag without this? BUGBUG
TabStop = TabBehavior.TabGroup,
Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped
};
Button button = new ()
{
Title = $"Button{id} _{GetNextHotKey ()}"
};
overlapped.Add (button);
button = new ()
{
Y = Pos.Bottom (button),
Title = $"Button{id} _{GetNextHotKey ()}"
};
overlapped.Add (button);
return overlapped;
}
}

View File

@@ -55,11 +55,36 @@ public class ShadowStyles : Scenario
};
app.Add (button);
Application.MouseEvent += ApplicationOnMouseEvent;
Application.Run (app);
app.Dispose ();
Application.Shutdown ();
return;
void ApplicationOnMouseEvent (object sender, MouseEvent e)
{
if (!editor.AutoSelectViewToEdit || editor.FrameToScreen ().Contains (e.Position))
{
return;
}
// TODO: Add a setting (property) so only subviews of a specified view are considered.
View view = e.View;
if (view is { } && e.Flags == MouseFlags.Button1Clicked)
{
if (view is Adornment adornment)
{
editor.ViewToEdit = adornment.Parent;
}
else
{
editor.ViewToEdit = view;
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Terminal.Gui;
using System;
using Terminal.Gui;
namespace UICatalog.Scenarios;
@@ -47,36 +48,6 @@ public class ViewExperiments : Scenario
testFrame.Add (button);
var tiledView1 = CreateTiledView (0, 2, 2);
var tiledView2 = CreateTiledView (1, Pos.Right (tiledView1), Pos.Top (tiledView1));
testFrame.Add (tiledView1);
testFrame.Add (tiledView2);
var tiledView3 = CreateTiledView (1, Pos.Right (tiledView2), Pos.Top (tiledView2));
tiledView3.TabStop = TabBehavior.TabGroup;
tiledView3.BorderStyle = LineStyle.Double;
testFrame.Add (tiledView3);
var overlappedView1 = CreateOverlappedView (2, Pos.Center () - 5, Pos.Center ());
var tiledSubView = CreateTiledView (4, 0, 2);
overlappedView1.Add (tiledSubView);
var overlappedView2 = CreateOverlappedView (3, Pos.Center () + 10, Pos.Center () + 5);
// BUGBUG: F6 through nested tab groups doesn't work yet.
#if NESTED_TABGROUPS
var overlappedInOverlapped1 = CreateOverlappedView (4, 1, 4);
overlappedView2.Add (overlappedInOverlapped1);
var overlappedInOverlapped2 = CreateOverlappedView (5, 10, 7);
overlappedView2.Add (overlappedInOverlapped2);
#endif
testFrame.Add (overlappedView1);
testFrame.Add (overlappedView2);
button = new ()
{
X = Pos.AnchorEnd (),
@@ -85,10 +56,54 @@ public class ViewExperiments : Scenario
};
testFrame.Add (button);
Application.MouseEvent += ApplicationOnMouseEvent;
Application.Navigation.FocusedChanged += NavigationOnFocusedChanged;
Application.Run (app);
app.Dispose ();
Application.Shutdown ();
return;
void NavigationOnFocusedChanged (object sender, EventArgs e)
{
if (!ApplicationNavigation.IsInHierarchy (testFrame, Application.Navigation!.GetFocused ()))
{
return;
}
editor.ViewToEdit = Application.Navigation!.GetFocused ();
}
void ApplicationOnMouseEvent (object sender, MouseEvent e)
{
if (e.Flags != MouseFlags.Button1Clicked)
{
return;
}
if (!editor.AutoSelectViewToEdit || !testFrame.FrameToScreen ().Contains (e.Position))
{
return;
}
// TODO: Add a setting (property) so only subviews of a specified view are considered.
View view = e.View;
if (view is { } && e.Flags == MouseFlags.Button1Clicked)
{
if (view is Adornment adornment)
{
editor.ViewToEdit = adornment.Parent;
}
else
{
editor.ViewToEdit = view;
}
}
}
}
private int _hotkeyCount;
@@ -110,7 +125,7 @@ public class ViewExperiments : Scenario
Id = $"Tiled{id}",
BorderStyle = LineStyle.Single,
CanFocus = true, // Can't drag without this? BUGBUG
TabStop = TabBehavior.TabStop,
TabStop = TabBehavior.TabGroup,
Arrangement = ViewArrangement.Fixed
};