Focus next/previous input element with Tab

This commit is contained in:
2023-08-14 14:05:28 +02:00
parent 1f4b938358
commit 2a595b2548
13 changed files with 197 additions and 20 deletions

View File

@@ -1,7 +1,10 @@
using FileTime.App.Core.ViewModels; using System.Collections.ObjectModel;
using FileTime.App.Core.ViewModels;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
public interface IConsoleAppState : IAppState public interface IConsoleAppState : IAppState
{ {
string ErrorText { get; set; }
ObservableCollection<string> PopupTexts { get; }
} }

View File

@@ -97,6 +97,7 @@ public class App : IApplication
if (focused is { }) if (focused is { })
{ {
focused.HandleKeyInput(keyEventArgs); focused.HandleKeyInput(keyEventArgs);
_applicationContext.FocusManager.HandleKeyInput(keyEventArgs);
} }
if (focused is null || (!keyEventArgs.Handled && KeysToFurtherProcess.Contains(keyEventArgs.Key))) if (focused is null || (!keyEventArgs.Handled && KeysToFurtherProcess.Contains(keyEventArgs.Key)))

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.ViewModels; using System.Collections.ObjectModel;
using FileTime.App.Core.ViewModels;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -6,4 +7,6 @@ namespace FileTime.ConsoleUI.App;
public partial class ConsoleAppState : AppStateBase, IConsoleAppState public partial class ConsoleAppState : AppStateBase, IConsoleAppState
{ {
[Notify] private string? _errorText; [Notify] private string? _errorText;
//TODO: make it thread safe
public ObservableCollection<string> PopupTexts { get; } = new();
} }

View File

