diff --git a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs index b1d476f..392a725 100644 --- a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs +++ b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs @@ -1,3 +1,4 @@ +using DeclarativeProperty; using FileTime.App.CommandPalette.Models; using FileTime.App.CommandPalette.ViewModels; @@ -5,7 +6,7 @@ namespace FileTime.App.CommandPalette.Services; public interface ICommandPaletteService { - IObservable ShowWindow { get; } + IDeclarativeProperty ShowWindow { get; } void OpenCommandPalette(); void CloseCommandPalette(); IReadOnlyList GetCommands(); diff --git a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs index 79992b1..622c91d 100644 --- a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs @@ -1,12 +1,14 @@ -using FileTime.App.Core.Models; +using DeclarativeProperty; +using FileTime.App.Core.Models; using FileTime.App.Core.ViewModels; using FileTime.App.FuzzyPanel; +using GeneralInputKey; namespace FileTime.App.CommandPalette.ViewModels; public interface ICommandPaletteViewModel : IFuzzyPanelViewModel, IModalViewModel { - IObservable ShowWindow { get; } + IDeclarativeProperty ShowWindow { get; } void Close(); Task HandleKeyUp(GeneralKeyEventArgs keyEventArgs); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs b/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs index 30ff0f7..2332e1b 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs +++ b/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs @@ -1,5 +1,4 @@ -using System.Reactive.Linq; -using System.Reactive.Subjects; +using DeclarativeProperty; using FileTime.App.CommandPalette.Models; using FileTime.App.CommandPalette.ViewModels; using FileTime.App.Core.Services; @@ -11,8 +10,8 @@ public partial class CommandPaletteService : ICommandPaletteService { private readonly IModalService _modalService; private readonly IIdentifiableUserCommandService _identifiableUserCommandService; - private readonly BehaviorSubject _showWindow = new(false); - IObservable ICommandPaletteService.ShowWindow => _showWindow.AsObservable(); + private readonly DeclarativeProperty _showWindow = new(false); + IDeclarativeProperty ICommandPaletteService.ShowWindow => _showWindow; [Notify] ICommandPaletteViewModel? _currentModal; public CommandPaletteService( @@ -24,13 +23,13 @@ public partial class CommandPaletteService : ICommandPaletteService } public void OpenCommandPalette() { - _showWindow.OnNext(true); + _showWindow.SetValueSafe(true); CurrentModal = _modalService.OpenModal(); } public void CloseCommandPalette() { - _showWindow.OnNext(false); + _showWindow.SetValueSafe(false); if (_currentModal is not null) { _modalService.CloseModal(_currentModal); diff --git a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs index 85f102c..3f121e2 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs @@ -1,10 +1,9 @@ -using System.Text; -using FileTime.App.CommandPalette.Services; -using FileTime.App.Core.Configuration; +using FileTime.App.CommandPalette.Services; using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.App.FuzzyPanel; +using GeneralInputKey; using Microsoft.Extensions.Logging; namespace FileTime.App.CommandPalette.ViewModels; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/CommandBindingConfiguration.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/CommandBindingConfiguration.cs index e176662..9631542 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/CommandBindingConfiguration.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/CommandBindingConfiguration.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using GeneralInputKey; namespace FileTime.App.Core.Configuration; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/KeyConfig.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/KeyConfig.cs index a3140af..5e520fd 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/KeyConfig.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Configuration/KeyConfig.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using GeneralInputKey; namespace FileTime.App.Core.Configuration; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/FileTime.App.Core.Abstraction.csproj b/src/AppCommon/FileTime.App.Core.Abstraction/FileTime.App.Core.Abstraction.csproj index 7afc8e6..0d64f1a 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/FileTime.App.Core.Abstraction.csproj +++ b/src/AppCommon/FileTime.App.Core.Abstraction/FileTime.App.Core.Abstraction.csproj @@ -22,6 +22,7 @@ + diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/SpecialKeysStatus.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Models/SpecialKeysStatus.cs deleted file mode 100644 index 99be174..0000000 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Models/SpecialKeysStatus.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace FileTime.App.Core.Models; - -public record struct SpecialKeysStatus(bool IsAltPressed, bool IsShiftPressed, bool IsCtrlPressed); \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IAppKeyService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IAppKeyService.cs index 676a745..d04c528 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IAppKeyService.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IAppKeyService.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using GeneralInputKey; namespace FileTime.App.Core.Services; diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IKeyInputHandler.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IKeyInputHandler.cs index 602ba68..96e6e04 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IKeyInputHandler.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IKeyInputHandler.cs @@ -1,8 +1,9 @@ using FileTime.App.Core.Models; +using GeneralInputKey; namespace FileTime.App.Core.Services; public interface IKeyInputHandler { - Task HandleInputKey(GeneralKeyEventArgs e, SpecialKeysStatus specialKeysStatus); + Task HandleInputKey(GeneralKeyEventArgs e); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Configuration/MainConfiguration.cs b/src/AppCommon/FileTime.App.Core/Configuration/MainConfiguration.cs index e2f024f..f98cd19 100644 --- a/src/AppCommon/FileTime.App.Core/Configuration/MainConfiguration.cs +++ b/src/AppCommon/FileTime.App.Core/Configuration/MainConfiguration.cs @@ -1,6 +1,7 @@ using FileTime.App.Core.Models; using FileTime.App.Core.UserCommand; using FileTime.Providers.LocalAdmin; +using GeneralInputKey; namespace FileTime.App.Core.Configuration; diff --git a/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs b/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs index d02f689..bfceeec 100644 --- a/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/DefaultModeKeyInputHandler.cs @@ -7,6 +7,7 @@ using FileTime.App.Core.ViewModels; using FileTime.Core.Extensions; using FileTime.Core.Models; using FileTime.Core.Models.Extensions; +using GeneralInputKey; using Microsoft.Extensions.Logging; namespace FileTime.App.Core.Services; @@ -57,9 +58,14 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler _keysToSkip.Add(new[] {new KeyConfig(Keys.RWin)}); } - public async Task HandleInputKey(GeneralKeyEventArgs args, SpecialKeysStatus specialKeysStatus) + public async Task HandleInputKey(GeneralKeyEventArgs args) { - var keyWithModifiers = new KeyConfig(args.Key, shift: specialKeysStatus.IsShiftPressed, alt: specialKeysStatus.IsAltPressed, ctrl: specialKeysStatus.IsCtrlPressed); + var keyWithModifiers = new KeyConfig( + args.Key, + shift: args.SpecialKeysStatus.IsShiftPressed, + alt: args.SpecialKeysStatus.IsAltPressed, + ctrl: args.SpecialKeysStatus.IsCtrlPressed); + _appState.PreviousKeys.Add(keyWithModifiers); var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys)); diff --git a/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs b/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs index 8c2d0ff..a05e62d 100644 --- a/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs +++ b/src/AppCommon/FileTime.App.Core/Services/RapidTravelModeKeyInputHandler.cs @@ -5,6 +5,7 @@ using FileTime.App.Core.UserCommand; using FileTime.App.Core.ViewModels; using FileTime.Core.Extensions; using FileTime.Core.Models; +using GeneralInputKey; using Humanizer; using Microsoft.Extensions.Logging; @@ -54,7 +55,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler }); } - public async Task HandleInputKey(GeneralKeyEventArgs args, SpecialKeysStatus specialKeysStatus) + public async Task HandleInputKey(GeneralKeyEventArgs args) { var keyString = args.Key.Humanize(); diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs index 0dc9126..d6acc79 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs @@ -140,10 +140,9 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase return Task.CompletedTask; } - private Task GoByFrequency() + private async Task GoByFrequency() { - _frequencyNavigationService.OpenNavigationWindow(); - return Task.CompletedTask; + await _frequencyNavigationService.OpenNavigationWindow(); } private async Task GoToPath() diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/Services/IFrequencyNavigationService.cs b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/Services/IFrequencyNavigationService.cs index 5d89e73..9d02cd6 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/Services/IFrequencyNavigationService.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/Services/IFrequencyNavigationService.cs @@ -1,12 +1,13 @@ +using DeclarativeProperty; using FileTime.App.FrequencyNavigation.ViewModels; namespace FileTime.App.FrequencyNavigation.Services; public interface IFrequencyNavigationService { - IObservable ShowWindow { get; } + IDeclarativeProperty ShowWindow { get; } IFrequencyNavigationViewModel? CurrentModal { get; } - void OpenNavigationWindow(); + Task OpenNavigationWindow(); void CloseNavigationWindow(); IList GetMatchingContainers(string searchText); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs index f5f6e0b..657ee6e 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation.Abstractions/ViewModels/IFrequencyNavigationViewModel.cs @@ -1,12 +1,14 @@ +using DeclarativeProperty; using FileTime.App.Core.Models; using FileTime.App.Core.ViewModels; using FileTime.App.FuzzyPanel; +using GeneralInputKey; namespace FileTime.App.FrequencyNavigation.ViewModels; public interface IFrequencyNavigationViewModel : IFuzzyPanelViewModel, IModalViewModel { - IObservable ShowWindow { get; } + IDeclarativeProperty ShowWindow { get; } void Close(); Task HandleKeyUp(GeneralKeyEventArgs keyEventArgs); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs b/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs index 2e386d8..d1e82c4 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation/Services/FrequencyNavigationService.cs @@ -1,6 +1,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Text.Json; +using DeclarativeProperty; using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.FrequencyNavigation.Models; @@ -22,10 +23,10 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I private readonly IModalService _modalService; private readonly SemaphoreSlim _saveLock = new(1, 1); private Dictionary _containerScores = new(); - private readonly BehaviorSubject _showWindow = new(false); + private readonly DeclarativeProperty _showWindow = new(false); private readonly string _dbPath; [Notify] IFrequencyNavigationViewModel? _currentModal; - IObservable IFrequencyNavigationService.ShowWindow => _showWindow.AsObservable(); + IDeclarativeProperty IFrequencyNavigationService.ShowWindow => _showWindow; public FrequencyNavigationService( ITabEvents tabEvents, @@ -51,15 +52,15 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I } } - public void OpenNavigationWindow() + public async Task OpenNavigationWindow() { - _showWindow.OnNext(true); + await _showWindow.SetValue(true); CurrentModal = _modalService.OpenModal(); } public void CloseNavigationWindow() { - _showWindow.OnNext(false); + _showWindow.SetValueSafe(false); if (_currentModal is not null) { _modalService.CloseModal(_currentModal); diff --git a/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs b/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs index 686b34b..f7d63f1 100644 --- a/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs +++ b/src/AppCommon/FileTime.App.FrequencyNavigation/ViewModels/FrequencyNavigationViewModel.cs @@ -6,6 +6,7 @@ using FileTime.App.FrequencyNavigation.Services; using FileTime.App.FuzzyPanel; using FileTime.Core.Models; using FileTime.Core.Timeline; +using GeneralInputKey; namespace FileTime.App.FrequencyNavigation.ViewModels; diff --git a/src/AppCommon/FileTime.App.FuzzyPanel.Abstraction/IFuzzyPanelViewModel.cs b/src/AppCommon/FileTime.App.FuzzyPanel.Abstraction/IFuzzyPanelViewModel.cs index 254320e..54eb8ba 100644 --- a/src/AppCommon/FileTime.App.FuzzyPanel.Abstraction/IFuzzyPanelViewModel.cs +++ b/src/AppCommon/FileTime.App.FuzzyPanel.Abstraction/IFuzzyPanelViewModel.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using GeneralInputKey; namespace FileTime.App.FuzzyPanel; diff --git a/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs b/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs index fd9a3c6..fc64302 100644 --- a/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs +++ b/src/AppCommon/FileTime.App.FuzzyPanel/FuzzyPanelViewModel.cs @@ -1,5 +1,7 @@ using System.ComponentModel; +using DeclarativeProperty; using FileTime.App.Core.Models; +using GeneralInputKey; using PropertyChanged.SourceGenerator; namespace FileTime.App.FuzzyPanel; @@ -9,7 +11,7 @@ public abstract partial class FuzzyPanelViewModel : IFuzzyPanelViewModel< private readonly Func _itemEquality; private string _searchText = String.Empty; - [Notify(set: Setter.Protected)] private IObservable _showWindow; + [Notify(set: Setter.Protected)] private IDeclarativeProperty _showWindow; [Notify(set: Setter.Protected)] private List _filteredMatches; [Notify(set: Setter.Protected)] private TItem? _selectedItem; diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj index 9108811..8df2c70 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/FileTime.ConsoleUI.App.Abstractions.csproj @@ -8,6 +8,7 @@ + diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs index 0957840..fed0df6 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/IRootViewModel.cs @@ -1,4 +1,5 @@ -using FileTime.App.Core.ViewModels; +using FileTime.App.CommandPalette.ViewModels; +using FileTime.App.Core.ViewModels; namespace FileTime.ConsoleUI.App; @@ -8,4 +9,5 @@ public interface IRootViewModel IPossibleCommandsViewModel PossibleCommands { get; } string UserName { get; } string MachineName { get; } + ICommandPaletteViewModel CommandPalette { get; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/KeyInputHandling/IKeyInputHandlerService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/KeyInputHandling/IKeyInputHandlerService.cs index e2ca6b8..66fbf6d 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/KeyInputHandling/IKeyInputHandlerService.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/KeyInputHandling/IKeyInputHandlerService.cs @@ -1,4 +1,5 @@ using FileTime.App.Core.Models; +using GeneralInputKey; namespace FileTime.ConsoleUI.App.KeyInputHandling; diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs similarity index 84% rename from src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs rename to src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs index efd29a9..c8e4b25 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/ITheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ITheme.cs @@ -1,6 +1,6 @@ using TerminalUI.Color; -namespace FileTime.ConsoleUI.App; +namespace FileTime.ConsoleUI.App.Styling; public interface ITheme { @@ -14,4 +14,5 @@ public interface ITheme IColor? MarkedSelectedItemBackgroundColor { get; } IColor? SelectedItemColor { get; } IColor? SelectedTabBackgroundColor { get; } + ListViewItemTheme ListViewItemTheme { get; } } \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ListViewItemTheme.cs b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ListViewItemTheme.cs new file mode 100644 index 0000000..5bd868b --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App.Abstractions/Styling/ListViewItemTheme.cs @@ -0,0 +1,8 @@ +using TerminalUI.Color; + +namespace FileTime.ConsoleUI.App.Styling; + +public record ListViewItemTheme( + IColor? SelectedBackgroundColor, + IColor? SelectedForegroundColor +); \ 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 0dc486f..e8ebe26 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/App.cs @@ -3,6 +3,7 @@ using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.ConsoleUI.App.KeyInputHandling; +using GeneralInputKey; using TerminalUI; using TerminalUI.ConsoleDrivers; @@ -54,7 +55,6 @@ public class App : IApplication _applicationContext.IsRunning = false; }; - _mainWindow.Initialize(); foreach (var rootView in _mainWindow.RootViews()) { _applicationContext.EventLoop.AddViewToRender(rootView); @@ -63,6 +63,8 @@ public class App : IApplication _applicationContext.IsRunning = true; _renderThread.Start(); + var focusManager = _applicationContext.FocusManager; + while (_applicationContext.IsRunning) { if (_consoleDriver.CanRead()) @@ -76,13 +78,22 @@ public class App : IApplication (key.Modifiers & ConsoleModifiers.Shift) != 0, (key.Modifiers & ConsoleModifiers.Control) != 0 ); - + var keyEventArgs = new GeneralKeyEventArgs { - Key = mappedKey + Key = mappedKey, + KeyChar = key.KeyChar, + SpecialKeysStatus = specialKeysStatus }; - - _keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus); + + if (focusManager.Focused is { } focused) + { + focused.HandleKeyInput(keyEventArgs); + } + else + { + _keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus); + } } } diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs new file mode 100644 index 0000000..f5d470e --- /dev/null +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs @@ -0,0 +1,129 @@ +using FileTime.App.CommandPalette.Services; +using FileTime.App.CommandPalette.ViewModels; +using FileTime.ConsoleUI.App.Styling; +using GeneralInputKey; +using TerminalUI.Controls; +using TerminalUI.Extensions; +using TerminalUI.Models; +using TerminalUI.ViewExtensions; + +namespace FileTime.ConsoleUI.App.Controls; + +public class CommandPalette +{ + private readonly ITheme _theme; + private readonly ICommandPaletteService _commandPaletteService; + + public CommandPalette(ITheme theme, ICommandPaletteService commandPaletteService) + { + _theme = theme; + _commandPaletteService = commandPaletteService; + } + public Border View() + { + var inputTextBox = new TextBox() + .WithKeyHandler(k => + { + if (k.Key == Keys.Escape) + { + _commandPaletteService.CloseCommandPalette(); + } + }); + + var root = new Border + { + Margin = 5, + Padding = 1, + MaxWidth = 50, + Content = new Grid + { + RowDefinitionsObject = "Auto *", + ChildInitializer = + { + new Border + { + Margin = new Thickness(0, 0, 0, 2), + Content = inputTextBox + }, + new ListView + { + Extensions = + { + new GridPositionExtension(0, 1) + }, + ItemTemplate = item => + { + var root = new Grid + { + ColumnDefinitionsObject = "* Auto", + ChildInitializer = + { + new TextBlock() + .Setup(t => t.Bind( + t, + d => d.Title, + t => t.Text)), + new TextBlock + { + Extensions = + { + new GridPositionExtension(1, 0) + } + } + .Setup(t => t.Bind( + t, + d => d.Shortcuts, + t => t.Text)) + } + }; + + item.Bind( + item.Parent, + d => d.CommandPalette.SelectedItem == item ? _theme.ListViewItemTheme.SelectedBackgroundColor : null, + t => t.Background + ); + + item.Bind( + item.Parent, + d => d.CommandPalette.SelectedItem == item ? _theme.ListViewItemTheme.SelectedForegroundColor : null, + t => t.Foreground + ); + + return root; + } + }.Setup(t => t.Bind( + t, + d => d.CommandPalette.FilteredMatches, + t => t.ItemsSource + )) + .Setup(t => t.Bind( + t, + d => d.CommandPalette.SelectedItem, + t => t.SelectedItem + )) + } + } + }; + + root.WithPropertyChangedHandler(r => r.IsVisible, + (_, _, isVisible) => + { + if (isVisible) + { + inputTextBox.Focus(); + } + else + { + inputTextBox.UnFocus(); + } + }); + + root.Bind( + root, + d => d.CommandPalette.ShowWindow.Value, + t => t.IsVisible, + r => r); + + return root; + } +} \ No newline at end of file diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj b/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj index 11cca50..c03e862 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/FileTime.ConsoleUI.App.csproj @@ -25,8 +25,4 @@ - - - - diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs index e29343e..92aeb72 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/ConsoleAppKeyService.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using FileTime.App.Core.Models; using FileTime.App.Core.Services; +using GeneralInputKey; namespace FileTime.ConsoleUI.App.KeyInputHandling; @@ -70,6 +71,7 @@ public class ConsoleAppKeyService : IAppKeyService {ConsoleKey.Enter, Keys.Enter}, {ConsoleKey.Escape, Keys.Escape}, {ConsoleKey.Backspace, Keys.Backspace}, + {ConsoleKey.Delete, Keys.Delete}, {ConsoleKey.Spacebar, Keys.Space}, {ConsoleKey.PageUp, Keys.PageUp}, {ConsoleKey.PageDown, Keys.PageDown}, diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/KeyInputHandlerService.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/KeyInputHandlerService.cs index 7baf10a..7ca1330 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/KeyInputHandlerService.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/KeyInputHandling/KeyInputHandlerService.cs @@ -2,6 +2,7 @@ using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; +using GeneralInputKey; namespace FileTime.ConsoleUI.App.KeyInputHandling; @@ -23,13 +24,15 @@ public class KeyInputHandlerService : IKeyInputHandlerService public void HandleKeyInput(GeneralKeyEventArgs keyEvent, SpecialKeysStatus specialKeysStatus) { + if (keyEvent.Handled) return; + if (_appState.ViewMode.Value == ViewMode.Default) { - Task.Run(async () => await _defaultModeKeyInputHandler.HandleInputKey(keyEvent, specialKeysStatus)).Wait(); + Task.Run(async () => await _defaultModeKeyInputHandler.HandleInputKey(keyEvent)).Wait(); } else { - Task.Run(async () => await _rapidTravelModeKeyInputHandler.HandleInputKey(keyEvent, specialKeysStatus)).Wait(); + Task.Run(async () => await _rapidTravelModeKeyInputHandler.HandleInputKey(keyEvent)).Wait(); } } } \ 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 314f24a..c632ec7 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/MainWindow.cs @@ -1,5 +1,7 @@ using FileTime.App.Core.Models.Enums; using FileTime.App.Core.ViewModels; +using FileTime.ConsoleUI.App.Controls; +using FileTime.ConsoleUI.App.Styling; using FileTime.Core.Enums; using TerminalUI; using TerminalUI.Color; @@ -15,25 +17,47 @@ public class MainWindow private readonly IRootViewModel _rootViewModel; private readonly IApplicationContext _applicationContext; private readonly ITheme _theme; + private readonly CommandPalette _commandPalette; - private IView _root; + private readonly Lazy _root; public MainWindow( IRootViewModel rootViewModel, IApplicationContext applicationContext, - ITheme theme) + ITheme theme, + CommandPalette commandPalette) { _rootViewModel = rootViewModel; _applicationContext = applicationContext; _theme = theme; + _commandPalette = commandPalette; + _root = new Lazy(Initialize); } - public void Initialize() + public IEnumerable RootViews() => new[] + { + _root.Value + }; + + public Grid Initialize() { var root = new Grid { + Name = "root", DataContext = _rootViewModel, ApplicationContext = _applicationContext, + ChildInitializer = + { + MainContent(), + _commandPalette.View() + } + }; + return root; + } + + private Grid MainContent() => + new() + { RowDefinitionsObject = "Auto * Auto", ChildInitializer = { @@ -108,8 +132,6 @@ public class MainWindow } } }; - _root = root; - } private IView PossibleCommands() { @@ -309,11 +331,6 @@ public class MainWindow return list; } - public IEnumerable RootViews() => new IView[] - { - _root - }; - private IColor? ToForegroundColor(ItemViewMode viewMode, AbsolutePathType absolutePathType) => (viewMode, absolutePathType) switch { diff --git a/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs index 8945808..7afe124 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/RootViewModel.cs @@ -1,4 +1,5 @@ -using FileTime.App.Core.ViewModels; +using FileTime.App.CommandPalette.ViewModels; +using FileTime.App.Core.ViewModels; namespace FileTime.ConsoleUI.App; @@ -8,12 +9,15 @@ public class RootViewModel : IRootViewModel public string MachineName => Environment.MachineName; public IPossibleCommandsViewModel PossibleCommands { get; } public IConsoleAppState AppState { get; } - + public ICommandPaletteViewModel CommandPalette { get; } + public RootViewModel( IConsoleAppState appState, - IPossibleCommandsViewModel possibleCommands) + IPossibleCommandsViewModel possibleCommands, + ICommandPaletteViewModel commandPalette) { AppState = appState; PossibleCommands = possibleCommands; + CommandPalette = commandPalette; } } \ 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 815b24d..4b45525 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.App/Startup.cs @@ -1,7 +1,9 @@ using FileTime.App.Core.Configuration; +using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.ConsoleUI.App.Configuration; +using FileTime.ConsoleUI.App.Controls; using FileTime.ConsoleUI.App.KeyInputHandling; using FileTime.ConsoleUI.App.Services; using FileTime.Core.Interactions; @@ -19,7 +21,6 @@ public static class Startup public static IServiceCollection AddConsoleServices(this IServiceCollection services, IConfigurationRoot configuration) { services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(sp => sp.GetRequiredService()); services.TryAddSingleton(); @@ -29,6 +30,7 @@ public static class Startup services.AddSingleton(); services.TryAddSingleton(new ApplicationConfiguration(true)); services.TryAddSingleton(); + services.TryAddSingleton(); services.Configure(configuration); @@ -36,9 +38,17 @@ public static class Startup => new ApplicationContext { ConsoleDriver = sp.GetRequiredService(), - LoggerFactory = sp.GetRequiredService() + LoggerFactory = sp.GetRequiredService(), + FocusManager = sp.GetRequiredService(), } ); return services; } + + public static IServiceCollection AddConsoleViews(this IServiceCollection services) + { + 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 5e8416b..ce35bde 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI.Styles/DefaultTheme.cs @@ -1,4 +1,5 @@ using FileTime.ConsoleUI.App; +using FileTime.ConsoleUI.App.Styling; using TerminalUI.Color; namespace FileTime.ConsoleUI.Styles; @@ -14,6 +15,7 @@ public record Theme( IColor? MarkedSelectedItemBackgroundColor, IColor? SelectedItemColor, IColor? SelectedTabBackgroundColor, + ListViewItemTheme ListViewItemTheme, Type? ForegroundColors, Type? BackgroundColors) : ITheme, IColorSampleProvider; @@ -30,6 +32,10 @@ public static class DefaultThemes MarkedSelectedItemBackgroundColor: Color256Colors.Foregrounds.Yellow, SelectedItemColor: Color256Colors.Foregrounds.Black, SelectedTabBackgroundColor: Color256Colors.Backgrounds.Green, + ListViewItemTheme: new( + SelectedBackgroundColor: Color256Colors.Backgrounds.Gray, + SelectedForegroundColor: Color256Colors.Foregrounds.Black + ), ForegroundColors: typeof(Color256Colors.Foregrounds), BackgroundColors: typeof(Color256Colors.Backgrounds) ); @@ -45,6 +51,10 @@ public static class DefaultThemes MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Yellow, SelectedItemColor: ConsoleColors.Foregrounds.Black, SelectedTabBackgroundColor: ConsoleColors.Backgrounds.Green, + ListViewItemTheme: new( + SelectedBackgroundColor: ConsoleColors.Backgrounds.Gray, + SelectedForegroundColor: ConsoleColors.Foregrounds.Black + ), ForegroundColors: typeof(ConsoleColors.Foregrounds), BackgroundColors: typeof(ConsoleColors.Backgrounds) ); diff --git a/src/ConsoleApp/FileTime.ConsoleUI/DI.cs b/src/ConsoleApp/FileTime.ConsoleUI/DI.cs index 68de79a..d7d4273 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/DI.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/DI.cs @@ -22,6 +22,7 @@ public static class DI => ServiceProvider = DependencyInjection .RegisterDefaultServices(configuration: configuration) .AddConsoleServices(configuration) + .AddConsoleViews() .AddLocalProviderServices() .AddServerCoreServices() .AddFrequencyNavigation() diff --git a/src/ConsoleApp/FileTime.ConsoleUI/InfoProviders/ColorSchema.cs b/src/ConsoleApp/FileTime.ConsoleUI/InfoProviders/ColorSchema.cs index 10620da..e3d54e6 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/InfoProviders/ColorSchema.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/InfoProviders/ColorSchema.cs @@ -1,4 +1,5 @@ using FileTime.ConsoleUI.App; +using FileTime.ConsoleUI.App.Styling; using TerminalUI.Color; using TerminalUI.ConsoleDrivers; using TerminalUI.Models; diff --git a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs index 9a5e3c9..cb9cfc2 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/Program.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/Program.cs @@ -3,6 +3,7 @@ using FileTime.App.Core; using FileTime.App.Core.Configuration; using FileTime.ConsoleUI; using FileTime.ConsoleUI.App; +using FileTime.ConsoleUI.App.Styling; using FileTime.ConsoleUI.InfoProviders; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/src/ConsoleApp/FileTime.ConsoleUI/Startup.cs b/src/ConsoleApp/FileTime.ConsoleUI/Startup.cs index 966f994..8087141 100644 --- a/src/ConsoleApp/FileTime.ConsoleUI/Startup.cs +++ b/src/ConsoleApp/FileTime.ConsoleUI/Startup.cs @@ -1,5 +1,6 @@ using FileTime.ConsoleUI.App; using FileTime.ConsoleUI.App.Configuration; +using FileTime.ConsoleUI.App.Styling; using FileTime.ConsoleUI.Styles; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/src/FileTime.sln b/src/FileTime.sln index 3b0fdda..a00444f 100644 --- a/src/FileTime.sln +++ b/src/FileTime.sln @@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.Styles", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableComputations.Extensions", "Library\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj", "{6C3C3151-9341-4792-9B0B-A11C0658524E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneralInputKey", "Library\GeneralInputKey\GeneralInputKey.csproj", "{91AE5B64-042B-4660-A8E8-D247E6E14A1E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -339,6 +341,10 @@ Global {6C3C3151-9341-4792-9B0B-A11C0658524E}.Debug|Any CPU.Build.0 = Debug|Any CPU {6C3C3151-9341-4792-9B0B-A11C0658524E}.Release|Any CPU.ActiveCfg = Release|Any CPU {6C3C3151-9341-4792-9B0B-A11C0658524E}.Release|Any CPU.Build.0 = Release|Any CPU + {91AE5B64-042B-4660-A8E8-D247E6E14A1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91AE5B64-042B-4660-A8E8-D247E6E14A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91AE5B64-042B-4660-A8E8-D247E6E14A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91AE5B64-042B-4660-A8E8-D247E6E14A1E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -397,6 +403,7 @@ Global {AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549} {6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} + {91AE5B64-042B-4660-A8E8-D247E6E14A1E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Extensions/KeyEventArgsExtension.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Extensions/KeyEventArgsExtension.cs index 262ae9f..91220bf 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Extensions/KeyEventArgsExtension.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Extensions/KeyEventArgsExtension.cs @@ -1,18 +1,47 @@ using Avalonia.Input; using FileTime.App.Core.Models; using FileTime.App.Core.Services; +using GeneralInputKey; namespace FileTime.GuiApp.App.Extensions; public static class KeyEventArgsExtension { - public static GeneralKeyEventArgs? ToGeneralKeyEventArgs(this KeyEventArgs args, IAppKeyService appKeyService) + public static GeneralKeyEventArgs? ToGeneralKeyEventArgs( + this KeyEventArgs args, + IAppKeyService appKeyService, + SpecialKeysStatus specialKeysStatus) { var maybeKey = appKeyService.MapKey(args.Key); - if (maybeKey is not {} key1) return null; + if (maybeKey is not { } key1) return null; + + var keyString = args.Key.ToString(); return new GeneralKeyEventArgs(h => args.Handled = h) { - Key = key1 + Key = key1, + KeyChar = keyString.Length > 0 ? keyString[0] : '\0', + SpecialKeysStatus = specialKeysStatus + }; + } + + public static GeneralKeyEventArgs? ToGeneralKeyEventArgs( + this KeyEventArgs args, + IAppKeyService appKeyService, + KeyModifiers keyModifiers) + { + var maybeKey = appKeyService.MapKey(args.Key); + if (maybeKey is not { } key1) return null; + + var keyString = args.Key.ToString(); + return new GeneralKeyEventArgs(h => args.Handled = h) + { + Key = key1, + KeyChar = keyString.Length > 0 ? keyString[0] : '\0', + SpecialKeysStatus = new SpecialKeysStatus( + IsAltPressed: (keyModifiers & KeyModifiers.Alt) != 0, + IsShiftPressed: (keyModifiers & KeyModifiers.Shift) != 0, + IsCtrlPressed: (keyModifiers & KeyModifiers.Control) != 0 + ) }; } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/GuiAppKeyService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/GuiAppKeyService.cs index e6c850d..c1cd474 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/GuiAppKeyService.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/GuiAppKeyService.cs @@ -2,6 +2,7 @@ using Avalonia.Input; using FileTime.App.Core.Models; using FileTime.App.Core.Services; +using GeneralInputKey; namespace FileTime.GuiApp.App.Services; @@ -71,6 +72,7 @@ public sealed class GuiAppKeyService : IAppKeyService {Key.Enter, Keys.Enter}, {Key.Escape, Keys.Escape}, {Key.Back, Keys.Backspace}, + {Key.Delete, Keys.Delete}, {Key.Space, Keys.Space}, {Key.PageUp, Keys.PageUp}, {Key.PageDown, Keys.PageDown}, diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/KeyInputHandlerService.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/KeyInputHandlerService.cs index 5747f9f..4da8e11 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/KeyInputHandlerService.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/KeyInputHandlerService.cs @@ -5,6 +5,7 @@ using FileTime.App.Core.Services; using FileTime.GuiApp.App.Extensions; using FileTime.GuiApp.App.Models; using FileTime.GuiApp.App.ViewModels; +using GeneralInputKey; namespace FileTime.GuiApp.App.Services; @@ -50,9 +51,9 @@ public class KeyInputHandlerService : IKeyInputHandlerService //_appState.NoCommandFound = false; - var isAltPressed = (e.KeyModifiers & KeyModifiers.Alt) == KeyModifiers.Alt; - var isShiftPressed = (e.KeyModifiers & KeyModifiers.Shift) == KeyModifiers.Shift; - var isCtrlPressed = (e.KeyModifiers & KeyModifiers.Control) == KeyModifiers.Control; + var isAltPressed = (e.KeyModifiers & KeyModifiers.Alt) != 0; + var isShiftPressed = (e.KeyModifiers & KeyModifiers.Shift) != 0; + var isCtrlPressed = (e.KeyModifiers & KeyModifiers.Control) != 0; if (isCtrlPressed && e.Key is Key.Left or Key.Right or Key.Up or Key.Down @@ -69,16 +70,16 @@ public class KeyInputHandlerService : IKeyInputHandlerService { if (_appState.ViewMode.Value == ViewMode.Default) { - if (e.ToGeneralKeyEventArgs(_appKeyService) is { } args) + if (e.ToGeneralKeyEventArgs(_appKeyService, specialKeyStatus) is { } args) { - await _defaultModeKeyInputHandler.HandleInputKey(args, specialKeyStatus); + await _defaultModeKeyInputHandler.HandleInputKey(args); } } else { - if (e.ToGeneralKeyEventArgs(_appKeyService) is { } args) + if (e.ToGeneralKeyEventArgs(_appKeyService, specialKeyStatus) is { } args) { - await _rapidTravelModeKeyInputHandler.HandleInputKey(args, specialKeyStatus); + await _rapidTravelModeKeyInputHandler.HandleInputKey(args); } } } diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/CommandPalette.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/CommandPalette.axaml.cs index 57e6d58..d600bfb 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/CommandPalette.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/CommandPalette.axaml.cs @@ -39,7 +39,7 @@ public partial class CommandPalette : UserControl } else { - if (e.ToGeneralKeyEventArgs(_appKeyService.Value) is not { } eventArgs) return; + if (e.ToGeneralKeyEventArgs(_appKeyService.Value, e.KeyModifiers) is not { } eventArgs) return; viewModel.HandleKeyDown(eventArgs); } @@ -50,7 +50,7 @@ public partial class CommandPalette : UserControl if (e.Handled || DataContext is not ICommandPaletteViewModel viewModel) return; - if (e.ToGeneralKeyEventArgs(_appKeyService.Value) is not { } eventArgs) return; + if (e.ToGeneralKeyEventArgs(_appKeyService.Value, e.KeyModifiers) is not { } eventArgs) return; viewModel.HandleKeyUp(eventArgs); } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/FrequencyNavigation.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/FrequencyNavigation.axaml.cs index 74f0f2b..967f3b9 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/FrequencyNavigation.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/FrequencyNavigation.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Input; +using FileTime.App.Core.Models; using FileTime.App.Core.Services; using FileTime.App.FrequencyNavigation.ViewModels; using FileTime.GuiApp.App.Extensions; @@ -39,7 +40,7 @@ public partial class FrequencyNavigation : UserControl } else { - if (e.ToGeneralKeyEventArgs(_appKeyService.Value) is not { } eventArgs) return; + if (e.ToGeneralKeyEventArgs(_appKeyService.Value, e.KeyModifiers) is not { } eventArgs) return; viewModel.HandleKeyDown(eventArgs); } @@ -50,7 +51,7 @@ public partial class FrequencyNavigation : UserControl if (e.Handled || DataContext is not IFrequencyNavigationViewModel viewModel) return; - if (e.ToGeneralKeyEventArgs(_appKeyService.Value) is not { } eventArgs) return; + if (e.ToGeneralKeyEventArgs(_appKeyService.Value, e.KeyModifiers) is not { } eventArgs) return; viewModel.HandleKeyUp(eventArgs); } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml index f475ad4..180a64e 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/MainWindow.axaml @@ -972,7 +972,7 @@ Background="{DynamicResource BarelyTransparentBackgroundColor}" DataContext="{Binding FrequencyNavigationService.CurrentModal}" HorizontalAlignment="Stretch" - IsVisible="{Binding ShowWindow^, FallbackValue=False}" + IsVisible="{Binding ShowWindow.Value, FallbackValue=False}" VerticalAlignment="Stretch"> @@ -983,7 +983,7 @@ Background="{DynamicResource BarelyTransparentBackgroundColor}" DataContext="{Binding CommandPaletteService.CurrentModal}" HorizontalAlignment="Stretch" - IsVisible="{Binding ShowWindow^, FallbackValue=False}" + IsVisible="{Binding ShowWindow.Value, FallbackValue=False}" VerticalAlignment="Stretch"> diff --git a/src/Library/GeneralInputKey/GeneralInputKey.csproj b/src/Library/GeneralInputKey/GeneralInputKey.csproj new file mode 100644 index 0000000..cfadb03 --- /dev/null +++ b/src/Library/GeneralInputKey/GeneralInputKey.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/GeneralKeyEventArgs.cs b/src/Library/GeneralInputKey/GeneralKeyEventArgs.cs similarity index 77% rename from src/AppCommon/FileTime.App.Core.Abstraction/Models/GeneralKeyEventArgs.cs rename to src/Library/GeneralInputKey/GeneralKeyEventArgs.cs index 4bc2a76..b971b43 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Models/GeneralKeyEventArgs.cs +++ b/src/Library/GeneralInputKey/GeneralKeyEventArgs.cs @@ -1,10 +1,12 @@ -namespace FileTime.App.Core.Models; +namespace GeneralInputKey; public class GeneralKeyEventArgs { private readonly Action? _handledChanged; private bool _handled; public required Keys Key { get; init; } + public required char KeyChar { get; init; } + public required SpecialKeysStatus SpecialKeysStatus { get; init; } public bool Handled { diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Models/Keys.cs b/src/Library/GeneralInputKey/Keys.cs similarity index 57% rename from src/AppCommon/FileTime.App.Core.Abstraction/Models/Keys.cs rename to src/Library/GeneralInputKey/Keys.cs index 78dbffd..7dcd8e4 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Models/Keys.cs +++ b/src/Library/GeneralInputKey/Keys.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace FileTime.App.Core.Models; +namespace GeneralInputKey; public enum Keys { @@ -49,6 +49,7 @@ public enum Keys Enter, Escape, Backspace, + Delete, Space, PageUp, PageDown, @@ -57,24 +58,14 @@ public enum Keys Tab, LWin, RWin, - [Description("0")] - Num0, - [Description("1")] - Num1, - [Description("2")] - Num2, - [Description("3")] - Num3, - [Description("4")] - Num4, - [Description("5")] - Num5, - [Description("6")] - Num6, - [Description("7")] - Num7, - [Description("8")] - Num8, - [Description("9")] - Num9, + [Description("0")] Num0, + [Description("1")] Num1, + [Description("2")] Num2, + [Description("3")] Num3, + [Description("4")] Num4, + [Description("5")] Num5, + [Description("6")] Num6, + [Description("7")] Num7, + [Description("8")] Num8, + [Description("9")] Num9 } \ No newline at end of file diff --git a/src/Library/GeneralInputKey/SpecialKeysStatus.cs b/src/Library/GeneralInputKey/SpecialKeysStatus.cs new file mode 100644 index 0000000..8c25abc --- /dev/null +++ b/src/Library/GeneralInputKey/SpecialKeysStatus.cs @@ -0,0 +1,6 @@ +namespace GeneralInputKey; + +public record struct SpecialKeysStatus(bool IsAltPressed, bool IsShiftPressed, bool IsCtrlPressed) +{ + public static SpecialKeysStatus Default { get; } = new(false, false, false); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/ApplicationContext.cs b/src/Library/TerminalUI/ApplicationContext.cs index ad5fb04..2f2592f 100644 --- a/src/Library/TerminalUI/ApplicationContext.cs +++ b/src/Library/TerminalUI/ApplicationContext.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using FileTime.App.Core.Models; +using Microsoft.Extensions.Logging; using TerminalUI.ConsoleDrivers; namespace TerminalUI; @@ -6,6 +7,7 @@ namespace TerminalUI; public class ApplicationContext : IApplicationContext { public required IConsoleDriver ConsoleDriver { get; init; } + public required IFocusManager FocusManager { get; init; } public ILoggerFactory? LoggerFactory { get; init; } public IEventLoop EventLoop { get; init; } public bool IsRunning { get; set; } diff --git a/src/Library/TerminalUI/Binding.cs b/src/Library/TerminalUI/Binding.cs index 521da25..73adfd6 100644 --- a/src/Library/TerminalUI/Binding.cs +++ b/src/Library/TerminalUI/Binding.cs @@ -6,7 +6,7 @@ using TerminalUI.Traits; namespace TerminalUI; -public class Binding : IDisposable +public sealed class Binding : PropertyTrackerBase { private readonly Func _dataContextMapper; private IView _dataSourceView; @@ -15,8 +15,6 @@ public class Binding : IDisposable private readonly Func _converter; private readonly TResult? _fallbackValue; private IDisposableCollection? _propertySourceDisposableCollection; - private PropertyTrackTreeItem? _propertyTrackTreeItem; - private IPropertyChangeTracker? _propertyChangeTracker; public Binding( IView dataSourceView, @@ -25,7 +23,7 @@ public class Binding : IDisposable PropertyInfo targetProperty, Func converter, TResult? fallbackValue = default - ) + ) : base(() => dataSourceView.DataContext, dataSourceExpression) { ArgumentNullException.ThrowIfNull(dataSourceView); ArgumentNullException.ThrowIfNull(dataSourceExpression); @@ -39,8 +37,6 @@ public class Binding : IDisposable _converter = converter; _fallbackValue = fallbackValue; - InitTrackingTree(dataSourceExpression); - UpdateTrackers(); dataSourceView.PropertyChanged += View_PropertyChanged; @@ -60,106 +56,6 @@ public class Binding : IDisposable } } - private void InitTrackingTree(Expression> dataContextExpression) - { - var properties = new List(); - FindReactiveProperties(dataContextExpression, properties); - - if (properties.Count > 0) - { - var rootItem = new PropertyTrackTreeItem(); - foreach (var property in properties) - { - var pathParts = property.Split('.'); - var currentItem = rootItem; - for (var i = 0; i < pathParts.Length; i++) - { - if (!currentItem.Children.TryGetValue(pathParts[i], out var child)) - { - child = new PropertyTrackTreeItem(); - currentItem.Children.Add(pathParts[i], child); - } - - currentItem = child; - } - } - - _propertyTrackTreeItem = rootItem; - } - } - - private string? FindReactiveProperties(Expression? expression, List properties) - { - if (expression is null) return ""; - - if (expression is LambdaExpression lambdaExpression) - { - SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties)); - } - else if (expression is ConditionalExpression conditionalExpression) - { - SavePropertyPath(FindReactiveProperties(conditionalExpression.Test, properties)); - SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties)); - SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties)); - } - else if (expression is MemberExpression memberExpression) - { - if (memberExpression.Expression is not null) - { - FindReactiveProperties(memberExpression.Expression, properties); - - if (FindReactiveProperties(memberExpression.Expression, properties) is { } path - && memberExpression.Member is PropertyInfo dataContextPropertyInfo) - { - path += "." + memberExpression.Member.Name; - return path; - } - } - } - else if (expression is MethodCallExpression methodCallExpression) - { - if (methodCallExpression.Object is - { - NodeType: - not ExpressionType.Parameter - and not ExpressionType.Constant - } methodObject) - { - SavePropertyPath(FindReactiveProperties(methodObject, properties)); - } - - foreach (var argument in methodCallExpression.Arguments) - { - SavePropertyPath(FindReactiveProperties(argument, properties)); - } - } - else if (expression is BinaryExpression binaryExpression) - { - SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties)); - SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties)); - } - else if (expression is UnaryExpression unaryExpression) - { - SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties)); - } - else if (expression is ParameterExpression parameterExpression) - { - if (parameterExpression.Type == typeof(TDataContext)) - { - return ""; - } - } - - return null; - - void SavePropertyPath(string? path) - { - if (path is null) return; - path = path.TrimStart('.'); - properties.Add(path); - } - } - private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(IView.DataContext)) return; @@ -168,22 +64,7 @@ public class Binding : IDisposable UpdateTargetProperty(); } - private void UpdateTrackers() - { - if (_propertyChangeTracker is not null) - { - _propertyChangeTracker.Dispose(); - } - - if (_propertyTrackTreeItem is not null) - { - _propertyChangeTracker = PropertyChangeHelper.TraverseDataContext( - _propertyTrackTreeItem, - _dataSourceView.DataContext, - UpdateTargetProperty - ); - } - } + protected override void Update(string propertyPath) => UpdateTargetProperty(); private void UpdateTargetProperty() { @@ -200,8 +81,9 @@ public class Binding : IDisposable _targetProperty.SetValue(_propertySource, value); } - public void Dispose() + public override void Dispose() { + base.Dispose(); _propertySourceDisposableCollection?.RemoveDisposable(this); _dataSourceView.RemoveDisposable(this); _dataSourceView.PropertyChanged -= View_PropertyChanged; diff --git a/src/Library/TerminalUI/Color/Color256.cs b/src/Library/TerminalUI/Color/Color256.cs index 65dadf7..b56094e 100644 --- a/src/Library/TerminalUI/Color/Color256.cs +++ b/src/Library/TerminalUI/Color/Color256.cs @@ -2,7 +2,7 @@ namespace TerminalUI.Color; -public record struct Color256(byte Color, ColorType Type) : IColor +public readonly record struct Color256(byte Color, ColorType Type) : IColor { public string ToConsoleColor() => Type switch diff --git a/src/Library/TerminalUI/Color/ColorRGB.cs b/src/Library/TerminalUI/Color/ColorRGB.cs index 3a49e35..e9eec05 100644 --- a/src/Library/TerminalUI/Color/ColorRGB.cs +++ b/src/Library/TerminalUI/Color/ColorRGB.cs @@ -2,7 +2,7 @@ namespace TerminalUI.Color; -public record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor +public readonly record struct ColorRgb(byte R, byte G, byte B, ColorType Type) : IColor { public string ToConsoleColor() => Type switch diff --git a/src/Library/TerminalUI/Color/ConsoleColor.cs b/src/Library/TerminalUI/Color/ConsoleColor.cs index d014a3b..4c71a77 100644 --- a/src/Library/TerminalUI/Color/ConsoleColor.cs +++ b/src/Library/TerminalUI/Color/ConsoleColor.cs @@ -1,6 +1,6 @@ namespace TerminalUI.Color; -public record ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor +public readonly record struct ConsoleColor(System.ConsoleColor Color, ColorType Type) : IColor { public string ToConsoleColor() => throw new NotImplementedException(); public IColor AsForeground() => this with {Type = ColorType.Foreground}; diff --git a/src/Library/TerminalUI/Controls/Border.cs b/src/Library/TerminalUI/Controls/Border.cs new file mode 100644 index 0000000..1ce97ae --- /dev/null +++ b/src/Library/TerminalUI/Controls/Border.cs @@ -0,0 +1,157 @@ +using PropertyChanged.SourceGenerator; +using TerminalUI.Models; + +namespace TerminalUI.Controls; + +public partial class Border : ContentView +{ + [Notify] private Thickness _borderThickness = 1; + [Notify] private Thickness _padding = 0; + [Notify] private char _topChar = '─'; + [Notify] private char _leftChar = '│'; + [Notify] private char _rightChar = '│'; + [Notify] private char _bottomChar = '─'; + [Notify] private char _topLeftChar = '┌'; + [Notify] private char _topRightChar = '┐'; + [Notify] private char _bottomLeftChar = '└'; + [Notify] private char _bottomRightChar = '┘'; + + public Border() + { + RerenderProperties.Add(nameof(BorderThickness)); + RerenderProperties.Add(nameof(Padding)); + RerenderProperties.Add(nameof(TopChar)); + RerenderProperties.Add(nameof(LeftChar)); + RerenderProperties.Add(nameof(RightChar)); + RerenderProperties.Add(nameof(BottomChar)); + } + + protected override Size CalculateSize() + { + var size = new Size( + _borderThickness.Left + _borderThickness.Right + _padding.Left + _padding.Right, + _borderThickness.Top + _borderThickness.Bottom + _padding.Top + _padding.Bottom + ); + if (Content is null || !Content.IsVisible) return size; + + var contentSize = Content.GetRequestedSize(); + return new Size(contentSize.Width + size.Width, contentSize.Height + size.Height); + } + + protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size) + { + if (ContentRendererMethod is null) + { + throw new NullReferenceException( + nameof(ContentRendererMethod) + + " is null, cannot render content of " + + Content?.GetType().Name + + " with DataContext of " + + DataContext?.GetType().Name); + } + + var childPosition = new Position(X: position.X + _borderThickness.Left, Y: position.Y + _borderThickness.Top); + var childSize = new Size( + Width: size.Width - _borderThickness.Left - _borderThickness.Right, + Height: size.Height - _borderThickness.Top - _borderThickness.Bottom + ); + + if (_padding.Left > 0 || _padding.Top > 0 || _padding.Right > 0 || _padding.Bottom > 0) + { + childPosition = new Position(X: childPosition.X + _padding.Left, Y: childPosition.Y + _padding.Top); + childSize = new Size( + Width: childSize.Width - _padding.Left - _padding.Right, + Height: childSize.Height - _padding.Top - _padding.Bottom + ); + } + + var contentRendered = ContentRendererMethod(renderContext, childPosition, childSize); + + if (contentRendered) + { + var driver = renderContext.ConsoleDriver; + driver.ResetColor(); + SetColorsForDriver(renderContext); + + RenderTopBorder(renderContext, position, size); + RenderBottomBorder(renderContext, position, size); + RenderLeftBorder(renderContext, position, size); + RenderRightBorder(renderContext, position, size); + + RenderTopLeftCorner(renderContext, position); + RenderTopRightCorner(renderContext, position, size); + RenderBottomLeftCorner(renderContext, position, size); + RenderBottomRightCorner(renderContext, position, size); + + //TODO render padding + } + + return contentRendered; + } + + private void RenderTopBorder(RenderContext renderContext, Position position, Size size) + { + position = position with {X = position.X + _borderThickness.Left}; + size = new Size(Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: _borderThickness.Top); + RenderText(_topChar, renderContext.ConsoleDriver, position, size); + } + + private void RenderBottomBorder(RenderContext renderContext, Position position, Size size) + { + position = new Position(X: position.X + _borderThickness.Left, Y: position.Y + size.Height - _borderThickness.Bottom); + size = new Size(Width: size.Width - _borderThickness.Left - _borderThickness.Right, Height: _borderThickness.Bottom); + RenderText(_bottomChar, renderContext.ConsoleDriver, position, size); + } + + private void RenderLeftBorder(RenderContext renderContext, Position position, Size size) + { + position = position with {Y = position.Y + _borderThickness.Top}; + size = new Size(Width: _borderThickness.Left, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom); + RenderText(_leftChar, renderContext.ConsoleDriver, position, size); + } + + private void RenderRightBorder(RenderContext renderContext, Position position, Size size) + { + position = new Position(X: position.X + size.Width - _borderThickness.Right, Y: position.Y + _borderThickness.Top); + size = new Size(Width: _borderThickness.Right, Height: size.Height - _borderThickness.Top - _borderThickness.Bottom); + RenderText(_rightChar, renderContext.ConsoleDriver, position, size); + } + + private void RenderTopLeftCorner(RenderContext renderContext, Position position) + { + if (_borderThickness.Left == 0 || _borderThickness.Top == 0) return; + + var size = new Size(Width: _borderThickness.Left, Height: _borderThickness.Top); + RenderText(_topLeftChar, renderContext.ConsoleDriver, position, size); + } + + private void RenderTopRightCorner(RenderContext renderContext, Position position, Size size) + { + if (_borderThickness.Right == 0 || _borderThickness.Top == 0) return; + + position = position with {X = position.X + size.Width - _borderThickness.Right}; + size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Top); + RenderText(_topRightChar, renderContext.ConsoleDriver, position, size); + } + + private void RenderBottomLeftCorner(RenderContext renderContext, Position position, Size size) + { + if (_borderThickness.Left == 0 || _borderThickness.Bottom == 0) return; + + position = position with {Y = position.Y + size.Height - _borderThickness.Bottom}; + size = new Size(Width: _borderThickness.Left, Height: _borderThickness.Bottom); + RenderText(_bottomLeftChar, renderContext.ConsoleDriver, position, size); + } + + private void RenderBottomRightCorner(RenderContext renderContext, Position position, Size size) + { + if (_borderThickness.Right == 0 || _borderThickness.Bottom == 0) return; + + position = new Position( + X: position.X + size.Width - _borderThickness.Right, + Y: position.Y + size.Height - _borderThickness.Bottom + ); + size = new Size(Width: _borderThickness.Right, Height: _borderThickness.Bottom); + RenderText(_bottomRightChar, renderContext.ConsoleDriver, position, size); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ChildContainerView.cs b/src/Library/TerminalUI/Controls/ChildContainerView.cs index 0da6fd6..381f3cc 100644 --- a/src/Library/TerminalUI/Controls/ChildContainerView.cs +++ b/src/Library/TerminalUI/Controls/ChildContainerView.cs @@ -1,11 +1,13 @@ using System.Collections.ObjectModel; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; namespace TerminalUI.Controls; public abstract class ChildContainerView : View, IChildContainer { private readonly ObservableCollection _children = new(); + private readonly Dictionary _visibilities = new(); public ReadOnlyObservableCollection Children { get; } public ChildInitializer ChildInitializer { get; } @@ -28,8 +30,8 @@ public abstract class ChildContainerView : View, IChildContainer ApplicationContext?.EventLoop.RequestRerender(); } }; - - ((INotifyPropertyChanged)this).PropertyChanged += (o, args) => + + ((INotifyPropertyChanged) this).PropertyChanged += (o, args) => { if (args.PropertyName == nameof(ApplicationContext)) { @@ -41,14 +43,18 @@ public abstract class ChildContainerView : View, IChildContainer }; } - protected override void AttachChildren() + protected void SaveVisibilities() { - foreach (var child in Children) + _visibilities.Clear(); + foreach (var child in _children) { - child.Attached = true; + _visibilities[child] = child.IsVisible; } } + protected bool? GetLastVisibility(IView view) + => _visibilities.TryGetValue(view, out var visibility) ? visibility : null; + public override TChild AddChild(TChild child) { child = base.AddChild(child); diff --git a/src/Library/TerminalUI/Controls/ContentView.cs b/src/Library/TerminalUI/Controls/ContentView.cs index 118a83a..709bef3 100644 --- a/src/Library/TerminalUI/Controls/ContentView.cs +++ b/src/Library/TerminalUI/Controls/ContentView.cs @@ -40,18 +40,9 @@ public abstract partial class ContentView : View, IContentRenderer RerenderProperties.Add(nameof(ContentRendererMethod)); } - protected override void AttachChildren() - { - base.AttachChildren(); - if (Content is not null) - { - Content.Attached = true; - } - } - private bool DefaultContentRender(RenderContext renderContext, Position position, Size size) { - if (Content is null) + if (Content is null || !Content.IsVisible) { if (_placeholderRenderDone) return false; _placeholderRenderDone = true; diff --git a/src/Library/TerminalUI/Controls/Grid.cs b/src/Library/TerminalUI/Controls/Grid.cs index 9ad331f..6e94ed7 100644 --- a/src/Library/TerminalUI/Controls/Grid.cs +++ b/src/Library/TerminalUI/Controls/Grid.cs @@ -1,5 +1,4 @@ -using System.Collections.ObjectModel; -using System.Diagnostics; +using System.Diagnostics; using Microsoft.Extensions.Logging; using TerminalUI.Extensions; using TerminalUI.Models; @@ -13,9 +12,9 @@ public class Grid : ChildContainerView private List _columnDefinitions = new() {ColumnDefinition.Star(1)}; private ILogger>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger>(); - private delegate void WithSizes(RenderContext renderContext, Span widths, Span heights); + private delegate void WithSizes(RenderContext renderContext, ReadOnlySpan widths, ReadOnlySpan heights); - private delegate TResult WithSizes(RenderContext renderContext, Span widths, Span heights); + private delegate TResult WithSizes(RenderContext renderContext, ReadOnlySpan widths, ReadOnlySpan heights); private const int ToBeCalculated = -1; @@ -132,14 +131,14 @@ public class Grid : ChildContainerView var width = 0; var height = 0; - for (var i = 0; i < columnWidths.Length; i++) + foreach (var t in columnWidths) { - width += columnWidths[i]; + width += t; } - for (var i = 0; i < rowHeights.Length; i++) + foreach (var t in rowHeights) { - height += rowHeights[i]; + height += t; } return new Size(width, height); @@ -151,49 +150,122 @@ public class Grid : ChildContainerView new Option(size, true), (context, columnWidths, rowHeights) => { - foreach (var child in Children) + context = new RenderContext( + context.ConsoleDriver, + context.ForceRerender, + Foreground ?? context.Foreground, + Background ?? context.Background + ); + var viewsByPosition = GroupViewsByPosition(columnWidths.Length, rowHeights.Length); + + for (var column = 0; column < columnWidths.Length; column++) { - var (x, y) = GetViewColumnAndRow(child, columnWidths.Length, rowHeights.Length); - - var width = columnWidths[x]; - var height = rowHeights[y]; - - var left = position.X; - var top = position.Y; - - for (var i = 0; i < x; i++) + for (var row = 0; row < rowHeights.Length; row++) { - left += columnWidths[i]; + RenderViewsByPosition( + context, + position, + columnWidths, + rowHeights, + viewsByPosition, + column, + row + ); } - - for (var i = 0; i < y; i++) - { - top += rowHeights[i]; - } - - child.Render(context, new Position(left, top), new Size(width, height)); } return true; - - /*var viewsByPosition = GroupViewsByPosition(columnWidths, rowHeights); - CleanUnusedArea(viewsByPosition, columnWidths, rowHeights);*/ }); - /*private void CleanUnusedArea(Dictionary<(int, int),List> viewsByPosition, Span columnWidths, Span rowHeights) + private void RenderViewsByPosition( + RenderContext context, + Position gridPosition, + ReadOnlySpan columnWidths, + ReadOnlySpan rowHeights, + IReadOnlyDictionary<(int, int), List> viewsByPosition, + int column, + int row) { - for (var x = 0; x < columnWidths.Length; x++) + if (!viewsByPosition.TryGetValue((column, row), out var children)) return; + + var anyChangedVisibility = false; + + foreach (var child in children) { - for (var y = 0; y < rowHeights.Length; y++) + var lastVisibility = GetLastVisibility(child); + if (lastVisibility is { } b && b != child.IsVisible) { - if (!viewsByPosition.TryGetValue((x, y), out var list)) continue; - - + anyChangedVisibility = true; + break; } } - }*/ - /*private Dictionary<(int, int), List> GroupViewsByPosition(int columns, int rows) + var width = columnWidths[column]; + var height = rowHeights[row]; + var renderSize = new Size(width, height); + + var renderPosition = GetRenderPosition( + gridPosition, + columnWidths, + rowHeights, + column, + row + ); + + var needsRerender = anyChangedVisibility; + if (needsRerender) + { + context = new RenderContext( + context.ConsoleDriver, + true, + context.Foreground, + context.Background + ); + RenderEmpty(context, renderPosition, renderSize); + } + + //This implies that children further back in the list will be rendered on top of children placed before in the list. + foreach (var child in children.Where(child => child.IsVisible)) + { + var rendered = child.Render(context, renderPosition, renderSize); + if (rendered && !needsRerender) + { + needsRerender = true; + context = new RenderContext( + context.ConsoleDriver, + true, + context.Foreground, + context.Background + ); + } + } + + static Position GetRenderPosition( + Position gridPosition, + ReadOnlySpan columnWidths, + ReadOnlySpan rowHeights, + int column, + int row + ) + { + var left = gridPosition.X; + var top = gridPosition.Y; + + for (var i = 0; i < column; i++) + { + left += columnWidths[i]; + } + + for (var i = 0; i < row; i++) + { + top += rowHeights[i]; + } + + return new Position(left, top); + } + } + + private Dictionary<(int, int), List> GroupViewsByPosition(int columns, int rows) { Dictionary, List> viewsByPosition = new(); foreach (var child in Children) @@ -210,7 +282,7 @@ public class Grid : ChildContainerView } return viewsByPosition; - }*/ + } private ValueTuple GetViewColumnAndRow(IView view, int columns, int rows) { @@ -237,7 +309,7 @@ public class Grid : ChildContainerView { WithCalculatedSize(renderContext, size, Helper); - object? Helper(RenderContext renderContext1, Span widths, Span heights) + object? Helper(RenderContext renderContext1, ReadOnlySpan widths, ReadOnlySpan heights) { actionWithSizes(renderContext1, widths, heights); return null; diff --git a/src/Library/TerminalUI/Controls/IView.cs b/src/Library/TerminalUI/Controls/IView.cs index bcd9f92..b703a51 100644 --- a/src/Library/TerminalUI/Controls/IView.cs +++ b/src/Library/TerminalUI/Controls/IView.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using TerminalUI.Color; using TerminalUI.Models; using TerminalUI.Traits; @@ -17,9 +18,12 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection int? MaxHeight { get; set; } int? Height { get; set; } int ActualHeight { get; } - Margin Margin { get; set; } + Thickness Margin { get; set; } + bool IsVisible { get; set; } bool Attached { get; set; } string? Name { get; set; } + IColor? Foreground { get; set; } + IColor? Background { get; set; } IApplicationContext? ApplicationContext { get; set; } List Extensions { get; } RenderMethod RenderMethod { get; set; } diff --git a/src/Library/TerminalUI/Controls/ListView.cs b/src/Library/TerminalUI/Controls/ListView.cs index b2fc53b..b07cd3b 100644 --- a/src/Library/TerminalUI/Controls/ListView.cs +++ b/src/Library/TerminalUI/Controls/ListView.cs @@ -9,12 +9,12 @@ namespace TerminalUI.Controls; public partial class ListView : View { - private static readonly ArrayPool> ListViewItemPool = ArrayPool>.Shared; + private static readonly ArrayPool> ListViewItemPool = ArrayPool>.Shared; private readonly List _itemsDisposables = new(); private Func?>? _getItems; private object? _itemsSource; - private ListViewItem[]? _listViewItems; + private ListViewItem[]? _listViewItems; private int _listViewItemLength; private int _selectedIndex = 0; private int _renderStartIndex = 0; @@ -30,6 +30,14 @@ public partial class ListView : View if (_selectedIndex != value) { _selectedIndex = value; + if (_listViewItems is not null) + { + for (var i = 0; i < _listViewItemLength; i++) + { + _listViewItems[i].IsSelected = i == value; + } + } + OnPropertyChanged(); OnPropertyChanged(nameof(SelectedItem)); } @@ -124,7 +132,7 @@ public partial class ListView : View } } - public Func, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; + public Func, IView?> ItemTemplate { get; set; } = DefaultItemTemplate; public ListView() { @@ -292,7 +300,7 @@ public partial class ListView : View return true; } - private Span> InstantiateItemViews() + private ReadOnlySpan> InstantiateItemViews() { var items = _getItems?.Invoke()?.ToList(); if (items is null) @@ -305,7 +313,7 @@ public partial class ListView : View return _listViewItems; } - Span> listViewItems; + ReadOnlySpan> listViewItems; if (_listViewItems is null || _listViewItemLength != items.Count) { @@ -313,7 +321,8 @@ public partial class ListView : View for (var i = 0; i < items.Count; i++) { var dataContext = items[i]; - var child = CreateChild, TItem>(_ => dataContext); + var child = new ListViewItem(this); + AddChild(child, _ => dataContext); var newContent = ItemTemplate(child); child.Content = newContent; newListViewItems[i] = child; @@ -336,12 +345,12 @@ public partial class ListView : View return listViewItems; } - private Span> InstantiateEmptyItemViews() + private ReadOnlySpan> InstantiateEmptyItemViews() { _listViewItems = ListViewItemPool.Rent(0); _listViewItemLength = 0; return _listViewItems; } - private static IView? DefaultItemTemplate(ListViewItem listViewItem) => null; + private static IView? DefaultItemTemplate(ListViewItem listViewItem) => null; } \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/ListViewItem.cs b/src/Library/TerminalUI/Controls/ListViewItem.cs index a4097e7..e0270e2 100644 --- a/src/Library/TerminalUI/Controls/ListViewItem.cs +++ b/src/Library/TerminalUI/Controls/ListViewItem.cs @@ -1,12 +1,23 @@ -using TerminalUI.Models; +using PropertyChanged.SourceGenerator; +using TerminalUI.Models; namespace TerminalUI.Controls; -public class ListViewItem : ContentView +public partial class ListViewItem : ContentView { + public ListView Parent { get; } + [Notify] private bool _isSelected; + + public ListViewItem(ListView parent) + { + Parent = parent; + + RerenderProperties.Add(nameof(IsSelected)); + } + protected override Size CalculateSize() { - if (Content is null) return new Size(0, 0); + if (Content is null || !Content.IsVisible) return new Size(0, 0); return Content.GetRequestedSize(); } diff --git a/src/Library/TerminalUI/Controls/Rectangle.cs b/src/Library/TerminalUI/Controls/Rectangle.cs index 6b2593a..c8ff4bf 100644 --- a/src/Library/TerminalUI/Controls/Rectangle.cs +++ b/src/Library/TerminalUI/Controls/Rectangle.cs @@ -19,7 +19,7 @@ public partial class Rectangle : View protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size) { var renderState = new RenderState(position, size, Fill); - if (!NeedsRerender(renderState) || Fill is null) return false; + if ((!renderContext.ForceRerender && !NeedsRerender(renderState)) || Fill is null) return false; _lastRenderState = renderState; var driver = renderContext.ConsoleDriver; @@ -34,7 +34,7 @@ public partial class Rectangle : View driver.SetCursorPosition(position with {Y = position.Y + i}); driver.Write(s); } - + return true; } diff --git a/src/Library/TerminalUI/Controls/StackPanel.cs b/src/Library/TerminalUI/Controls/StackPanel.cs index 43d915d..a8307fe 100644 --- a/src/Library/TerminalUI/Controls/StackPanel.cs +++ b/src/Library/TerminalUI/Controls/StackPanel.cs @@ -17,6 +17,8 @@ public partial class StackPanel : ChildContainerView foreach (var child in Children) { + if (!child.IsVisible) continue; + var childSize = child.GetRequestedSize(); _requestedSizes.Add(child, childSize); @@ -41,6 +43,8 @@ public partial class StackPanel : ChildContainerView var neededRerender = false; foreach (var child in Children) { + if (!child.IsVisible) continue; + if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found"); var childPosition = Orientation == Orientation.Vertical @@ -49,12 +53,13 @@ public partial class StackPanel : ChildContainerView var endX = position.X + size.Width; var endY = position.Y + size.Height; - + if (childPosition.X > endX || childPosition.Y > endY) break; if (childPosition.X + childSize.Width > endX) { childSize = childSize with {Width = endX - childPosition.X}; } + if (childPosition.Y + childSize.Height > endY) { childSize = childSize with {Height = endY - childPosition.Y}; diff --git a/src/Library/TerminalUI/Controls/TextBlock.cs b/src/Library/TerminalUI/Controls/TextBlock.cs index a215253..6c7730d 100644 --- a/src/Library/TerminalUI/Controls/TextBlock.cs +++ b/src/Library/TerminalUI/Controls/TextBlock.cs @@ -1,12 +1,13 @@ using System.ComponentModel; +using System.Diagnostics; using PropertyChanged.SourceGenerator; using TerminalUI.Color; -using TerminalUI.ConsoleDrivers; using TerminalUI.Extensions; using TerminalUI.Models; namespace TerminalUI.Controls; +[DebuggerDisplay("Text = {Text}")] public partial class TextBlock : View { private record RenderState( @@ -21,8 +22,6 @@ public partial class TextBlock : View private bool _placeholderRenderDone; [Notify] private string? _text = string.Empty; - [Notify] private IColor? _foreground; - [Notify] private IColor? _background; [Notify] private TextAlignment _textAlignment = TextAlignment.Left; public TextBlock() @@ -34,8 +33,6 @@ public partial class TextBlock : View ); RerenderProperties.Add(nameof(Text)); - RerenderProperties.Add(nameof(Foreground)); - RerenderProperties.Add(nameof(Background)); RerenderProperties.Add(nameof(TextAlignment)); ((INotifyPropertyChanged) this).PropertyChanged += (o, e) => @@ -53,9 +50,16 @@ public partial class TextBlock : View { if (size.Width == 0 || size.Height == 0) return false; - var driver = renderContext.ConsoleDriver; - var renderState = new RenderState(position, size, Text, _foreground, _background); - if (!NeedsRerender(renderState)) return false; + var foreground = Foreground ?? renderContext.Foreground; + var background = Background ?? renderContext.Background; + var renderState = new RenderState( + position, + size, + Text, + foreground, + background); + + if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false; _lastRenderState = renderState; @@ -72,41 +76,29 @@ public partial class TextBlock : View _placeholderRenderDone = false; + var driver = renderContext.ConsoleDriver; driver.ResetColor(); - if (Foreground is { } foreground) + if (foreground is not null) { driver.SetForegroundColor(foreground); } - if (Background is { } background) + if (background is not null) { driver.SetBackgroundColor(background); } - RenderText(_textLines, driver, position, size); + RenderText(_textLines, driver, position, size, TransformText); return true; } - private void RenderText(string[] textLines, IConsoleDriver driver, Position position, Size size) - { - for (var i = 0; i < textLines.Length; i++) + private string TransformText(string text, Position position, Size size) + => TextAlignment switch { - var text = textLines[i]; - text = TextAlignment switch - { - TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text), - _ => string.Format($"{{0,{-size.Width}}}", text) - }; - if (text.Length > size.Width) - { - text = text[..size.Width]; - } - - driver.SetCursorPosition(position with {Y = position.Y + i}); - driver.Write(text); - } - } + TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text), + _ => string.Format($"{{0,{-size.Width}}}", text) + }; private bool NeedsRerender(RenderState renderState) => _lastRenderState is null || _lastRenderState != renderState; diff --git a/src/Library/TerminalUI/Controls/TextBox.cs b/src/Library/TerminalUI/Controls/TextBox.cs new file mode 100644 index 0000000..376d82f --- /dev/null +++ b/src/Library/TerminalUI/Controls/TextBox.cs @@ -0,0 +1,296 @@ +using GeneralInputKey; +using PropertyChanged.SourceGenerator; +using TerminalUI.Color; +using TerminalUI.ConsoleDrivers; +using TerminalUI.Models; +using TerminalUI.Traits; + +namespace TerminalUI.Controls; + +public partial class TextBox : View, IFocusable +{ + private record RenderState( + string? Text, + Position Position, + Size Size, + IColor? ForegroundColor, + IColor? BackgroundColor + ); + + private readonly List> _keyHandlers = new(); + + private RenderState? _lastRenderState; + private string _text = string.Empty; + private List _textLines; + + private Position? _cursorPosition; + private Position _relativeCursorPosition = new(0, 0); + + [Notify] private bool _multiLine; + public bool SetKeyHandledIfKnown { get; set; } + + public string Text + { + get => _text; + set + { + ArgumentNullException.ThrowIfNull(value); + + if (value == _text) return; + _text = MultiLine ? value : value.Split(Environment.NewLine)[0]; + + _textLines = _text.Split(Environment.NewLine).ToList(); + UpdateRelativeCursorPosition(); + + OnPropertyChanged(); + } + } + + public TextBox() + { + _textLines = _text.Split(Environment.NewLine).ToList(); + RerenderProperties.Add(nameof(Text)); + RerenderProperties.Add(nameof(MultiLine)); + } + + private void UpdateTextField() + { + _text = string.Join(Environment.NewLine, _textLines); + UpdateRelativeCursorPosition(); + OnPropertyChanged(nameof(Text)); + } + + private void UpdateRelativeCursorPosition() + { + if (_relativeCursorPosition.Y > _textLines.Count - 1) + _relativeCursorPosition = _relativeCursorPosition with {Y = _textLines.Count - 1}; + + if (_relativeCursorPosition.X > _textLines[_relativeCursorPosition.Y].Length) + _relativeCursorPosition = _relativeCursorPosition with {X = _textLines[_relativeCursorPosition.Y].Length}; + } + + protected override Size CalculateSize() => new(Width ?? 10, Height ?? 1); + + protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size) + { + var foreground = Foreground ?? renderContext.Foreground; + var background = Background ?? renderContext.Background; + var renderStatus = new RenderState( + Text, + position, + size, + foreground, + background); + + if (!renderContext.ForceRerender && !NeedsRerender(renderStatus)) return false; + _lastRenderState = renderStatus; + + var driver = renderContext.ConsoleDriver; + driver.ResetColor(); + if (foreground is not null) + { + driver.SetForegroundColor(foreground); + } + + if (background is not null) + { + driver.SetBackgroundColor(background); + } + + RenderEmpty(renderContext, position, size); + RenderText(_textLines, driver, position, size); + _cursorPosition = position + _relativeCursorPosition; + + return true; + } + + private bool NeedsRerender(RenderState renderState) + => _lastRenderState is null || _lastRenderState != renderState; + + public void Focus() + => ApplicationContext?.FocusManager.SetFocus(this); + + public void UnFocus() + => ApplicationContext?.FocusManager.UnFocus(this); + + public void SetCursorPosition(IConsoleDriver consoleDriver) + { + if (_cursorPosition is null) return; + consoleDriver.SetCursorPosition(_cursorPosition.Value); + } + + public void HandleKeyInput(GeneralKeyEventArgs keyEventArgs) + { + HandleKeyInputInternal(keyEventArgs); + if (keyEventArgs.Handled) + { + ApplicationContext?.EventLoop.RequestRerender(); + } + } + + private void HandleKeyInputInternal(GeneralKeyEventArgs keyEventArgs) + { + if (keyEventArgs.Handled) return; + + if (HandleBackspace(keyEventArgs, out var known)) + return; + + if (!known && HandleDelete(keyEventArgs, out known)) + return; + + if (!known && HandleNavigation(keyEventArgs, out known)) + return; + + if (!known && ProcessKeyHandlers(keyEventArgs)) + return; + + if (!known + && keyEventArgs.KeyChar != '\0' + && keyEventArgs.KeyChar.ToString() is {Length: 1} keyString) + { + var y = _relativeCursorPosition.Y; + var x = _relativeCursorPosition.X; + _textLines[y] = _textLines[y][..x] + keyString + _textLines[y][x..]; + _relativeCursorPosition = _relativeCursorPosition with {X = x + 1}; + + keyEventArgs.Handled = true; + UpdateTextField(); + } + } + + private bool HandleBackspace(GeneralKeyEventArgs keyEventArgs, out bool known) + { + if (keyEventArgs.Key != Keys.Backspace) + { + known = false; + return false; + } + + known = true; + + if (_relativeCursorPosition is {X: 0, Y: 0}) + { + return keyEventArgs.Handled = SetKeyHandledIfKnown; + } + + if (_relativeCursorPosition.X == 0) + { + var y = _relativeCursorPosition.Y; + _textLines[y - 1] += _textLines[y]; + _textLines.RemoveAt(y); + _relativeCursorPosition = new Position(Y: y - 1, X: _textLines[y - 1].Length); + } + else + { + var y = _relativeCursorPosition.Y; + var x = _relativeCursorPosition.X; + _textLines[y] = _textLines[y].Remove(x - 1, 1); + _relativeCursorPosition = _relativeCursorPosition with {X = x - 1}; + } + + UpdateTextField(); + return keyEventArgs.Handled = true; + } + + private bool HandleDelete(GeneralKeyEventArgs keyEventArgs, out bool known) + { + if (keyEventArgs.Key != Keys.Delete) + { + known = false; + return false; + } + + known = true; + + if (_relativeCursorPosition.Y == _textLines.Count - 1 + && _relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length) + { + return keyEventArgs.Handled = SetKeyHandledIfKnown; + } + + if (_relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length) + { + var y = _relativeCursorPosition.Y; + _textLines[y] += _textLines[y + 1]; + _textLines.RemoveAt(y + 1); + } + else + { + var y = _relativeCursorPosition.Y; + var x = _relativeCursorPosition.X; + _textLines[y] = _textLines[y].Remove(x, 1); + } + + UpdateTextField(); + return keyEventArgs.Handled = true; + } + + private bool HandleNavigation(GeneralKeyEventArgs keyEventArgs, out bool known) + { + if (keyEventArgs.Key == Keys.Left) + { + known = true; + keyEventArgs.Handled = SetKeyHandledIfKnown; + + if (_relativeCursorPosition is {X: 0, Y: 0}) + { + return keyEventArgs.Handled; + } + + if (_relativeCursorPosition.X == 0) + { + var y = _relativeCursorPosition.Y - 1; + _relativeCursorPosition = new Position(_textLines[y].Length, y); + } + else + { + _relativeCursorPosition = _relativeCursorPosition with {X = _relativeCursorPosition.X - 1}; + } + + return keyEventArgs.Handled = true; + } + else if (keyEventArgs.Key == Keys.Right) + { + known = true; + keyEventArgs.Handled = SetKeyHandledIfKnown; + + if (_relativeCursorPosition.Y == _textLines.Count - 1 + && _relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length) + { + return keyEventArgs.Handled; + } + + if (_relativeCursorPosition.X == _textLines[_relativeCursorPosition.Y].Length) + { + _relativeCursorPosition = new Position(0, _relativeCursorPosition.Y + 1); + } + else + { + _relativeCursorPosition = _relativeCursorPosition with {X = _relativeCursorPosition.X + 1}; + } + + return keyEventArgs.Handled = true; + } + + + known = false; + return false; + } + + private bool ProcessKeyHandlers(GeneralKeyEventArgs keyEventArgs) + { + foreach (var keyHandler in _keyHandlers) + { + keyHandler(keyEventArgs); + if (keyEventArgs.Handled) return true; + } + + return false; + } + + public TextBox WithKeyHandler(Action keyHandler) + { + _keyHandlers.Add(keyHandler); + return this; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Controls/View.cs b/src/Library/TerminalUI/Controls/View.cs index aa122aa..7966c0c 100644 --- a/src/Library/TerminalUI/Controls/View.cs +++ b/src/Library/TerminalUI/Controls/View.cs @@ -1,11 +1,16 @@ using System.Buffers; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using PropertyChanged.SourceGenerator; +using TerminalUI.Color; +using TerminalUI.ConsoleDrivers; using TerminalUI.Models; namespace TerminalUI.Controls; +public delegate string TextTransformer(string text, Position position, Size size); + public abstract partial class View : IView { private readonly List _disposables = new(); @@ -18,24 +23,15 @@ public abstract partial class View : IView [Notify] private int? _maxHeight; [Notify] private int? _height; [Notify] private int _actualHeight; - [Notify] private Margin _margin = new Margin(0, 0, 0, 0); + [Notify] private bool _isVisible = true; + [Notify] private Thickness _margin = 0; + [Notify] private IColor? _foreground; + [Notify] private IColor? _background; [Notify] private string? _name; [Notify] private IApplicationContext? _applicationContext; - private bool _attached; - - public bool Attached - { - get => _attached; - set - { - if (_attached == value) return; - _attached = value; - if (value) - { - AttachChildren(); - } - } - } + [Notify] private bool _attached; + + protected ObservableCollection VisualChildren { get; } = new(); public List Extensions { get; } = new(); public RenderMethod RenderMethod { get; set; } @@ -46,11 +42,16 @@ public abstract partial class View : IView { RenderMethod = DefaultRenderer; + RerenderProperties.Add(nameof(Width)); RerenderProperties.Add(nameof(MinWidth)); RerenderProperties.Add(nameof(MaxWidth)); + RerenderProperties.Add(nameof(Height)); RerenderProperties.Add(nameof(MinHeight)); RerenderProperties.Add(nameof(MaxHeight)); + RerenderProperties.Add(nameof(IsVisible)); RerenderProperties.Add(nameof(Margin)); + RerenderProperties.Add(nameof(Foreground)); + RerenderProperties.Add(nameof(Background)); ((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged; } @@ -80,11 +81,6 @@ public abstract partial class View : IView protected abstract Size CalculateSize(); - - protected virtual void AttachChildren() - { - } - private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (Attached @@ -96,6 +92,21 @@ public abstract partial class View : IView { ApplicationContext?.EventLoop.RequestRerender(); } + + if (e.PropertyName == nameof(Attached)) + { + foreach (var visualChild in VisualChildren) + { + visualChild.Attached = Attached; + } + } + else if(e.PropertyName == nameof(ApplicationContext)) + { + foreach (var visualChild in VisualChildren) + { + visualChild.ApplicationContext = ApplicationContext; + } + } } protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size); @@ -105,6 +116,8 @@ public abstract partial class View : IView if (!Attached) throw new InvalidOperationException("Cannot render unattached view"); + if (!IsVisible) return false; + ActualWidth = size.Width; ActualHeight = size.Height; @@ -137,6 +150,8 @@ public abstract partial class View : IView protected void RenderEmpty(RenderContext renderContext, Position position, Size size) { var driver = renderContext.ConsoleDriver; + driver.ResetColor(); + var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width); for (var i = 0; i < size.Height; i++) { @@ -145,6 +160,94 @@ public abstract partial class View : IView } } + protected void RenderText( + IList textLines, + IConsoleDriver driver, + Position position, + Size size, + TextTransformer? textTransformer = null) + { + for (var i = 0; i < textLines.Count; i++) + { + var currentPosition = position with {Y = position.Y + i}; + var text = textLines[i]; + + if (textTransformer is not null) + { + text = textTransformer(text, currentPosition, size); + } + + if (text.Length > size.Width) + { + text = text[..size.Width]; + } + + driver.SetCursorPosition(currentPosition); + driver.Write(text); + } + } + + protected void RenderText( + string text, + IConsoleDriver driver, + Position position, + Size size, + TextTransformer? textTransformer = null) + { + for (var i = 0; i < size.Height; i++) + { + var currentPosition = position with {Y = position.Y + i}; + var finalText = text; + + if (textTransformer is not null) + { + finalText = textTransformer(finalText, currentPosition, size); + } + + if (finalText.Length > size.Width) + { + finalText = finalText[..size.Width]; + } + + driver.SetCursorPosition(currentPosition); + driver.Write(finalText); + } + } + + protected void RenderText( + char content, + IConsoleDriver driver, + Position position, + Size size) + { + var contentString = new string(content, size.Width); + + for (var i = 0; i < size.Height; i++) + { + var currentPosition = position with {Y = position.Y + i}; + + driver.SetCursorPosition(currentPosition); + driver.Write(contentString); + } + } + + protected void SetColorsForDriver(RenderContext renderContext) + { + var driver = renderContext.ConsoleDriver; + + var foreground = Foreground ?? renderContext.Foreground; + var background = Background ?? renderContext.Background; + if (foreground is not null) + { + driver.SetForegroundColor(foreground); + } + + if (background is not null) + { + driver.SetBackgroundColor(background); + } + } + public TChild CreateChild() where TChild : IView, new() { var child = new TChild(); @@ -162,6 +265,7 @@ public abstract partial class View : IView { child.DataContext = DataContext; CopyCommonPropertiesToNewChild(child); + VisualChildren.Add(child); var mapper = new DataContextMapper(this, child, d => d); AddDisposable(mapper); @@ -175,6 +279,7 @@ public abstract partial class View : IView { child.DataContext = dataContextMapper(DataContext); CopyCommonPropertiesToNewChild(child); + VisualChildren.Add(child); var mapper = new DataContextMapper(this, child, dataContextMapper); AddDisposable(mapper); diff --git a/src/Library/TerminalUI/EventLoop.cs b/src/Library/TerminalUI/EventLoop.cs index af9f53c..6f169c8 100644 --- a/src/Library/TerminalUI/EventLoop.cs +++ b/src/Library/TerminalUI/EventLoop.cs @@ -9,6 +9,7 @@ public class EventLoop : IEventLoop private readonly object _lock = new(); private readonly List _viewsToRender = new(); private bool _rerenderRequested; + private bool _lastCursorVisible; public EventLoop(IApplicationContext applicationContext) { @@ -44,14 +45,35 @@ public class EventLoop : IEventLoop viewsToRender = _viewsToRender.ToList(); } - var size = _applicationContext.ConsoleDriver.GetWindowSize(); - var renderContext = new RenderContext(_applicationContext.ConsoleDriver); + var driver = _applicationContext.ConsoleDriver; + var size = driver.GetWindowSize(); + var renderContext = new RenderContext( + driver, + false, + null, + null + ); foreach (var view in viewsToRender) { view.Attached = true; view.GetRequestedSize(); view.Render(renderContext, new Position(0, 0), size); } + + if (_applicationContext.FocusManager.Focused is { } focused) + { + focused.SetCursorPosition(driver); + if (!_lastCursorVisible) + { + driver.SetCursorVisible(true); + _lastCursorVisible = true; + } + } + else if (_lastCursorVisible) + { + driver.SetCursorVisible(false); + _lastCursorVisible = false; + } } public void AddViewToRender(IView view) diff --git a/src/Library/TerminalUI/Extensions/ViewExtensions.cs b/src/Library/TerminalUI/Extensions/ViewExtensions.cs index 168d434..69078cb 100644 --- a/src/Library/TerminalUI/Extensions/ViewExtensions.cs +++ b/src/Library/TerminalUI/Extensions/ViewExtensions.cs @@ -1,4 +1,5 @@ -using TerminalUI.Controls; +using System.Linq.Expressions; +using TerminalUI.Controls; namespace TerminalUI.Extensions; @@ -6,7 +7,7 @@ public static class ViewExtensions { public static T? GetExtension(this IView view) => (T?) view.Extensions.FirstOrDefault(e => e is T); - + public static IView WithExtension(this IView view, object extension) { view.Extensions.Add(extension); @@ -23,4 +24,19 @@ public static class ViewExtensions action(view); return view; } + + public static TItem WithPropertyChangedHandler( + this TItem dataSource, + Expression> dataSourceExpression, + Action handler) + { + new PropertyChangedHandler + ( + dataSource, + dataSourceExpression, + handler + ); + + return dataSource; + } } \ No newline at end of file diff --git a/src/Library/TerminalUI/FocusManager.cs b/src/Library/TerminalUI/FocusManager.cs new file mode 100644 index 0000000..447fe52 --- /dev/null +++ b/src/Library/TerminalUI/FocusManager.cs @@ -0,0 +1,31 @@ +using TerminalUI.Traits; + +namespace TerminalUI; + +public class FocusManager : IFocusManager +{ + private IFocusable? _focused; + + public IFocusable? Focused + { + get + { + if (_focused is not null && !_focused.IsVisible) + { + _focused = null; + } + + return _focused; + } + + private set => _focused = value; + } + + public void SetFocus(IFocusable focusable) => Focused = focusable; + + public void UnFocus(IFocusable focusable) + { + if (Focused == focusable) + Focused = null; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/IApplicationContext.cs b/src/Library/TerminalUI/IApplicationContext.cs index 88c9aed..d4f66e7 100644 --- a/src/Library/TerminalUI/IApplicationContext.cs +++ b/src/Library/TerminalUI/IApplicationContext.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using FileTime.App.Core.Models; +using Microsoft.Extensions.Logging; using TerminalUI.ConsoleDrivers; namespace TerminalUI; @@ -10,4 +11,5 @@ public interface IApplicationContext IConsoleDriver ConsoleDriver { get; init; } ILoggerFactory? LoggerFactory { get; init; } char EmptyCharacter { get; init; } + IFocusManager FocusManager { get; init; } } \ No newline at end of file diff --git a/src/Library/TerminalUI/IFocusManager.cs b/src/Library/TerminalUI/IFocusManager.cs new file mode 100644 index 0000000..767d5d0 --- /dev/null +++ b/src/Library/TerminalUI/IFocusManager.cs @@ -0,0 +1,10 @@ +using TerminalUI.Traits; + +namespace TerminalUI; + +public interface IFocusManager +{ + void SetFocus(IFocusable focusable); + void UnFocus(IFocusable focusable); + IFocusable? Focused { get; } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Margin.cs b/src/Library/TerminalUI/Models/Margin.cs deleted file mode 100644 index efba72b..0000000 --- a/src/Library/TerminalUI/Models/Margin.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace TerminalUI.Models; - -public record Margin(int Left, int Top, int Right, int Bottom) -{ - public static implicit operator Margin(int value) => new(value, value, value, value); - public static implicit operator Margin((int Left, int Top, int Right, int Bottom) value) => new(value.Left, value.Top, value.Right, value.Bottom); - public static implicit operator Margin(string s) - { - var parts = s.Split(' '); - return parts.Length switch - { - 1 => new Margin(int.Parse(parts[0])), - 2 => new Margin(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[0]), int.Parse(parts[1])), - 4 => new Margin(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]), int.Parse(parts[3])), - _ => throw new ArgumentException("Invalid margin format", nameof(s)) - }; - } -} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Position.cs b/src/Library/TerminalUI/Models/Position.cs index c845179..293d26b 100644 --- a/src/Library/TerminalUI/Models/Position.cs +++ b/src/Library/TerminalUI/Models/Position.cs @@ -1,3 +1,10 @@ -namespace TerminalUI.Models; +using System.Diagnostics; -public record struct Position(int X, int Y); \ No newline at end of file +namespace TerminalUI.Models; + +[DebuggerDisplay("X = {X}, Y = {Y}")] +public readonly record struct Position(int X, int Y) +{ + public static Position operator +(Position left, Position right) => new(left.X + right.X, left.Y + right.Y); + public static Position operator -(Position left, Position right) => new(left.X - right.X, left.Y - right.Y); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/RenderContext.cs b/src/Library/TerminalUI/Models/RenderContext.cs index 9f328c9..68e170a 100644 --- a/src/Library/TerminalUI/Models/RenderContext.cs +++ b/src/Library/TerminalUI/Models/RenderContext.cs @@ -1,18 +1,32 @@ -using TerminalUI.ConsoleDrivers; +using System.Diagnostics; +using TerminalUI.Color; +using TerminalUI.ConsoleDrivers; namespace TerminalUI.Models; +[DebuggerDisplay("RenderId = {RenderId}, ForceRerender = {ForceRerender}, Driver = {ConsoleDriver.GetType().Name}")] public readonly ref struct RenderContext { - private static int _renderId = 0; + private static int _renderId; public readonly int RenderId; public readonly IConsoleDriver ConsoleDriver; + public readonly bool ForceRerender; + public readonly IColor? Foreground; + public readonly IColor? Background; - public RenderContext(IConsoleDriver consoleDriver) + public RenderContext( + IConsoleDriver consoleDriver, + bool forceRerender, + IColor? foreground, + IColor? background) { - ConsoleDriver = consoleDriver; RenderId = _renderId++; + + ConsoleDriver = consoleDriver; + ForceRerender = forceRerender; + Foreground = foreground; + Background = background; } - public static RenderContext Empty => new(null!); + public static RenderContext Empty => new(null!, false, null, null); } \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Size.cs b/src/Library/TerminalUI/Models/Size.cs index c96dc30..5d38d47 100644 --- a/src/Library/TerminalUI/Models/Size.cs +++ b/src/Library/TerminalUI/Models/Size.cs @@ -1,3 +1,6 @@ -namespace TerminalUI.Models; +using System.Diagnostics; +namespace TerminalUI.Models; + +[DebuggerDisplay("Width = {Width}, Height = {Height}")] public readonly record struct Size(int Width, int Height); \ No newline at end of file diff --git a/src/Library/TerminalUI/Models/Thickness.cs b/src/Library/TerminalUI/Models/Thickness.cs new file mode 100644 index 0000000..c0528f5 --- /dev/null +++ b/src/Library/TerminalUI/Models/Thickness.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace TerminalUI.Models; + +[DebuggerDisplay("Left = {Left}, Top = {Top}, Right = {Right}, Bottom = {Bottom}")] +public record Thickness(int Left, int Top, int Right, int Bottom) +{ + public static implicit operator Thickness(int value) => new(value, value, value, value); + public static implicit operator Thickness((int Left, int Top, int Right, int Bottom) value) => new(value.Left, value.Top, value.Right, value.Bottom); + public static implicit operator Thickness(string s) + { + var parts = s.Split(' '); + return parts.Length switch + { + 1 => new Thickness(int.Parse(parts[0])), + 2 => new Thickness(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[0]), int.Parse(parts[1])), + 4 => new Thickness(int.Parse(parts[0]), int.Parse(parts[1]), int.Parse(parts[2]), int.Parse(parts[3])), + _ => throw new ArgumentException("Invalid margin format", nameof(s)) + }; + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/PropertyChangeTracker.cs b/src/Library/TerminalUI/PropertyChangeTracker.cs index 3c24e49..08d04aa 100644 --- a/src/Library/TerminalUI/PropertyChangeTracker.cs +++ b/src/Library/TerminalUI/PropertyChangeTracker.cs @@ -2,15 +2,25 @@ namespace TerminalUI; -internal interface IPropertyChangeTracker : IDisposable +public interface IPropertyChangeTracker : IDisposable { + string Name { get; } + string Path { get; } Dictionary Children { get; } } -internal abstract class PropertyChangeTrackerBase : IPropertyChangeTracker +public abstract class PropertyChangeTrackerBase : IPropertyChangeTracker { + public string Name { get; } + public string Path { get; } public Dictionary Children { get; } = new(); + protected PropertyChangeTrackerBase(string name, string path) + { + Name = name; + Path = path; + } + public virtual void Dispose() { foreach (var propertyChangeTracker in Children.Values) @@ -20,18 +30,20 @@ internal abstract class PropertyChangeTrackerBase : IPropertyChangeTracker } } -internal class PropertyChangeTracker : PropertyChangeTrackerBase +public class PropertyChangeTracker : PropertyChangeTrackerBase { private readonly PropertyTrackTreeItem _propertyTrackTreeItem; private readonly INotifyPropertyChanged _target; private readonly IEnumerable _propertiesToListen; - private readonly Action _updateBinding; + private readonly Action _updateBinding; public PropertyChangeTracker( + string name, + string path, PropertyTrackTreeItem propertyTrackTreeItem, INotifyPropertyChanged target, IEnumerable propertiesToListen, - Action updateBinding) + Action updateBinding) : base(name, path) { _propertyTrackTreeItem = propertyTrackTreeItem; _target = target; @@ -48,10 +60,10 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase return; } - _updateBinding(); Children.Remove(propertyName); - - var newChild = PropertyChangeHelper.TraverseDataContext( + + var newChild = PropertyChangeHelper.CreatePropertyTracker( + Path, _propertyTrackTreeItem.Children[propertyName], _target.GetType().GetProperty(propertyName)?.GetValue(_target), _updateBinding @@ -61,6 +73,8 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase { Children.Add(propertyName, newChild); } + + _updateBinding(propertyName); } public override void Dispose() @@ -71,32 +85,54 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase } } -internal class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase +public class NonSubscriberPropertyChangeTracker : PropertyChangeTrackerBase { + public NonSubscriberPropertyChangeTracker(string name, string path) : base(name, path) + { + } } -internal class PropertyTrackTreeItem +public class PropertyTrackTreeItem { + public string Name { get; } public Dictionary Children { get; } = new(); + + public PropertyTrackTreeItem(string name) + { + Name = name; + } } -internal static class PropertyChangeHelper +public static class PropertyChangeHelper { - internal static IPropertyChangeTracker? TraverseDataContext( + internal static IPropertyChangeTracker? CreatePropertyTracker( + string? path, PropertyTrackTreeItem propertyTrackTreeItem, object? obj, - Action updateBinding + Action updateBinding ) { if (obj is null) return null; + path = path is null ? propertyTrackTreeItem.Name : path + "." + propertyTrackTreeItem.Name; + IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged - ? new PropertyChangeTracker(propertyTrackTreeItem, notifyPropertyChanged, propertyTrackTreeItem.Children.Keys, updateBinding) - : new NonSubscriberPropertyChangeTracker(); + ? new PropertyChangeTracker( + propertyTrackTreeItem.Name, + path, + propertyTrackTreeItem, + notifyPropertyChanged, + propertyTrackTreeItem.Children.Keys, + updateBinding + ) + : new NonSubscriberPropertyChangeTracker( + propertyTrackTreeItem.Name, + path); foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children) { - var childTracker = TraverseDataContext( + var childTracker = CreatePropertyTracker( + path, trackerTreeItem, obj.GetType().GetProperty(propertyName)?.GetValue(obj), updateBinding diff --git a/src/Library/TerminalUI/PropertyChangedHandler.cs b/src/Library/TerminalUI/PropertyChangedHandler.cs new file mode 100644 index 0000000..c639f8c --- /dev/null +++ b/src/Library/TerminalUI/PropertyChangedHandler.cs @@ -0,0 +1,45 @@ +using System.Linq.Expressions; + +namespace TerminalUI; + +public sealed class PropertyChangedHandler : PropertyTrackerBase, IDisposable +{ + private readonly TItem _dataSource; + private readonly Action _handler; + private readonly PropertyTrackTreeItem? _propertyTrackTreeItem; + private readonly Func _propertyValueGenerator; + + public PropertyChangedHandler( + TItem dataSource, + Expression> dataSourceExpression, + Action handler + ) : base(() => dataSource, dataSourceExpression) + { + _dataSource = dataSource; + _handler = handler; + ArgumentNullException.ThrowIfNull(dataSource); + ArgumentNullException.ThrowIfNull(dataSourceExpression); + ArgumentNullException.ThrowIfNull(handler); + + _propertyTrackTreeItem = CreateTrackingTree(dataSourceExpression); + _propertyValueGenerator = dataSourceExpression.Compile(); + UpdateTrackers(); + } + + protected override void Update(string propertyPath) + { + TExpressionResult? value = default; + var parsed = false; + + try + { + value = _propertyValueGenerator(_dataSource); + parsed = true; + } + catch + { + } + + _handler(propertyPath, parsed, value); + } +} \ No newline at end of file diff --git a/src/Library/TerminalUI/PropertyTrackerBase.cs b/src/Library/TerminalUI/PropertyTrackerBase.cs new file mode 100644 index 0000000..6bebea7 --- /dev/null +++ b/src/Library/TerminalUI/PropertyTrackerBase.cs @@ -0,0 +1,145 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace TerminalUI; + +public abstract class PropertyTrackerBase : IDisposable +{ + private readonly Func _source; + protected PropertyTrackTreeItem? PropertyTrackTreeItem { get; } + protected IPropertyChangeTracker? PropertyChangeTracker { get; private set; } + + protected PropertyTrackerBase( + Func source, + Expression> dataSourceExpression) + { + ArgumentNullException.ThrowIfNull(dataSourceExpression); + + _source = source; + PropertyTrackTreeItem = CreateTrackingTree(dataSourceExpression); + } + + protected PropertyTrackTreeItem? CreateTrackingTree(Expression> dataContextExpression) + { + var properties = new List(); + FindReactiveProperties(dataContextExpression, properties); + + if (properties.Count > 0) + { + var rootItem = new PropertyTrackTreeItem(null!); + foreach (var property in properties) + { + var pathParts = property.Split('.'); + var currentItem = rootItem; + for (var i = 0; i < pathParts.Length; i++) + { + if (!currentItem.Children.TryGetValue(pathParts[i], out var child)) + { + child = new PropertyTrackTreeItem(pathParts[i]); + currentItem.Children.Add(pathParts[i], child); + } + + currentItem = child; + } + } + + return rootItem; + } + + return null; + } + + private string? FindReactiveProperties(Expression? expression, List properties) + { + if (expression is null) return ""; + + if (expression is LambdaExpression lambdaExpression) + { + SavePropertyPath(FindReactiveProperties(lambdaExpression.Body, properties)); + } + else if (expression is ConditionalExpression conditionalExpression) + { + SavePropertyPath(FindReactiveProperties(conditionalExpression.Test, properties)); + SavePropertyPath(FindReactiveProperties(conditionalExpression.IfTrue, properties)); + SavePropertyPath(FindReactiveProperties(conditionalExpression.IfFalse, properties)); + } + else if (expression is MemberExpression memberExpression) + { + if (memberExpression.Expression is not null) + { + FindReactiveProperties(memberExpression.Expression, properties); + + if (FindReactiveProperties(memberExpression.Expression, properties) is { } path + && memberExpression.Member is PropertyInfo dataContextPropertyInfo) + { + path += "." + memberExpression.Member.Name; + return path; + } + } + } + else if (expression is MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Object is + { + NodeType: + not ExpressionType.Parameter + and not ExpressionType.Constant + } methodObject) + { + SavePropertyPath(FindReactiveProperties(methodObject, properties)); + } + + foreach (var argument in methodCallExpression.Arguments) + { + SavePropertyPath(FindReactiveProperties(argument, properties)); + } + } + else if (expression is BinaryExpression binaryExpression) + { + SavePropertyPath(FindReactiveProperties(binaryExpression.Left, properties)); + SavePropertyPath(FindReactiveProperties(binaryExpression.Right, properties)); + } + else if (expression is UnaryExpression unaryExpression) + { + SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties)); + } + else if (expression is ParameterExpression parameterExpression) + { + if (parameterExpression.Type == typeof(TSource)) + { + return ""; + } + } + + return null; + + void SavePropertyPath(string? path) + { + if (path is null) return; + path = path.TrimStart('.'); + properties.Add(path); + } + } + + protected void UpdateTrackers() + { + if (PropertyChangeTracker is not null) + { + PropertyChangeTracker.Dispose(); + } + + if (PropertyTrackTreeItem is not null) + { + PropertyChangeTracker = PropertyChangeHelper.CreatePropertyTracker( + null, + PropertyTrackTreeItem, + _source(), + Update + ); + } + } + + protected abstract void Update(string propertyPath); + + public virtual void Dispose() => PropertyChangeTracker?.Dispose(); +} \ No newline at end of file diff --git a/src/Library/TerminalUI/TerminalUI.csproj b/src/Library/TerminalUI/TerminalUI.csproj index b0374e1..b23bfd2 100644 --- a/src/Library/TerminalUI/TerminalUI.csproj +++ b/src/Library/TerminalUI/TerminalUI.csproj @@ -7,6 +7,7 @@ + @@ -18,4 +19,8 @@ + + + + diff --git a/src/Library/TerminalUI/Traits/IFocusable.cs b/src/Library/TerminalUI/Traits/IFocusable.cs new file mode 100644 index 0000000..cec0d8b --- /dev/null +++ b/src/Library/TerminalUI/Traits/IFocusable.cs @@ -0,0 +1,13 @@ +using GeneralInputKey; +using TerminalUI.ConsoleDrivers; +using TerminalUI.Controls; + +namespace TerminalUI.Traits; + +public interface IFocusable : IView +{ + void Focus(); + void UnFocus(); + void SetCursorPosition(IConsoleDriver consoleDriver); + void HandleKeyInput(GeneralKeyEventArgs keyEventArgs); +} \ No newline at end of file