TextBox, PropertyChangeHandler

This commit is contained in:
2023-08-11 21:51:44 +02:00
parent e989a65e81
commit 1fde0df2d6
81 changed files with 1539 additions and 390 deletions

View File

@@ -1,3 +1,4 @@
using DeclarativeProperty;
using FileTime.App.CommandPalette.Models; using FileTime.App.CommandPalette.Models;
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
@@ -5,7 +6,7 @@ namespace FileTime.App.CommandPalette.Services;
public interface ICommandPaletteService public interface ICommandPaletteService
{ {
IObservable<bool> ShowWindow { get; } IDeclarativeProperty<bool> ShowWindow { get; }
void OpenCommandPalette(); void OpenCommandPalette();
void CloseCommandPalette(); void CloseCommandPalette();
IReadOnlyList<ICommandPaletteEntry> GetCommands(); IReadOnlyList<ICommandPaletteEntry> GetCommands();

View File

@@ -1,12 +1,14 @@
using FileTime.App.Core.Models; using DeclarativeProperty;
using FileTime.App.Core.Models;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.FuzzyPanel; using FileTime.App.FuzzyPanel;
using GeneralInputKey;
namespace FileTime.App.CommandPalette.ViewModels; namespace FileTime.App.CommandPalette.ViewModels;
public interface ICommandPaletteViewModel : IFuzzyPanelViewModel<ICommandPaletteEntryViewModel>, IModalViewModel public interface ICommandPaletteViewModel : IFuzzyPanelViewModel<ICommandPaletteEntryViewModel>, IModalViewModel
{ {
IObservable<bool> ShowWindow { get; } IDeclarativeProperty<bool> ShowWindow { get; }
void Close(); void Close();
Task<bool> HandleKeyUp(GeneralKeyEventArgs keyEventArgs); Task<bool> HandleKeyUp(GeneralKeyEventArgs keyEventArgs);
} }

View File

@@ -1,5 +1,4 @@
using System.Reactive.Linq; using DeclarativeProperty;
using System.Reactive.Subjects;
using FileTime.App.CommandPalette.Models; using FileTime.App.CommandPalette.Models;
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
@@ -11,8 +10,8 @@ public partial class CommandPaletteService : ICommandPaletteService
{ {
private readonly IModalService _modalService; private readonly IModalService _modalService;
private readonly IIdentifiableUserCommandService _identifiableUserCommandService; private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
private readonly BehaviorSubject<bool> _showWindow = new(false); private readonly DeclarativeProperty<bool> _showWindow = new(false);
IObservable<bool> ICommandPaletteService.ShowWindow => _showWindow.AsObservable(); IDeclarativeProperty<bool> ICommandPaletteService.ShowWindow => _showWindow;
[Notify] ICommandPaletteViewModel? _currentModal; [Notify] ICommandPaletteViewModel? _currentModal;
public CommandPaletteService( public CommandPaletteService(
@@ -24,13 +23,13 @@ public partial class CommandPaletteService : ICommandPaletteService
} }
public void OpenCommandPalette() public void OpenCommandPalette()
{ {
_showWindow.OnNext(true); _showWindow.SetValueSafe(true);
CurrentModal = _modalService.OpenModal<ICommandPaletteViewModel>(); CurrentModal = _modalService.OpenModal<ICommandPaletteViewModel>();
} }
public void CloseCommandPalette() public void CloseCommandPalette()
{ {
_showWindow.OnNext(false); _showWindow.SetValueSafe(false);
if (_currentModal is not null) if (_currentModal is not null)
{ {
_modalService.CloseModal(_currentModal); _modalService.CloseModal(_currentModal);

View File

@@ -1,10 +1,9 @@
using System.Text; using FileTime.App.CommandPalette.Services;
using FileTime.App.CommandPalette.Services;
using FileTime.App.Core.Configuration;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.FuzzyPanel; using FileTime.App.FuzzyPanel;
using GeneralInputKey;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace FileTime.App.CommandPalette.ViewModels; namespace FileTime.App.CommandPalette.ViewModels;

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using GeneralInputKey;
namespace FileTime.App.Core.Configuration; namespace FileTime.App.Core.Configuration;

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using GeneralInputKey;
namespace FileTime.App.Core.Configuration; namespace FileTime.App.Core.Configuration;

View File

@@ -22,6 +22,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" /> <ProjectReference Include="..\..\Core\FileTime.Core.Abstraction\FileTime.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Library\GeneralInputKey\GeneralInputKey.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -1,3 +0,0 @@
namespace FileTime.App.Core.Models;
public record struct SpecialKeysStatus(bool IsAltPressed, bool IsShiftPressed, bool IsCtrlPressed);

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using GeneralInputKey;
namespace FileTime.App.Core.Services; namespace FileTime.App.Core.Services;

View File

@@ -1,8 +1,9 @@
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using GeneralInputKey;
namespace FileTime.App.Core.Services; namespace FileTime.App.Core.Services;
public interface IKeyInputHandler public interface IKeyInputHandler
{ {
Task HandleInputKey(GeneralKeyEventArgs e, SpecialKeysStatus specialKeysStatus); Task HandleInputKey(GeneralKeyEventArgs e);
} }

View File

@@ -1,6 +1,7 @@
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.UserCommand; using FileTime.App.Core.UserCommand;
using FileTime.Providers.LocalAdmin; using FileTime.Providers.LocalAdmin;
using GeneralInputKey;
namespace FileTime.App.Core.Configuration; namespace FileTime.App.Core.Configuration;

View File

@@ -7,6 +7,7 @@ using FileTime.App.Core.ViewModels;
using FileTime.Core.Extensions; using FileTime.Core.Extensions;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Models.Extensions; using FileTime.Core.Models.Extensions;
using GeneralInputKey;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace FileTime.App.Core.Services; namespace FileTime.App.Core.Services;
@@ -57,9 +58,14 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
_keysToSkip.Add(new[] {new KeyConfig(Keys.RWin)}); _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); _appState.PreviousKeys.Add(keyWithModifiers);
var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys)); var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys));

View File

@@ -5,6 +5,7 @@ using FileTime.App.Core.UserCommand;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.Core.Extensions; using FileTime.Core.Extensions;
using FileTime.Core.Models; using FileTime.Core.Models;
using GeneralInputKey;
using Humanizer; using Humanizer;
using Microsoft.Extensions.Logging; 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(); var keyString = args.Key.Humanize();

View File

@@ -140,10 +140,9 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase
return Task.CompletedTask; return Task.CompletedTask;
} }
private Task GoByFrequency() private async Task GoByFrequency()
{ {
_frequencyNavigationService.OpenNavigationWindow(); await _frequencyNavigationService.OpenNavigationWindow();
return Task.CompletedTask;
} }
private async Task GoToPath() private async Task GoToPath()

View File

@@ -1,12 +1,13 @@
using DeclarativeProperty;
using FileTime.App.FrequencyNavigation.ViewModels; using FileTime.App.FrequencyNavigation.ViewModels;
namespace FileTime.App.FrequencyNavigation.Services; namespace FileTime.App.FrequencyNavigation.Services;
public interface IFrequencyNavigationService public interface IFrequencyNavigationService
{ {
IObservable<bool> ShowWindow { get; } IDeclarativeProperty<bool> ShowWindow { get; }
IFrequencyNavigationViewModel? CurrentModal { get; } IFrequencyNavigationViewModel? CurrentModal { get; }
void OpenNavigationWindow(); Task OpenNavigationWindow();
void CloseNavigationWindow(); void CloseNavigationWindow();
IList<string> GetMatchingContainers(string searchText); IList<string> GetMatchingContainers(string searchText);
} }

View File

@@ -1,12 +1,14 @@
using DeclarativeProperty;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.FuzzyPanel; using FileTime.App.FuzzyPanel;
using GeneralInputKey;
namespace FileTime.App.FrequencyNavigation.ViewModels; namespace FileTime.App.FrequencyNavigation.ViewModels;
public interface IFrequencyNavigationViewModel : IFuzzyPanelViewModel<string>, IModalViewModel public interface IFrequencyNavigationViewModel : IFuzzyPanelViewModel<string>, IModalViewModel
{ {
IObservable<bool> ShowWindow { get; } IDeclarativeProperty<bool> ShowWindow { get; }
void Close(); void Close();
Task<bool> HandleKeyUp(GeneralKeyEventArgs keyEventArgs); Task<bool> HandleKeyUp(GeneralKeyEventArgs keyEventArgs);
} }

View File

@@ -1,6 +1,7 @@
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text.Json; using System.Text.Json;
using DeclarativeProperty;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.FrequencyNavigation.Models; using FileTime.App.FrequencyNavigation.Models;
@@ -22,10 +23,10 @@ public partial class FrequencyNavigationService : IFrequencyNavigationService, I
private readonly IModalService _modalService; private readonly IModalService _modalService;
private readonly SemaphoreSlim _saveLock = new(1, 1); private readonly SemaphoreSlim _saveLock = new(1, 1);
private Dictionary<string, ContainerFrequencyData> _containerScores = new(); private Dictionary<string, ContainerFrequencyData> _containerScores = new();
private readonly BehaviorSubject<bool> _showWindow = new(false); private readonly DeclarativeProperty<bool> _showWindow = new(false);
private readonly string _dbPath; private readonly string _dbPath;
[Notify] IFrequencyNavigationViewModel? _currentModal; [Notify] IFrequencyNavigationViewModel? _currentModal;
IObservable<bool> IFrequencyNavigationService.ShowWindow => _showWindow.AsObservable(); IDeclarativeProperty<bool> IFrequencyNavigationService.ShowWindow => _showWindow;
public FrequencyNavigationService( public FrequencyNavigationService(
ITabEvents tabEvents, 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<IFrequencyNavigationViewModel>(); CurrentModal = _modalService.OpenModal<IFrequencyNavigationViewModel>();
} }
public void CloseNavigationWindow() public void CloseNavigationWindow()
{ {
_showWindow.OnNext(false); _showWindow.SetValueSafe(false);
if (_currentModal is not null) if (_currentModal is not null)
{ {
_modalService.CloseModal(_currentModal); _modalService.CloseModal(_currentModal);

View File

@@ -6,6 +6,7 @@ using FileTime.App.FrequencyNavigation.Services;
using FileTime.App.FuzzyPanel; using FileTime.App.FuzzyPanel;
using FileTime.Core.Models; using FileTime.Core.Models;
using FileTime.Core.Timeline; using FileTime.Core.Timeline;
using GeneralInputKey;
namespace FileTime.App.FrequencyNavigation.ViewModels; namespace FileTime.App.FrequencyNavigation.ViewModels;

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using GeneralInputKey;
namespace FileTime.App.FuzzyPanel; namespace FileTime.App.FuzzyPanel;

View File

@@ -1,5 +1,7 @@
using System.ComponentModel; using System.ComponentModel;
using DeclarativeProperty;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using GeneralInputKey;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
namespace FileTime.App.FuzzyPanel; namespace FileTime.App.FuzzyPanel;
@@ -9,7 +11,7 @@ public abstract partial class FuzzyPanelViewModel<TItem> : IFuzzyPanelViewModel<
private readonly Func<TItem, TItem, bool> _itemEquality; private readonly Func<TItem, TItem, bool> _itemEquality;
private string _searchText = String.Empty; private string _searchText = String.Empty;
[Notify(set: Setter.Protected)] private IObservable<bool> _showWindow; [Notify(set: Setter.Protected)] private IDeclarativeProperty<bool> _showWindow;
[Notify(set: Setter.Protected)] private List<TItem> _filteredMatches; [Notify(set: Setter.Protected)] private List<TItem> _filteredMatches;
[Notify(set: Setter.Protected)] private TItem? _selectedItem; [Notify(set: Setter.Protected)] private TItem? _selectedItem;

View File

@@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\AppCommon\FileTime.App.CommandPalette.Abstractions\FileTime.App.CommandPalette.Abstractions.csproj" />
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" /> <ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" /> <ProjectReference Include="..\..\Library\TerminalUI\TerminalUI.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.ViewModels;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -8,4 +9,5 @@ public interface IRootViewModel
IPossibleCommandsViewModel PossibleCommands { get; } IPossibleCommandsViewModel PossibleCommands { get; }
string UserName { get; } string UserName { get; }
string MachineName { get; } string MachineName { get; }
ICommandPaletteViewModel CommandPalette { get; }
} }

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using GeneralInputKey;
namespace FileTime.ConsoleUI.App.KeyInputHandling; namespace FileTime.ConsoleUI.App.KeyInputHandling;

View File

@@ -1,6 +1,6 @@
using TerminalUI.Color; using TerminalUI.Color;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App.Styling;
public interface ITheme public interface ITheme
{ {
@@ -14,4 +14,5 @@ public interface ITheme
IColor? MarkedSelectedItemBackgroundColor { get; } IColor? MarkedSelectedItemBackgroundColor { get; }
IColor? SelectedItemColor { get; } IColor? SelectedItemColor { get; }
IColor? SelectedTabBackgroundColor { get; } IColor? SelectedTabBackgroundColor { get; }
ListViewItemTheme ListViewItemTheme { get; }
} }

View File

@@ -0,0 +1,8 @@
using TerminalUI.Color;
namespace FileTime.ConsoleUI.App.Styling;
public record ListViewItemTheme(
IColor? SelectedBackgroundColor,
IColor? SelectedForegroundColor
);

View File

@@ -3,6 +3,7 @@ using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.KeyInputHandling; using FileTime.ConsoleUI.App.KeyInputHandling;
using GeneralInputKey;
using TerminalUI; using TerminalUI;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
@@ -54,7 +55,6 @@ public class App : IApplication
_applicationContext.IsRunning = false; _applicationContext.IsRunning = false;
}; };
_mainWindow.Initialize();
foreach (var rootView in _mainWindow.RootViews()) foreach (var rootView in _mainWindow.RootViews())
{ {
_applicationContext.EventLoop.AddViewToRender(rootView); _applicationContext.EventLoop.AddViewToRender(rootView);
@@ -63,6 +63,8 @@ public class App : IApplication
_applicationContext.IsRunning = true; _applicationContext.IsRunning = true;
_renderThread.Start(); _renderThread.Start();
var focusManager = _applicationContext.FocusManager;
while (_applicationContext.IsRunning) while (_applicationContext.IsRunning)
{ {
if (_consoleDriver.CanRead()) if (_consoleDriver.CanRead())
@@ -79,12 +81,21 @@ public class App : IApplication
var keyEventArgs = new GeneralKeyEventArgs var keyEventArgs = new GeneralKeyEventArgs
{ {
Key = mappedKey Key = mappedKey,
KeyChar = key.KeyChar,
SpecialKeysStatus = specialKeysStatus
}; };
if (focusManager.Focused is { } focused)
{
focused.HandleKeyInput(keyEventArgs);
}
else
{
_keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus); _keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus);
} }
} }
}
Thread.Sleep(10); Thread.Sleep(10);
} }

