Preview refactor, Console rename form
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.Core.Models;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace FileTime.App.Core.Interactions;
|
||||
|
||||
public partial class DoubleItemNamePartListPreview : IPreviewElement
|
||||
{
|
||||
[Notify] private List<ItemNamePart> _itemNameParts1 = new();
|
||||
[Notify] private List<ItemNamePart> _itemNameParts2 = new();
|
||||
public PreviewType PreviewType => PreviewType.DoubleItemNamePartList;
|
||||
object IPreviewElement.PreviewType => PreviewType;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using FileTime.Core.Interactions;
|
||||
|
||||
namespace FileTime.App.Core.Interactions;
|
||||
|
||||
public class DoubleTextListPreview : IPreviewElement
|
||||
{
|
||||
public ObservableCollection<DoubleTextPreview> Items { get; } = new();
|
||||
public PreviewType PreviewType { get; } = PreviewType.DoubleTextList;
|
||||
object IPreviewElement.PreviewType => PreviewType;
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
using System.Reactive.Subjects;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.Core.Models;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
|
||||
namespace FileTime.App.Core.Interactions;
|
||||
|
||||
public class DoubleTextPreview : IPreviewElement
|
||||
public partial class DoubleTextPreview : IPreviewElement
|
||||
{
|
||||
public IObservable<List<ItemNamePart>> Text1 { get; init; } = new BehaviorSubject<List<ItemNamePart>>(new());
|
||||
public IObservable<List<ItemNamePart>> Text2 { get; init; } = new BehaviorSubject<List<ItemNamePart>>(new());
|
||||
[Notify] private string _text1;
|
||||
[Notify] private string _text2;
|
||||
|
||||
public PreviewType PreviewType => PreviewType.DoubleTextList;
|
||||
public PreviewType PreviewType => PreviewType.DoubleText;
|
||||
object IPreviewElement.PreviewType => PreviewType;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using FileTime.Core.Interactions;
|
||||
|
||||
namespace FileTime.App.Core.Interactions;
|
||||
|
||||
public class PreviewList : IPreviewElement
|
||||
{
|
||||
public ObservableCollection<IPreviewElement> Items { get; } = new();
|
||||
public PreviewType PreviewType { get; } = PreviewType.PreviewList;
|
||||
object IPreviewElement.PreviewType => PreviewType;
|
||||
}
|
||||
@@ -3,5 +3,6 @@
|
||||
public enum PreviewType
|
||||
{
|
||||
DoubleText,
|
||||
DoubleTextList
|
||||
PreviewList,
|
||||
DoubleItemNamePartList
|
||||
}
|
||||
@@ -61,8 +61,9 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
|
||||
|
||||
public async Task HandleInputKey(GeneralKeyEventArgs args)
|
||||
{
|
||||
if (args.Key is not { } key) return;
|
||||
var keyWithModifiers = new KeyConfig(
|
||||
args.Key,
|
||||
key,
|
||||
shift: args.SpecialKeysStatus.IsShiftPressed,
|
||||
alt: args.SpecialKeysStatus.IsAltPressed,
|
||||
ctrl: args.SpecialKeysStatus.IsCtrlPressed);
|
||||
@@ -72,7 +73,7 @@ public class DefaultModeKeyInputHandler : IDefaultModeKeyInputHandler
|
||||
var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys));
|
||||
selectedCommandBinding ??= _keyboardConfigurationService.CommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(_appState.PreviousKeys));
|
||||
|
||||
if (args.Key == Keys.Escape)
|
||||
if (key == Keys.Escape)
|
||||
{
|
||||
var doGeneralReset = _appState.PreviousKeys.Count > 1;
|
||||
|
||||
|
||||
@@ -54,9 +54,10 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
|
||||
|
||||
public async Task HandleInputKey(GeneralKeyEventArgs args)
|
||||
{
|
||||
var keyString = args.Key.Humanize();
|
||||
if (args.Key is not { } key) return;
|
||||
var keyString = key.Humanize();
|
||||
|
||||
if (args.Key == Keys.Escape)
|
||||
if (key == Keys.Escape)
|
||||
{
|
||||
args.Handled = true;
|
||||
if (_modalService.OpenModals.Count > 0)
|
||||
@@ -68,7 +69,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
|
||||
await CallCommandAsync(ExitRapidTravelCommand.Instance);
|
||||
}
|
||||
}
|
||||
else if (args.Key == Keys.Backspace)
|
||||
else if (key == Keys.Backspace)
|
||||
{
|
||||
if (_appState.RapidTravelText.Value!.Length > 0)
|
||||
{
|
||||
@@ -87,7 +88,7 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentKeyAsList = new List<KeyConfig> {new(args.Key)};
|
||||
var currentKeyAsList = new List<KeyConfig> {new(key)};
|
||||
var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(currentKeyAsList));
|
||||
if (selectedCommandBinding != null)
|
||||
{
|
||||
|
||||
@@ -217,12 +217,15 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
||||
{
|
||||
BehaviorSubject<string> templateRegexValue = new(string.Empty);
|
||||
BehaviorSubject<string> newNameSchemaValue = new(string.Empty);
|
||||
List<IDisposable> subscriptions = new();
|
||||
|
||||
var itemsToRename = new List<FullName>(_markedItems.Value!);
|
||||
|
||||
var itemPreviews = itemsToRename
|
||||
.Select(item =>
|
||||
{
|
||||
var preview = new DoubleItemNamePartListPreview();
|
||||
|
||||
var originalName = item.GetName();
|
||||
|
||||
var decoratedOriginalName = templateRegexValue.Select(templateRegex =>
|
||||
@@ -284,17 +287,19 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
||||
}
|
||||
);
|
||||
|
||||
var preview = new DoubleTextPreview
|
||||
{
|
||||
Text1 = decoratedOriginalName,
|
||||
Text2 = text2
|
||||
};
|
||||
subscriptions.Add(decoratedOriginalName.Subscribe(
|
||||
n => preview.ItemNameParts1 = n
|
||||
));
|
||||
subscriptions.Add(text2.Subscribe(
|
||||
n => preview.ItemNameParts2 = n
|
||||
));
|
||||
|
||||
return preview;
|
||||
}
|
||||
);
|
||||
|
||||
DoubleTextListPreview doubleTextListPreview = new();
|
||||
doubleTextListPreview.Items.AddRange(itemPreviews);
|
||||
PreviewList previewList = new();
|
||||
previewList.Items.AddRange(itemPreviews);
|
||||
|
||||
var templateRegex = new TextInputElement("Template regex", string.Empty,
|
||||
s => templateRegexValue.OnNext(s!));
|
||||
@@ -303,7 +308,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
||||
|
||||
var success = await _userCommunicationService.ReadInputs(
|
||||
new[] {templateRegex, newNameSchema},
|
||||
new[] {doubleTextListPreview}
|
||||
new[] {previewList}
|
||||
);
|
||||
|
||||
if (success)
|
||||
@@ -338,6 +343,8 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
|
||||
itemsToMove.AddRange(itemsToMoveWithPath);
|
||||
}
|
||||
}
|
||||
|
||||
subscriptions.ForEach(s => s.Dispose());
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace FileTime.ConsoleUI.App.Styling;
|
||||
public interface ITheme
|
||||
{
|
||||
IColor? DefaultForegroundColor { get; }
|
||||
IColor? DefaultForegroundAccentColor { get; }
|
||||
IColor? DefaultBackgroundColor { get; }
|
||||
IColor? ElementColor { get; }
|
||||
IColor? ContainerColor { get; }
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
using System.Collections.Specialized;
|
||||
using FileTime.App.Core.Models;
|
||||
using FileTime.App.Core.Services;
|
||||
using FileTime.App.Core.ViewModels;
|
||||
using FileTime.ConsoleUI.App.KeyInputHandling;
|
||||
using GeneralInputKey;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using TerminalUI;
|
||||
using TerminalUI.ConsoleDrivers;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace FileTime.ConsoleUI.App;
|
||||
|
||||
@@ -27,6 +26,7 @@ public class App : IApplication
|
||||
private readonly IApplicationContext _applicationContext;
|
||||
private readonly IConsoleDriver _consoleDriver;
|
||||
private readonly IAppState _appState;
|
||||
private readonly ILogger<App> _logger;
|
||||
private readonly IKeyInputHandlerService _keyInputHandlerService;
|
||||
private readonly Thread _renderThread;
|
||||
|
||||
@@ -38,7 +38,8 @@ public class App : IApplication
|
||||
MainWindow mainWindow,
|
||||
IApplicationContext applicationContext,
|
||||
IConsoleDriver consoleDriver,
|
||||
IAppState appState)
|
||||
IAppState appState,
|
||||
ILogger<App> logger)
|
||||
{
|
||||
_lifecycleService = lifecycleService;
|
||||
_keyInputHandlerService = keyInputHandlerService;
|
||||
@@ -48,6 +49,7 @@ public class App : IApplication
|
||||
_applicationContext = applicationContext;
|
||||
_consoleDriver = consoleDriver;
|
||||
_appState = appState;
|
||||
_logger = logger;
|
||||
|
||||
_renderThread = new Thread(Render);
|
||||
}
|
||||
@@ -73,13 +75,14 @@ public class App : IApplication
|
||||
var focusManager = _applicationContext.FocusManager;
|
||||
|
||||
while (_applicationContext.IsRunning)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_consoleDriver.CanRead())
|
||||
{
|
||||
var key = _consoleDriver.ReadKey();
|
||||
|
||||
if (_appKeyService.MapKey(key.Key) is { } mappedKey)
|
||||
{
|
||||
var mappedKey = _appKeyService.MapKey(key.Key);
|
||||
SpecialKeysStatus specialKeysStatus = new(
|
||||
(key.Modifiers & ConsoleModifiers.Alt) != 0,
|
||||
(key.Modifiers & ConsoleModifiers.Shift) != 0,
|
||||
@@ -100,12 +103,16 @@ public class App : IApplication
|
||||
_applicationContext.FocusManager.HandleKeyInput(keyEventArgs);
|
||||
}
|
||||
|
||||
if (focused is null || (!keyEventArgs.Handled && KeysToFurtherProcess.Contains(keyEventArgs.Key)))
|
||||
if (focused is null || (keyEventArgs is {Handled: false, Key: { } k} && KeysToFurtherProcess.Contains(k)))
|
||||
{
|
||||
_keyInputHandlerService.HandleKeyInput(keyEventArgs, specialKeysStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while handling key input");
|
||||
}
|
||||
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
|
||||
356
src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Dialogs.cs
Normal file
356
src/ConsoleApp/FileTime.ConsoleUI.App/Controls/Dialogs.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using FileTime.App.Core.Interactions;
|
||||
using FileTime.ConsoleUI.App.Styling;
|
||||
using FileTime.Core.Interactions;
|
||||
using FileTime.Core.Models;
|
||||
using GeneralInputKey;
|
||||
using TerminalUI.Controls;
|
||||
using TerminalUI.Extensions;
|
||||
using TerminalUI.Models;
|
||||
using TerminalUI.Traits;
|
||||
using TerminalUI.ViewExtensions;
|
||||
|
||||
namespace FileTime.ConsoleUI.App.Controls;
|
||||
|
||||
public class Dialogs
|
||||
{
|
||||
private readonly IRootViewModel _rootViewModel;
|
||||
private readonly ITheme _theme;
|
||||
private ItemsControl<IRootViewModel, IInputElement> _readInputs = null!;
|
||||
private IInputElement? _inputElementToFocus;
|
||||
|
||||
private Action? _readInputChildHandlerUnSubscriber;
|
||||
|
||||
public Dialogs(IRootViewModel rootViewModel, ITheme theme)
|
||||
{
|
||||
_rootViewModel = rootViewModel;
|
||||
_theme = theme;
|
||||
|
||||
rootViewModel.FocusReadInputElement += element =>
|
||||
{
|
||||
_inputElementToFocus = element;
|
||||
UpdateReadInputsFocus();
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateReadInputsFocus()
|
||||
{
|
||||
foreach (var readInputsChild in _readInputs.Children)
|
||||
{
|
||||
if (readInputsChild.DataContext == _inputElementToFocus)
|
||||
{
|
||||
if (FindFocusable(readInputsChild) is { } focusable)
|
||||
{
|
||||
focusable.Focus();
|
||||
_inputElementToFocus = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IFocusable? FindFocusable(IView view)
|
||||
{
|
||||
if (view is IFocusable focusable) return focusable;
|
||||
foreach (var viewVisualChild in view.VisualChildren)
|
||||
{
|
||||
if (FindFocusable(viewVisualChild) is { } focusableChild)
|
||||
return focusableChild;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IView<IRootViewModel> View()
|
||||
{
|
||||
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);
|
||||
|
||||
((INotifyPropertyChanged) _readInputs).PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ItemsControl<object, object>.Children))
|
||||
{
|
||||
_readInputChildHandlerUnSubscriber?.Invoke();
|
||||
|
||||
if (_readInputs.Children.Count > 0)
|
||||
{
|
||||
UpdateReadInputsFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputElementToFocus = null;
|
||||
}
|
||||
|
||||
if (_readInputs.Children is INotifyCollectionChanged notifyCollectionChanged)
|
||||
{
|
||||
notifyCollectionChanged.CollectionChanged += NotifyCollectionChangedEventHandler;
|
||||
_readInputChildHandlerUnSubscriber = () => { notifyCollectionChanged.CollectionChanged -= NotifyCollectionChangedEventHandler; };
|
||||
}
|
||||
|
||||
void NotifyCollectionChangedEventHandler(
|
||||
object? sender,
|
||||
NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateReadInputsFocus();
|
||||
}
|
||||
}
|
||||
};
|
||||
return root;
|
||||
}
|
||||
|
||||
private IView<IRootViewModel> ReadInputs()
|
||||
=> new Grid<IRootViewModel>
|
||||
{
|
||||
RowDefinitionsObject = "Auto Auto",
|
||||
ChildInitializer =
|
||||
{
|
||||
ReadInputsList(),
|
||||
new ItemsControl<IRootViewModel, IPreviewElement>
|
||||
{
|
||||
ItemTemplate = ReadInputPreviewItemTemplate
|
||||
}
|
||||
.Setup(i => i.Bind(
|
||||
i,
|
||||
dc => dc.DialogService.ReadInput.Value.Previews,
|
||||
c => c.ItemsSource
|
||||
))
|
||||
.WithExtension(new GridPositionExtension(0, 1))
|
||||
}
|
||||
};
|
||||
|
||||
private IView<IPreviewElement> ReadInputPreviewItemTemplate()
|
||||
{
|
||||
var grid = new Grid<IPreviewElement>
|
||||
{
|
||||
ChildInitializer =
|
||||
{
|
||||
new ItemsControl<IPreviewElement, IPreviewElement>
|
||||
{
|
||||
ItemTemplate = ReadInputPreviewItemTemplate
|
||||
}
|
||||
.Setup(i => i.Bind(
|
||||
i,
|
||||
dc => (PreviewType) dc.PreviewType == PreviewType.PreviewList,
|
||||
c => c.IsVisible))
|
||||
.Setup(i => i.Bind(
|
||||
i,
|
||||
dc => ((PreviewList) dc).Items,
|
||||
c => c.ItemsSource)),
|
||||
new Grid<IPreviewElement>
|
||||
{
|
||||
ColumnDefinitionsObject = "* *",
|
||||
ChildInitializer =
|
||||
{
|
||||
new TextBlock<IPreviewElement>()
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
dc => ((DoubleTextPreview) dc).Text1,
|
||||
tb => tb.Text
|
||||
)),
|
||||
new TextBlock<IPreviewElement>
|
||||
{
|
||||
Extensions =
|
||||
{
|
||||
new GridPositionExtension(1, 0)
|
||||
}
|
||||
}
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
dc => ((DoubleTextPreview) dc).Text2,
|
||||
tb => tb.Text
|
||||
))
|
||||
}
|
||||
}.Setup(g => g.Bind(
|
||||
g,
|
||||
dc => (PreviewType) dc.PreviewType == PreviewType.DoubleText,
|
||||
g => g.IsVisible)),
|
||||
new Grid<IPreviewElement>
|
||||
{
|
||||
ColumnDefinitionsObject = "* *",
|
||||
ChildInitializer =
|
||||
{
|
||||
new ItemsControl<IPreviewElement, ItemNamePart>()
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
ItemTemplate = ItemNamePartItemTemplate
|
||||
}.Setup(i => i.Bind(
|
||||
i,
|
||||
dc => ((DoubleItemNamePartListPreview) dc).ItemNameParts1,
|
||||
c => c.ItemsSource,
|
||||
v => v)),
|
||||
new ItemsControl<IPreviewElement, ItemNamePart>()
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Extensions =
|
||||
{
|
||||
new GridPositionExtension(1, 0)
|
||||
},
|
||||
ItemTemplate = ItemNamePartItemTemplate
|
||||
}.Setup(i => i.Bind(
|
||||
i,
|
||||
dc => ((DoubleItemNamePartListPreview) dc).ItemNameParts2,
|
||||
c => c.ItemsSource))
|
||||
}
|
||||
}.Setup(g => g.Bind(
|
||||
g,
|
||||
dc => (PreviewType) dc.PreviewType == PreviewType.DoubleItemNamePartList,
|
||||
g => g.IsVisible))
|
||||
}
|
||||
};
|
||||
|
||||
return grid;
|
||||
|
||||
IView<ItemNamePart> ItemNamePartItemTemplate()
|
||||
{
|
||||
var textBlock = new TextBlock<ItemNamePart>();
|
||||
textBlock.Bind(
|
||||
textBlock,
|
||||
dc => dc.Text,
|
||||
tb => tb.Text
|
||||
);
|
||||
textBlock.Bind(
|
||||
textBlock,
|
||||
dc => dc.IsSpecial ? _theme.DefaultForegroundAccentColor : null,
|
||||
tb => tb.Foreground
|
||||
);
|
||||
|
||||
return textBlock;
|
||||
}
|
||||
}
|
||||
|
||||
private IView<IRootViewModel> ReadInputsList()
|
||||
{
|
||||
var readInputs = new ItemsControl<IRootViewModel, IInputElement>
|
||||
{
|
||||
IsFocusBoundary = true,
|
||||
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
|
||||
))
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => ((TextInputElement) d).Label,
|
||||
tb => tb.Name))
|
||||
.WithTextHandler((tb, t) =>
|
||||
{
|
||||
if (tb.DataContext is TextInputElement textInputElement)
|
||||
textInputElement.Value = t;
|
||||
})
|
||||
}
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => d.Type == InputType.Text,
|
||||
tb => tb.IsVisible
|
||||
)),
|
||||
new Border<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
|
||||
))
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => ((PasswordInputElement) d).Label,
|
||||
tb => tb.Name))
|
||||
.WithTextHandler((tb, t) =>
|
||||
{
|
||||
if (tb.DataContext is PasswordInputElement textInputElement)
|
||||
textInputElement.Value = t;
|
||||
})
|
||||
}
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => d.Type == InputType.Password,
|
||||
tb => tb.IsVisible
|
||||
))
|
||||
//TODO: OptionInputElement
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => d.DialogService.ReadInput.Value.Inputs,
|
||||
c => c.ItemsSource,
|
||||
v => v
|
||||
));
|
||||
|
||||
readInputs.WithKeyHandler((_, e) =>
|
||||
{
|
||||
if (e.Key == Keys.Enter)
|
||||
{
|
||||
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
|
||||
readInputsViewModel.Process();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Keys.Escape)
|
||||
{
|
||||
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
|
||||
readInputsViewModel.Cancel();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
});
|
||||
|
||||
_readInputs = readInputs;
|
||||
|
||||
return readInputs;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ using FileTime.ConsoleUI.App.Controls;
|
||||
using FileTime.ConsoleUI.App.Styling;
|
||||
using FileTime.Core.Enums;
|
||||
using FileTime.Core.Interactions;
|
||||
using GeneralInputKey;
|
||||
using TerminalUI;
|
||||
using TerminalUI.Color;
|
||||
using TerminalUI.Controls;
|
||||
@@ -23,57 +22,23 @@ public class MainWindow
|
||||
private readonly IApplicationContext _applicationContext;
|
||||
private readonly ITheme _theme;
|
||||
private readonly CommandPalette _commandPalette;
|
||||
private readonly Dialogs _dialogs;
|
||||
private readonly Lazy<IView> _root;
|
||||
|
||||
private ItemsControl<IRootViewModel, IInputElement> _readInputs = null!;
|
||||
private IInputElement? _inputElementToFocus;
|
||||
private Action? _readInputChildHandlerUnsubscriber;
|
||||
|
||||
public MainWindow(
|
||||
IRootViewModel rootViewModel,
|
||||
IApplicationContext applicationContext,
|
||||
ITheme theme,
|
||||
CommandPalette commandPalette)
|
||||
CommandPalette commandPalette,
|
||||
Dialogs dialogs)
|
||||
{
|
||||
_rootViewModel = rootViewModel;
|
||||
_applicationContext = applicationContext;
|
||||
_theme = theme;
|
||||
_commandPalette = commandPalette;
|
||||
_dialogs = dialogs;
|
||||
_root = new Lazy<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[]
|
||||
@@ -93,28 +58,7 @@ public class MainWindow
|
||||
{
|
||||
MainContent(),
|
||||
_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();
|
||||
}
|
||||
_dialogs.View(),
|
||||
}
|
||||
};
|
||||
return root;
|
||||
@@ -441,146 +385,4 @@ public class MainWindow
|
||||
(ItemViewMode.MarkedAlternative, _) => _theme.MarkedItemBackgroundColor,
|
||||
_ => 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>
|
||||
{
|
||||
IsFocusBoundary = true,
|
||||
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
|
||||
))
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => ((TextInputElement) d).Label,
|
||||
tb => tb.Name))
|
||||
.WithTextHandler((tb, t) =>
|
||||
{
|
||||
if (tb.DataContext is TextInputElement textInputElement)
|
||||
textInputElement.Value = t;
|
||||
})
|
||||
}
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => d.Type == InputType.Text,
|
||||
tb => tb.IsVisible
|
||||
)),
|
||||
new Border<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
|
||||
))
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => ((PasswordInputElement) d).Label,
|
||||
tb => tb.Name))
|
||||
.WithTextHandler((tb, t) =>
|
||||
{
|
||||
if (tb.DataContext is PasswordInputElement textInputElement)
|
||||
textInputElement.Value = t;
|
||||
})
|
||||
}
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => d.Type == InputType.Password,
|
||||
tb => tb.IsVisible
|
||||
))
|
||||
//TODO: OptionInputElement
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
.Setup(t => t.Bind(
|
||||
t,
|
||||
d => d.DialogService.ReadInput.Value.Inputs,
|
||||
c => c.ItemsSource,
|
||||
v => v
|
||||
));
|
||||
|
||||
readInputs.WithKeyHandler((_, e) =>
|
||||
{
|
||||
if (e.Key == Keys.Enter)
|
||||
{
|
||||
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
|
||||
readInputsViewModel.Process();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Keys.Escape)
|
||||
{
|
||||
if (_rootViewModel.DialogService.ReadInput.Value is { } readInputsViewModel)
|
||||
readInputsViewModel.Cancel();
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
});
|
||||
|
||||
_readInputs = readInputs;
|
||||
|
||||
return readInputs;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ public static class Startup
|
||||
{
|
||||
services.TryAddSingleton<MainWindow>();
|
||||
services.TryAddSingleton<CommandPalette>();
|
||||
services.TryAddSingleton<Dialogs>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace FileTime.ConsoleUI.Styles;
|
||||
|
||||
public record Theme(
|
||||
IColor? DefaultForegroundColor,
|
||||
IColor? DefaultForegroundAccentColor,
|
||||
IColor? DefaultBackgroundColor,
|
||||
IColor? ElementColor,
|
||||
IColor? ContainerColor,
|
||||
@@ -23,6 +24,7 @@ public static class DefaultThemes
|
||||
{
|
||||
public static Theme Color256Theme => new(
|
||||
DefaultForegroundColor: Color256Colors.Foregrounds.Gray,
|
||||
DefaultForegroundAccentColor: Color256Colors.Foregrounds.Red,
|
||||
DefaultBackgroundColor: null,
|
||||
ElementColor: Color256Colors.Foregrounds.Gray,
|
||||
ContainerColor: Color256Colors.Foregrounds.Blue,
|
||||
@@ -42,6 +44,7 @@ public static class DefaultThemes
|
||||
|
||||
public static Theme ConsoleColorTheme => new(
|
||||
DefaultForegroundColor: ConsoleColors.Foregrounds.Gray,
|
||||
DefaultForegroundAccentColor: ConsoleColors.Foregrounds.Red,
|
||||
DefaultBackgroundColor: null,
|
||||
ElementColor: ConsoleColors.Foregrounds.Gray,
|
||||
ContainerColor: ConsoleColors.Foregrounds.Blue,
|
||||
|
||||
@@ -840,72 +840,7 @@
|
||||
<ItemsControl Grid.Row="1" ItemsSource="{Binding DialogService.ReadInput.Value.Previews}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid IsVisible="{Binding PreviewType, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static appInteractions:PreviewType.DoubleTextList}}">
|
||||
<ItemsControl ItemsSource="{Binding Items}" x:DataType="appInteractions:DoubleTextListPreview">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<ItemsControl ItemsSource="{Binding Text1^}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" TextDecorations="{Binding IsSpecial, Converter={StaticResource TextDecorationConverter}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ItemsControl Grid.Column="1" ItemsSource="{Binding Text2^}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" TextDecorations="{Binding IsSpecial, Converter={StaticResource TextDecorationConverter}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!--DataGrid ItemsSource="{Binding Items}" x:DataType="appInteractions:DoubleTextListPreview">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ItemsControl
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding Text1^}"
|
||||
Margin="5,0,0,0"
|
||||
VerticalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Grid>
|
||||
<TextBlock Text="{Binding}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid-->
|
||||
</Grid>
|
||||
|
||||
|
||||
</Grid>
|
||||
<local:ReadInputPreview />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<UserControl
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d"
|
||||
x:Class="FileTime.GuiApp.App.Views.ReadInputPreview"
|
||||
x:CompileBindings="True"
|
||||
x:DataType="interactionsCore:IPreviewElement"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactions="clr-namespace:FileTime.App.Core.Interactions;assembly=FileTime.App.Core.Abstraction"
|
||||
xmlns:interactionsCore="clr-namespace:FileTime.Core.Interactions;assembly=FileTime.Core.Abstraction"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:FileTime.GuiApp.App.Views"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Grid>
|
||||
<Grid IsVisible="{Binding PreviewType, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static interactions:PreviewType.PreviewList}}">
|
||||
<ItemsControl ItemsSource="{Binding Items}" x:DataType="interactions:PreviewList">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<views:ReadInputPreview />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding PreviewType, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static interactions:PreviewType.DoubleItemNamePartList}}">
|
||||
<Grid ColumnDefinitions="*,*" x:DataType="interactions:DoubleTextPreview">
|
||||
<TextBlock Text="{Binding Text1}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Text2}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid IsVisible="{Binding PreviewType, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static interactions:PreviewType.DoubleItemNamePartList}}">
|
||||
<Grid ColumnDefinitions="*,*" x:DataType="interactions:DoubleItemNamePartListPreview">
|
||||
<ItemsControl ItemsSource="{Binding ItemNameParts1}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" TextDecorations="{Binding IsSpecial, Converter={StaticResource TextDecorationConverter}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<ItemsControl Grid.Column="1" ItemsSource="{Binding ItemNameParts2}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" TextDecorations="{Binding IsSpecial, Converter={StaticResource TextDecorationConverter}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,18 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace FileTime.GuiApp.App.Views;
|
||||
|
||||
public partial class ReadInputPreview : UserControl
|
||||
{
|
||||
public ReadInputPreview()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ public class GeneralKeyEventArgs
|
||||
{
|
||||
private readonly Action<bool>? _handledChanged;
|
||||
private bool _handled;
|
||||
public required Keys Key { get; init; }
|
||||
public required Keys? Key { get; init; }
|
||||
public required char KeyChar { get; init; }
|
||||
public required SpecialKeysStatus SpecialKeysStatus { get; init; }
|
||||
|
||||
|
||||
@@ -78,8 +78,14 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
|
||||
value = _fallbackValue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_targetProperty.SetValue(_propertySource, value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
private object? _itemsSource;
|
||||
[Notify] private Orientation _orientation = Orientation.Vertical;
|
||||
|
||||
public Func<IView<TItem>?> ItemTemplate { get; set; } = DefaultItemTemplate;
|
||||
public Func<IView<TItem>> ItemTemplate { get; set; } = DefaultItemTemplate;
|
||||
|
||||
public IReadOnlyList<IView<TItem>> Children => _children.AsReadOnly();
|
||||
|
||||
@@ -41,7 +41,6 @@ public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
var consumer = new OcConsumer();
|
||||
_children = observableDeclarative
|
||||
.Selecting(i => CreateItem(i))
|
||||
.OfTypeComputing<IView<TItem>>()
|
||||
.For(consumer);
|
||||
_itemsDisposables.Add(consumer);
|
||||
}
|
||||
@@ -50,16 +49,15 @@ public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
var consumer = new OcConsumer();
|
||||
_children = readOnlyObservableDeclarative
|
||||
.Selecting(i => CreateItem(i))
|
||||
.OfTypeComputing<IView<TItem>>()
|
||||
.For(consumer);
|
||||
_itemsDisposables.Add(consumer);
|
||||
}
|
||||
else if (_itemsSource is ICollection<TItem> collection)
|
||||
_children = collection.Select(CreateItem).OfType<IView<TItem>>().ToList();
|
||||
_children = collection.Select(CreateItem).ToList();
|
||||
else if (_itemsSource is TItem[] array)
|
||||
_children = array.Select(CreateItem).OfType<IView<TItem>>().ToList();
|
||||
_children = array.Select(CreateItem).ToList();
|
||||
else if (_itemsSource is IEnumerable<TItem> enumerable)
|
||||
_children = enumerable.Select(CreateItem).OfType<IView<TItem>>().ToList();
|
||||
_children = enumerable.Select(CreateItem).ToList();
|
||||
else if (value is null)
|
||||
{
|
||||
_children = new List<IView<TItem>>();
|
||||
@@ -173,14 +171,14 @@ public sealed partial class ItemsControl<TDataContext, TItem>
|
||||
return neededRerender;
|
||||
}
|
||||
|
||||
private IView<TItem>? CreateItem(TItem dataContext)
|
||||
private IView<TItem> CreateItem(TItem dataContext)
|
||||
{
|
||||
var newItem = ItemTemplate();
|
||||
AddChild(newItem, _ => dataContext);
|
||||
return newItem;
|
||||
}
|
||||
|
||||
private static IView<TItem>? DefaultItemTemplate() => null;
|
||||
private static IView<TItem> DefaultItemTemplate() => new TextBlock<TItem> {Text = typeof(TItem).ToString()};
|
||||
|
||||
public void ChildVisibilityChanged(IView child)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GeneralInputKey;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
@@ -321,6 +322,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
|
||||
|
||||
public virtual TChild AddChild<TChild>(TChild child) where TChild : IView<T>
|
||||
{
|
||||
Debug.Assert(child != null);
|
||||
child.DataContext = DataContext;
|
||||
var mapper = new DataContextMapper<T, T>(this, child, d => d);
|
||||
SetupNewChild(child, mapper);
|
||||
@@ -331,6 +333,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
|
||||
public virtual TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
|
||||
where TChild : IView<TDataContext>
|
||||
{
|
||||
Debug.Assert(child != null);
|
||||
child.DataContext = dataContextMapper(DataContext);
|
||||
var mapper = new DataContextMapper<T, TDataContext>(this, child, dataContextMapper);
|
||||
SetupNewChild(child, mapper);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
namespace TerminalUI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace TerminalUI;
|
||||
|
||||
public class EventLoop : IEventLoop
|
||||
{
|
||||
private readonly IApplicationContext _applicationContext;
|
||||
private readonly ILogger<EventLoop> _logger;
|
||||
private readonly List<Action> _permanentQueue = new();
|
||||
|
||||
public EventLoop(IApplicationContext applicationContext)
|
||||
public EventLoop(
|
||||
IApplicationContext applicationContext,
|
||||
ILogger<EventLoop> logger)
|
||||
{
|
||||
_applicationContext = applicationContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void AddToPermanentQueue(Action action) => _permanentQueue.Add(action);
|
||||
@@ -25,8 +31,15 @@ public class EventLoop : IEventLoop
|
||||
private void ProcessQueues()
|
||||
{
|
||||
foreach (var action in _permanentQueue)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error while processing action in permanent queue");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public class FocusManager : IFocusManager
|
||||
{
|
||||
private readonly IRenderEngine _renderEngine;
|
||||
private IFocusable? _focused;
|
||||
private DateTime _focusLostCandidateTime = DateTime.MinValue;
|
||||
|
||||
public IFocusable? Focused
|
||||
{
|
||||
@@ -18,15 +19,30 @@ public class FocusManager : IFocusManager
|
||||
{
|
||||
var visible = _focused.IsVisible;
|
||||
var parent = _focused.VisualParent;
|
||||
while (parent != null)
|
||||
while (parent != null && visible)
|
||||
{
|
||||
visible &= parent.IsVisible;
|
||||
visible = parent.IsVisible && visible;
|
||||
parent = parent.VisualParent;
|
||||
}
|
||||
|
||||
if (!visible)
|
||||
{
|
||||
if (_focusLostCandidateTime != DateTime.MinValue)
|
||||
{
|
||||
if (DateTime.Now - _focusLostCandidateTime > TimeSpan.FromMilliseconds(10))
|
||||
{
|
||||
_focused = null;
|
||||
_focusLostCandidateTime = DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_focusLostCandidateTime = DateTime.Now;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_focusLostCandidateTime = DateTime.MinValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,52 +69,66 @@ public class FocusManager : IFocusManager
|
||||
{
|
||||
if (keyEventArgs.Handled || Focused is null) return;
|
||||
|
||||
if (keyEventArgs.Key == Keys.Tab && keyEventArgs.SpecialKeysStatus.IsShiftPressed)
|
||||
if (keyEventArgs is {Key: Keys.Tab, SpecialKeysStatus.IsShiftPressed: true})
|
||||
{
|
||||
FocusElement(
|
||||
(views, from) => views.TakeWhile(x => x != from).Reverse(),
|
||||
c => c.Reverse()
|
||||
);
|
||||
FocusLastElement(Focused);
|
||||
keyEventArgs.Handled = true;
|
||||
}
|
||||
else if (keyEventArgs.Key == Keys.Tab)
|
||||
{
|
||||
FocusElement(
|
||||
(views, from) => views.SkipWhile(x => x != from).Skip(1),
|
||||
c => c
|
||||
);
|
||||
FocusFirstElement(Focused);
|
||||
keyEventArgs.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void FocusFirstElement(IView view, IView? from = null) =>
|
||||
FocusElement(
|
||||
view,
|
||||
(views, fromView) => views.SkipWhile(x => x != fromView).Skip(1),
|
||||
c => c.Reverse(),
|
||||
from
|
||||
);
|
||||
|
||||
public void FocusLastElement(IView view, IView? from = null) =>
|
||||
FocusElement(
|
||||
view,
|
||||
(views, fromView) => views.TakeWhile(x => x != fromView).Reverse(),
|
||||
c => c,
|
||||
from
|
||||
);
|
||||
|
||||
|
||||
private void FocusElement(
|
||||
IView view,
|
||||
Func<IEnumerable<IView>, IView, IEnumerable<IView>> fromChildSelector,
|
||||
Func<IEnumerable<IView>, IEnumerable<IView>> childSelector
|
||||
Func<IEnumerable<IView>, IEnumerable<IView>> childSelector,
|
||||
IView? from = null
|
||||
)
|
||||
{
|
||||
if (Focused is null) return;
|
||||
|
||||
var element = FindElement(Focused,
|
||||
Focused,
|
||||
fromChildSelector
|
||||
var element = FindElement(view,
|
||||
view,
|
||||
fromChildSelector,
|
||||
from: from
|
||||
);
|
||||
|
||||
if (element is null)
|
||||
{
|
||||
var topParent = FindLastFocusParent(Focused);
|
||||
var topParent = FindLastFocusParent(view);
|
||||
element = FindElement(
|
||||
topParent,
|
||||
Focused,
|
||||
view,
|
||||
fromChildSelector,
|
||||
childSelector
|
||||
childSelector,
|
||||
from
|
||||
);
|
||||
}
|
||||
|
||||
if (element is null) return;
|
||||
|
||||
_renderEngine.RequestRerender(element);
|
||||
_renderEngine.RequestRerender(Focused);
|
||||
_renderEngine.RequestRerender(view);
|
||||
Focused = element;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using GeneralInputKey;
|
||||
using TerminalUI.Controls;
|
||||
using TerminalUI.Traits;
|
||||
|
||||
namespace TerminalUI;
|
||||
@@ -9,4 +10,6 @@ public interface IFocusManager
|
||||
void UnFocus(IFocusable focusable);
|
||||
IFocusable? Focused { get; }
|
||||
void HandleKeyInput(GeneralKeyEventArgs keyEventArgs);
|
||||
void FocusFirstElement(IView view, IView? from = null);
|
||||
void FocusLastElement(IView view, IView? from = null);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ public abstract class PropertyTrackerBase<TSource, TExpressionResult> : IDisposa
|
||||
}
|
||||
else if (expression is UnaryExpression unaryExpression)
|
||||
{
|
||||
SavePropertyPath(FindReactiveProperties(unaryExpression.Operand, properties));
|
||||
return FindReactiveProperties(unaryExpression.Operand, properties);
|
||||
}
|
||||
else if (expression is ParameterExpression parameterExpression)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user