TextBox, PropertyChangeHandler
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace FileTime.App.Core.Models;
|
|
||||||
|
|
||||||
public record struct SpecialKeysStatus(bool IsAltPressed, bool IsShiftPressed, bool IsCtrlPressed);
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using FileTime.App.Core.Models;
|
using FileTime.App.Core.Models;
|
||||||
|
using GeneralInputKey;
|
||||||
|
|
||||||
namespace FileTime.App.FuzzyPanel;
|
namespace FileTime.App.FuzzyPanel;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using TerminalUI.Color;
|
||||||
|
|
||||||
|
namespace FileTime.ConsoleUI.App.Styling;
|
||||||
|
|
||||||
|
public record ListViewItemTheme(
|
||||||
|
IColor? SelectedBackgroundColor,
|
||||||
|
IColor? SelectedForegroundColor
|
||||||
|
);
|
||||||
@@ -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,10 +81,19 @@ public class App : IApplication
|
|||||||
|
|
||||||
var keyEventArgs = new GeneralKeyEventArgs
|
var keyEventArgs = new GeneralKeyEventArgs
|
||||||
{
|
{
|
||||||
Key = mappedKey
|
Key = mappedKey,
|
||||||
|
KeyChar = key.KeyChar,
|
||||||
|
SpecialKeysStatus = specialKeysStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
_keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus);
|
if (focusManager.Focused is { } focused)
|
||||||
|
{
|
||||||
|
focused.HandleKeyInput(keyEventArgs);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
129
src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs
Normal file
129
src/ConsoleApp/FileTime.ConsoleUI.App/Controls/CommandPalette.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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},
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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}" />
|
||||||
|
|||||||
9
src/Library/GeneralInputKey/GeneralInputKey.csproj
Normal file
9
src/Library/GeneralInputKey/GeneralInputKey.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -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,
|
|
||||||
}
|
}
|
||||||
6
src/Library/GeneralInputKey/SpecialKeysStatus.cs
Normal file
6
src/Library/GeneralInputKey/SpecialKeysStatus.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
157
src/Library/TerminalUI/Controls/Border.cs
Normal file
157
src/Library/TerminalUI/Controls/Border.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ public abstract class ChildContainerView<T> : View<T>, IChildContainer<T>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
((INotifyPropertyChanged)this).PropertyChanged += (o, args) =>
|
((INotifyPropertyChanged) this).PropertyChanged += (o, args) =>
|
||||||
{
|
{
|
||||||
if (args.PropertyName == nameof(ApplicationContext))
|
if (args.PropertyName == nameof(ApplicationContext))
|
||||||
{
|
{
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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++)
|
||||||
|
|
||||||
var width = columnWidths[x];
|
|
||||||
var height = rowHeights[y];
|
|
||||||
|
|
||||||
var left = position.X;
|
|
||||||
var top = position.Y;
|
|
||||||
|
|
||||||
for (var i = 0; i < x; i++)
|
|
||||||
{
|
{
|
||||||
left += columnWidths[i];
|
RenderViewsByPosition(
|
||||||
|
context,
|
||||||
|
position,
|
||||||
|
columnWidths,
|
||||||
|
rowHeights,
|
||||||
|
viewsByPosition,
|
||||||
|
column,
|
||||||
|
row
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < y; i++)
|
|
||||||
{
|
|
||||||
top += rowHeights[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
child.Render(context, new Position(left, top), new Size(width, height));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
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)
|
private void RenderViewsByPosition(
|
||||||
|
RenderContext context,
|
||||||
|
Position gridPosition,
|
||||||
|
ReadOnlySpan<int> columnWidths,
|
||||||
|
ReadOnlySpan<int> rowHeights,
|
||||||
|
IReadOnlyDictionary<(int, int), List<IView>> viewsByPosition,
|
||||||
|
int column,
|
||||||
|
int row)
|
||||||
{
|
{
|
||||||
for (var x = 0; x < columnWidths.Length; x++)
|
if (!viewsByPosition.TryGetValue((column, row), out var children)) return;
|
||||||
|
|
||||||
|
var anyChangedVisibility = false;
|
||||||
|
|
||||||
|
foreach (var child in children)
|
||||||
{
|
{
|
||||||
for (var y = 0; y < rowHeights.Length; y++)
|
var lastVisibility = GetLastVisibility(child);
|
||||||
|
if (lastVisibility is { } b && b != child.IsVisible)
|
||||||
{
|
{
|
||||||
if (!viewsByPosition.TryGetValue((x, y), out var list)) continue;
|
anyChangedVisibility = true;
|
||||||
|
break;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
|
|
||||||
/*private Dictionary<(int, int), List<IView>> GroupViewsByPosition(int columns, int rows)
|
var width = columnWidths[column];
|
||||||
|
var height = rowHeights[row];
|
||||||
|
var renderSize = new Size(width, height);
|
||||||
|
|
||||||
|
var renderPosition = GetRenderPosition(
|
||||||
|
gridPosition,
|
||||||
|
columnWidths,
|
||||||
|
rowHeights,
|
||||||
|
column,
|
||||||
|
row
|
||||||
|
);
|
||||||
|
|
||||||
|
var needsRerender = anyChangedVisibility;
|
||||||
|
if (needsRerender)
|
||||||
|
{
|
||||||
|
context = new RenderContext(
|
||||||
|
context.ConsoleDriver,
|
||||||
|
true,
|
||||||
|
context.Foreground,
|
||||||
|
context.Background
|
||||||
|
);
|
||||||
|
RenderEmpty(context, renderPosition, renderSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
//This implies that children further back in the list will be rendered on top of children placed before in the list.
|
||||||
|
foreach (var child in children.Where(child => child.IsVisible))
|
||||||
|
{
|
||||||
|
var rendered = child.Render(context, renderPosition, renderSize);
|
||||||
|
if (rendered && !needsRerender)
|
||||||
|
{
|
||||||
|
needsRerender = true;
|
||||||
|
context = new RenderContext(
|
||||||
|
context.ConsoleDriver,
|
||||||
|
true,
|
||||||
|
context.Foreground,
|
||||||
|
context.Background
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Position GetRenderPosition(
|
||||||
|
Position gridPosition,
|
||||||
|
ReadOnlySpan<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];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < row; i++)
|
||||||
|
{
|
||||||
|
top += rowHeights[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Position(left, top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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];
|
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text),
|
||||||
text = TextAlignment switch
|
_ => string.Format($"{{0,{-size.Width}}}", text)
|
||||||
{
|
};
|
||||||
TextAlignment.Right => string.Format($"{{0,{size.Width}}}", text),
|
|
||||||
_ => string.Format($"{{0,{-size.Width}}}", text)
|
|
||||||
};
|
|
||||||
if (text.Length > size.Width)
|
|
||||||
{
|
|
||||||
text = text[..size.Width];
|
|
||||||
}
|
|
||||||
|
|
||||||
driver.SetCursorPosition(position with {Y = position.Y + i});
|
|
||||||
driver.Write(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool NeedsRerender(RenderState renderState)
|
private bool NeedsRerender(RenderState renderState)
|
||||||
=> _lastRenderState is null || _lastRenderState != renderState;
|
=> _lastRenderState is null || _lastRenderState != renderState;
|
||||||
|
|||||||
296
src/Library/TerminalUI/Controls/TextBox.cs
Normal file
296
src/Library/TerminalUI/Controls/TextBox.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
31
src/Library/TerminalUI/FocusManager.cs
Normal file
31
src/Library/TerminalUI/FocusManager.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
10
src/Library/TerminalUI/IFocusManager.cs
Normal file
10
src/Library/TerminalUI/IFocusManager.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using TerminalUI.Traits;
|
||||||
|
|
||||||
|
namespace TerminalUI;
|
||||||
|
|
||||||
|
public interface IFocusManager
|
||||||
|
{
|
||||||
|
void SetFocus(IFocusable focusable);
|
||||||
|
void UnFocus(IFocusable focusable);
|
||||||
|
IFocusable? Focused { get; }
|
||||||
|
}
|
||||||
@@ -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))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
21
src/Library/TerminalUI/Models/Thickness.cs
Normal file
21
src/Library/TerminalUI/Models/Thickness.cs
Normal 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))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
45
src/Library/TerminalUI/PropertyChangedHandler.cs
Normal file
45
src/Library/TerminalUI/PropertyChangedHandler.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/Library/TerminalUI/PropertyTrackerBase.cs
Normal file
145
src/Library/TerminalUI/PropertyTrackerBase.cs
Normal 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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
13
src/Library/TerminalUI/Traits/IFocusable.cs
Normal file
13
src/Library/TerminalUI/Traits/IFocusable.cs
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user