View File

@@ -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<IRootViewModel> View()
{
var inputTextBox = new TextBox<IRootViewModel>()
.WithKeyHandler(k =>
{
if (k.Key == Keys.Escape)
{
_commandPaletteService.CloseCommandPalette();
}
});
var root = new Border<IRootViewModel>
{
Margin = 5,
Padding = 1,
MaxWidth = 50,
Content = new Grid<IRootViewModel>
{
RowDefinitionsObject = "Auto *",
ChildInitializer =
{
new Border<IRootViewModel>
{
Margin = new Thickness(0, 0, 0, 2),
Content = inputTextBox
},
new ListView<IRootViewModel, ICommandPaletteEntryViewModel>
{
Extensions =
{
new GridPositionExtension(0, 1)
},
ItemTemplate = item =>
{
var root = new Grid<ICommandPaletteEntryViewModel>
{
ColumnDefinitionsObject = "* Auto",
ChildInitializer =
{
new TextBlock<ICommandPaletteEntryViewModel>()
.Setup(t => t.Bind(
t,
d => d.Title,
t => t.Text)),
new TextBlock<ICommandPaletteEntryViewModel>
{
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;
}
}

View File

@@ -25,8 +25,4 @@
<ProjectReference Include="..\FileTime.ConsoleUI.App.Abstractions\FileTime.ConsoleUI.App.Abstractions.csproj" /> <ProjectReference Include="..\FileTime.ConsoleUI.App.Abstractions\FileTime.ConsoleUI.App.Abstractions.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Controls\" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using GeneralInputKey;
namespace FileTime.ConsoleUI.App.KeyInputHandling; namespace FileTime.ConsoleUI.App.KeyInputHandling;
@@ -70,6 +71,7 @@ public class ConsoleAppKeyService : IAppKeyService<ConsoleKey>
{ConsoleKey.Enter, Keys.Enter}, {ConsoleKey.Enter, Keys.Enter},
{ConsoleKey.Escape, Keys.Escape}, {ConsoleKey.Escape, Keys.Escape},
{ConsoleKey.Backspace, Keys.Backspace}, {ConsoleKey.Backspace, Keys.Backspace},
{ConsoleKey.Delete, Keys.Delete},
{ConsoleKey.Spacebar, Keys.Space}, {ConsoleKey.Spacebar, Keys.Space},
{ConsoleKey.PageUp, Keys.PageUp}, {ConsoleKey.PageUp, Keys.PageUp},
{ConsoleKey.PageDown, Keys.PageDown}, {ConsoleKey.PageDown, Keys.PageDown},

View File

@@ -2,6 +2,7 @@
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using GeneralInputKey;
namespace FileTime.ConsoleUI.App.KeyInputHandling; namespace FileTime.ConsoleUI.App.KeyInputHandling;
@@ -23,13 +24,15 @@ public class KeyInputHandlerService : IKeyInputHandlerService
public void HandleKeyInput(GeneralKeyEventArgs keyEvent, SpecialKeysStatus specialKeysStatus) public void HandleKeyInput(GeneralKeyEventArgs keyEvent, SpecialKeysStatus specialKeysStatus)
{ {
if (keyEvent.Handled) return;
if (_appState.ViewMode.Value == ViewMode.Default) if (_appState.ViewMode.Value == ViewMode.Default)
{ {
Task.Run(async () => await _defaultModeKeyInputHandler.HandleInputKey(keyEvent, specialKeysStatus)).Wait(); Task.Run(async () => await _defaultModeKeyInputHandler.HandleInputKey(keyEvent)).Wait();
} }
else else
{ {
Task.Run(async () => await _rapidTravelModeKeyInputHandler.HandleInputKey(keyEvent, specialKeysStatus)).Wait(); Task.Run(async () => await _rapidTravelModeKeyInputHandler.HandleInputKey(keyEvent)).Wait();
} }
} }
} }

View File

@@ -1,5 +1,7 @@
using FileTime.App.Core.Models.Enums; using FileTime.App.Core.Models.Enums;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.Controls;
using FileTime.ConsoleUI.App.Styling;
using FileTime.Core.Enums; using FileTime.Core.Enums;
using TerminalUI; using TerminalUI;
using TerminalUI.Color; using TerminalUI.Color;
@@ -15,25 +17,47 @@ public class MainWindow
private readonly IRootViewModel _rootViewModel; private readonly IRootViewModel _rootViewModel;
private readonly IApplicationContext _applicationContext; private readonly IApplicationContext _applicationContext;
private readonly ITheme _theme; private readonly ITheme _theme;
private readonly CommandPalette _commandPalette;
private IView _root; private readonly Lazy<IView> _root;
public MainWindow( public MainWindow(
IRootViewModel rootViewModel, IRootViewModel rootViewModel,
IApplicationContext applicationContext, IApplicationContext applicationContext,
ITheme theme) ITheme theme,
CommandPalette commandPalette)
{ {
_rootViewModel = rootViewModel; _rootViewModel = rootViewModel;
_applicationContext = applicationContext; _applicationContext = applicationContext;
_theme = theme; _theme = theme;
_commandPalette = commandPalette;
_root = new Lazy<IView>(Initialize);
} }
public void Initialize() public IEnumerable<IView> RootViews() => new[]
{
_root.Value
};
public Grid<IRootViewModel> Initialize()
{ {
var root = new Grid<IRootViewModel> var root = new Grid<IRootViewModel>
{ {
Name = "root",
DataContext = _rootViewModel, DataContext = _rootViewModel,
ApplicationContext = _applicationContext, ApplicationContext = _applicationContext,
ChildInitializer =
{
MainContent(),
_commandPalette.View()
}
};
return root;
}
private Grid<IRootViewModel> MainContent() =>
new()
{
RowDefinitionsObject = "Auto * Auto", RowDefinitionsObject = "Auto * Auto",
ChildInitializer = ChildInitializer =
{ {
@@ -108,8 +132,6 @@ public class MainWindow
} }
} }
}; };
_root = root;
}
private IView<IRootViewModel> PossibleCommands() private IView<IRootViewModel> PossibleCommands()
{ {
@@ -309,11 +331,6 @@ public class MainWindow
return list; return list;
} }
public IEnumerable<IView> RootViews() => new IView[]
{
_root
};
private IColor? ToForegroundColor(ItemViewMode viewMode, AbsolutePathType absolutePathType) => private IColor? ToForegroundColor(ItemViewMode viewMode, AbsolutePathType absolutePathType) =>
(viewMode, absolutePathType) switch (viewMode, absolutePathType) switch
{ {

View File

@@ -1,4 +1,5 @@
using FileTime.App.Core.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.ViewModels;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -8,12 +9,15 @@ public class RootViewModel : IRootViewModel
public string MachineName => Environment.MachineName; public string MachineName => Environment.MachineName;
public IPossibleCommandsViewModel PossibleCommands { get; } public IPossibleCommandsViewModel PossibleCommands { get; }
public IConsoleAppState AppState { get; } public IConsoleAppState AppState { get; }
public ICommandPaletteViewModel CommandPalette { get; }
public RootViewModel( public RootViewModel(
IConsoleAppState appState, IConsoleAppState appState,
IPossibleCommandsViewModel possibleCommands) IPossibleCommandsViewModel possibleCommands,
ICommandPaletteViewModel commandPalette)
{ {
AppState = appState; AppState = appState;
PossibleCommands = possibleCommands; PossibleCommands = possibleCommands;
CommandPalette = commandPalette;
} }
} }

View File

@@ -1,7 +1,9 @@
using FileTime.App.Core.Configuration; using FileTime.App.Core.Configuration;
using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.Configuration; using FileTime.ConsoleUI.App.Configuration;
using FileTime.ConsoleUI.App.Controls;
using FileTime.ConsoleUI.App.KeyInputHandling; using FileTime.ConsoleUI.App.KeyInputHandling;
using FileTime.ConsoleUI.App.Services; using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
@@ -19,7 +21,6 @@ public static class Startup
public static IServiceCollection AddConsoleServices(this IServiceCollection services, IConfigurationRoot configuration) public static IServiceCollection AddConsoleServices(this IServiceCollection services, IConfigurationRoot configuration)
{ {
services.TryAddSingleton<IApplication, App>(); services.TryAddSingleton<IApplication, App>();
services.TryAddSingleton<MainWindow>();
services.TryAddSingleton<IConsoleAppState, ConsoleAppState>(); services.TryAddSingleton<IConsoleAppState, ConsoleAppState>();
services.TryAddSingleton<IAppState>(sp => sp.GetRequiredService<IConsoleAppState>()); services.TryAddSingleton<IAppState>(sp => sp.GetRequiredService<IConsoleAppState>());
services.TryAddSingleton<IUserCommunicationService, ConsoleUserCommunicationService>(); services.TryAddSingleton<IUserCommunicationService, ConsoleUserCommunicationService>();
@@ -29,6 +30,7 @@ public static class Startup
services.AddSingleton<CustomLoggerSink>(); services.AddSingleton<CustomLoggerSink>();
services.TryAddSingleton(new ApplicationConfiguration(true)); services.TryAddSingleton(new ApplicationConfiguration(true));
services.TryAddSingleton<IRootViewModel, RootViewModel>(); services.TryAddSingleton<IRootViewModel, RootViewModel>();
services.TryAddSingleton<IFocusManager, FocusManager>();
services.Configure<ConsoleApplicationConfiguration>(configuration); services.Configure<ConsoleApplicationConfiguration>(configuration);
@@ -36,9 +38,17 @@ public static class Startup
=> new ApplicationContext => new ApplicationContext
{ {
ConsoleDriver = sp.GetRequiredService<IConsoleDriver>(), ConsoleDriver = sp.GetRequiredService<IConsoleDriver>(),
LoggerFactory = sp.GetRequiredService<ILoggerFactory>() LoggerFactory = sp.GetRequiredService<ILoggerFactory>(),
FocusManager = sp.GetRequiredService<IFocusManager>(),
} }
); );
return services; return services;
} }
public static IServiceCollection AddConsoleViews(this IServiceCollection services)
{
services.TryAddSingleton<MainWindow>();
services.TryAddSingleton<CommandPalette>();
return services;
}
} }

