Console DialogService

This commit is contained in:
2023-08-14 11:50:59 +02:00
parent 6797c26bf9
commit 1f4b938358
41 changed files with 807 additions and 269 deletions

View File

@@ -0,0 +1,11 @@
using FileTime.App.Core.ViewModels;
using FileTime.Core.Interactions;
using ObservableComputations;
namespace FileTime.App.Core.Services;
public interface IDialogServiceBase : IUserCommunicationService
{
ScalarComputing<ReadInputsViewModel?> ReadInput { get; }
ScalarComputing<MessageBoxViewModel?> LastMessageBox { get; }
}

View File

@@ -1,11 +1,11 @@
using DynamicData; using System.Collections.ObjectModel;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
namespace FileTime.App.Core.Services; namespace FileTime.App.Core.Services;
public interface IModalService public interface IModalService
{ {
IObservable<IChangeSet<IModalViewModel>> OpenModals { get; } ReadOnlyObservableCollection<IModalViewModel> OpenModals { get; }
void OpenModal(IModalViewModel modalToOpen); void OpenModal(IModalViewModel modalToOpen);
void CloseModal(IModalViewModel modalToClose); void CloseModal(IModalViewModel modalToClose);

View File

@@ -1,11 +1,8 @@
using FileTime.App.Core.ViewModels;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
using MvvmGen;
namespace FileTime.GuiApp.App.ViewModels; namespace FileTime.App.Core.ViewModels;
[ViewModel] public class MessageBoxViewModel : IModalViewModel
public partial class MessageBoxViewModel : IModalViewModel
{ {
private readonly Action<MessageBoxViewModel, MessageBoxResult> _handler; private readonly Action<MessageBoxViewModel, MessageBoxResult> _handler;
public string Text { get; } public string Text { get; }
@@ -19,7 +16,7 @@ public partial class MessageBoxViewModel : IModalViewModel
Action<MessageBoxViewModel, MessageBoxResult> handler, Action<MessageBoxViewModel, MessageBoxResult> handler,
bool showCancel = true, bool showCancel = true,
string? okText = null, string? okText = null,
string? cancelText = null) : this() string? cancelText = null)
{ {
_handler = handler; _handler = handler;
Text = text; Text = text;
@@ -28,9 +25,7 @@ public partial class MessageBoxViewModel : IModalViewModel
CancelText = cancelText ?? "No"; CancelText = cancelText ?? "No";
} }
[Command]
public void Ok() => _handler.Invoke(this, MessageBoxResult.Ok); public void Ok() => _handler.Invoke(this, MessageBoxResult.Ok);
[Command]
public void Cancel() => _handler.Invoke(this, MessageBoxResult.Cancel); public void Cancel() => _handler.Invoke(this, MessageBoxResult.Cancel);
} }

View File

@@ -1,8 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using FileTime.App.Core.ViewModels;
using FileTime.Core.Interactions; using FileTime.Core.Interactions;
namespace FileTime.GuiApp.App.ViewModels; namespace FileTime.App.Core.ViewModels;
public class ReadInputsViewModel : IModalViewModel public class ReadInputsViewModel : IModalViewModel
{ {

View File

@@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using DeclarativeProperty; using DeclarativeProperty;
using FileTime.App.Core.Configuration; using FileTime.App.Core.Configuration;
using FileTime.App.Core.Extensions; using FileTime.App.Core.Extensions;
@@ -23,7 +24,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
private readonly IUserCommandHandlerService _userCommandHandlerService; private readonly IUserCommandHandlerService _userCommandHandlerService;
private readonly IIdentifiableUserCommandService _identifiableUserCommandService; private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
private readonly IPossibleCommandsService _possibleCommandsService; private readonly IPossibleCommandsService _possibleCommandsService;
private readonly BindedCollection<IModalViewModel> _openModals; private readonly ReadOnlyObservableCollection<IModalViewModel> _openModals;
public DefaultModeKeyInputHandler( public DefaultModeKeyInputHandler(
IAppState appState, IAppState appState,
@@ -46,7 +47,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
.Map(t => t?.CurrentLocation) .Map(t => t?.CurrentLocation)
.Switch(); .Switch();
_openModals = modalService.OpenModals.ToBindedCollection(); _openModals = modalService.OpenModals;
_keysToSkip.Add(new[] {new KeyConfig(Keys.Up)}); _keysToSkip.Add(new[] {new KeyConfig(Keys.Up)});
_keysToSkip.Add(new[] {new KeyConfig(Keys.Down)}); _keysToSkip.Add(new[] {new KeyConfig(Keys.Down)});
@@ -75,9 +76,9 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
{ {
var doGeneralReset = _appState.PreviousKeys.Count > 1; var doGeneralReset = _appState.PreviousKeys.Count > 1;
if ((_openModals.Collection?.Count ?? 0) > 0) if (_openModals.Count > 0)
{ {
_modalService.CloseModal(_openModals.Collection!.Last()); _modalService.CloseModal(_openModals.Last());
} }
else if (_currentLocation.Value?.GetExtension<EscHandlerContainerExtension>() is { } escHandler) else if (_currentLocation.Value?.GetExtension<EscHandlerContainerExtension>() is { } escHandler)
{ {

View File

@@ -0,0 +1,130 @@
using DynamicData;
using FileTime.App.Core.ViewModels;
using FileTime.Core.Interactions;
using ObservableComputations;
namespace FileTime.App.Core.Services;
public abstract class DialogServiceBase : IDialogServiceBase
{
private readonly IModalService _modalService;
private OcConsumer _readInputConsumer = new();
private OcConsumer _lastMessageBoxConsumer = new();
public ScalarComputing<ReadInputsViewModel?> ReadInput { get; }
public ScalarComputing<MessageBoxViewModel?> LastMessageBox { get; }
protected DialogServiceBase(IModalService modalService)
{
_modalService = modalService;
ReadInput = modalService
.OpenModals
.OfTypeComputing<ReadInputsViewModel>()
.FirstComputing()
.For(_readInputConsumer);
LastMessageBox =
modalService
.OpenModals
.OfTypeComputing<MessageBoxViewModel>()
.LastComputing()
.For(_lastMessageBoxConsumer);
}
private void ReadInputs(
IEnumerable<IInputElement> inputs,
Action inputHandler,
Action? cancelHandler = null,
IEnumerable<IPreviewElement>? previews = null)
{
var modalViewModel = new ReadInputsViewModel
{
Inputs = inputs.ToList(),
SuccessHandler = HandleReadInputsSuccess,
CancelHandler = HandleReadInputsCancel
};
if (previews is not null)
{
modalViewModel.Previews.AddRange(previews);
}
_modalService.OpenModal(modalViewModel);
void HandleReadInputsSuccess(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
inputHandler();
}
void HandleReadInputsCancel(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
cancelHandler?.Invoke();
}
}
public Task<bool> ReadInputs(IEnumerable<IInputElement> fields, IEnumerable<IPreviewElement>? previews = null)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
fields,
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false),
previews
);
return taskCompletionSource.Task;
}
public Task<bool> ReadInputs(params IInputElement[] fields)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
fields,
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false)
);
return taskCompletionSource.Task;
}
public Task<bool> ReadInputs(IInputElement field, IEnumerable<IPreviewElement>? previews = null)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
new[] {field},
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false),
previews
);
return taskCompletionSource.Task;
}
public abstract void ShowToastMessage(string text);
public Task<MessageBoxResult> ShowMessageBox(
string text,
bool showCancel = true,
string? okText = null,
string? cancelText = null)
{
var taskCompletionSource = new TaskCompletionSource<MessageBoxResult>();
_modalService.OpenModal(
new MessageBoxViewModel(
text,
(vm, result) =>
{
_modalService.CloseModal(vm);
taskCompletionSource.SetResult(result);
},
showCancel,
okText,
cancelText
)
);
return taskCompletionSource.Task;
}
}

