Preview refactor, Console rename form

This commit is contained in:
2023-08-14 16:42:22 +02:00
parent 2a595b2548
commit 8aa8d83598
25 changed files with 610 additions and 348 deletions

View File

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

View File

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

View File

@@ -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());
public PreviewType PreviewType => PreviewType.DoubleTextList;
[Notify] private string _text1;
[Notify] private string _text2;
public PreviewType PreviewType => PreviewType.DoubleText;
object IPreviewElement.PreviewType => PreviewType;
}

View File

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

View File

@@ -3,5 +3,6 @@
public enum PreviewType
{
DoubleText,
DoubleTextList
PreviewList,
DoubleItemNamePartList
}

View File

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

View File

@@ -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)
{

View File

@@ -83,7 +83,7 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
{
list.AddRange(_markedItems.Value!);
}
else if(_currentSelectedItem?.Value?.BaseItem?.FullName is { } selectedItemName)
else if (_currentSelectedItem?.Value?.BaseItem?.FullName is { } selectedItemName)
{
list.Add(selectedItemName);
}
@@ -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
{
@@ -460,6 +467,6 @@ public class ItemManipulationUserCommandHandlerService : UserCommandHandlerServi
_selectedTab?.ClearMarkedItems();
}
private async Task AddCommandAsync(ICommand command)
private async Task AddCommandAsync(ICommand command)
=> await _commandScheduler.AddCommand(command);
}

View File

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

View File

@@ -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);
}
@@ -74,12 +76,13 @@ public class App : IApplication
while (_applicationContext.IsRunning)
{
if (_consoleDriver.CanRead())
try
{
var key = _consoleDriver.ReadKey();
if (_appKeyService.MapKey(key.Key) is { } mappedKey)
if (_consoleDriver.CanRead())
{
var key = _consoleDriver.ReadKey();
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);
}

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

View File

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

View File

@@ -36,6 +36,7 @@ public static class Startup
{
services.TryAddSingleton<MainWindow>();
services.TryAddSingleton<CommandPalette>();
services.TryAddSingleton<Dialogs>();
return services;
}
}

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -78,7 +78,13 @@ public sealed class Binding<TDataContext, TExpressionResult, TResult> : Property
value = _fallbackValue;
}
_targetProperty.SetValue(_propertySource, value);
try
{
_targetProperty.SetValue(_propertySource, value);
}
catch
{
}
}
public override void Dispose()

View File

@@ -6,7 +6,7 @@ using TerminalUI.Traits;
namespace TerminalUI.Controls;
public sealed partial class ItemsControl<TDataContext, TItem>
public sealed partial class ItemsControl<TDataContext, TItem>
: View<ItemsControl<TDataContext, TItem>, TDataContext>, IVisibilityChangeHandler
{
private readonly List<IView> _forceRerenderChildren = new();
@@ -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)
{

View File

@@ -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);
@@ -395,7 +398,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
_disposables.Clear();
KeyHandlers.Clear();
Disposed?.Invoke(this);
}
}
@@ -411,7 +414,7 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
{
foreach (var keyHandler in KeyHandlers)
{
keyHandler((TConcrete)this, keyEventArgs);
keyHandler((TConcrete) this, keyEventArgs);
if (keyEventArgs.Handled) return;
}
}
@@ -427,6 +430,6 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
public TConcrete WithKeyHandler(Action<TConcrete, GeneralKeyEventArgs> keyHandler)
{
KeyHandlers.Add(keyHandler);
return (TConcrete)this;
return (TConcrete) this;
}
}

View File

@@ -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);
@@ -26,7 +32,14 @@ public class EventLoop : IEventLoop
{
foreach (var action in _permanentQueue)
{
action();
try
{
action();
}
catch (Exception e)
{
_logger.LogError(e, "Error while processing action in permanent queue");
}
}
}
}

View File

@@ -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)
{
_focused = null;
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;
}

View File

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

View File

@@ -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)
{