View File

@@ -1,4 +1,5 @@
using FileTime.ConsoleUI.App; using FileTime.ConsoleUI.App;
using FileTime.ConsoleUI.App.Styling;
using TerminalUI.Color; using TerminalUI.Color;
namespace FileTime.ConsoleUI.Styles; namespace FileTime.ConsoleUI.Styles;
@@ -14,6 +15,7 @@ public record Theme(
IColor? MarkedSelectedItemBackgroundColor, IColor? MarkedSelectedItemBackgroundColor,
IColor? SelectedItemColor, IColor? SelectedItemColor,
IColor? SelectedTabBackgroundColor, IColor? SelectedTabBackgroundColor,
ListViewItemTheme ListViewItemTheme,
Type? ForegroundColors, Type? ForegroundColors,
Type? BackgroundColors) : ITheme, IColorSampleProvider; Type? BackgroundColors) : ITheme, IColorSampleProvider;
@@ -30,6 +32,10 @@ public static class DefaultThemes
MarkedSelectedItemBackgroundColor: Color256Colors.Foregrounds.Yellow, MarkedSelectedItemBackgroundColor: Color256Colors.Foregrounds.Yellow,
SelectedItemColor: Color256Colors.Foregrounds.Black, SelectedItemColor: Color256Colors.Foregrounds.Black,
SelectedTabBackgroundColor: Color256Colors.Backgrounds.Green, SelectedTabBackgroundColor: Color256Colors.Backgrounds.Green,
ListViewItemTheme: new(
SelectedBackgroundColor: Color256Colors.Backgrounds.Gray,
SelectedForegroundColor: Color256Colors.Foregrounds.Black
),
ForegroundColors: typeof(Color256Colors.Foregrounds), ForegroundColors: typeof(Color256Colors.Foregrounds),
BackgroundColors: typeof(Color256Colors.Backgrounds) BackgroundColors: typeof(Color256Colors.Backgrounds)
); );
@@ -45,6 +51,10 @@ public static class DefaultThemes
MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Yellow, MarkedSelectedItemBackgroundColor: ConsoleColors.Foregrounds.Yellow,
SelectedItemColor: ConsoleColors.Foregrounds.Black, SelectedItemColor: ConsoleColors.Foregrounds.Black,
SelectedTabBackgroundColor: ConsoleColors.Backgrounds.Green, SelectedTabBackgroundColor: ConsoleColors.Backgrounds.Green,
ListViewItemTheme: new(
SelectedBackgroundColor: ConsoleColors.Backgrounds.Gray,
SelectedForegroundColor: ConsoleColors.Foregrounds.Black
),
ForegroundColors: typeof(ConsoleColors.Foregrounds), ForegroundColors: typeof(ConsoleColors.Foregrounds),
BackgroundColors: typeof(ConsoleColors.Backgrounds) BackgroundColors: typeof(ConsoleColors.Backgrounds)
); );

View File

@@ -22,6 +22,7 @@ public static class DI
=> ServiceProvider = DependencyInjection => ServiceProvider = DependencyInjection
.RegisterDefaultServices(configuration: configuration) .RegisterDefaultServices(configuration: configuration)
.AddConsoleServices(configuration) .AddConsoleServices(configuration)
.AddConsoleViews()
.AddLocalProviderServices() .AddLocalProviderServices()
.AddServerCoreServices() .AddServerCoreServices()
.AddFrequencyNavigation() .AddFrequencyNavigation()