View File

@@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using DynamicData; using DynamicData;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -7,14 +8,14 @@ namespace FileTime.App.Core.Services;
public class ModalService : IModalService public class ModalService : IModalService
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly SourceList<IModalViewModel> _openModals = new(); private readonly ObservableCollection<IModalViewModel> _openModals = new();
public IObservable<IChangeSet<IModalViewModel>> OpenModals { get; } public ReadOnlyObservableCollection<IModalViewModel> OpenModals { get; }
public event EventHandler? AllModalClosed; public event EventHandler? AllModalClosed;
public ModalService(IServiceProvider serviceProvider) public ModalService(IServiceProvider serviceProvider)
{ {
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
OpenModals = _openModals.Connect().StartWithEmpty(); OpenModals = new ReadOnlyObservableCollection<IModalViewModel>(_openModals);
} }
public void OpenModal(IModalViewModel modalToOpen) => _openModals.Add(modalToOpen); public void OpenModal(IModalViewModel modalToOpen) => _openModals.Add(modalToOpen);

View File

@@ -21,7 +21,6 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
private readonly IUserCommandHandlerService _userCommandHandlerService; private readonly IUserCommandHandlerService _userCommandHandlerService;
private readonly ILogger<RapidTravelModeKeyInputHandler> _logger; private readonly ILogger<RapidTravelModeKeyInputHandler> _logger;
private readonly IIdentifiableUserCommandService _identifiableUserCommandService; private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
private readonly BindedCollection<IModalViewModel> _openModals;
private ITabViewModel? _selectedTab; private ITabViewModel? _selectedTab;
public RapidTravelModeKeyInputHandler( public RapidTravelModeKeyInputHandler(
@@ -41,8 +40,6 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
_appState.SelectedTab.Subscribe(t => _selectedTab = t); _appState.SelectedTab.Subscribe(t => _selectedTab = t);
_openModals = modalService.OpenModals.ToBindedCollection();
_appState.RapidTravelTextDebounced.Subscribe((v, _) => _appState.RapidTravelTextDebounced.Subscribe((v, _) =>
{ {
if (_selectedTab?.Tab is not { } tab) return Task.CompletedTask; if (_selectedTab?.Tab is not { } tab) return Task.CompletedTask;
@@ -62,9 +59,9 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
if (args.Key == Keys.Escape) if (args.Key == Keys.Escape)
{ {
args.Handled = true; args.Handled = true;
if ((_openModals.Collection?.Count ?? 0) > 0) if (_modalService.OpenModals.Count > 0)
{ {
_modalService.CloseModal(_openModals.Collection!.Last()); _modalService.CloseModal(_modalService.OpenModals.Last());
} }
else else
{ {

View File

@@ -1,5 +1,7 @@
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -10,4 +12,6 @@ public interface IRootViewModel
string UserName { get; } string UserName { get; }
string MachineName { get; } string MachineName { get; }
ICommandPaletteViewModel CommandPalette { get; } ICommandPaletteViewModel CommandPalette { get; }
IDialogService DialogService { get; }
event Action<IInputElement>? FocusReadInputElement;
} }

View File

@@ -0,0 +1,8 @@
using FileTime.App.Core.Services;
namespace FileTime.ConsoleUI.App.Services;
public interface IDialogService : IDialogServiceBase
{
}

View File

@@ -6,11 +6,18 @@ using FileTime.ConsoleUI.App.KeyInputHandling;
using GeneralInputKey; using GeneralInputKey;
using TerminalUI; using TerminalUI;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
using TerminalUI.Traits;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
public class App : IApplication public class App : IApplication
{ {
private static readonly List<Keys> KeysToFurtherProcess = new()
{
Keys.Enter,
Keys.Escape
};
private readonly ILifecycleService _lifecycleService; private readonly ILifecycleService _lifecycleService;
private readonly IConsoleAppState _consoleAppState; private readonly IConsoleAppState _consoleAppState;
@@ -86,11 +93,13 @@ public class App : IApplication
SpecialKeysStatus = specialKeysStatus SpecialKeysStatus = specialKeysStatus
}; };
if (focusManager.Focused is { } focused) var focused = focusManager.Focused;
if (focused is { })
{ {
focused.HandleKeyInput(keyEventArgs); focused.HandleKeyInput(keyEventArgs);
} }
else
if (focused is null || (!keyEventArgs.Handled && KeysToFurtherProcess.Contains(keyEventArgs.Key)))
{ {
_keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus); _keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus);
} }

View File

@@ -1,16 +0,0 @@
using FileTime.Core.Interactions;
namespace FileTime.ConsoleUI.App;
public class ConsoleUserCommunicationService : IUserCommunicationService
{
public Task<bool> ReadInputs(params IInputElement[] fields) => throw new NotImplementedException();
public Task<bool> ReadInputs(IInputElement field, IEnumerable<IPreviewElement>? previews = null) => throw new NotImplementedException();
public Task<bool> ReadInputs(IEnumerable<IInputElement> fields, IEnumerable<IPreviewElement>? previews = null) => throw new NotImplementedException();
public void ShowToastMessage(string text) => throw new NotImplementedException();
public Task<MessageBoxResult> ShowMessageBox(string text, bool showCancel = true, string? okText = null, string? cancelText = null) => throw new NotImplementedException();
}

View File

@@ -65,7 +65,7 @@ public class CommandPalette
{ {
new Border<IRootViewModel> new Border<IRootViewModel>
{ {
Margin = new Thickness(0, 0, 0, 2), Margin = new Thickness(0, 0, 0, 1),
Content = inputTextBox Content = inputTextBox
}, },
new ListView<IRootViewModel, ICommandPaletteEntryViewModel> new ListView<IRootViewModel, ICommandPaletteEntryViewModel>
@@ -103,15 +103,13 @@ public class CommandPalette
item.Bind( item.Bind(
item.Parent, item.Parent,
d => d.CommandPalette.SelectedItem == item.DataContext ? _theme.ListViewItemTheme.SelectedBackgroundColor : null, d => d.CommandPalette.SelectedItem == item.DataContext ? _theme.ListViewItemTheme.SelectedBackgroundColor : null,
t => t.Background, t => t.Background
v => v
); );
item.Bind( item.Bind(
item.Parent, item.Parent,
d => d.CommandPalette.SelectedItem == item.DataContext ? _theme.ListViewItemTheme.SelectedForegroundColor : null, d => d.CommandPalette.SelectedItem == item.DataContext ? _theme.ListViewItemTheme.SelectedForegroundColor : null,
t => t.Foreground, t => t.Foreground
v => v
); );
return root; return root;

View File

@@ -28,11 +28,11 @@ public class KeyInputHandlerService : IKeyInputHandlerService
if (_appState.ViewMode.Value == ViewMode.Default) if (_appState.ViewMode.Value == ViewMode.Default)
{ {
Task.Run(async () => await _defaultModeKeyInputHandler.HandleInputKey(keyEvent)).Wait(); Task.Run(async () => await _defaultModeKeyInputHandler.HandleInputKey(keyEvent));
} }
else else
{ {
Task.Run(async () => await _rapidTravelModeKeyInputHandler.HandleInputKey(keyEvent)).Wait(); Task.Run(async () => await _rapidTravelModeKeyInputHandler.HandleInputKey(keyEvent));
} }
} }
} }

View File

@@ -1,13 +1,18 @@
using FileTime.App.Core.Models.Enums; using System.Collections.Specialized;
using System.ComponentModel;
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.Controls;
using FileTime.ConsoleUI.App.Styling; using FileTime.ConsoleUI.App.Styling;
using FileTime.Core.Enums; using FileTime.Core.Enums;
using FileTime.Core.Interactions;
using GeneralInputKey;
using TerminalUI; using TerminalUI;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.Controls; using TerminalUI.Controls;
using TerminalUI.Extensions; using TerminalUI.Extensions;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.Traits;
using TerminalUI.ViewExtensions; using TerminalUI.ViewExtensions;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -18,9 +23,12 @@ public class MainWindow
private readonly IApplicationContext _applicationContext; private readonly IApplicationContext _applicationContext;
private readonly ITheme _theme; private readonly ITheme _theme;
private readonly CommandPalette _commandPalette; private readonly CommandPalette _commandPalette;
private readonly Lazy<IView> _root; private readonly Lazy<IView> _root;
private ItemsControl<IRootViewModel, IInputElement> _readInputs = null!;
private IInputElement? _inputElementToFocus;
private Action? _readInputChildHandlerUnsubscriber;
public MainWindow( public MainWindow(
IRootViewModel rootViewModel, IRootViewModel rootViewModel,
IApplicationContext applicationContext, IApplicationContext applicationContext,
@@ -32,6 +40,40 @@ public class MainWindow
_theme = theme; _theme = theme;
_commandPalette = commandPalette; _commandPalette = commandPalette;
_root = new Lazy<IView>(Initialize); _root = new Lazy<IView>(Initialize);
rootViewModel.FocusReadInputElement += element =>
{
_inputElementToFocus = element;
UpdateReadInputsFocus();
};
}
private void UpdateReadInputsFocus()
{
foreach (var readInputsChild in _readInputs.Children)
{
if (readInputsChild.DataContext == _inputElementToFocus)
{
if (FindFocusable(readInputsChild) is { } focusable)
{
focusable.Focus();
_inputElementToFocus = null;
break;
}
}
}
IFocusable? FindFocusable(IView view)
{
if (view is IFocusable focusable) return focusable;
foreach (var viewVisualChild in view.VisualChildren)
{
if (FindFocusable(viewVisualChild) is { } focusableChild)
return focusableChild;
}
return null;
}
} }
public IEnumerable<IView> RootViews() => new[] public IEnumerable<IView> RootViews() => new[]
@@ -46,10 +88,33 @@ public class MainWindow
Name = "root", Name = "root",
DataContext = _rootViewModel, DataContext = _rootViewModel,
ApplicationContext = _applicationContext, ApplicationContext = _applicationContext,
Foreground = _theme.DefaultForegroundColor,
ChildInitializer = ChildInitializer =
{ {
MainContent(), MainContent(),
_commandPalette.View() _commandPalette.View(),
Dialogs(),
}
};
((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(ItemsControl<object, object>.Children))
{
_readInputChildHandlerUnsubscriber?.Invoke();
UpdateReadInputsFocus();
if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged)
{
notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler;
_readInputChildHandlerUnsubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; };
}
void NotifyCollectionChangedEventHandler(
object? sender,
NotifyCollectionChangedEventArgs e)
{
UpdateReadInputsFocus();
}
} }
}; };
return root; return root;
@@ -188,7 +253,6 @@ public class MainWindow
ItemTemplate = item => ItemTemplate = item =>
{ {
var textBlock = item.CreateChild<TextBlock<ITabViewModel>>(); var textBlock = item.CreateChild<TextBlock<ITabViewModel>>();
textBlock.Foreground = _theme.DefaultForegroundColor;
textBlock.Bind( textBlock.Bind(
textBlock, textBlock,
@@ -356,4 +420,137 @@ public class MainWindow
(ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemBackgroundColor, (ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemBackgroundColor,
_ => throw new NotImplementedException() _ => throw new NotImplementedException()
}; };
private IView<IRootViewModel> Dialogs()
{
var root = new Border<IRootViewModel>()
{
Margin = 5,
BorderThickness = 1,
Content = new Grid<IRootViewModel>()
{
ChildInitializer =
{
ReadInputs()
}
}
};
root.Bind(
root,
d => d.DialogService.ReadInput.Value != null,
v => v.IsVisible);
return root;
}
private ItemsControl<IRootViewModel, IInputElement> ReadInputs()
{
var readInputs = new ItemsControl<IRootViewModel, IInputElement>
{
ItemTemplate = () =>
{
var root = new Grid<IInputElement>
{
ColumnDefinitionsObject = "* *",
ChildInitializer =
{
new TextBlock<IInputElement>()
.Setup(t => t.Bind(
t,
c => c.Label,
tb => tb.Text
)),
new Grid<IInputElement>()
{
Extensions =
{
new GridPositionExtension(1, 0)
},
ChildInitializer =
{
new Border<IInputElement>
{
Content =
new TextBox<IInputElement>()
.Setup(t => t.Bind(
t,
d => ((TextInputElement) d).Value,
tb => tb.Text,
v => v ?? string.Empty,
fallbackValue: string.Empty
))
.WithTextHandler((tb, t) =>
{
if (tb.DataContext is TextInputElement textInputElement)
textInputElement.Value = t;
})
}
.Setup(t => t.Bind(
t,
d => d.Type == InputType.Text,
tb => tb.IsVisible
)),
new Border<IInputElement>
{
Content =
new TextBox<IInputElement>
{
PasswordChar = '*'
}
.Setup(t => t.Bind(
t,
d => ((PasswordInputElement) d).Value,
tb => tb.Text,
v => v ?? string.Empty,
fallbackValue: string.Empty
))
.WithTextHandler((tb, t) =>
{
if (tb.DataContext is PasswordInputElement textInputElement)
textInputElement.Value = t;
})
}
.Setup(t => t.Bind(
t,
d => d.Type == InputType.Password,
tb => tb.IsVisible
))
//TODO: OptionInputElement
}
}
}
};
return root;
}
}
.Setup(t => t.Bind(
t,
d => d.DialogService.ReadInput.Value.Inputs,
c => c.ItemsSource,
v => v
));
readInputs.WithKeyHandler((_, e) =>
{
if (e.Key == Keys.Enter)
{
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
readInputsViewModel.Process();
e.Handled = true;
}
else if (e.Key == Keys.Escape)
{
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
readInputsViewModel.Cancel();
e.Handled = true;
}
});
_readInputs = readInputs;
return readInputs;
}
} }

View File

@@ -1,5 +1,7 @@
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.ConsoleUI.App.Services;
using FileTime.Core.Interactions;
namespace FileTime.ConsoleUI.App; namespace FileTime.ConsoleUI.App;
@@ -10,14 +12,29 @@ public class RootViewModel : IRootViewModel
public IPossibleCommandsViewModel PossibleCommands { get; } public IPossibleCommandsViewModel PossibleCommands { get; }
public IConsoleAppState AppState { get; } public IConsoleAppState AppState { get; }
public ICommandPaletteViewModel CommandPalette { get; } public ICommandPaletteViewModel CommandPalette { get; }
public IDialogService DialogService { get; }
public event Action<IInputElement>? FocusReadInputElement;
public RootViewModel( public RootViewModel(
IConsoleAppState appState, IConsoleAppState appState,
IPossibleCommandsViewModel possibleCommands, IPossibleCommandsViewModel possibleCommands,
ICommandPaletteViewModel commandPalette) ICommandPaletteViewModel commandPalette,
IDialogService dialogService)
{ {
AppState = appState; AppState = appState;
PossibleCommands = possibleCommands; PossibleCommands = possibleCommands;
CommandPalette = commandPalette; CommandPalette = commandPalette;
DialogService = dialogService;
DialogService.ReadInput.PropertyChanged += (o, e) =>
{
if (e.PropertyName == nameof(DialogService.ReadInput.Value))
{
if (DialogService.ReadInput.Value is {Inputs.Count: > 0} readInputs)
{
FocusReadInputElement?.Invoke(readInputs.Inputs[0]);
}
}
};
} }
} }

