From 8aa8d83598c2be9dcb3ace065f84c4150b998be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81d=C3=A1m=20Kov=C3=A1cs?= Date: Mon, 14 Aug 2023 16:42:22 +0200 Subject: [PATCH] Preview refactor, Console rename form --- .../DoubleItemNamePartListPreview.cs | 13 + .../Interactions/DoubleTextListPreview.cs | 11 - .../Interactions/DoubleTextPreview.cs | 11 +- .../Interactions/PreviewList.cs | 11 + .../Interactions/PreviewType.cs | 3 +- .../Services/DefaultModeKeyInputHandler.cs | 5 +- .../RapidTravelModeKeyInputHandler.cs | 9 +- ...emManipulationUserCommandHandlerService.cs | 27 +- .../Styling/ITheme.cs | 1 + src/ConsoleApp/FileTime.ConsoleUI.App/App.cs | 23 +- .../Controls/Dialogs.cs | 356 ++++++++++++++++++ .../FileTime.ConsoleUI.App/MainWindow.cs | 208 +--------- .../FileTime.ConsoleUI.App/Startup.cs | 1 + .../FileTime.ConsoleUI.Styles/DefaultTheme.cs | 3 + .../Views/MainWindow.axaml | 67 +--- .../Views/ReadInputPreview.axaml | 62 +++ .../Views/ReadInputPreview.axaml.cs | 18 + .../GeneralInputKey/GeneralKeyEventArgs.cs | 2 +- src/Library/TerminalUI/Binding.cs | 8 +- .../TerminalUI/Controls/ItemsControl.cs | 16 +- src/Library/TerminalUI/Controls/View.cs | 9 +- src/Library/TerminalUI/EventLoop.cs | 19 +- src/Library/TerminalUI/FocusManager.cs | 70 +++- src/Library/TerminalUI/IFocusManager.cs | 3 + src/Library/TerminalUI/PropertyTrackerBase.cs | 2 +- 25 files changed, 610 insertions(+), 348 deletions(-) create mode 100644 src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleItemNamePartListPreview.cs delete mode 100644 src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextListPreview.cs create mode 100644 src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewList.cs create mode 100644 src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Dialogs.cs create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml create mode 100644 src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml.cs diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleItemNamePartListPreview.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleItemNamePartListPreview.cs new file mode 100644 index 0000000..ba2190f --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleItemNamePartListPreview.cs @@ -0,0 +1,13 @@ +using FileTime.Core.Interactions; +using FileTime.Core.Models; +using PropertyChanged.SourceGenerator; + +namespace FileTime.App.Core.Interactions; + +public partial class DoubleItemNamePartListPreview : IPreviewElement +{ + [Notify] private List _itemNameParts1 = new(); + [Notify] private List _itemNameParts2 = new(); + public PreviewType PreviewType => PreviewType.DoubleItemNamePartList; + object IPreviewElement.PreviewType => PreviewType; +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextListPreview.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextListPreview.cs deleted file mode 100644 index 2530761..0000000 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextListPreview.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.ObjectModel; -using FileTime.Core.Interactions; - -namespace FileTime.App.Core.Interactions; - -public class DoubleTextListPreview : IPreviewElement -{ - public ObservableCollection Items { get; } = new(); - public PreviewType PreviewType { get; } = PreviewType.DoubleTextList; - object IPreviewElement.PreviewType => PreviewType; -} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs index 2942ffe..78dbd5e 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/DoubleTextPreview.cs @@ -1,14 +1,15 @@ using System.Reactive.Subjects; using FileTime.Core.Interactions; using FileTime.Core.Models; +using PropertyChanged.SourceGenerator; namespace FileTime.App.Core.Interactions; -public class DoubleTextPreview : IPreviewElement +public partial class DoubleTextPreview : IPreviewElement { - public IObservable> Text1 { get; init; } = new BehaviorSubject>(new()); - public IObservable> Text2 { get; init; } = new BehaviorSubject>(new()); - - public PreviewType PreviewType => PreviewType.DoubleTextList; + [Notify] private string _text1; + [Notify] private string _text2; + + public PreviewType PreviewType => PreviewType.DoubleText; object IPreviewElement.PreviewType => PreviewType; } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewList.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewList.cs new file mode 100644 index 0000000..64b647f --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewList.cs @@ -0,0 +1,11 @@ +using System.Collections.ObjectModel; +using FileTime.Core.Interactions; + +namespace FileTime.App.Core.Interactions; + +public class PreviewList : IPreviewElement +{ + public ObservableCollection Items { get; } = new(); + public PreviewType PreviewType { get; } = PreviewType.PreviewList; + object IPreviewElement.PreviewType => PreviewType; +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewType.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewType.cs index c98b8db..5b644da 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewType.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Interactions/PreviewType.cs @@ -3,5 +3,6 @@ public enum PreviewType { DoubleText, - DoubleTextList + PreviewList, + DoubleItemNamePartList } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs b/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs index f28984c..ad84386 100644 --- a/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs @@ -61,8 +61,9 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler public async Task HandleInputKey(GeneralKeyEventArgs args) { + if (args.Key is not { } key) return; var keyWithModifiers = new KeyConfig( - args.Key, + key, shift: args.SpecialKeysStatus.IsShiftPressed, alt: args.SpecialKeysStatus.IsAltPressed, ctrl: args.SpecialKeysStatus.IsCtrlPressed); @@ -72,7 +73,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys)); selectedCommandBinding ??= _keyboardConfigurationService.CommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys)); - if (args.Key == Keys.Escape) + if (key == Keys.Escape) { var doGeneralReset = _appState.PreviousKeys.Count > 1; diff --git a/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs b/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs index 85562e7..74fe9ad 100644 --- a/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs @@ -54,9 +54,10 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler public async Task HandleInputKey(GeneralKeyEventArgs args) { - var keyString = args.Key.Humanize(); + if (args.Key is not { } key) return; + var keyString = key.Humanize(); - if (args.Key == Keys.Escape) + if (key == Keys.Escape) { args.Handled = true; if (_modalService.OpenModals.Count > 0) @@ -68,7 +69,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler await CallCommandAsync(ExitRapidTravelCommand.Instance); } } - else if (args.Key == Keys.Backspace) + else if (key == Keys.Backspace) { if (_appState.RapidTravelText.Value!.Length > 0) { @@ -87,7 +88,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler } else { - var currentKeyAsList = new List {new(args.Key)}; + var currentKeyAsList = new List {new(key)}; var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(currentKeyAsList)); if (selectedCommandBinding != null) { diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs index a743530..e638fb0 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/ItemManipulationUserCommandHandlerService.cs @@ -83,7 +83,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi { list.AddRange(_markedItems.Value!); } - else if(_currentSelectedItem?.Value?.BaseItem?.FullName is { } selectedItemName) + else if (_currentSelectedItem?.Value?.BaseItem?.FullName is { } selectedItemName) { list.Add(selectedItemName); } @@ -217,12 +217,15 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi { BehaviorSubject templateRegexValue = new(string.Empty); BehaviorSubject newNameSchemaValue = new(string.Empty); + List subscriptions = new(); var itemsToRename = new List(_markedItems.Value!); var itemPreviews = itemsToRename .Select(item => { + var preview = new DoubleItemNamePartListPreview(); + var originalName = item.GetName(); var decoratedOriginalName = templateRegexValue.Select(templateRegex => @@ -284,17 +287,19 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi } ); - var preview = new DoubleTextPreview - { - Text1 = decoratedOriginalName, - Text2 = text2 - }; + subscriptions.Add(decoratedOriginalName.Subscribe( + n => preview.ItemNameParts1 = n + )); + subscriptions.Add(text2.Subscribe( + n => preview.ItemNameParts2 = n + )); + return preview; } ); - DoubleTextListPreview doubleTextListPreview = new(); - doubleTextListPreview.Items.AddRange(itemPreviews); + PreviewList previewList = new(); + previewList.Items.AddRange(itemPreviews); var templateRegex = new TextInputElement("Template regex", string.Empty, s => templateRegexValue.OnNext(s!)); @@ -303,7 +308,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi var success = await _userCommunicationService.ReadInputs( new[] {templateRegex, newNameSchema}, - new[] {doubleTextListPreview} + new[] {previewList} ); if (success) @@ -338,6 +343,8 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi itemsToMove.AddRange(itemsToMoveWithPath); } } + + subscriptions.ForEach(s => s.Dispose()); } else { @@ -460,6 +467,6 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi _selectedTab?.ClearMarkedItems(); } - private async Task AddCommandAsync(ICommand command) + private async Task AddCommandAsync(ICommand command) => await _commandScheduler.AddCommand(command); } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs index c8e4b25..9eb3f77 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs @@ -5,6 +5,7 @@ namespace FileTime.ConsoleUI.App.Styling; public interface ITheme { IColor? DefaultForegroundColor { get; } + IColor? DefaultForegroundAccentColor { get; } IColor? DefaultBackgroundColor { get; } IColor? ElementColor { get; } IColor? ContainerColor { get; } diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs index 8d8cbe1..27f7c16 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -1,12 +1,11 @@ using System.Collections.Specialized; -using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.ConsoleUI.App.KeyInputHandling; using GeneralInputKey; +using Microsoft.Extensions.Logging; using TerminalUI; using TerminalUI.ConsoleDrivers; -using TerminalUI.Traits; namespace FileTime.ConsoleUI.App; @@ -27,6 +26,7 @@ public class App : IApplication private readonly IApplicationContext _applicationContext; private readonly IConsoleDriver _consoleDriver; private readonly IAppState _appState; + private readonly ILogger _logger; private readonly IKeyInputHandlerService _keyInputHandlerService; private readonly Thread _renderThread; @@ -38,7 +38,8 @@ public class App : IApplication MainWindow mainWindow, IApplicationContext applicationContext, IConsoleDriver consoleDriver, - IAppState appState) + IAppState appState, + ILogger logger) { _lifecycleService = lifecycleService; _keyInputHandlerService = keyInputHandlerService; @@ -48,6 +49,7 @@ public class App : IApplication _applicationContext = applicationContext; _consoleDriver = consoleDriver; _appState = appState; + _logger = logger; _renderThread = new Thread(Render); } @@ -74,12 +76,13 @@ public class App : IApplication while (_applicationContext.IsRunning) { - if (_consoleDriver.CanRead()) + try { - var key = _consoleDriver.ReadKey(); - - if (_appKeyService.MapKey(key.Key) is { } mappedKey) + if (_consoleDriver.CanRead()) { + var key = _consoleDriver.ReadKey(); + + var mappedKey = _appKeyService.MapKey(key.Key); SpecialKeysStatus specialKeysStatus = new( (key.Modifiers & ConsoleModifiers.Alt) != 0, (key.Modifiers & ConsoleModifiers.Shift) != 0, @@ -100,12 +103,16 @@ public class App : IApplication _applicationContext.FocusManager.HandleKeyInput(keyEventArgs); } - if (focused is null || (!keyEventArgs.Handled && KeysToFurtherProcess.Contains(keyEventArgs.Key))) + if (focused is null || (keyEventArgs is {Handled: false, Key: { } k} && KeysToFurtherProcess.Contains(k))) { _keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus); } } } + catch (Exception e) + { + _logger.LogError(e, "Error while handling key input"); + } Thread.Sleep(10); } diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Dialogs.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Dialogs.cs new file mode 100644 index 0000000..8d28a7e --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Dialogs.cs @@ -0,0 +1,356 @@ +using System.Collections.Specialized; +using System.ComponentModel; +using FileTime.App.Core.Interactions; +using FileTime.ConsoleUI.App.Styling; +using FileTime.Core.Interactions; +using FileTime.Core.Models; +using GeneralInputKey; +using TerminalUI.Controls; +using TerminalUI.Extensions; +using TerminalUI.Models; +using TerminalUI.Traits; +using TerminalUI.ViewExtensions; + +namespace FileTime.ConsoleUI.App.Controls; + +public class Dialogs +{ + private readonly IRootViewModel _rootViewModel; + private readonly ITheme _theme; + private ItemsControl _readInputs = null!; + private IInputElement? _inputElementToFocus; + + private Action? _readInputChildHandlerUnSubscriber; + + public Dialogs(IRootViewModel rootViewModel, ITheme theme) + { + _rootViewModel = rootViewModel; + _theme = theme; + + rootViewModel.FocusReadInputElement += element => + { + _inputElementToFocus = element; + UpdateReadInputsFocus(); + }; + } + + private void UpdateReadInputsFocus() + { + foreach (var readInputsChild in _readInputs.Children) + { + if (readInputsChild.DataContext == _inputElementToFocus) + { + if (FindFocusable(readInputsChild) is { } focusable) + { + focusable.Focus(); + _inputElementToFocus = null; + break; + } + } + } + + IFocusable? FindFocusable(IView view) + { + if (view is IFocusable focusable) return focusable; + foreach (var viewVisualChild in view.VisualChildren) + { + if (FindFocusable(viewVisualChild) is { } focusableChild) + return focusableChild; + } + + return null; + } + } + + public IView View() + { + var root = new Border + { + Margin = 5, + BorderThickness = 1, + Content = new Grid + { + ChildInitializer = + { + ReadInputs() + } + } + }; + + root.Bind( + root, + d => d.DialogService.ReadInput.Value != null, + v => v.IsVisible); + + ((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(ItemsControl.Children)) + { + _readInputChildHandlerUnSubscriber?.Invoke(); + + if (_readInputs.Children.Count > 0) + { + UpdateReadInputsFocus(); + } + else + { + _inputElementToFocus = null; + } + + if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged) + { + notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler; + _readInputChildHandlerUnSubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; }; + } + + void NotifyCollectionChangedEventHandler( + object? sender, + NotifyCollectionChangedEventArgs e) + { + UpdateReadInputsFocus(); + } + } + }; + return root; + } + + private IView ReadInputs() + => new Grid + { + RowDefinitionsObject = "Auto Auto", + ChildInitializer = + { + ReadInputsList(), + new ItemsControl + { + ItemTemplate = ReadInputPreviewItemTemplate + } + .Setup(i => i.Bind( + i, + dc => dc.DialogService.ReadInput.Value.Previews, + c => c.ItemsSource + )) + .WithExtension(new GridPositionExtension(0, 1)) + } + }; + + private IView ReadInputPreviewItemTemplate() + { + var grid = new Grid + { + ChildInitializer = + { + new ItemsControl + { + ItemTemplate = ReadInputPreviewItemTemplate + } + .Setup(i => i.Bind( + i, + dc => (PreviewType) dc.PreviewType == PreviewType.PreviewList, + c => c.IsVisible)) + .Setup(i => i.Bind( + i, + dc => ((PreviewList) dc).Items, + c => c.ItemsSource)), + new Grid + { + ColumnDefinitionsObject = "* *", + ChildInitializer = + { + new TextBlock() + .Setup(t => t.Bind( + t, + dc => ((DoubleTextPreview) dc).Text1, + tb => tb.Text + )), + new TextBlock + { + Extensions = + { + new GridPositionExtension(1, 0) + } + } + .Setup(t => t.Bind( + t, + dc => ((DoubleTextPreview) dc).Text2, + tb => tb.Text + )) + } + }.Setup(g => g.Bind( + g, + dc => (PreviewType) dc.PreviewType == PreviewType.DoubleText, + g => g.IsVisible)), + new Grid + { + ColumnDefinitionsObject = "* *", + ChildInitializer = + { + new ItemsControl() + { + Orientation = Orientation.Horizontal, + ItemTemplate = ItemNamePartItemTemplate + }.Setup(i => i.Bind( + i, + dc => ((DoubleItemNamePartListPreview) dc).ItemNameParts1, + c => c.ItemsSource, + v => v)), + new ItemsControl() + { + Orientation = Orientation.Horizontal, + Extensions = + { + new GridPositionExtension(1, 0) + }, + ItemTemplate = ItemNamePartItemTemplate + }.Setup(i => i.Bind( + i, + dc => ((DoubleItemNamePartListPreview) dc).ItemNameParts2, + c => c.ItemsSource)) + } + }.Setup(g => g.Bind( + g, + dc => (PreviewType) dc.PreviewType == PreviewType.DoubleItemNamePartList, + g => g.IsVisible)) + } + }; + + return grid; + + IView ItemNamePartItemTemplate() + { + var textBlock = new TextBlock(); + textBlock.Bind( + textBlock, + dc => dc.Text, + tb => tb.Text + ); + textBlock.Bind( + textBlock, + dc => dc.IsSpecial ? _theme.DefaultForegroundAccentColor : null, + tb => tb.Foreground + ); + + return textBlock; + } + } + + private IView ReadInputsList() + { + var readInputs = new ItemsControl + { + IsFocusBoundary = true, + ItemTemplate = () => + { + var root = new Grid + { + ColumnDefinitionsObject = "* *", + ChildInitializer = + { + new TextBlock() + .Setup(t => t.Bind( + t, + c => c.Label, + tb => tb.Text + )), + new Grid + { + Extensions = + { + new GridPositionExtension(1, 0) + }, + ChildInitializer = + { + new Border + { + Content = + new TextBox() + .Setup(t => t.Bind( + t, + d => ((TextInputElement) d).Value, + tb => tb.Text, + 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) + textInputElement.Value = t; + }) + } + .Setup(t => t.Bind( + t, + d => d.Type == InputType.Text, + tb => tb.IsVisible + )), + new Border + { + Content = + new TextBox + { + PasswordChar = '*' + } + .Setup(t => t.Bind( + t, + d => ((PasswordInputElement) d).Value, + tb => tb.Text, + 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) + textInputElement.Value = t; + }) + } + .Setup(t => t.Bind( + t, + d => d.Type == InputType.Password, + tb => tb.IsVisible + )) + //TODO: OptionInputElement + } + } + } + }; + + return root; + } + } + .Setup(t => t.Bind( + t, + d => d.DialogService.ReadInput.Value.Inputs, + c => c.ItemsSource, + v => v + )); + + readInputs.WithKeyHandler((_, e) => + { + if (e.Key == Keys.Enter) + { + 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; + } + }); + + _readInputs = readInputs; + + return readInputs; + } +} \ 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 f42df9a..ec7187c 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -6,7 +6,6 @@ using FileTime.ConsoleUI.App.Controls; using FileTime.ConsoleUI.App.Styling; using FileTime.Core.Enums; using FileTime.Core.Interactions; -using GeneralInputKey; using TerminalUI; using TerminalUI.Color; using TerminalUI.Controls; @@ -23,57 +22,23 @@ public class MainWindow private readonly IApplicationContext _applicationContext; private readonly ITheme _theme; private readonly CommandPalette _commandPalette; + private readonly Dialogs _dialogs; private readonly Lazy _root; - private ItemsControl _readInputs = null!; - private IInputElement? _inputElementToFocus; - private Action? _readInputChildHandlerUnsubscriber; public MainWindow( IRootViewModel rootViewModel, IApplicationContext applicationContext, ITheme theme, - CommandPalette commandPalette) + CommandPalette commandPalette, + Dialogs dialogs) { _rootViewModel = rootViewModel; _applicationContext = applicationContext; _theme = theme; _commandPalette = commandPalette; + _dialogs = dialogs; _root = new Lazy(Initialize); - - rootViewModel.FocusReadInputElement += element => - { - _inputElementToFocus = element; - UpdateReadInputsFocus(); - }; - } - - private void UpdateReadInputsFocus() - { - foreach (var readInputsChild in _readInputs.Children) - { - if (readInputsChild.DataContext == _inputElementToFocus) - { - if (FindFocusable(readInputsChild) is { } focusable) - { - focusable.Focus(); - _inputElementToFocus = null; - break; - } - } - } - - IFocusable? FindFocusable(IView view) - { - if (view is IFocusable focusable) return focusable; - foreach (var viewVisualChild in view.VisualChildren) - { - if (FindFocusable(viewVisualChild) is { } focusableChild) - return focusableChild; - } - - return null; - } } public IEnumerable RootViews() => new[] @@ -93,28 +58,7 @@ public class MainWindow { MainContent(), _commandPalette.View(), - Dialogs(), - } - }; - - ((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) => - { - if (e.PropertyName == nameof(ItemsControl.Children)) - { - _readInputChildHandlerUnsubscriber?.Invoke(); - UpdateReadInputsFocus(); - if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged) - { - notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler; - _readInputChildHandlerUnsubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; }; - } - - void NotifyCollectionChangedEventHandler( - object? sender, - NotifyCollectionChangedEventArgs e) - { - UpdateReadInputsFocus(); - } + _dialogs.View(), } }; return root; @@ -441,146 +385,4 @@ public class MainWindow (ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemBackgroundColor, _ => throw new NotImplementedException() }; - - private IView Dialogs() - { - var root = new Border() - { - Margin = 5, - BorderThickness = 1, - Content = new Grid() - { - ChildInitializer = - { - ReadInputs() - } - } - }; - - root.Bind( - root, - d => d.DialogService.ReadInput.Value != null, - v => v.IsVisible); - return root; - } - - private ItemsControl ReadInputs() - { - var readInputs = new ItemsControl - { - IsFocusBoundary = true, - ItemTemplate = () => - { - var root = new Grid - { - ColumnDefinitionsObject = "* *", - ChildInitializer = - { - new TextBlock() - .Setup(t => t.Bind( - t, - c => c.Label, - tb => tb.Text - )), - new Grid() - { - Extensions = - { - new GridPositionExtension(1, 0) - }, - ChildInitializer = - { - new Border - { - Content = - new TextBox() - .Setup(t => t.Bind( - t, - d => ((TextInputElement) d).Value, - tb => tb.Text, - 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) - textInputElement.Value = t; - }) - } - .Setup(t => t.Bind( - t, - d => d.Type == InputType.Text, - tb => tb.IsVisible - )), - new Border - { - Content = - new TextBox - { - PasswordChar = '*' - } - .Setup(t => t.Bind( - t, - d => ((PasswordInputElement) d).Value, - tb => tb.Text, - 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) - textInputElement.Value = t; - }) - } - .Setup(t => t.Bind( - t, - d => d.Type == InputType.Password, - tb => tb.IsVisible - )) - //TODO: OptionInputElement - } - } - } - }; - - return root; - } - } - .Setup(t => t.Bind( - t, - d => d.DialogService.ReadInput.Value.Inputs, - c => c.ItemsSource, - v => v - )); - - readInputs.WithKeyHandler((_, e) => - { - if (e.Key == Keys.Enter) - { - 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; - } - }); - - _readInputs = readInputs; - - return readInputs; - } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs index df9a1df..d05678e 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -36,6 +36,7 @@ public static class Startup { services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs index ce35bde..a312c40 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs @@ -6,6 +6,7 @@ namespace FileTime.ConsoleUI.Styles; public record Theme( IColor? DefaultForegroundColor, + IColor? DefaultForegroundAccentColor, IColor? DefaultBackgroundColor, IColor? ElementColor, IColor? ContainerColor, @@ -23,6 +24,7 @@ public static class DefaultThemes { public static Theme Color256Theme => new( DefaultForegroundColor: Color256Colors.Foregrounds.Gray, + DefaultForegroundAccentColor: Color256Colors.Foregrounds.Red, DefaultBackgroundColor: null, ElementColor: Color256Colors.Foregrounds.Gray, ContainerColor: Color256Colors.Foregrounds.Blue, @@ -42,6 +44,7 @@ public static class DefaultThemes public static Theme ConsoleColorTheme => new( DefaultForegroundColor: ConsoleColors.Foregrounds.Gray, + DefaultForegroundAccentColor: ConsoleColors.Foregrounds.Red, DefaultBackgroundColor: null, ElementColor: ConsoleColors.Foregrounds.Gray, ContainerColor: ConsoleColors.Foregrounds.Blue, diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml index 5210bbe..7491ffc 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml @@ -840,72 +840,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml new file mode 100644 index 0000000..b18222f --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml.cs new file mode 100644 index 0000000..baf0f43 --- /dev/null +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/ReadInputPreview.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace FileTime.GuiApp.App.Views; + +public partial class ReadInputPreview : UserControl +{ + public ReadInputPreview() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Library/GeneralInputKey/GeneralKeyEventArgs.cs b/src/Library/GeneralInputKey/GeneralKeyEventArgs.cs index b971b43..fab5eb1 100644 --- a/src/Library/GeneralInputKey/GeneralKeyEventArgs.cs +++ b/src/Library/GeneralInputKey/GeneralKeyEventArgs.cs @@ -4,7 +4,7 @@ public class GeneralKeyEventArgs { private readonly Action? _handledChanged; private bool _handled; - public required Keys Key { get; init; } + public required Keys? Key { get; init; } public required char KeyChar { get; init; } public required SpecialKeysStatus SpecialKeysStatus { get; init; } diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs index 73adfd6..29f792a 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -78,7 +78,13 @@ public sealed class Binding : Property value = _fallbackValue; } - _targetProperty.SetValue(_propertySource, value); + try + { + _targetProperty.SetValue(_propertySource, value); + } + catch + { + } } public override void Dispose() diff --git a/src/Library/TerminalUI/Controls/ItemsControl.cs b/src/Library/TerminalUI/Controls/ItemsControl.cs index 1600685..8a51c46 100644 --- a/src/Library/TerminalUI/Controls/ItemsControl.cs +++ b/src/Library/TerminalUI/Controls/ItemsControl.cs @@ -6,7 +6,7 @@ using TerminalUI.Traits; namespace TerminalUI.Controls; -public sealed partial class ItemsControl +public sealed partial class ItemsControl : View, TDataContext>, IVisibilityChangeHandler { private readonly List _forceRerenderChildren = new(); @@ -17,7 +17,7 @@ public sealed partial class ItemsControl private object? _itemsSource; [Notify] private Orientation _orientation = Orientation.Vertical; - public Func?> ItemTemplate { get; set; } = DefaultItemTemplate; + public Func> ItemTemplate { get; set; } = DefaultItemTemplate; public IReadOnlyList> Children => _children.AsReadOnly(); @@ -41,7 +41,6 @@ public sealed partial class ItemsControl var consumer = new OcConsumer(); _children = observableDeclarative .Selecting(i => CreateItem(i)) - .OfTypeComputing>() .For(consumer); _itemsDisposables.Add(consumer); } @@ -50,16 +49,15 @@ public sealed partial class ItemsControl var consumer = new OcConsumer(); _children = readOnlyObservableDeclarative .Selecting(i => CreateItem(i)) - .OfTypeComputing>() .For(consumer); _itemsDisposables.Add(consumer); } else if (_itemsSource is ICollection collection) - _children = collection.Select(CreateItem).OfType>().ToList(); + _children = collection.Select(CreateItem).ToList(); else if (_itemsSource is TItem[] array) - _children = array.Select(CreateItem).OfType>().ToList(); + _children = array.Select(CreateItem).ToList(); else if (_itemsSource is IEnumerable enumerable) - _children = enumerable.Select(CreateItem).OfType>().ToList(); + _children = enumerable.Select(CreateItem).ToList(); else if (value is null) { _children = new List>(); @@ -173,14 +171,14 @@ public sealed partial class ItemsControl return neededRerender; } - private IView? CreateItem(TItem dataContext) + private IView CreateItem(TItem dataContext) { var newItem = ItemTemplate(); AddChild(newItem, _ => dataContext); return newItem; } - private static IView? DefaultItemTemplate() => null; + private static IView DefaultItemTemplate() => new TextBlock {Text = typeof(TItem).ToString()}; public void ChildVisibilityChanged(IView child) { diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index d24ce4d..6300df3 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics; using System.Runtime.CompilerServices; using GeneralInputKey; using PropertyChanged.SourceGenerator; @@ -321,6 +322,7 @@ public abstract partial class View : IView where TConcrete : Vi public virtual TChild AddChild(TChild child) where TChild : IView { + Debug.Assert(child != null); child.DataContext = DataContext; var mapper = new DataContextMapper(this, child, d => d); SetupNewChild(child, mapper); @@ -331,6 +333,7 @@ public abstract partial class View : IView where TConcrete : Vi public virtual TChild AddChild(TChild child, Func dataContextMapper) where TChild : IView { + Debug.Assert(child != null); child.DataContext = dataContextMapper(DataContext); var mapper = new DataContextMapper(this, child, dataContextMapper); SetupNewChild(child, mapper); @@ -395,7 +398,7 @@ public abstract partial class View : IView where TConcrete : Vi _disposables.Clear(); KeyHandlers.Clear(); - + Disposed?.Invoke(this); } } @@ -411,7 +414,7 @@ public abstract partial class View : IView where TConcrete : Vi { foreach (var keyHandler in KeyHandlers) { - keyHandler((TConcrete)this, keyEventArgs); + keyHandler((TConcrete) this, keyEventArgs); if (keyEventArgs.Handled) return; } } @@ -427,6 +430,6 @@ public abstract partial class View : IView where TConcrete : Vi public TConcrete WithKeyHandler(Action keyHandler) { KeyHandlers.Add(keyHandler); - return (TConcrete)this; + return (TConcrete) this; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index 1f2d17c..e5e8e34 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -1,13 +1,19 @@ -namespace TerminalUI; +using Microsoft.Extensions.Logging; + +namespace TerminalUI; public class EventLoop : IEventLoop { private readonly IApplicationContext _applicationContext; + private readonly ILogger _logger; private readonly List _permanentQueue = new(); - public EventLoop(IApplicationContext applicationContext) + public EventLoop( + IApplicationContext applicationContext, + ILogger logger) { _applicationContext = applicationContext; + _logger = logger; } public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action); @@ -26,7 +32,14 @@ public class EventLoop : IEventLoop { foreach (var action in _permanentQueue) { - action(); + try + { + action(); + } + catch (Exception e) + { + _logger.LogError(e, "Error while processing action in permanent queue"); + } } } } \ No newline at end of file diff --git a/src/Library/TerminalUI/FocusManager.cs b/src/Library/TerminalUI/FocusManager.cs index 7c71b0c..1c86117 100644 --- a/src/Library/TerminalUI/FocusManager.cs +++ b/src/Library/TerminalUI/FocusManager.cs @@ -9,6 +9,7 @@ public class FocusManager : IFocusManager { private readonly IRenderEngine _renderEngine; private IFocusable? _focused; + private DateTime _focusLostCandidateTime = DateTime.MinValue; public IFocusable? Focused { @@ -18,15 +19,30 @@ public class FocusManager : IFocusManager { var visible = _focused.IsVisible; var parent = _focused.VisualParent; - while (parent != null) + while (parent != null && visible) { - visible &= parent.IsVisible; + visible = parent.IsVisible && visible; parent = parent.VisualParent; } if (!visible) { - _focused = null; + if (_focusLostCandidateTime != DateTime.MinValue) + { + if (DateTime.Now - _focusLostCandidateTime > TimeSpan.FromMilliseconds(10)) + { + _focused = null; + _focusLostCandidateTime = DateTime.MinValue; + } + } + else + { + _focusLostCandidateTime = DateTime.Now; + } + } + else + { + _focusLostCandidateTime = DateTime.MinValue; } } @@ -53,52 +69,66 @@ public class FocusManager : IFocusManager { if (keyEventArgs.Handled || Focused is null) return; - if (keyEventArgs.Key == Keys.Tab && keyEventArgs.SpecialKeysStatus.IsShiftPressed) + if (keyEventArgs is {Key: Keys.Tab, SpecialKeysStatus.IsShiftPressed: true}) { - FocusElement( - (views, from) => views.TakeWhile(x => x != from).Reverse(), - c => c.Reverse() - ); + FocusLastElement(Focused); keyEventArgs.Handled = true; } else if (keyEventArgs.Key == Keys.Tab) { - FocusElement( - (views, from) => views.SkipWhile(x => x != from).Skip(1), - c => c - ); + FocusFirstElement(Focused); keyEventArgs.Handled = true; } } + public void FocusFirstElement(IView view, IView? from = null) => + FocusElement( + view, + (views, fromView) => views.SkipWhile(x => x != fromView).Skip(1), + c => c.Reverse(), + from + ); + + public void FocusLastElement(IView view, IView? from = null) => + FocusElement( + view, + (views, fromView) => views.TakeWhile(x => x != fromView).Reverse(), + c => c, + from + ); + private void FocusElement( + IView view, Func, IView, IEnumerable> fromChildSelector, - Func, IEnumerable> childSelector + Func, IEnumerable> childSelector, + IView? from = null ) { if (Focused is null) return; - var element = FindElement(Focused, - Focused, - fromChildSelector + var element = FindElement(view, + view, + fromChildSelector, + from: from ); if (element is null) { - var topParent = FindLastFocusParent(Focused); + var topParent = FindLastFocusParent(view); element = FindElement( topParent, - Focused, + view, fromChildSelector, - childSelector + childSelector, + from ); } if (element is null) return; _renderEngine.RequestRerender(element); - _renderEngine.RequestRerender(Focused); + _renderEngine.RequestRerender(view); Focused = element; } diff --git a/src/Library/TerminalUI/IFocusManager.cs b/src/Library/TerminalUI/IFocusManager.cs index 74af872..39842cc 100644 --- a/src/Library/TerminalUI/IFocusManager.cs +++ b/src/Library/TerminalUI/IFocusManager.cs @@ -1,4 +1,5 @@ using GeneralInputKey; +using TerminalUI.Controls; using TerminalUI.Traits; namespace TerminalUI; @@ -9,4 +10,6 @@ public interface IFocusManager void UnFocus(IFocusable focusable); IFocusable? Focused { get; } void HandleKeyInput(GeneralKeyEventArgs keyEventArgs); + void FocusFirstElement(IView view, IView? from = null); + void FocusLastElement(IView view, IView? from = null); } \ No newline at end of file diff --git a/src/Library/TerminalUI/PropertyTrackerBase.cs b/src/Library/TerminalUI/PropertyTrackerBase.cs index 6bebea7..54aa344 100644 --- a/src/Library/TerminalUI/PropertyTrackerBase.cs +++ b/src/Library/TerminalUI/PropertyTrackerBase.cs @@ -101,7 +101,7 @@ public abstract class PropertyTrackerBase : IDisposa } else if (expression is UnaryExpression unaryExpression) { - SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties)); + return FindReactiveProperties(unaryExpression.Operand, properties); } else if (expression is ParameterExpression parameterExpression) {