View File

@@ -1,4 +1,5 @@
using FileTime.ConsoleUI.App; using FileTime.ConsoleUI.App;
using FileTime.ConsoleUI.App.Styling;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
using TerminalUI.Models; using TerminalUI.Models;

View File

@@ -3,6 +3,7 @@ using FileTime.App.Core;
using FileTime.App.Core.Configuration; using FileTime.App.Core.Configuration;
using FileTime.ConsoleUI; using FileTime.ConsoleUI;
using FileTime.ConsoleUI.App; using FileTime.ConsoleUI.App;
using FileTime.ConsoleUI.App.Styling;
using FileTime.ConsoleUI.InfoProviders; using FileTime.ConsoleUI.InfoProviders;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;

View File

@@ -1,5 +1,6 @@
using FileTime.ConsoleUI.App; using FileTime.ConsoleUI.App;
using FileTime.ConsoleUI.App.Configuration; using FileTime.ConsoleUI.App.Configuration;
using FileTime.ConsoleUI.App.Styling;
using FileTime.ConsoleUI.Styles; using FileTime.ConsoleUI.Styles;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;

View File

@@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileTime.ConsoleUI.Styles",
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableComputations.Extensions", "Library\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj", "{6C3C3151-9341-4792-9B0B-A11C0658524E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ObservableComputations.Extensions", "Library\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj", "{6C3C3151-9341-4792-9B0B-A11C0658524E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneralInputKey", "Library\GeneralInputKey\GeneralInputKey.csproj", "{91AE5B64-042B-4660-A8E8-D247E6E14A1E}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{6C3C3151-9341-4792-9B0B-A11C0658524E}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -397,6 +403,7 @@ Global
{AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {AF4FE804-12D9-46E2-A584-BFF6D4509766} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
{CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549} {CCB6F86A-7E80-448E-B543-DF9DB337C42A} = {CAEEAD3C-41EB-405C-ACA9-BA1E4C352549}
{6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853} {6C3C3151-9341-4792-9B0B-A11C0658524E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
{91AE5B64-042B-4660-A8E8-D247E6E14A1E} = {07CA18AA-B85D-4DEE-BB86-F569F6029853}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF} SolutionGuid = {859FB3DF-C60A-46B1-82E5-90274905D1EF}

View File

@@ -1,18 +1,47 @@
using Avalonia.Input; using Avalonia.Input;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using GeneralInputKey;
namespace FileTime.GuiApp.App.Extensions; namespace FileTime.GuiApp.App.Extensions;
public static class KeyEventArgsExtension public static class KeyEventArgsExtension
{ {
public static GeneralKeyEventArgs? ToGeneralKeyEventArgs(this KeyEventArgs args, IAppKeyService<Key> appKeyService) public static GeneralKeyEventArgs? ToGeneralKeyEventArgs(
this KeyEventArgs args,
IAppKeyService<Key> appKeyService,
SpecialKeysStatus specialKeysStatus)
{ {
var maybeKey = appKeyService.MapKey(args.Key); 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) 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<Key> 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
)
}; };
} }
} }

View File

@@ -2,6 +2,7 @@
using Avalonia.Input; using Avalonia.Input;
using FileTime.App.Core.Models; using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using GeneralInputKey;
namespace FileTime.GuiApp.App.Services; namespace FileTime.GuiApp.App.Services;
@@ -71,6 +72,7 @@ public sealed class GuiAppKeyService : IAppKeyService<Key>
{Key.Enter, Keys.Enter}, {Key.Enter, Keys.Enter},
{Key.Escape, Keys.Escape}, {Key.Escape, Keys.Escape},
{Key.Back, Keys.Backspace}, {Key.Back, Keys.Backspace},
{Key.Delete, Keys.Delete},
{Key.Space, Keys.Space}, {Key.Space, Keys.Space},
{Key.PageUp, Keys.PageUp}, {Key.PageUp, Keys.PageUp},
{Key.PageDown, Keys.PageDown}, {Key.PageDown, Keys.PageDown},

View File

@@ -5,6 +5,7 @@ using FileTime.App.Core.Services;
using FileTime.GuiApp.App.Extensions; using FileTime.GuiApp.App.Extensions;
using FileTime.GuiApp.App.Models; using FileTime.GuiApp.App.Models;
using FileTime.GuiApp.App.ViewModels; using FileTime.GuiApp.App.ViewModels;
using GeneralInputKey;
namespace FileTime.GuiApp.App.Services; namespace FileTime.GuiApp.App.Services;
@@ -50,9 +51,9 @@ public class KeyInputHandlerService : IKeyInputHandlerService
//_appState.NoCommandFound = false; //_appState.NoCommandFound = false;
var isAltPressed = (e.KeyModifiers & KeyModifiers.Alt) == KeyModifiers.Alt; var isAltPressed = (e.KeyModifiers & KeyModifiers.Alt) != 0;
var isShiftPressed = (e.KeyModifiers & KeyModifiers.Shift) == KeyModifiers.Shift; var isShiftPressed = (e.KeyModifiers & KeyModifiers.Shift) != 0;
var isCtrlPressed = (e.KeyModifiers & KeyModifiers.Control) == KeyModifiers.Control; var isCtrlPressed = (e.KeyModifiers & KeyModifiers.Control) != 0;
if (isCtrlPressed if (isCtrlPressed
&& e.Key is Key.Left or Key.Right or Key.Up or Key.Down && 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 (_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 else
{ {
if (e.ToGeneralKeyEventArgs(_appKeyService) is { } args) if (e.ToGeneralKeyEventArgs(_appKeyService, specialKeyStatus) is { } args)
{ {
await _rapidTravelModeKeyInputHandler.HandleInputKey(args, specialKeyStatus); await _rapidTravelModeKeyInputHandler.HandleInputKey(args);
} }
} }
} }

View File

@@ -39,7 +39,7 @@ public partial class CommandPalette : UserControl
} }
else else
{ {
if (e.ToGeneralKeyEventArgs(_appKeyService.Value) is not { } eventArgs) return; if (e.ToGeneralKeyEventArgs(_appKeyService.Value, e.KeyModifiers) is not { } eventArgs) return;
viewModel.HandleKeyDown(eventArgs); viewModel.HandleKeyDown(eventArgs);
} }
@@ -50,7 +50,7 @@ public partial class CommandPalette : UserControl
if (e.Handled if (e.Handled
|| DataContext is not ICommandPaletteViewModel viewModel) return; || 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); viewModel.HandleKeyUp(eventArgs);
} }
} }

View File

@@ -1,6 +1,7 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using FileTime.App.Core.Models;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.App.FrequencyNavigation.ViewModels; using FileTime.App.FrequencyNavigation.ViewModels;
using FileTime.GuiApp.App.Extensions; using FileTime.GuiApp.App.Extensions;
@@ -39,7 +40,7 @@ public partial class FrequencyNavigation : UserControl
} }
else else
{ {
if (e.ToGeneralKeyEventArgs(_appKeyService.Value) is not { } eventArgs) return; if (e.ToGeneralKeyEventArgs(_appKeyService.Value, e.KeyModifiers) is not { } eventArgs) return;
viewModel.HandleKeyDown(eventArgs); viewModel.HandleKeyDown(eventArgs);
} }
@@ -50,7 +51,7 @@ public partial class FrequencyNavigation : UserControl
if (e.Handled if (e.Handled
|| DataContext is not IFrequencyNavigationViewModel viewModel) return; || 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); viewModel.HandleKeyUp(eventArgs);
} }
} }

View File