View File

@@ -0,0 +1,15 @@
using FileTime.App.Core.Services;
namespace FileTime.ConsoleUI.App.Services;
public class DialogService : DialogServiceBase, IDialogService
{
public DialogService(IModalService modalService) : base(modalService)
{
}
public override void ShowToastMessage(string text)
{
// TODO: Implement
}
}

View File

@@ -19,13 +19,14 @@ public static class Startup
services.TryAddSingleton<IApplication, App>(); services.TryAddSingleton<IApplication, App>();
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<IKeyInputHandlerService, KeyInputHandlerService>(); services.TryAddSingleton<IKeyInputHandlerService, KeyInputHandlerService>();
services.TryAddSingleton<IAppKeyService<ConsoleKey>, ConsoleAppKeyService>(); services.TryAddSingleton<IAppKeyService<ConsoleKey>, ConsoleAppKeyService>();
services.TryAddSingleton<ISystemClipboardService, ConsoleSystemClipboardService>(); services.TryAddSingleton<ISystemClipboardService, ConsoleSystemClipboardService>();
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<IDialogService, DialogService>();
services.TryAddSingleton<IUserCommunicationService>(sp => sp.GetRequiredService<IDialogService>());
services.Configure<ConsoleApplicationConfiguration>(configuration); services.Configure<ConsoleApplicationConfiguration>(configuration);
return services; return services;

