diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs index 794417a..9647d2b 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IConsoleAppState.cs @@ -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 PopupTexts { get; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index 9220598..8d8cbe1 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -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))) diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs index 6ff5d5c..05dcc8d 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/ConsoleAppState.cs @@ -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 PopupTexts { get; } = new(); } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs index 8858c6d..f42df9a 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -123,7 +123,7 @@ public class MainWindow private Grid MainContent() => new() { - RowDefinitionsObject = "Auto * Auto", + RowDefinitionsObject = "Auto * Auto Auto", ChildInitializer = { new Grid @@ -194,7 +194,28 @@ public class MainWindow { PossibleCommands() } - } + }, + new ItemsControl + { + MaxHeight = 5, + Extensions = + { + new GridPositionExtension(0, 3) + }, + ItemTemplate = () => + { + return new TextBlock() + .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 { + IsFocusBoundary = true, ItemTemplate = () => { var root = new Grid @@ -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; } }); diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/CustomLoggerSink.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/CustomLoggerSink.cs index a3b0fa3..ff11885 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/CustomLoggerSink.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/CustomLoggerSink.cs @@ -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 _dialogService; + + public CustomLoggerSink(IServiceProvider serviceProvider) + { + _dialogService = new Lazy(() => serviceProvider.GetRequiredService()); + } + 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); } } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/DialogService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/DialogService.cs index 82ed560..ba04110 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Services/DialogService.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Services/DialogService.cs @@ -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); + }); } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Logging/ToastMessageSink.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Logging/ToastMessageSink.cs index 2b1d8fe..9d3bee0 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Logging/ToastMessageSink.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Logging/ToastMessageSink.cs @@ -9,8 +9,7 @@ public class ToastMessageSink : ILogEventSink { private readonly Lazy _dialogService; - public ToastMessageSink( - IServiceProvider serviceProvider) + public ToastMessageSink(IServiceProvider serviceProvider) { _dialogService = new Lazy(() => serviceProvider.GetRequiredService()); } diff --git a/src/Library/TerminalUI/Controls/Grid.cs b/src/Library/TerminalUI/Controls/Grid.cs index e44bf12..b89ac67 100644 --- a/src/Library/TerminalUI/Controls/Grid.cs +++ b/src/Library/TerminalUI/Controls/Grid.cs @@ -301,13 +301,15 @@ public sealed class Grid : ChildContainerView, 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 : ChildContainerView, T>, IVisibilityChangeH Span allWidth = stackalloc int[columns * rows]; Span 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 columnWidths = stackalloc int[columns]; Span rowHeights = stackalloc int[rows]; @@ -407,6 +421,7 @@ public sealed class Grid : ChildContainerView, 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; diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs index 03b963a..98dff52 100644 --- a/src/Library/TerminalUI/Controls/IView.cs +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -31,6 +31,7 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection RenderMethod RenderMethod { get; set; } IView? VisualParent { get; set; } ReadOnlyObservableCollection VisualChildren { get; } + bool IsFocusBoundary { get; set; } event Action Disposed; Size GetRequestedSize(); diff --git a/src/Library/TerminalUI/Controls/ItemsControl.cs b/src/Library/TerminalUI/Controls/ItemsControl.cs index 1c69b20..1600685 100644 --- a/src/Library/TerminalUI/Controls/ItemsControl.cs +++ b/src/Library/TerminalUI/Controls/ItemsControl.cs @@ -38,15 +38,21 @@ public sealed partial class ItemsControl if (_itemsSource is ObservableCollection observableDeclarative) { + var consumer = new OcConsumer(); _children = observableDeclarative .Selecting(i => CreateItem(i)) - .OfTypeComputing>(); + .OfTypeComputing>() + .For(consumer); + _itemsDisposables.Add(consumer); } else if (_itemsSource is ReadOnlyObservableCollection readOnlyObservableDeclarative) { + var consumer = new OcConsumer(); _children = readOnlyObservableDeclarative .Selecting(i => CreateItem(i)) - .OfTypeComputing>(); + .OfTypeComputing>() + .For(consumer); + _itemsDisposables.Add(consumer); } else if (_itemsSource is ICollection collection) _children = collection.Select(CreateItem).OfType>().ToList(); diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index 18f0dab..d24ce4d 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -34,6 +34,7 @@ public abstract partial class View : IView where TConcrete : Vi [Notify] private IApplicationContext? _applicationContext; [Notify] private bool _attached; [Notify] private IView? _visualParent; + [Notify] private bool _isFocusBoundary; protected List> KeyHandlers { get; } = new(); protected ObservableCollection VisualChildren { get; } = new(); diff --git a/src/Library/TerminalUI/FocusManager.cs b/src/Library/TerminalUI/FocusManager.cs index 68030ba..7c71b0c 100644 --- a/src/Library/TerminalUI/FocusManager.cs +++ b/src/Library/TerminalUI/FocusManager.cs @@ -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, IView, IEnumerable> fromChildSelector, + Func, IEnumerable> 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, IView, IEnumerable> fromChildSelector, + Func, IEnumerable>? 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); + } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IFocusManager.cs b/src/Library/TerminalUI/IFocusManager.cs index 767d5d0..74af872 100644 --- a/src/Library/TerminalUI/IFocusManager.cs +++ b/src/Library/TerminalUI/IFocusManager.cs @@ -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); } \ No newline at end of file