@@ -972,7 +972,7 @@
Background="{DynamicResource BarelyTransparentBackgroundColor}" Background="{DynamicResource BarelyTransparentBackgroundColor}"
DataContext="{Binding FrequencyNavigationService.CurrentModal}" DataContext="{Binding FrequencyNavigationService.CurrentModal}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding ShowWindow^, FallbackValue=False}" IsVisible="{Binding ShowWindow.Value, FallbackValue=False}"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<Grid Background="{DynamicResource ContainerBackgroundColor}" Margin="100"> <Grid Background="{DynamicResource ContainerBackgroundColor}" Margin="100">
<local:FrequencyNavigation IsVisible="{Binding ShowWindow^, FallbackValue=False}" /> <local:FrequencyNavigation IsVisible="{Binding ShowWindow^, FallbackValue=False}" />
@@ -983,7 +983,7 @@
Background="{DynamicResource BarelyTransparentBackgroundColor}" Background="{DynamicResource BarelyTransparentBackgroundColor}"
DataContext="{Binding CommandPaletteService.CurrentModal}" DataContext="{Binding CommandPaletteService.CurrentModal}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding ShowWindow^, FallbackValue=False}" IsVisible="{Binding ShowWindow.Value, FallbackValue=False}"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<Grid Background="{DynamicResource ContainerBackgroundColor}" Margin="100"> <Grid Background="{DynamicResource ContainerBackgroundColor}" Margin="100">
<local:CommandPalette IsVisible="{Binding ShowWindow^, FallbackValue=False}" /> <local:CommandPalette IsVisible="{Binding ShowWindow^, FallbackValue=False}" />

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -1,10 +1,12 @@
namespace FileTime.App.Core.Models; namespace GeneralInputKey;
public class GeneralKeyEventArgs public class GeneralKeyEventArgs
{ {
private readonly Action<bool>? _handledChanged; private readonly Action<bool>? _handledChanged;
private bool _handled; private bool _handled;
public required Keys Key { get; init; } public required Keys Key { get; init; }
public required char KeyChar { get; init; }
public required SpecialKeysStatus SpecialKeysStatus { get; init; }
public bool Handled public bool Handled
{ {

View File

@@ -1,6 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
namespace FileTime.App.Core.Models; namespace GeneralInputKey;
public enum Keys public enum Keys
{ {
@@ -49,6 +49,7 @@ public enum Keys
Enter, Enter,
Escape, Escape,
Backspace, Backspace,
Delete,
Space, Space,
PageUp, PageUp,
PageDown, PageDown,
@@ -57,24 +58,14 @@ public enum Keys
Tab, Tab,
LWin, LWin,
RWin, RWin,
[Description("0")] [Description("0")] Num0,
Num0, [Description("1")] Num1,
[Description("1")] [Description("2")] Num2,
Num1, [Description("3")] Num3,
[Description("2")] [Description("4")] Num4,
Num2, [Description("5")] Num5,
[Description("3")] [Description("6")] Num6,
Num3, [Description("7")] Num7,
[Description("4")] [Description("8")] Num8,
Num4, [Description("9")] Num9
[Description("5")]
Num5,
[Description("6")]
Num6,
[Description("7")]
Num7,
[Description("8")]
Num8,
[Description("9")]
Num9,
} }

View File

@@ -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);
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging; using FileTime.App.Core.Models;
using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
namespace TerminalUI; namespace TerminalUI;
@@ -6,6 +7,7 @@ namespace TerminalUI;
public class ApplicationContext : IApplicationContext public class ApplicationContext : IApplicationContext
{ {
public required IConsoleDriver ConsoleDriver { get; init; } public required IConsoleDriver ConsoleDriver { get; init; }
public required IFocusManager FocusManager { get; init; }
public ILoggerFactory? LoggerFactory { get; init; } public ILoggerFactory? LoggerFactory { get; init; }
public IEventLoop EventLoop { get; init; } public IEventLoop EventLoop { get; init; }
public bool IsRunning { get; set; } public bool IsRunning { get; set; }

View File

@@ -6,7 +6,7 @@ using TerminalUI.Traits;
namespace TerminalUI; namespace TerminalUI;
public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable public sealed class Binding<TDataContext, TExpressionResult, TResult> : PropertyTrackerBase<TDataContext, TExpressionResult>
{ {
private readonly Func<TDataContext, TExpressionResult> _dataContextMapper; private readonly Func<TDataContext, TExpressionResult> _dataContextMapper;
private IView<TDataContext> _dataSourceView; private IView<TDataContext> _dataSourceView;
@@ -15,8 +15,6 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
private readonly Func<TExpressionResult, TResult> _converter; private readonly Func<TExpressionResult, TResult> _converter;
private readonly TResult? _fallbackValue; private readonly TResult? _fallbackValue;
private IDisposableCollection? _propertySourceDisposableCollection; private IDisposableCollection? _propertySourceDisposableCollection;
private PropertyTrackTreeItem? _propertyTrackTreeItem;
private IPropertyChangeTracker? _propertyChangeTracker;
public Binding( public Binding(
IView<TDataContext> dataSourceView, IView<TDataContext> dataSourceView,
@@ -25,7 +23,7 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
PropertyInfo targetProperty, PropertyInfo targetProperty,
Func<TExpressionResult, TResult> converter, Func<TExpressionResult, TResult> converter,
TResult? fallbackValue = default TResult? fallbackValue = default
) ) : base(() => dataSourceView.DataContext, dataSourceExpression)
{ {
ArgumentNullException.ThrowIfNull(dataSourceView); ArgumentNullException.ThrowIfNull(dataSourceView);
ArgumentNullException.ThrowIfNull(dataSourceExpression); ArgumentNullException.ThrowIfNull(dataSourceExpression);
@@ -39,8 +37,6 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
_converter = converter; _converter = converter;
_fallbackValue = fallbackValue; _fallbackValue = fallbackValue;
InitTrackingTree(dataSourceExpression);
UpdateTrackers(); UpdateTrackers();
dataSourceView.PropertyChanged += View_PropertyChanged; dataSourceView.PropertyChanged += View_PropertyChanged;
@@ -60,106 +56,6 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
} }
} }
private void InitTrackingTree(Expression<Func<TDataContext?, TExpressionResult>> dataContextExpression)
{
var properties = new List<string>();
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<string> 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) private void View_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return; if (e.PropertyName != nameof(IView<TDataContext>.DataContext)) return;
@@ -168,22 +64,7 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
UpdateTargetProperty(); UpdateTargetProperty();
} }
private void UpdateTrackers() protected override void Update(string propertyPath) => UpdateTargetProperty();
{
if (_propertyChangeTracker is not null)
{
_propertyChangeTracker.Dispose();
}
if (_propertyTrackTreeItem is not null)
{
_propertyChangeTracker = PropertyChangeHelper.TraverseDataContext(
_propertyTrackTreeItem,
_dataSourceView.DataContext,
UpdateTargetProperty
);
}
}
private void UpdateTargetProperty() private void UpdateTargetProperty()
{ {
@@ -200,8 +81,9 @@ public class Binding<TDataContext, TExpressionResult, TResult> : IDisposable
_targetProperty.SetValue(_propertySource, value); _targetProperty.SetValue(_propertySource, value);
} }
public void Dispose() public override void Dispose()
{ {
base.Dispose();
_propertySourceDisposableCollection?.RemoveDisposable(this); _propertySourceDisposableCollection?.RemoveDisposable(this);
_dataSourceView.RemoveDisposable(this); _dataSourceView.RemoveDisposable(this);
_dataSourceView.PropertyChanged -= View_PropertyChanged; _dataSourceView.PropertyChanged -= View_PropertyChanged;

View File

@@ -2,7 +2,7 @@
namespace TerminalUI.Color; 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() public string ToConsoleColor()
=> Type switch => Type switch

View File

@@ -2,7 +2,7 @@
namespace TerminalUI.Color; 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() public string ToConsoleColor()
=> Type switch => Type switch

View File

@@ -1,6 +1,6 @@
namespace TerminalUI.Color; 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 string ToConsoleColor() => throw new NotImplementedException();
public IColor AsForeground() => this with {Type = ColorType.Foreground}; public IColor AsForeground() => this with {Type = ColorType.Foreground};

View File

@@ -0,0 +1,157 @@
using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
namespace TerminalUI.Controls;
public partial class Border<T> : ContentView<T>
{
[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);
}
}

View File

@@ -1,11 +1,13 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public abstract class ChildContainerView<T> : View<T>, IChildContainer<T> public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
{ {
private readonly ObservableCollection<IView> _children = new(); private readonly ObservableCollection<IView> _children = new();
private readonly Dictionary<IView, bool> _visibilities = new();
public ReadOnlyObservableCollection<IView> Children { get; } public ReadOnlyObservableCollection<IView> Children { get; }
public ChildInitializer<T> ChildInitializer { get; } public ChildInitializer<T> ChildInitializer { get; }
@@ -41,14 +43,18 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
}; };
} }
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>(TChild child) public override TChild AddChild<TChild>(TChild child)
{ {
child = base.AddChild(child); child = base.AddChild(child);

View File

@@ -40,18 +40,9 @@ public abstract partial class ContentView<T> : View<T>, IContentRenderer<T>
RerenderProperties.Add(nameof(ContentRendererMethod)); 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) private bool DefaultContentRender(RenderContext renderContext, Position position, Size size)
{ {
if (Content is null) if (Content is null || !Content.IsVisible)
{ {
if (_placeholderRenderDone) return false; if (_placeholderRenderDone) return false;
_placeholderRenderDone = true; _placeholderRenderDone = true;

View File

@@ -1,5 +1,4 @@
using System.Collections.ObjectModel; using System.Diagnostics;
using System.Diagnostics;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
@@ -13,9 +12,9 @@ public class Grid<T> : ChildContainerView<T>
private List<ColumnDefinition> _columnDefinitions = new() {ColumnDefinition.Star(1)}; private List<ColumnDefinition> _columnDefinitions = new() {ColumnDefinition.Star(1)};
private ILogger<Grid<T>>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger<Grid<T>>(); private ILogger<Grid<T>>? Logger => ApplicationContext?.LoggerFactory?.CreateLogger<Grid<T>>();
private delegate void WithSizes(RenderContext renderContext, Span<int> widths, Span<int> heights); private delegate void WithSizes(RenderContext renderContext, ReadOnlySpan<int> widths, ReadOnlySpan<int> heights);
private delegate TResult WithSizes<TResult>(RenderContext renderContext, Span<int> widths, Span<int> heights); private delegate TResult WithSizes<TResult>(RenderContext renderContext, ReadOnlySpan<int> widths, ReadOnlySpan<int> heights);
private const int ToBeCalculated = -1; private const int ToBeCalculated = -1;
@@ -132,14 +131,14 @@ public class Grid<T> : ChildContainerView<T>
var width = 0; var width = 0;
var height = 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); return new Size(width, height);
@@ -151,49 +150,122 @@ public class Grid<T> : ChildContainerView<T>
new Option<Size>(size, true), new Option<Size>(size, true),
(context, columnWidths, rowHeights) => (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); for (var row = 0; row < rowHeights.Length; row++)
{
RenderViewsByPosition(
context,
position,
columnWidths,
rowHeights,
viewsByPosition,
column,
row
);
}
}
var width = columnWidths[x]; return true;
var height = rowHeights[y]; });
var left = position.X; private void RenderViewsByPosition(
var top = position.Y; RenderContext context,
Position gridPosition,
ReadOnlySpan<int> columnWidths,
ReadOnlySpan<int> rowHeights,
IReadOnlyDictionary<(int, int), List<IView>> viewsByPosition,
int column,
int row)
{
if (!viewsByPosition.TryGetValue((column, row), out var children)) return;
for (var i = 0; i < x; i++) var anyChangedVisibility = false;
foreach (var child in children)
{
var lastVisibility = GetLastVisibility(child);
if (lastVisibility is { } b && b != child.IsVisible)
{
anyChangedVisibility = true;
break;
}
}
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<int> columnWidths,
ReadOnlySpan<int> rowHeights,
int column,
int row
)
{
var left = gridPosition.X;
var top = gridPosition.Y;
for (var i = 0; i < column; i++)
{ {
left += columnWidths[i]; left += columnWidths[i];
} }
for (var i = 0; i < y; i++) for (var i = 0; i < row; i++)
{ {
top += rowHeights[i]; top += rowHeights[i];
} }
child.Render(context, new Position(left, top), new Size(width, height)); return new Position(left, top);
}
return true;
/*var viewsByPosition = GroupViewsByPosition(columnWidths, rowHeights);
CleanUnusedArea(viewsByPosition, columnWidths, rowHeights);*/
});
/*private void CleanUnusedArea(Dictionary<(int, int),List<IView>> viewsByPosition, Span<int> columnWidths, Span<int> rowHeights)
{
for (var x = 0; x < columnWidths.Length; x++)
{
for (var y = 0; y < rowHeights.Length; y++)
{
if (!viewsByPosition.TryGetValue((x, y), out var list)) continue;
} }
} }
}*/
/*private Dictionary<(int, int), List<IView>> GroupViewsByPosition(int columns, int rows) private Dictionary<(int, int), List<IView>> GroupViewsByPosition(int columns, int rows)
{ {
Dictionary<ValueTuple<int, int>, List<IView>> viewsByPosition = new(); Dictionary<ValueTuple<int, int>, List<IView>> viewsByPosition = new();
foreach (var child in Children) foreach (var child in Children)
@@ -210,7 +282,7 @@ public class Grid<T> : ChildContainerView<T>
} }
return viewsByPosition; return viewsByPosition;
}*/ }
private ValueTuple<int, int> GetViewColumnAndRow(IView view, int columns, int rows) private ValueTuple<int, int> GetViewColumnAndRow(IView view, int columns, int rows)
{ {
@@ -237,7 +309,7 @@ public class Grid<T> : ChildContainerView<T>
{ {
WithCalculatedSize(renderContext, size, Helper); WithCalculatedSize(renderContext, size, Helper);
object? Helper(RenderContext renderContext1, Span<int> widths, Span<int> heights) object? Helper(RenderContext renderContext1, ReadOnlySpan<int> widths, ReadOnlySpan<int> heights)
{ {
actionWithSizes(renderContext1, widths, heights); actionWithSizes(renderContext1, widths, heights);
return null; return null;

View File

@@ -1,4 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using TerminalUI.Color;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.Traits; using TerminalUI.Traits;
@@ -17,9 +18,12 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection
int? MaxHeight { get; set; } int? MaxHeight { get; set; }
int? Height { get; set; } int? Height { get; set; }
int ActualHeight { get; } int ActualHeight { get; }
Margin Margin { get; set; } Thickness Margin { get; set; }
bool IsVisible { get; set; }
bool Attached { get; set; } bool Attached { get; set; }
string? Name { get; set; } string? Name { get; set; }
IColor? Foreground { get; set; }
IColor? Background { get; set; }
IApplicationContext? ApplicationContext { get; set; } IApplicationContext? ApplicationContext { get; set; }
List<object> Extensions { get; } List<object> Extensions { get; }
RenderMethod RenderMethod { get; set; } RenderMethod RenderMethod { get; set; }

View File

@@ -9,12 +9,12 @@ namespace TerminalUI.Controls;
public partial class ListView<TDataContext, TItem> : View<TDataContext> public partial class ListView<TDataContext, TItem> : View<TDataContext>
{ {
private static readonly ArrayPool<ListViewItem<TItem>> ListViewItemPool = ArrayPool<ListViewItem<TItem>>.Shared; private static readonly ArrayPool<ListViewItem<TItem, TDataContext>> ListViewItemPool = ArrayPool<ListViewItem<TItem, TDataContext>>.Shared;
private readonly List<IDisposable> _itemsDisposables = new(); private readonly List<IDisposable> _itemsDisposables = new();
private Func<IEnumerable<TItem>?>? _getItems; private Func<IEnumerable<TItem>?>? _getItems;
private object? _itemsSource; private object? _itemsSource;
private ListViewItem<TItem>[]? _listViewItems; private ListViewItem<TItem, TDataContext>[]? _listViewItems;
private int _listViewItemLength; private int _listViewItemLength;
private int _selectedIndex = 0; private int _selectedIndex = 0;
private int _renderStartIndex = 0; private int _renderStartIndex = 0;
@@ -30,6 +30,14 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
if (_selectedIndex != value) if (_selectedIndex != value)
{ {
_selectedIndex = value; _selectedIndex = value;
if (_listViewItems is not null)
{
for (var i = 0; i < _listViewItemLength; i++)
{
_listViewItems[i].IsSelected = i == value;
}
}
OnPropertyChanged(); OnPropertyChanged();
OnPropertyChanged(nameof(SelectedItem)); OnPropertyChanged(nameof(SelectedItem));
} }
@@ -124,7 +132,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
} }
} }
public Func<ListViewItem<TItem>, IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate; public Func<ListViewItem<TItem, TDataContext>, IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
public ListView() public ListView()
{ {
@@ -292,7 +300,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
return true; return true;
} }
private Span<ListViewItem<TItem>> InstantiateItemViews() private ReadOnlySpan<ListViewItem<TItem, TDataContext>> InstantiateItemViews()
{ {
var items = _getItems?.Invoke()?.ToList(); var items = _getItems?.Invoke()?.ToList();
if (items is null) if (items is null)
@@ -305,7 +313,7 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
return _listViewItems; return _listViewItems;
} }
Span<ListViewItem<TItem>> listViewItems; ReadOnlySpan<ListViewItem<TItem, TDataContext>> listViewItems;
if (_listViewItems is null || _listViewItemLength != items.Count) if (_listViewItems is null || _listViewItemLength != items.Count)
{ {
@@ -313,7 +321,8 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
for (var i = 0; i < items.Count; i++) for (var i = 0; i < items.Count; i++)
{ {
var dataContext = items[i]; var dataContext = items[i];
var child = CreateChild<ListViewItem<TItem>, TItem>(_ => dataContext); var child = new ListViewItem<TItem, TDataContext>(this);
AddChild(child, _ => dataContext);
var newContent = ItemTemplate(child); var newContent = ItemTemplate(child);
child.Content = newContent; child.Content = newContent;
newListViewItems[i] = child; newListViewItems[i] = child;
@@ -336,12 +345,12 @@ public partial class ListView<TDataContext, TItem> : View<TDataContext>
return listViewItems; return listViewItems;
} }
private Span<ListViewItem<TItem>> InstantiateEmptyItemViews() private ReadOnlySpan<ListViewItem<TItem, TDataContext>> InstantiateEmptyItemViews()
{ {
_listViewItems = ListViewItemPool.Rent(0); _listViewItems = ListViewItemPool.Rent(0);
_listViewItemLength = 0; _listViewItemLength = 0;
return _listViewItems; return _listViewItems;
} }
private static IView<TItem>? DefaultItemTemplate(ListViewItem<TItem> listViewItem) => null; private static IView<TItem>? DefaultItemTemplate(ListViewItem<TItem, TDataContext> listViewItem) => null;
} }

View File

@@ -1,12 +1,23 @@
using TerminalUI.Models; using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public class ListViewItem<T> : ContentView<T> public partial class ListViewItem<T, TParentDataContext> : ContentView<T>
{ {
public ListView<TParentDataContext, T> Parent { get; }
[Notify] private bool _isSelected;
public ListViewItem(ListView<TParentDataContext, T> parent)
{
Parent = parent;
RerenderProperties.Add(nameof(IsSelected));
}
protected override Size CalculateSize() 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(); return Content.GetRequestedSize();
} }

View File

@@ -19,7 +19,7 @@ public partial class Rectangle<T> : View<T>
protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size) protected override bool DefaultRenderer(RenderContext renderContext, Position position, Size size)
{ {
var renderState = new RenderState(position, size, Fill); 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; _lastRenderState = renderState;
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;

View File

@@ -17,6 +17,8 @@ public partial class StackPanel<T> : ChildContainerView<T>
foreach (var child in Children) foreach (var child in Children)
{ {
if (!child.IsVisible) continue;
var childSize = child.GetRequestedSize(); var childSize = child.GetRequestedSize();
_requestedSizes.Add(child, childSize); _requestedSizes.Add(child, childSize);
@@ -41,6 +43,8 @@ public partial class StackPanel<T> : ChildContainerView<T>
var neededRerender = false; var neededRerender = false;
foreach (var child in Children) foreach (var child in Children)
{ {
if (!child.IsVisible) continue;
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found"); if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found");
var childPosition = Orientation == Orientation.Vertical var childPosition = Orientation == Orientation.Vertical
@@ -55,6 +59,7 @@ public partial class StackPanel<T> : ChildContainerView<T>
{ {
childSize = childSize with {Width = endX - childPosition.X}; childSize = childSize with {Width = endX - childPosition.X};
} }
if (childPosition.Y + childSize.Height > endY) if (childPosition.Y + childSize.Height > endY)
{ {
childSize = childSize with {Height = endY - childPosition.Y}; childSize = childSize with {Height = endY - childPosition.Y};

View File

@@ -1,12 +1,13 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
[DebuggerDisplay("Text = {Text}")]
public partial class TextBlock<T> : View<T> public partial class TextBlock<T> : View<T>
{ {
private record RenderState( private record RenderState(
@@ -21,8 +22,6 @@ public partial class TextBlock<T> : View<T>
private bool _placeholderRenderDone; private bool _placeholderRenderDone;
[Notify] private string? _text = string.Empty; [Notify] private string? _text = string.Empty;
[Notify] private IColor? _foreground;
[Notify] private IColor? _background;
[Notify] private TextAlignment _textAlignment = TextAlignment.Left; [Notify] private TextAlignment _textAlignment = TextAlignment.Left;
public TextBlock() public TextBlock()
@@ -34,8 +33,6 @@ public partial class TextBlock<T> : View<T>
); );
RerenderProperties.Add(nameof(Text)); RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(Foreground));
RerenderProperties.Add(nameof(Background));
RerenderProperties.Add(nameof(TextAlignment)); RerenderProperties.Add(nameof(TextAlignment));
((INotifyPropertyChanged) this).PropertyChanged += (o, e) => ((INotifyPropertyChanged) this).PropertyChanged += (o, e) =>
@@ -53,9 +50,16 @@ public partial class TextBlock<T> : View<T>
{ {
if (size.Width == 0 || size.Height == 0) return false; if (size.Width == 0 || size.Height == 0) return false;
var driver = renderContext.ConsoleDriver; var foreground = Foreground ?? renderContext.Foreground;
var renderState = new RenderState(position, size, Text, _foreground, _background); var background = Background ?? renderContext.Background;
if (!NeedsRerender(renderState)) return false; var renderState = new RenderState(
position,
size,
Text,
foreground,
background);
if (!renderContext.ForceRerender && !NeedsRerender(renderState)) return false;
_lastRenderState = renderState; _lastRenderState = renderState;
@@ -72,41 +76,29 @@ public partial class TextBlock<T> : View<T>
_placeholderRenderDone = false; _placeholderRenderDone = false;
var driver = renderContext.ConsoleDriver;
driver.ResetColor(); driver.ResetColor();
if (Foreground is { } foreground) if (foreground is not null)
{ {
driver.SetForegroundColor(foreground); driver.SetForegroundColor(foreground);
} }
if (Background is { } background) if (background is not null)
{ {
driver.SetBackgroundColor(background); driver.SetBackgroundColor(background);
} }
RenderText(_textLines, driver, position, size); RenderText(_textLines, driver, position, size, TransformText);
return true; return true;
} }
private void RenderText(string[] textLines, IConsoleDriver driver, Position position, Size size) private string TransformText(string text, Position position, Size size)
{ => TextAlignment switch
for (var i = 0; i < textLines.Length; i++)
{
var text = textLines[i];
text = TextAlignment switch
{ {
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text), TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text),
_ => 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);
}
}
private bool NeedsRerender(RenderState renderState) private bool NeedsRerender(RenderState renderState)
=> _lastRenderState is null || _lastRenderState != renderState; => _lastRenderState is null || _lastRenderState != renderState;

View File

@@ -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<T> : View<T>, IFocusable
{
private record RenderState(
string? Text,
Position Position,
Size Size,
IColor? ForegroundColor,
IColor? BackgroundColor
);
private readonly List<Action<GeneralKeyEventArgs>> _keyHandlers = new();
private RenderState? _lastRenderState;
private string _text = string.Empty;
private List<string> _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<T> WithKeyHandler(Action<GeneralKeyEventArgs> keyHandler)
{
_keyHandlers.Add(keyHandler);
return this;
}
}

View File

@@ -1,11 +1,16 @@
using System.Buffers; using System.Buffers;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
using TerminalUI.Models; using TerminalUI.Models;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public delegate string TextTransformer(string text, Position position, Size size);
public abstract partial class View<T> : IView<T> public abstract partial class View<T> : IView<T>
{ {
private readonly List<IDisposable> _disposables = new(); private readonly List<IDisposable> _disposables = new();
@@ -18,24 +23,15 @@ public abstract partial class View<T> : IView<T>
[Notify] private int? _maxHeight; [Notify] private int? _maxHeight;
[Notify] private int? _height; [Notify] private int? _height;
[Notify] private int _actualHeight; [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 string? _name;
[Notify] private IApplicationContext? _applicationContext; [Notify] private IApplicationContext? _applicationContext;
private bool _attached; [Notify] private bool _attached;
public bool Attached protected ObservableCollection<IView> VisualChildren { get; } = new();
{
get => _attached;
set
{
if (_attached == value) return;
_attached = value;
if (value)
{
AttachChildren();
}
}
}
public List<object> Extensions { get; } = new(); public List<object> Extensions { get; } = new();
public RenderMethod RenderMethod { get; set; } public RenderMethod RenderMethod { get; set; }
@@ -46,11 +42,16 @@ public abstract partial class View<T> : IView<T>
{ {
RenderMethod = DefaultRenderer; RenderMethod = DefaultRenderer;
RerenderProperties.Add(nameof(Width));
RerenderProperties.Add(nameof(MinWidth)); RerenderProperties.Add(nameof(MinWidth));
RerenderProperties.Add(nameof(MaxWidth)); RerenderProperties.Add(nameof(MaxWidth));
RerenderProperties.Add(nameof(Height));
RerenderProperties.Add(nameof(MinHeight)); RerenderProperties.Add(nameof(MinHeight));
RerenderProperties.Add(nameof(MaxHeight)); RerenderProperties.Add(nameof(MaxHeight));
RerenderProperties.Add(nameof(IsVisible));
RerenderProperties.Add(nameof(Margin)); RerenderProperties.Add(nameof(Margin));
RerenderProperties.Add(nameof(Foreground));
RerenderProperties.Add(nameof(Background));
((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged; ((INotifyPropertyChanged) this).PropertyChanged += Handle_PropertyChanged;
} }
@@ -80,11 +81,6 @@ public abstract partial class View<T> : IView<T>
protected abstract Size CalculateSize(); protected abstract Size CalculateSize();
protected virtual void AttachChildren()
{
}
private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e) private void Handle_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
if (Attached if (Attached
@@ -96,6 +92,21 @@ public abstract partial class View<T> : IView<T>
{ {
ApplicationContext?.EventLoop.RequestRerender(); 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); protected abstract bool DefaultRenderer(RenderContext renderContext, Position position, Size size);
@@ -105,6 +116,8 @@ public abstract partial class View<T> : IView<T>
if (!Attached) if (!Attached)
throw new InvalidOperationException("Cannot render unattached view"); throw new InvalidOperationException("Cannot render unattached view");
if (!IsVisible) return false;
ActualWidth = size.Width; ActualWidth = size.Width;
ActualHeight = size.Height; ActualHeight = size.Height;
@@ -137,6 +150,8 @@ public abstract partial class View<T> : IView<T>
protected void RenderEmpty(RenderContext renderContext, Position position, Size size) protected void RenderEmpty(RenderContext renderContext, Position position, Size size)
{ {
var driver = renderContext.ConsoleDriver; var driver = renderContext.ConsoleDriver;
driver.ResetColor();
var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width); var placeHolder = new string(ApplicationContext!.EmptyCharacter, size.Width);
for (var i = 0; i < size.Height; i++) for (var i = 0; i < size.Height; i++)
{ {
@@ -145,6 +160,94 @@ public abstract partial class View<T> : IView<T>
} }
} }
protected void RenderText(
IList<string> 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<TChild>() where TChild : IView<T>, new() public TChild CreateChild<TChild>() where TChild : IView<T>, new()
{ {
var child = new TChild(); var child = new TChild();
@@ -162,6 +265,7 @@ public abstract partial class View<T> : IView<T>
{ {
child.DataContext = DataContext; child.DataContext = DataContext;
CopyCommonPropertiesToNewChild(child); CopyCommonPropertiesToNewChild(child);
VisualChildren.Add(child);
var mapper = new DataContextMapper<T, T>(this, child, d => d); var mapper = new DataContextMapper<T, T>(this, child, d => d);
AddDisposable(mapper); AddDisposable(mapper);
@@ -175,6 +279,7 @@ public abstract partial class View<T> : IView<T>
{ {
child.DataContext = dataContextMapper(DataContext); child.DataContext = dataContextMapper(DataContext);
CopyCommonPropertiesToNewChild(child); CopyCommonPropertiesToNewChild(child);
VisualChildren.Add(child);
var mapper = new DataContextMapper<T, TDataContext>(this, child, dataContextMapper); var mapper = new DataContextMapper<T, TDataContext>(this, child, dataContextMapper);
AddDisposable(mapper); AddDisposable(mapper);

View File

@@ -9,6 +9,7 @@ public class EventLoop : IEventLoop
private readonly object _lock = new(); private readonly object _lock = new();
private readonly List<IView> _viewsToRender = new(); private readonly List<IView> _viewsToRender = new();
private bool _rerenderRequested; private bool _rerenderRequested;
private bool _lastCursorVisible;
public EventLoop(IApplicationContext applicationContext) public EventLoop(IApplicationContext applicationContext)
{ {
@@ -44,14 +45,35 @@ public class EventLoop : IEventLoop
viewsToRender = _viewsToRender.ToList(); viewsToRender = _viewsToRender.ToList();
} }
var size = _applicationContext.ConsoleDriver.GetWindowSize(); var driver = _applicationContext.ConsoleDriver;
var renderContext = new RenderContext(_applicationContext.ConsoleDriver); var size = driver.GetWindowSize();
var renderContext = new RenderContext(
driver,
false,
null,
null
);
foreach (var view in viewsToRender) foreach (var view in viewsToRender)
{ {
view.Attached = true; view.Attached = true;
view.GetRequestedSize(); view.GetRequestedSize();
view.Render(renderContext, new Position(0, 0), size); 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) public void AddViewToRender(IView view)

View File

@@ -1,4 +1,5 @@
using TerminalUI.Controls; using System.Linq.Expressions;
using TerminalUI.Controls;
namespace TerminalUI.Extensions; namespace TerminalUI.Extensions;
@@ -23,4 +24,19 @@ public static class ViewExtensions
action(view); action(view);
return view; return view;
} }
public static TItem WithPropertyChangedHandler<TItem, TExpressionResult>(
this TItem dataSource,
Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
Action<string, bool, TExpressionResult> handler)
{
new PropertyChangedHandler<TItem, TExpressionResult>
(
dataSource,
dataSourceExpression,
handler
);
return dataSource;
}
} }

View File

@@ -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;
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging; using FileTime.App.Core.Models;
using Microsoft.Extensions.Logging;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
namespace TerminalUI; namespace TerminalUI;
@@ -10,4 +11,5 @@ public interface IApplicationContext
IConsoleDriver ConsoleDriver { get; init; } IConsoleDriver ConsoleDriver { get; init; }
ILoggerFactory? LoggerFactory { get; init; } ILoggerFactory? LoggerFactory { get; init; }
char EmptyCharacter { get; init; } char EmptyCharacter { get; init; }
IFocusManager FocusManager { get; init; }
} }

View File

@@ -0,0 +1,10 @@
using TerminalUI.Traits;
namespace TerminalUI;
public interface IFocusManager
{
void SetFocus(IFocusable focusable);
void UnFocus(IFocusable focusable);
IFocusable? Focused { get; }
}

View File

@@ -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))
};
}
}

View File

@@ -1,3 +1,10 @@
namespace TerminalUI.Models; using System.Diagnostics;
public record struct Position(int X, int Y); 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);
}

View File

@@ -1,18 +1,32 @@
using TerminalUI.ConsoleDrivers; using System.Diagnostics;
using TerminalUI.Color;
using TerminalUI.ConsoleDrivers;
namespace TerminalUI.Models; namespace TerminalUI.Models;
[DebuggerDisplay("RenderId = {RenderId}, ForceRerender = {ForceRerender}, Driver = {ConsoleDriver.GetType().Name}")]
public readonly ref struct RenderContext public readonly ref struct RenderContext
{ {
private static int _renderId = 0; private static int _renderId;
public readonly int RenderId; public readonly int RenderId;
public readonly IConsoleDriver ConsoleDriver; 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++; 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);
} }

View File

@@ -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); public readonly record struct Size(int Width, int Height);

View File

@@ -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))
};
}
}

View File

@@ -2,15 +2,25 @@
namespace TerminalUI; namespace TerminalUI;
internal interface IPropertyChangeTracker : IDisposable public interface IPropertyChangeTracker : IDisposable
{ {
string Name { get; }
string Path { get; }
Dictionary<string, IPropertyChangeTracker> Children { get; } Dictionary<string, IPropertyChangeTracker> Children { get; }
} }
internal abstract class PropertyChangeTrackerBase : IPropertyChangeTracker public abstract class PropertyChangeTrackerBase : IPropertyChangeTracker
{ {
public string Name { get; }
public string Path { get; }
public Dictionary<string, IPropertyChangeTracker> Children { get; } = new(); public Dictionary<string, IPropertyChangeTracker> Children { get; } = new();
protected PropertyChangeTrackerBase(string name, string path)
{
Name = name;
Path = path;
}
public virtual void Dispose() public virtual void Dispose()
{ {
foreach (var propertyChangeTracker in Children.Values) 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 PropertyTrackTreeItem _propertyTrackTreeItem;
private readonly INotifyPropertyChanged _target; private readonly INotifyPropertyChanged _target;
private readonly IEnumerable<string> _propertiesToListen; private readonly IEnumerable<string> _propertiesToListen;
private readonly Action _updateBinding; private readonly Action<string> _updateBinding;
public PropertyChangeTracker( public PropertyChangeTracker(
string name,
string path,
PropertyTrackTreeItem propertyTrackTreeItem, PropertyTrackTreeItem propertyTrackTreeItem,
INotifyPropertyChanged target, INotifyPropertyChanged target,
IEnumerable<string> propertiesToListen, IEnumerable<string> propertiesToListen,
Action updateBinding) Action<string> updateBinding) : base(name, path)
{ {
_propertyTrackTreeItem = propertyTrackTreeItem; _propertyTrackTreeItem = propertyTrackTreeItem;
_target = target; _target = target;
@@ -48,10 +60,10 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase
return; return;
} }
_updateBinding();
Children.Remove(propertyName); Children.Remove(propertyName);
var newChild = PropertyChangeHelper.TraverseDataContext( var newChild = PropertyChangeHelper.CreatePropertyTracker(
Path,
_propertyTrackTreeItem.Children[propertyName], _propertyTrackTreeItem.Children[propertyName],
_target.GetType().GetProperty(propertyName)?.GetValue(_target), _target.GetType().GetProperty(propertyName)?.GetValue(_target),
_updateBinding _updateBinding
@@ -61,6 +73,8 @@ internal class PropertyChangeTracker : PropertyChangeTrackerBase
{ {
Children.Add(propertyName, newChild); Children.Add(propertyName, newChild);
} }
_updateBinding(propertyName);
} }
public override void Dispose() 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<string, PropertyTrackTreeItem> Children { get; } = new(); public Dictionary<string, PropertyTrackTreeItem> 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, PropertyTrackTreeItem propertyTrackTreeItem,
object? obj, object? obj,
Action updateBinding Action<string> updateBinding
) )
{ {
if (obj is null) return null; if (obj is null) return null;
path = path is null ? propertyTrackTreeItem.Name : path + "." + propertyTrackTreeItem.Name;
IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged IPropertyChangeTracker tracker = obj is INotifyPropertyChanged notifyPropertyChanged
? new PropertyChangeTracker(propertyTrackTreeItem, notifyPropertyChanged, propertyTrackTreeItem.Children.Keys, updateBinding) ? new PropertyChangeTracker(
: new NonSubscriberPropertyChangeTracker(); propertyTrackTreeItem.Name,
path,
propertyTrackTreeItem,
notifyPropertyChanged,
propertyTrackTreeItem.Children.Keys,
updateBinding
)
: new NonSubscriberPropertyChangeTracker(
propertyTrackTreeItem.Name,
path);
foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children) foreach (var (propertyName, trackerTreeItem) in propertyTrackTreeItem.Children)
{ {
var childTracker = TraverseDataContext( var childTracker = CreatePropertyTracker(
path,
trackerTreeItem, trackerTreeItem,
obj.GetType().GetProperty(propertyName)?.GetValue(obj), obj.GetType().GetProperty(propertyName)?.GetValue(obj),
updateBinding updateBinding

View File

@@ -0,0 +1,45 @@
using System.Linq.Expressions;
namespace TerminalUI;
public sealed class PropertyChangedHandler<TItem, TExpressionResult> : PropertyTrackerBase<TItem, TExpressionResult>, IDisposable
{
private readonly TItem _dataSource;
private readonly Action<string, bool, TExpressionResult?> _handler;
private readonly PropertyTrackTreeItem? _propertyTrackTreeItem;
private readonly Func<TItem, TExpressionResult> _propertyValueGenerator;
public PropertyChangedHandler(
TItem dataSource,
Expression<Func<TItem, TExpressionResult>> dataSourceExpression,
Action<string, bool, TExpressionResult?> 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);
}
}

View File

@@ -0,0 +1,145 @@
using System.Linq.Expressions;
using System.Reflection;
namespace TerminalUI;
public abstract class PropertyTrackerBase<TSource, TExpressionResult> : IDisposable
{
private readonly Func<TSource?> _source;
protected PropertyTrackTreeItem? PropertyTrackTreeItem { get; }
protected IPropertyChangeTracker? PropertyChangeTracker { get; private set; }
protected PropertyTrackerBase(
Func<TSource?> source,
Expression<Func<TSource?, TExpressionResult>> dataSourceExpression)
{
ArgumentNullException.ThrowIfNull(dataSourceExpression);
_source = source;
PropertyTrackTreeItem = CreateTrackingTree(dataSourceExpression);
}
protected PropertyTrackTreeItem? CreateTrackingTree(Expression<Func<TSource?, TExpressionResult>> dataContextExpression)
{
var properties = new List<string>();
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<string> 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();
}

View File

@@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\AppCommon\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" /> <ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" />
</ItemGroup> </ItemGroup>
@@ -18,4 +19,8 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="KeyHandling\" />
</ItemGroup>
</Project> </Project>

View File

@@ -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);
}