View File

@@ -21,6 +21,7 @@
<ItemGroup> <ItemGroup>
<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="..\..\..\AppCommon\FileTime.App.Core\FileTime.App.Core.csproj" />
<ProjectReference Include="..\..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" /> <ProjectReference Include="..\..\..\Core\FileTime.Core.Models\FileTime.Core.Models.csproj" />
<ProjectReference Include="..\..\..\Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj" /> <ProjectReference Include="..\..\..\Providers\FileTime.Providers.LocalAdmin.Abstractions\FileTime.Providers.LocalAdmin.Abstractions.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -1,10 +1,7 @@
using FileTime.Core.Interactions; using FileTime.App.Core.Services;
using FileTime.GuiApp.App.ViewModels;
namespace FileTime.GuiApp.App.Services; namespace FileTime.GuiApp.App.Services;
public interface IDialogService : IUserCommunicationService public interface IDialogService : IDialogServiceBase
{ {
IObservable<ReadInputsViewModel?> ReadInput { get; }
IObservable<MessageBoxViewModel?> LastMessageBox { get; }
} }

View File

@@ -1,145 +1,24 @@
using System.Reactive.Linq;
using Avalonia.Threading; using Avalonia.Threading;
using DynamicData;
using FileTime.App.Core.Services; using FileTime.App.Core.Services;
using FileTime.Core.Interactions;
using FileTime.GuiApp.App.ViewModels; using FileTime.GuiApp.App.ViewModels;
namespace FileTime.GuiApp.App.Services; namespace FileTime.GuiApp.App.Services;
public class DialogService : IDialogService public class DialogService : DialogServiceBase, IDialogService
{ {
private readonly IModalService _modalService;
private readonly IGuiAppState _guiAppState; private readonly IGuiAppState _guiAppState;
public IObservable<ReadInputsViewModel?> ReadInput { get; } public DialogService(IModalService modalService, IGuiAppState guiAppState) : base(modalService)
public IObservable<MessageBoxViewModel?> LastMessageBox { get; }
public DialogService(IModalService modalService, IGuiAppState guiAppState)
{ {
_modalService = modalService;
_guiAppState = guiAppState; _guiAppState = guiAppState;
ReadInput = modalService
.OpenModals
.ToCollection()
.Select(modals =>
(ReadInputsViewModel?) modals.FirstOrDefault(m => m is ReadInputsViewModel)
)
.Publish(null)
.RefCount();
LastMessageBox =
modalService
.OpenModals
.Filter(m => m is MessageBoxViewModel)
.Transform(m => (MessageBoxViewModel) m)
.ToCollection()
.Select(m => m.LastOrDefault());
} }
private void ReadInputs( public override void ShowToastMessage(string text)
IEnumerable<IInputElement> inputs, =>
Action inputHandler, Task.Run(async () =>
Action? cancelHandler = null, {
IEnumerable<IPreviewElement>? previews = null) await Dispatcher.UIThread.InvokeAsync(() => _guiAppState.PopupTexts.Add(text));
{ await Task.Delay(5000);
var modalViewModel = new ReadInputsViewModel await Dispatcher.UIThread.InvokeAsync(() => _guiAppState.PopupTexts.Remove(text));
{ });
Inputs = inputs.ToList(),
SuccessHandler = HandleReadInputsSuccess,
CancelHandler = HandleReadInputsCancel
};
if (previews is not null)
{
modalViewModel.Previews.AddRange(previews);
}
_modalService.OpenModal(modalViewModel);
void HandleReadInputsSuccess(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
inputHandler();
}
void HandleReadInputsCancel(ReadInputsViewModel readInputsViewModel)
{
_modalService.CloseModal(readInputsViewModel);
cancelHandler?.Invoke();
}
}
public Task<bool> ReadInputs(IEnumerable<IInputElement> fields, IEnumerable<IPreviewElement>? previews = null)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
fields,
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false),
previews
);
return taskCompletionSource.Task;
}
public Task<bool> ReadInputs(params IInputElement[] fields)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
fields,
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false)
);
return taskCompletionSource.Task;
}
public Task<bool> ReadInputs(IInputElement field, IEnumerable<IPreviewElement>? previews = null)
{
var taskCompletionSource = new TaskCompletionSource<bool>();
ReadInputs(
new[] {field},
() => taskCompletionSource.SetResult(true),
() => taskCompletionSource.SetResult(false),
previews
);
return taskCompletionSource.Task;
}
public void ShowToastMessage(string text)
{
Task.Run(async () =>
{
await Dispatcher.UIThread.InvokeAsync(() => _guiAppState.PopupTexts.Add(text));
await Task.Delay(5000);
await Dispatcher.UIThread.InvokeAsync(() => _guiAppState.PopupTexts.Remove(text));
});
}
public Task<MessageBoxResult> ShowMessageBox(
string text,
bool showCancel = true,
string? okText = null,
string? cancelText = null)
{
var taskCompletionSource = new TaskCompletionSource<MessageBoxResult>();
_modalService.OpenModal(
new MessageBoxViewModel(
text,
(vm, result) =>
{
_modalService.CloseModal(vm);
taskCompletionSource.SetResult(result);
},
showCancel,
okText,
cancelText
)
);
return taskCompletionSource.Task;
}
} }

View File