@@ -123,7 +123,7 @@ public class MainWindow
private Grid<IRootViewModel> MainContent() => private Grid<IRootViewModel> MainContent() =>
new() new()
{ {
RowDefinitionsObject = "Auto * Auto", RowDefinitionsObject = "Auto * Auto Auto",
ChildInitializer = ChildInitializer =
{ {
new Grid<IRootViewModel> new Grid<IRootViewModel>
@@ -194,7 +194,28 @@ public class MainWindow
{ {
PossibleCommands() PossibleCommands()
} }
} },
new ItemsControl<IRootViewModel, string>
{
MaxHeight = 5,
Extensions =
{
new GridPositionExtension(0, 3)
},
ItemTemplate = () =>
{
return new TextBlock<string>()
.Setup(t => t.Bind(
t,
dc => dc,
t => t.Text));
}
}
.Setup(i => i.Bind(
i,
root => root.AppState.PopupTexts,
c => c.ItemsSource
))
} }
}; };
@@ -447,6 +468,7 @@ public class MainWindow
{ {
var readInputs = new ItemsControl<IRootViewModel, IInputElement> var readInputs = new ItemsControl<IRootViewModel, IInputElement>
{ {
IsFocusBoundary = true,
ItemTemplate = () => ItemTemplate = () =>
{ {
var root = new Grid<IInputElement> var root = new Grid<IInputElement>
@@ -479,6 +501,10 @@ public class MainWindow
v => v ?? string.Empty, v => v ?? string.Empty,
fallbackValue: string.Empty fallbackValue: string.Empty
)) ))
.Setup(t => t.Bind(
t,
d => ((TextInputElement) d).Label,
tb => tb.Name))
.WithTextHandler((tb, t) => .WithTextHandler((tb, t) =>
{ {
if (tb.DataContext is TextInputElement textInputElement) if (tb.DataContext is TextInputElement textInputElement)
@@ -504,6 +530,10 @@ public class MainWindow
v => v ?? string.Empty, v => v ?? string.Empty,
fallbackValue: string.Empty fallbackValue: string.Empty
)) ))
.Setup(t => t.Bind(
t,
d => ((PasswordInputElement) d).Label,
tb => tb.Name))
.WithTextHandler((tb, t) => .WithTextHandler((tb, t) =>
{ {
if (tb.DataContext is PasswordInputElement textInputElement) if (tb.DataContext is PasswordInputElement textInputElement)

View File

@@ -1,4 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Serilog.Core; using Serilog.Core;
using Serilog.Events; using Serilog.Events;
@@ -6,6 +7,13 @@ namespace FileTime.ConsoleUI.App.Services;
public class CustomLoggerSink : ILogEventSink public class CustomLoggerSink : ILogEventSink
{ {
private readonly Lazy<IDialogService> _dialogService;
public CustomLoggerSink(IServiceProvider serviceProvider)
{
_dialogService = new Lazy<IDialogService>(() => serviceProvider.GetRequiredService<IDialogService>());
}
public void Emit(LogEvent logEvent) public void Emit(LogEvent logEvent)
{ {
if (logEvent.Level >= LogEventLevel.Error) if (logEvent.Level >= LogEventLevel.Error)
@@ -14,6 +22,7 @@ public class CustomLoggerSink : ILogEventSink
if (logEvent.Exception is not null) if (logEvent.Exception is not null)
message += $" {logEvent.Exception.Message}"; message += $" {logEvent.Exception.Message}";
Debug.WriteLine(message); Debug.WriteLine(message);
_dialogService.Value.ShowToastMessage(message);
} }
} }
} }

View File

@@ -4,12 +4,19 @@ namespace FileTime.ConsoleUI.App.Services;
public class DialogService : DialogServiceBase, IDialogService public class DialogService : DialogServiceBase, IDialogService
{ {
public DialogService(IModalService modalService) : base(modalService) private readonly IConsoleAppState _consoleAppState;
public DialogService(IModalService modalService, IConsoleAppState consoleAppState) : base(modalService)
{ {
_consoleAppState = consoleAppState;
} }
public override void ShowToastMessage(string text) public override void ShowToastMessage(string text)
{ => Task.Run(async () =>
// TODO: Implement {
} _consoleAppState.PopupTexts.Add(text);
await Task.Delay(5000);
_consoleAppState.PopupTexts.Remove(text);
});
} }

View File

@@ -9,8 +9,7 @@ public class ToastMessageSink : ILogEventSink
{ {
private readonly Lazy<IDialogService> _dialogService; private readonly Lazy<IDialogService> _dialogService;
public ToastMessageSink( public ToastMessageSink(IServiceProvider serviceProvider)
IServiceProvider serviceProvider)
{ {
_dialogService = new Lazy<IDialogService>(() => serviceProvider.GetRequiredService<IDialogService>()); _dialogService = new Lazy<IDialogService>(() => serviceProvider.GetRequiredService<IDialogService>());
} }

View File

@@ -301,13 +301,15 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
var x = positionExtension?.Column ?? 0; var x = positionExtension?.Column ?? 0;
var y = positionExtension?.Row ?? 0; var y = positionExtension?.Row ?? 0;
if (x > columns) Debug.Assert(x < columns, "Child requests column outside of grid");
if (x >= columns)
{ {
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y); Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
x = 0; x = 0;
} }
if (y > rows) Debug.Assert(y < rows, "Child requests row outside of grid");
if (y >= rows)
{ {
Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y); Logger?.LogWarning("Child {Child} is out of bounds, x: {X}, y: {Y}", view, x, y);
y = 0; y = 0;
@@ -339,15 +341,27 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
Span<int> allWidth = stackalloc int[columns * rows]; Span<int> allWidth = stackalloc int[columns * rows];
Span<int> allHeight = stackalloc int[columns * rows]; Span<int> allHeight = stackalloc int[columns * rows];
//Store the largest width and height for a cell
foreach (var child in Children) foreach (var child in Children)
{ {
var childSize = child.GetRequestedSize(); var childSize = child.GetRequestedSize();
var (x, y) = GetViewColumnAndRow(child, columns, rows); var (x, y) = GetViewColumnAndRow(child, columns, rows);
allWidth.SetToMatrix(childSize.Width, x, y, columns); var currentWidth = allWidth.GetFromMatrix(x, y, columns);
allHeight.SetToMatrix(childSize.Height, x, y, columns); var currentHeight = allHeight.GetFromMatrix(x, y, columns);
if (currentWidth < childSize.Width)
{
allWidth.SetToMatrix(childSize.Width, x, y, columns);
}
if (currentHeight < childSize.Height)
{
allHeight.SetToMatrix(childSize.Height, x, y, columns);
}
} }
//Calculate the width and height for each column and row
Span<int> columnWidths = stackalloc int[columns]; Span<int> columnWidths = stackalloc int[columns];
Span<int> rowHeights = stackalloc int[rows]; Span<int> rowHeights = stackalloc int[rows];
@@ -407,6 +421,7 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
usedHeight += rowHeights[i]; usedHeight += rowHeights[i];
} }
//Calculate the width and height for each column and row with star value if size of the current grid is given
if (size.IsSome) if (size.IsSome)
{ {
var widthLeft = size.Value.Width - usedWidth; var widthLeft = size.Value.Width - usedWidth;

View File

@@ -31,6 +31,7 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection
RenderMethod RenderMethod { get; set; } RenderMethod RenderMethod { get; set; }
IView? VisualParent { get; set; } IView? VisualParent { get; set; }
ReadOnlyObservableCollection<IView> VisualChildren { get; } ReadOnlyObservableCollection<IView> VisualChildren { get; }
bool IsFocusBoundary { get; set; }
event Action<IView> Disposed; event Action<IView> Disposed;
Size GetRequestedSize(); Size GetRequestedSize();

View File

@@ -38,15 +38,21 @@ public sealed partial class ItemsControl<TDataContext, TItem>
if (_itemsSource is ObservableCollection<TItem> observableDeclarative) if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
{ {
var consumer = new OcConsumer();
_children = observableDeclarative _children = observableDeclarative
.Selecting(i => CreateItem(i)) .Selecting(i => CreateItem(i))
.OfTypeComputing<IView<TItem>>(); .OfTypeComputing<IView<TItem>>()
.For(consumer);
_itemsDisposables.Add(consumer);
} }
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative) else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
{ {
var consumer = new OcConsumer();
_children = readOnlyObservableDeclarative _children = readOnlyObservableDeclarative
.Selecting(i => CreateItem(i)) .Selecting(i => CreateItem(i))
.OfTypeComputing<IView<TItem>>(); .OfTypeComputing<IView<TItem>>()
.For(consumer);
_itemsDisposables.Add(consumer);
} }
else if (_itemsSource is ICollection<TItem> collection) else if (_itemsSource is ICollection<TItem> collection)
_children = collection.Select(CreateItem).OfType<IView<TItem>>().ToList(); _children = collection.Select(CreateItem).OfType<IView<TItem>>().ToList();

View File

@@ -34,6 +34,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
[Notify] private IApplicationContext? _applicationContext; [Notify] private IApplicationContext? _applicationContext;
[Notify] private bool _attached; [Notify] private bool _attached;
[Notify] private IView? _visualParent; [Notify] private IView? _visualParent;
[Notify] private bool _isFocusBoundary;
protected List<Action<TConcrete, GeneralKeyEventArgs>> KeyHandlers { get; } = new(); protected List<Action<TConcrete, GeneralKeyEventArgs>> KeyHandlers { get; } = new();
protected ObservableCollection<IView> VisualChildren { get; } = new(); protected ObservableCollection<IView> VisualChildren { get; } = new();

View File

@@ -1,9 +1,13 @@
using TerminalUI.Traits; using System.Diagnostics.Contracts;
using GeneralInputKey;
using TerminalUI.Controls;
using TerminalUI.Traits;
namespace TerminalUI; namespace TerminalUI;
public class FocusManager : IFocusManager public class FocusManager : IFocusManager
{ {
private readonly IRenderEngine _renderEngine;
private IFocusable? _focused; private IFocusable? _focused;
public IFocusable? Focused public IFocusable? Focused
@@ -32,6 +36,11 @@ public class FocusManager : IFocusManager
private set => _focused = value; private set => _focused = value;
} }
public FocusManager(IRenderEngine renderEngine)
{
_renderEngine = renderEngine;
}
public void SetFocus(IFocusable focusable) => Focused = focusable; public void SetFocus(IFocusable focusable) => Focused = focusable;
public void UnFocus(IFocusable focusable) public void UnFocus(IFocusable focusable)
@@ -39,4 +48,95 @@ public class FocusManager : IFocusManager
if (Focused == focusable) if (Focused == focusable)
Focused = null; Focused = null;
} }
public void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
{
if (keyEventArgs.Handled || Focused is null) return;
if (keyEventArgs.Key == Keys.Tab && keyEventArgs.SpecialKeysStatus.IsShiftPressed)
{
FocusElement(
(views, from) => views.TakeWhile(x => x != from).Reverse(),
c => c.Reverse()
);
keyEventArgs.Handled = true;
}
else if (keyEventArgs.Key == Keys.Tab)
{
FocusElement(
(views, from) => views.SkipWhile(x => x != from).Skip(1),
c => c
);
keyEventArgs.Handled = true;
}
}
private void FocusElement(
Func<IEnumerable<IView>, IView, IEnumerable<IView>> fromChildSelector,
Func<IEnumerable<IView>, IEnumerable<IView>> childSelector
)
{
if (Focused is null) return;
var element = FindElement(Focused,
Focused,
fromChildSelector
);
if (element is null)
{
var topParent = FindLastFocusParent(Focused);
element = FindElement(
topParent,
Focused,
fromChildSelector,
childSelector
);
}
if (element is null) return;
_renderEngine.RequestRerender(element);
_renderEngine.RequestRerender(Focused);
Focused = element;
}
[Pure]
private static IFocusable? FindElement(IView view, IView original,
Func<IEnumerable<IView>, IView, IEnumerable<IView>> fromChildSelector,
Func<IEnumerable<IView>, IEnumerable<IView>>? childSelector = null,
IView? from = null)
{
if (!view.IsVisible) return null;
childSelector ??= views => views;
if (view != original && view is IFocusable focusable)
return focusable;
var visualChildren = from != null && view.VisualChildren.Contains(from)
? fromChildSelector(view.VisualChildren, from)
: childSelector(view.VisualChildren);
foreach (var viewVisualChild in visualChildren)
{
var result = FindElement(viewVisualChild, original, fromChildSelector, childSelector);
if (result is not null)
{
return result;
}
}
if (view.VisualParent is null || view.IsFocusBoundary)
return null;
return FindElement(view.VisualParent, original, fromChildSelector, childSelector, view);
}
[Pure]
private static IView FindLastFocusParent(IView view)
{
if (view.IsFocusBoundary || view.VisualParent is null) return view;
return FindLastFocusParent(view.VisualParent);
}
} }

View File

@@ -1,4 +1,5 @@
using TerminalUI.Traits; using GeneralInputKey;
using TerminalUI.Traits;
namespace TerminalUI; namespace TerminalUI;
@@ -7,4 +8,5 @@ public interface IFocusManager
void SetFocus(IFocusable focusable); void SetFocus(IFocusable focusable);
void UnFocus(IFocusable focusable); void UnFocus(IFocusable focusable);
IFocusable? Focused { get; } IFocusable? Focused { get; }
void HandleKeyInput(GeneralKeyEventArgs keyEventArgs);
} }