Focus next/previous input element with Tab
This commit is contained in:
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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,8 +194,29 @@ 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
|
||||||
|
))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private IView<IRootViewModel> PossibleCommands()
|
private IView<IRootViewModel> PossibleCommands()
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -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>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
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);
|
allWidth.SetToMatrix(childSize.Width, x, y, columns);
|
||||||
allHeight.SetToMatrix(childSize.Height, 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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user