@@ -22,8 +22,6 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:appCoreModels="using:FileTime.App.Core.Models" xmlns:appCoreModels="using:FileTime.App.Core.Models"
xmlns:appInteractions="using:FileTime.App.Core.Interactions" xmlns:appInteractions="using:FileTime.App.Core.Interactions"
xmlns:config="using:FileTime.GuiApp.App.Configuration"
xmlns:configuration="clr-namespace:FileTime.App.Core.Configuration;assembly=FileTime.App.Core.Abstraction"
xmlns:corevm="using:FileTime.App.Core.ViewModels" xmlns:corevm="using:FileTime.App.Core.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity" xmlns:i="clr-namespace:Avalonia.Xaml.Interactivity;assembly=Avalonia.Xaml.Interactivity"
@@ -787,7 +785,7 @@
<Border <Border
Background="{DynamicResource BarelyTransparentBackgroundColor}" Background="{DynamicResource BarelyTransparentBackgroundColor}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding DialogService.ReadInput^, Converter={x:Static ObjectConverters.IsNotNull}}" IsVisible="{Binding DialogService.ReadInput.Value, Converter={x:Static ObjectConverters.IsNotNull}}"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
x:Name="ReadInputContainer"> x:Name="ReadInputContainer">
<Border <Border
@@ -798,7 +796,7 @@
<Grid RowDefinitions="Auto,Auto,Auto"> <Grid RowDefinitions="Auto,Auto,Auto">
<ItemsControl <ItemsControl
ItemsSource="{Binding DialogService.ReadInput^.Inputs}" ItemsSource="{Binding DialogService.ReadInput.Value.Inputs}"
KeyUp="InputList_OnKeyUp" KeyUp="InputList_OnKeyUp"
x:Name="InputList"> x:Name="InputList">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
@@ -839,7 +837,7 @@
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<ItemsControl Grid.Row="1" ItemsSource="{Binding DialogService.ReadInput^.Previews}"> <ItemsControl Grid.Row="1" ItemsSource="{Binding DialogService.ReadInput.Value.Previews}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid> <Grid>
@@ -913,7 +911,7 @@
</ItemsControl> </ItemsControl>
<StackPanel <StackPanel
DataContext="{Binding DialogService.ReadInput^}" DataContext="{Binding DialogService.ReadInput.Value}"
Grid.Row="2" Grid.Row="2"
Margin="0,10,0,0" Margin="0,10,0,0"
Orientation="Horizontal"> Orientation="Horizontal">
@@ -935,7 +933,7 @@
<Border <Border
Background="{DynamicResource BarelyTransparentBackgroundColor}" Background="{DynamicResource BarelyTransparentBackgroundColor}"
DataContext="{Binding DialogService.LastMessageBox^}" DataContext="{Binding DialogService.LastMessageBox.Value}"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
IsVisible="{Binding Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}" IsVisible="{Binding Converter={x:Static ObjectConverters.IsNotNull}, FallbackValue=False}"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
@@ -952,12 +950,12 @@
Margin="0,10,0,0" Margin="0,10,0,0"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Command="{Binding OkCommand}" Command="{Binding Ok}"
Content="{Binding OkText}" Content="{Binding OkText}"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
Width="80" /> Width="80" />
<Button <Button
Command="{Binding CancelCommand}" Command="{Binding Cancel}"
Content="{Binding CancelText}" Content="{Binding CancelText}"
HorizontalContentAlignment="Center" HorizontalContentAlignment="Center"
IsVisible="{Binding ShowCancel}" IsVisible="{Binding ShowCancel}"

View File

@@ -20,9 +20,6 @@ public partial class MainWindow : Window, IUiAccessor
private readonly Action? _initializer; private readonly Action? _initializer;
private ILogger<MainWindow>? _logger; private ILogger<MainWindow>? _logger;
private IModalService? _modalService; private IModalService? _modalService;
private IReadOnlyCollection<IModalViewModel>? _openModals;
private ReadInputsViewModel? _inputViewModel;
private IDisposable? _inputViewModelSubscription;
private bool _isShuttingDown; private bool _isShuttingDown;
private bool _shutdownCompleted; private bool _shutdownCompleted;
private readonly object _isClosingLock = new(); private readonly object _isClosingLock = new();
@@ -61,19 +58,9 @@ public partial class MainWindow : Window, IUiAccessor
_logger = DI.ServiceProvider.GetService<ILogger<MainWindow>>(); _logger = DI.ServiceProvider.GetService<ILogger<MainWindow>>();
_modalService = DI.ServiceProvider.GetRequiredService<IModalService>(); _modalService = DI.ServiceProvider.GetRequiredService<IModalService>();
_modalService.OpenModals.ToCollection().Subscribe(m => _openModals = m); DI.ServiceProvider.GetRequiredService<SystemClipboardService>().UiAccessor = this;
DI.ServiceProvider.GetRequiredService<Services.SystemClipboardService>().UiAccessor = this;
ReadInputContainer.PropertyChanged += ReadInputContainerOnPropertyChanged; ReadInputContainer.PropertyChanged += ReadInputContainerOnPropertyChanged;
DataContextChanged += (_, _) =>
{
if (DataContext is not MainWindowViewModel mainWindowViewModel) return;
_inputViewModelSubscription?.Dispose();
_inputViewModelSubscription = mainWindowViewModel.DialogService.ReadInput.Subscribe(
inputViewModel => _inputViewModel = inputViewModel
);
};
_logger?.LogInformation( _logger?.LogInformation(
$"{nameof(MainWindow)} opened, starting {nameof(MainWindowViewModel)} initialization..."); $"{nameof(MainWindow)} opened, starting {nameof(MainWindowViewModel)} initialization...");
@@ -99,7 +86,7 @@ public partial class MainWindow : Window, IUiAccessor
private void OnKeyDown(object sender, KeyEventArgs e) private void OnKeyDown(object sender, KeyEventArgs e)
{ {
if ((_openModals?.Count ?? 0) > 0) return; if (_modalService!.OpenModals.Count > 0) return;
ViewModel?.ProcessKeyDown(e); ViewModel?.ProcessKeyDown(e);
} }
@@ -177,15 +164,14 @@ public partial class MainWindow : Window, IUiAccessor
private void InputList_OnKeyUp(object? sender, KeyEventArgs e) private void InputList_OnKeyUp(object? sender, KeyEventArgs e)
{ {
var inputViewModel = ViewModel!.DialogService.ReadInput.Value;
if (e.Key == Key.Escape) if (e.Key == Key.Escape)
{ {
_inputViewModel?.Cancel(); inputViewModel?.Cancel();
_inputViewModel = null;
} }
else if (e.Key == Key.Enter) else if (e.Key == Key.Enter)
{ {
_inputViewModel?.Process(); inputViewModel?.Process();
_inputViewModel = null;
} }
} }

View File

