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;
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 { })
{
focused.HandleKeyInput(keyEventArgs);
_applicationContext.FocusManager.HandleKeyInput(keyEventArgs);
}
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;
namespace FileTime.ConsoleUI.App;
@@ -6,4 +7,6 @@ namespace FileTime.ConsoleUI.App;
public partial class ConsoleAppState : AppStateBase, IConsoleAppState
{
[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() =>
new()
{
RowDefinitionsObject = "Auto * Auto",
RowDefinitionsObject = "Auto * Auto Auto",
ChildInitializer =
{
new Grid<IRootViewModel>
@@ -194,7 +194,28 @@ public class MainWindow
{
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>
{
IsFocusBoundary = true,
ItemTemplate = () =>
{
var root = new Grid<IInputElement>
@@ -479,6 +501,10 @@ public class MainWindow
v => v ?? string.Empty,
fallbackValue: string.Empty
))
.Setup(t => t.Bind(
t,
d => ((TextInputElement) d).Label,
tb => tb.Name))
.WithTextHandler((tb, t) =>
{
if (tb.DataContext is TextInputElement textInputElement)
@@ -504,6 +530,10 @@ public class MainWindow
v => v ?? string.Empty,
fallbackValue: string.Empty
))
.Setup(t => t.Bind(
t,
d => ((PasswordInputElement) d).Label,
tb => tb.Name))
.WithTextHandler((tb, t) =>
{
if (tb.DataContext is PasswordInputElement textInputElement)
@@ -537,14 +567,14 @@ public class MainWindow
{
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
readInputsViewModel.Process();
e.Handled = true;
}
else if (e.Key == Keys.Escape)
{
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
readInputsViewModel.Cancel();
e.Handled = true;
}
});

View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using Microsoft.Extensions.DependencyInjection;
using Serilog.Core;
using Serilog.Events;
@@ -6,6 +7,13 @@ namespace FileTime.ConsoleUI.App.Services;
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)
{
if (logEvent.Level >= LogEventLevel.Error)
@@ -14,6 +22,7 @@ public class CustomLoggerSink : ILogEventSink
if (logEvent.Exception is not null)
message += $" {logEvent.Exception.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 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)
{
// TODO: Implement
}
=> Task.Run(async () =>
{
_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;
public ToastMessageSink(
IServiceProvider serviceProvider)
public ToastMessageSink(IServiceProvider serviceProvider)
{
_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 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);
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);
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> allHeight = stackalloc int[columns * rows];
//Store the largest width and height for a cell
foreach (var child in Children)
{
var childSize = child.GetRequestedSize();
var (x, y) = GetViewColumnAndRow(child, columns, rows);
allWidth.SetToMatrix(childSize.Width, x, y, columns);
allHeight.SetToMatrix(childSize.Height, x, y, columns);
var currentWidth = allWidth.GetFromMatrix(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> rowHeights = stackalloc int[rows];
@@ -407,6 +421,7 @@ public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeH
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)
{
var widthLeft = size.Value.Width - usedWidth;

View File

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

View File

@@ -38,15 +38,21 @@ public sealed partial class ItemsControl<TDataContext, TItem>
if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
{
var consumer = new OcConsumer();
_children = observableDeclarative
.Selecting(i => CreateItem(i))
.OfTypeComputing<IView<TItem>>();
.OfTypeComputing<IView<TItem>>()
.For(consumer);
_itemsDisposables.Add(consumer);
}
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
{
var consumer = new OcConsumer();
_children = readOnlyObservableDeclarative
.Selecting(i => CreateItem(i))
.OfTypeComputing<IView<TItem>>();
.OfTypeComputing<IView<TItem>>()
.For(consumer);
_itemsDisposables.Add(consumer);
}
else if (_itemsSource is ICollection<TItem> collection)
_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 bool _attached;
[Notify] private IView? _visualParent;
[Notify] private bool _isFocusBoundary;
protected List<Action<TConcrete, GeneralKeyEventArgs>> KeyHandlers { 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;
public class FocusManager : IFocusManager
{
private readonly IRenderEngine _renderEngine;
private IFocusable? _focused;
public IFocusable? Focused
@@ -32,6 +36,11 @@ public class FocusManager : IFocusManager
private set => _focused = value;
}
public FocusManager(IRenderEngine renderEngine)
{
_renderEngine = renderEngine;
}
public void SetFocus(IFocusable focusable) => Focused = focusable;
public void UnFocus(IFocusable focusable)
@@ -39,4 +48,95 @@ public class FocusManager : IFocusManager
if (Focused == focusable)
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;
@@ -7,4 +8,5 @@ public interface IFocusManager
void SetFocus(IFocusable focusable);
void UnFocus(IFocusable focusable);
IFocusable? Focused { get; }
void HandleKeyInput(GeneralKeyEventArgs keyEventArgs);
}