@@ -1,10 +1,18 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq.Expressions; using System.Linq.Expressions;
namespace ObservableComputations; namespace ObservableComputations;
public static class Extensions public static class Extensions
{ {
[ObservableComputationsCall]
public static Filtering<TSourceItem> Filtering<TSourceItem>(
this ReadOnlyObservableCollection<TSourceItem> source,
Expression<Func<TSourceItem, bool>> predicateExpression,
int initialCapacity = 0)
=> new(source, predicateExpression, initialCapacity);
[ObservableComputationsCall] [ObservableComputationsCall]
public static Selecting<TSourceItem, TResultItem> Selecting<TSourceItem, TResultItem>( public static Selecting<TSourceItem, TResultItem> Selecting<TSourceItem, TResultItem>(
this ReadOnlyObservableCollection<TSourceItem> source, this ReadOnlyObservableCollection<TSourceItem> source,

View File

@@ -22,9 +22,10 @@ public class DotnetDriver : IConsoleDriver
return new(x, y); return new(x, y);
} }
public void Write(string text) => Console.Write(text); public void Write(string text) => Console.Out.Write(text);
public void Write(ReadOnlySpan<char> text) => Console.Out.Write(text);
public void Write(char text) => Console.Write(text); public void Write(char text) => Console.Out.Write(text);
public virtual void Dispose() => Console.Clear(); public virtual void Dispose() => Console.Clear();

View File

@@ -11,6 +11,7 @@ public interface IConsoleDriver
void ResetColor(); void ResetColor();
Position GetCursorPosition(); Position GetCursorPosition();
void Write(string text); void Write(string text);
void Write(ReadOnlySpan<char> text);
void Write(char text); void Write(char text);
bool CanRead(); bool CanRead();
ConsoleKeyInfo ReadKey(); ConsoleKeyInfo ReadKey();

View File

@@ -4,7 +4,7 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public partial class Border<T> : ContentView<T>, IDisplayView public sealed partial class Border<T> : ContentView<Border<T>, T>, IDisplayView
{ {
[Notify] private Thickness _borderThickness = 1; [Notify] private Thickness _borderThickness = 1;
[Notify] private Thickness _padding = 0; [Notify] private Thickness _padding = 0;

View File

@@ -4,7 +4,9 @@ using System.Diagnostics.CodeAnalysis;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public abstract class ChildContainerView<T> : View<T>, IChildContainer<T> public abstract class ChildContainerView<TConcrete, T>
: View<TConcrete, T>, IChildContainer<T>
where TConcrete : View<TConcrete, T>
{ {
private readonly ObservableCollection<IView> _children = new(); private readonly ObservableCollection<IView> _children = new();
public ReadOnlyObservableCollection<IView> Children { get; } public ReadOnlyObservableCollection<IView> Children { get; }

View File

@@ -4,7 +4,9 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public abstract partial class ContentView<T> : View<T>, IContentRenderer<T> public abstract partial class ContentView<TConcrete, T>
: View<TConcrete, T>, IContentRenderer<T>
where TConcrete : View<TConcrete, T>
{ {
private bool _placeholderRenderDone; private bool _placeholderRenderDone;
[Notify] private RenderMethod _contentRendererMethod; [Notify] private RenderMethod _contentRendererMethod;

View File

@@ -7,7 +7,7 @@ using TerminalUI.ViewExtensions;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public class Grid<T> : ChildContainerView<T>, IVisibilityChangeHandler public sealed class Grid<T> : ChildContainerView<Grid<T>, T>, IVisibilityChangeHandler
{ {
private readonly List<IView> _forceRerenderChildren = new(); private readonly List<IView> _forceRerenderChildren = new();
private readonly object _forceRerenderChildrenLock = new(); private readonly object _forceRerenderChildrenLock = new();

View File

@@ -1,4 +1,6 @@
using System.ComponentModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using GeneralInputKey;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.Models; using TerminalUI.Models;
using TerminalUI.Traits; using TerminalUI.Traits;
@@ -28,10 +30,12 @@ public interface IView : INotifyPropertyChanged, IDisposableCollection
List<object> Extensions { get; } List<object> Extensions { get; }
RenderMethod RenderMethod { get; set; } RenderMethod RenderMethod { get; set; }
IView? VisualParent { get; set; } IView? VisualParent { get; set; }
ReadOnlyObservableCollection<IView> VisualChildren { get; }
event Action<IView> Disposed; event Action<IView> Disposed;
Size GetRequestedSize(); Size GetRequestedSize();
bool Render(in RenderContext renderContext, Position position, Size size); bool Render(in RenderContext renderContext, Position position, Size size);
void HandleKeyInput(GeneralKeyEventArgs keyEventArgs);
} }
public interface IView<T> : IView public interface IView<T> : IView

View File

@@ -0,0 +1,194 @@
using System.Collections.ObjectModel;
using ObservableComputations;
using PropertyChanged.SourceGenerator;
using TerminalUI.Models;
using TerminalUI.Traits;
namespace TerminalUI.Controls;
public sealed partial class ItemsControl<TDataContext, TItem>
: View<ItemsControl<TDataContext, TItem>, TDataContext>, IVisibilityChangeHandler
{
private readonly List<IView> _forceRerenderChildren = new();
private readonly object _forceRerenderChildrenLock = new();
private readonly List<IDisposable> _itemsDisposables = new();
private readonly Dictionary<IView, Size> _requestedSizes = new();
private IList<IView<TItem>> _children = new List<IView<TItem>>();
private object? _itemsSource;
[Notify] private Orientation _orientation = Orientation.Vertical;
public Func<IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
public IReadOnlyList<IView<TItem>> Children => _children.AsReadOnly();
public object? ItemsSource
{
get => _itemsSource;
set
{
if (_itemsSource == value) return;
_itemsSource = value;
foreach (var disposable in _itemsDisposables)
{
disposable.Dispose();
}
_itemsDisposables.Clear();
if (_itemsSource is ObservableCollection<TItem> observableDeclarative)
{
_children = observableDeclarative
.Selecting(i => CreateItem(i))
.OfTypeComputing<IView<TItem>>();
}
else if (_itemsSource is ReadOnlyObservableCollection<TItem> readOnlyObservableDeclarative)
{
_children = readOnlyObservableDeclarative
.Selecting(i => CreateItem(i))
.OfTypeComputing<IView<TItem>>();
}
else if (_itemsSource is ICollection<TItem> collection)
_children = collection.Select(CreateItem).OfType<IView<TItem>>().ToList();
else if (_itemsSource is TItem[] array)
_children = array.Select(CreateItem).OfType<IView<TItem>>().ToList();
else if (_itemsSource is IEnumerable<TItem> enumerable)
_children = enumerable.Select(CreateItem).OfType<IView<TItem>>().ToList();
else if (value is null)
{
_children = new List<IView<TItem>>();
}
else
{
throw new NotSupportedException();
}
OnPropertyChanged();
OnPropertyChanged(nameof(Children));
}
}
public ItemsControl()
{
RerenderProperties.Add(nameof(ItemsSource));
RerenderProperties.Add(nameof(Orientation));
}
protected override Size CalculateSize()
{
_requestedSizes.Clear();
double width = 0;
double height = 0;
foreach (var child in _children)
{
if (!child.IsVisible) continue;
var childSize = child.GetRequestedSize();
_requestedSizes.Add(child, childSize);
if (Orientation == Orientation.Vertical)
{
width = Math.Max(width, childSize.Width);
height += childSize.Height;
}
else
{
width += childSize.Width;
height = Math.Max(height, childSize.Height);
}
}
return new Size((int) width, (int) height);
}
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
{
var neededRerender = false;
IReadOnlyList<IView> forceRerenderChildren;
lock (_forceRerenderChildrenLock)
{
forceRerenderChildren = _forceRerenderChildren.ToList();
_forceRerenderChildren.Clear();
}
var delta = 0;
foreach (var child in _children)
{
if (!child.IsVisible) continue;
if (!_requestedSizes.TryGetValue(child, out var childSize)) throw new Exception("Child size not found");
var childPosition = Orientation == Orientation.Vertical
? position with {Y = position.Y + delta}
: position with {X = position.X + delta};
childSize = Orientation == Orientation.Vertical
? childSize with {Width = size.Width}
: childSize with {Height = size.Height};
var endX = position.X + size.Width;
var endY = position.Y + size.Height;
if (childPosition.X > endX || childPosition.Y > endY) break;
if (childPosition.X + childSize.Width > endX)
{
childSize = childSize with {Width = endX - childPosition.X};
}
if (childPosition.Y + childSize.Height > endY)
{
childSize = childSize with {Height = endY - childPosition.Y};
}
if (forceRerenderChildren.Contains(child))
{
var rerenderContext = new RenderContext(
renderContext.ConsoleDriver,
true,
renderContext.Foreground,
renderContext.Background,
renderContext.Statistics
);
neededRerender = child.Render(rerenderContext, childPosition, childSize) || neededRerender;
}
else
{
neededRerender = child.Render(renderContext, childPosition, childSize) || neededRerender;
}
delta += Orientation == Orientation.Vertical
? childSize.Height
: childSize.Width;
}
return neededRerender;
}
private IView<TItem>? CreateItem(TItem dataContext)
{
var newItem = ItemTemplate();
AddChild(newItem, _ => dataContext);
return newItem;
}
private static IView<TItem>? DefaultItemTemplate() => null;
public void ChildVisibilityChanged(IView child)
{
var viewToForceRerender = child;
while (viewToForceRerender.VisualParent != null && viewToForceRerender.VisualParent != this)
{
viewToForceRerender = viewToForceRerender.VisualParent;
}
if (viewToForceRerender.VisualParent != this) return;
lock (_forceRerenderChildrenLock)
{
_forceRerenderChildren.Add(viewToForceRerender);
}
}
}

View File

@@ -6,7 +6,7 @@ using TerminalUI.Models;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public partial class ListView<TDataContext, TItem> : View<TDataContext> public sealed partial class ListView<TDataContext, TItem> : View<ListView<TDataContext, TItem>, TDataContext>
{ {
private static readonly ArrayPool<ListViewItem<TItem, TDataContext>> ListViewItemPool = ArrayPool<ListViewItem<TItem, TDataContext>>.Shared; private static readonly ArrayPool<ListViewItem<TItem, TDataContext>> ListViewItemPool = ArrayPool<ListViewItem<TItem, TDataContext>>.Shared;

View File

@@ -3,7 +3,7 @@ using TerminalUI.Models;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public partial class ListViewItem<T, TParentDataContext> : ContentView<T> public sealed partial class ListViewItem<T, TParentDataContext> : ContentView<ListViewItem<T, TParentDataContext>, T>
{ {
public ListView<TParentDataContext, T> Parent { get; } public ListView<TParentDataContext, T> Parent { get; }
[Notify] private bool _isSelected; [Notify] private bool _isSelected;

View File

@@ -5,7 +5,7 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public partial class Rectangle<T> : View<T>, IDisplayView public sealed partial class Rectangle<T> : View<Rectangle<T>, T>, IDisplayView
{ {
private record RenderState( private record RenderState(
Position Position, Position Position,

View File

@@ -4,7 +4,7 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public partial class StackPanel<T> : ChildContainerView<T>, IVisibilityChangeHandler public sealed partial class StackPanel<T> : ChildContainerView<StackPanel<T>, T>, IVisibilityChangeHandler
{ {
private readonly List<IView> _forceRerenderChildren = new(); private readonly List<IView> _forceRerenderChildren = new();
private readonly object _forceRerenderChildrenLock = new(); private readonly object _forceRerenderChildrenLock = new();
@@ -41,7 +41,6 @@ public partial class StackPanel<T> : ChildContainerView<T>, IVisibilityChangeHan
protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size) protected override bool DefaultRenderer(in RenderContext renderContext, Position position, Size size)
{ {
var delta = 0;
var neededRerender = false; var neededRerender = false;
IReadOnlyList<IView> forceRerenderChildren; IReadOnlyList<IView> forceRerenderChildren;
lock (_forceRerenderChildrenLock) lock (_forceRerenderChildrenLock)
@@ -50,17 +49,21 @@ public partial class StackPanel<T> : ChildContainerView<T>, IVisibilityChangeHan
_forceRerenderChildren.Clear(); _forceRerenderChildren.Clear();
} }
var delta = 0;
foreach (var child in Children) foreach (var child in Children)
{ {
if (!child.IsVisible) continue; 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
? position with {Y = position.Y + delta} ? position with {Y = position.Y + delta}
: position with {X = position.X + delta}; : position with {X = position.X + delta};
childSize = Orientation == Orientation.Vertical
? childSize with {Width = size.Width}
: childSize with {Height = size.Height};
var endX = position.X + size.Width; var endX = position.X + size.Width;
var endY = position.Y + size.Height; var endY = position.Y + size.Height;

View File

@@ -9,7 +9,7 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
[DebuggerDisplay("Text = {Text}")] [DebuggerDisplay("Text = {Text}")]
public partial class TextBlock<T> : View<T>, IDisplayView public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
{ {
private record RenderState( private record RenderState(
Position Position, Position Position,

View File

@@ -10,7 +10,7 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
[DebuggerDisplay("Text = {Text}")] [DebuggerDisplay("Text = {Text}")]
public partial class TextBox<T> : View<T>, IFocusable, IDisplayView public sealed partial class TextBox<T> : View<TextBox<T>, T>, IFocusable, IDisplayView
{ {
private record RenderState( private record RenderState(
string? Text, string? Text,
@@ -20,7 +20,6 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
IColor? BackgroundColor IColor? BackgroundColor
); );
private readonly List<Action<TextBox<T>, GeneralKeyEventArgs>> _keyHandlers = new();
private readonly List<Action<TextBox<T>, string>> _textHandlers = new(); private readonly List<Action<TextBox<T>, string>> _textHandlers = new();
private RenderState? _lastRenderState; private RenderState? _lastRenderState;
@@ -31,6 +30,7 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
private Position _relativeCursorPosition = new(0, 0); private Position _relativeCursorPosition = new(0, 0);
[Notify] private bool _multiLine; [Notify] private bool _multiLine;
[Notify] private char? _passwordChar;
public bool SetKeyHandledIfKnown { get; set; } public bool SetKeyHandledIfKnown { get; set; }
public string Text public string Text
@@ -55,6 +55,7 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
_textLines = _text.Split(Environment.NewLine).ToList(); _textLines = _text.Split(Environment.NewLine).ToList();
RerenderProperties.Add(nameof(Text)); RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(MultiLine)); RerenderProperties.Add(nameof(MultiLine));
RerenderProperties.Add(nameof(PasswordChar));
((INotifyPropertyChanged) this).PropertyChanged += OnPropertyChangedEventHandler; ((INotifyPropertyChanged) this).PropertyChanged += OnPropertyChangedEventHandler;
} }
@@ -115,12 +116,41 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
} }
RenderEmpty(renderContext, position, size); RenderEmpty(renderContext, position, size);
RenderText(_textLines, driver, position, size);
if (PasswordChar is { } passwordChar && !char.IsControl(passwordChar))
{
for (var i = 0; i < _textLines.Count; i++)
{
var pos = position with {Y = position.Y + i};
RenderPasswordTextLine(_textLines[i], passwordChar, driver, pos, size);
}
}
else
{
RenderText(_textLines, driver, position, size);
}
_cursorPosition = position + _relativeCursorPosition; _cursorPosition = position + _relativeCursorPosition;
return true; return true;
} }
private void RenderPasswordTextLine(
string sourceText,
char passwordChar,
IConsoleDriver driver,
Position position,
Size size)
{
Span<char> text = stackalloc char[sourceText.Length];
for (var j = 0; j < text.Length; j++)
{
text[j] = passwordChar;
}
RenderText(text, driver, position, size);
}
private bool NeedsRerender(RenderState renderState) private bool NeedsRerender(RenderState renderState)
=> _lastRenderState is null || _lastRenderState != renderState; => _lastRenderState is null || _lastRenderState != renderState;
@@ -136,13 +166,22 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
consoleDriver.SetCursorPosition(_cursorPosition.Value); consoleDriver.SetCursorPosition(_cursorPosition.Value);
} }
public void HandleKeyInput(GeneralKeyEventArgs keyEventArgs) public override void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
{ {
HandleKeyInputInternal(keyEventArgs); HandleKeyInputInternal(keyEventArgs);
if (keyEventArgs.Handled) if (keyEventArgs.Handled)
{ {
ApplicationContext?.RenderEngine.RequestRerender(this); ApplicationContext?.RenderEngine.RequestRerender(this);
} }
else
{
var view = VisualParent;
while (view != null && !keyEventArgs.Handled)
{
view.HandleKeyInput(keyEventArgs);
view = view.VisualParent;
}
}
} }
private void HandleKeyInputInternal(GeneralKeyEventArgs keyEventArgs) private void HandleKeyInputInternal(GeneralKeyEventArgs keyEventArgs)
@@ -158,11 +197,14 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
if (!known && HandleNavigation(keyEventArgs, out known)) if (!known && HandleNavigation(keyEventArgs, out known))
return; return;
if (!known && ProcessKeyHandlers(keyEventArgs)) if (!known)
return; {
ProcessKeyHandlers(keyEventArgs);
if (keyEventArgs.Handled) return;
}
if (!known if (!known
&& keyEventArgs.KeyChar != '\0' && !char.IsControl(keyEventArgs.KeyChar)
&& keyEventArgs.KeyChar.ToString() is {Length: 1} keyString) && keyEventArgs.KeyChar.ToString() is {Length: 1} keyString)
{ {
var y = _relativeCursorPosition.Y; var y = _relativeCursorPosition.Y;
@@ -172,6 +214,12 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
keyEventArgs.Handled = true; keyEventArgs.Handled = true;
UpdateTextField(); UpdateTextField();
return;
}
if (!known)
{
ProcessParentKeyHandlers(keyEventArgs);
} }
} }
@@ -294,29 +342,6 @@ public partial class TextBox<T> : View<T>, IFocusable, IDisplayView
return false; return false;
} }
private bool ProcessKeyHandlers(GeneralKeyEventArgs keyEventArgs)
{
foreach (var keyHandler in _keyHandlers)
{
keyHandler(this, keyEventArgs);
if (keyEventArgs.Handled) return true;
}
return false;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_keyHandlers.Clear();
}
public TextBox<T> WithKeyHandler(Action<TextBox<T>, GeneralKeyEventArgs> keyHandler)
{
_keyHandlers.Add(keyHandler);
return this;
}
public TextBox<T> WithTextHandler(Action<TextBox<T>, string> textChanged) public TextBox<T> WithTextHandler(Action<TextBox<T>, string> textChanged)
{ {
_textHandlers.Add(textChanged); _textHandlers.Add(textChanged);

View File

@@ -2,6 +2,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using GeneralInputKey;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using TerminalUI.Color; using TerminalUI.Color;
using TerminalUI.ConsoleDrivers; using TerminalUI.ConsoleDrivers;
@@ -12,9 +13,10 @@ namespace TerminalUI.Controls;
public delegate string TextTransformer(string text, Position position, Size size); public delegate string TextTransformer(string text, Position position, Size size);
public abstract partial class View<T> : IView<T> public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : View<TConcrete, T>
{ {
private readonly List<IDisposable> _disposables = new(); private readonly List<IDisposable> _disposables = new();
private readonly ReadOnlyObservableCollection<IView> _readOnlyVisualChildren;
[Notify] private T? _dataContext; [Notify] private T? _dataContext;
[Notify] private int? _minWidth; [Notify] private int? _minWidth;
[Notify] private int? _maxWidth; [Notify] private int? _maxWidth;
@@ -33,7 +35,9 @@ public abstract partial class View<T> : IView<T>
[Notify] private bool _attached; [Notify] private bool _attached;
[Notify] private IView? _visualParent; [Notify] private IView? _visualParent;
protected List<Action<TConcrete, GeneralKeyEventArgs>> KeyHandlers { get; } = new();
protected ObservableCollection<IView> VisualChildren { get; } = new(); protected ObservableCollection<IView> VisualChildren { get; } = new();
ReadOnlyObservableCollection<IView> IView.VisualChildren => _readOnlyVisualChildren;
public List<object> Extensions { get; } = new(); public List<object> Extensions { get; } = new();
public RenderMethod RenderMethod { get; set; } public RenderMethod RenderMethod { get; set; }
@@ -43,6 +47,7 @@ public abstract partial class View<T> : IView<T>
protected View() protected View()
{ {
RenderMethod = DefaultRenderer; RenderMethod = DefaultRenderer;
_readOnlyVisualChildren = new ReadOnlyObservableCollection<IView>(VisualChildren);
RerenderProperties.Add(nameof(Width)); RerenderProperties.Add(nameof(Width));
RerenderProperties.Add(nameof(MinWidth)); RerenderProperties.Add(nameof(MinWidth));
@@ -245,6 +250,27 @@ public abstract partial class View<T> : IView<T>
} }
} }
protected void RenderText(
in ReadOnlySpan<char> text,
IConsoleDriver driver,
Position position,
Size size)
{
for (var i = 0; i < size.Height; i++)
{
var currentPosition = position with {Y = position.Y + i};
var finalText = text;
if (finalText.Length > size.Width)
{
finalText = finalText[..size.Width];
}
driver.SetCursorPosition(currentPosition);
driver.Write(finalText);
}
}
protected void RenderText( protected void RenderText(
char content, char content,
IConsoleDriver driver, IConsoleDriver driver,
@@ -367,7 +393,39 @@ public abstract partial class View<T> : IView<T>
arrayPool.Return(disposables, true); arrayPool.Return(disposables, true);
_disposables.Clear(); _disposables.Clear();
KeyHandlers.Clear();
Disposed?.Invoke(this); Disposed?.Invoke(this);
} }
} }
public virtual void HandleKeyInput(GeneralKeyEventArgs keyEventArgs)
{
ProcessKeyHandlers(keyEventArgs);
ProcessParentKeyHandlers(keyEventArgs);
}
protected void ProcessKeyHandlers(GeneralKeyEventArgs keyEventArgs)
{
foreach (var keyHandler in KeyHandlers)
{
keyHandler((TConcrete)this, keyEventArgs);
if (keyEventArgs.Handled) return;
}
}
protected void ProcessParentKeyHandlers(GeneralKeyEventArgs keyEventArgs)
{
if (VisualParent is { } parent)
{
parent.HandleKeyInput(keyEventArgs);
}
}
public TConcrete WithKeyHandler(Action<TConcrete, GeneralKeyEventArgs> keyHandler)
{
KeyHandlers.Add(keyHandler);
return (TConcrete)this;
}
} }

View File

@@ -10,9 +10,20 @@ public class FocusManager : IFocusManager
{ {
get get
{ {
if (_focused is not null && !_focused.IsVisible) if (_focused is not null)
{ {
_focused = null; var visible = _focused.IsVisible;
var parent = _focused.VisualParent;
while (parent != null)
{
visible &= parent.IsVisible;
parent = parent.VisualParent;
}
if (!visible)
{
_focused = null;
}
} }
return _focused; return _focused;

View File

@@ -9,6 +9,7 @@
<ItemGroup> <ItemGroup>
<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="..\DeclarativeProperty\DeclarativeProperty.csproj" /> <ProjectReference Include="..\DeclarativeProperty\DeclarativeProperty.csproj" />
<ProjectReference Include="..\ObservableComputations.Extensions\ObservableComputations.Extensions.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>