Console ItemPreview, select item after delete

This commit is contained in:
2023-08-18 10:41:30 +02:00
parent fd9a20e888
commit 1b60af389b
17 changed files with 280 additions and 61 deletions

View File

@@ -5,4 +5,6 @@ namespace FileTime.App.Core.ViewModels.ItemPreview;
public interface IElementPreviewViewModel : IItemPreviewViewModel public interface IElementPreviewViewModel : IItemPreviewViewModel
{ {
ItemPreviewMode Mode { get; } ItemPreviewMode Mode { get; }
string TextContent { get; }
string TextEncoding { get; }
} }

View File

@@ -6,20 +6,17 @@ namespace FileTime.App.Core.Services;
public class ItemPreviewService : IItemPreviewService public class ItemPreviewService : IItemPreviewService
{ {
private readonly IServiceProvider _serviceProvider;
private readonly IEnumerable<IItemPreviewProvider> _itemPreviewProviders; private readonly IEnumerable<IItemPreviewProvider> _itemPreviewProviders;
public IDeclarativeProperty<IItemPreviewViewModel?> ItemPreview { get; } public IDeclarativeProperty<IItemPreviewViewModel?> ItemPreview { get; }
public ItemPreviewService( public ItemPreviewService(
IAppState appState, IAppState appState,
IServiceProvider serviceProvider,
IEnumerable<IItemPreviewProvider> itemPreviewProviders) IEnumerable<IItemPreviewProvider> itemPreviewProviders)
{ {
_serviceProvider = serviceProvider;
_itemPreviewProviders = itemPreviewProviders; _itemPreviewProviders = itemPreviewProviders;
ItemPreview = appState ItemPreview = appState
.SelectedTab .SelectedTab
.Map(t => t.CurrentSelectedItem) .Map(t => t?.CurrentSelectedItem)
.Switch() .Switch()
.Debounce(TimeSpan.FromMilliseconds(250)) .Debounce(TimeSpan.FromMilliseconds(250))
.Map(async (item, _) => .Map(async (item, _) =>

View File

@@ -1,5 +1,6 @@
using DeclarativeProperty; using DeclarativeProperty;
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.Core.ViewModels.Timeline; using FileTime.App.Core.ViewModels.Timeline;
using FileTime.App.FrequencyNavigation.ViewModels; using FileTime.App.FrequencyNavigation.ViewModels;
@@ -20,5 +21,6 @@ public interface IRootViewModel
ITimelineViewModel TimelineViewModel { get; } ITimelineViewModel TimelineViewModel { get; }
IDeclarativeProperty<VolumeSizeInfo?> VolumeSizeInfo { get; } IDeclarativeProperty<VolumeSizeInfo?> VolumeSizeInfo { get; }
IFrequencyNavigationViewModel FrequencyNavigation { get; } IFrequencyNavigationViewModel FrequencyNavigation { get; }
IItemPreviewService ItemPreviewService { get; }
event Action<IInputElement>? FocusReadInputElement; event Action<IInputElement>? FocusReadInputElement;
} }

View File

@@ -117,6 +117,8 @@ public class App : IApplication
Thread.Sleep(10); Thread.Sleep(10);
} }
Task.Run(async () => await _lifecycleService.ExitAsync()).Wait();
} }
private void Render() => _applicationContext.RenderEngine.Run(); private void Render() => _applicationContext.RenderEngine.Run();

View File

@@ -0,0 +1,110 @@
using FileTime.App.Core.Models;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels.ItemPreview;
using FileTime.ConsoleUI.App.Styling;
using TerminalUI.Controls;
using TerminalUI.Extensions;
using TerminalUI.Models;
using TerminalUI.ViewExtensions;
namespace FileTime.ConsoleUI.App.Controls;
public class ItemPreviews
{
private readonly ITheme _theme;
public ItemPreviews(ITheme theme)
{
_theme = theme;
}
public IView<IRootViewModel> View()
{
var view = new Grid<IRootViewModel>()
{
ChildInitializer =
{
new TextBlock<IRootViewModel>
{
TextAlignment = TextAlignment.Center,
Text = "Empty",
Foreground = _theme.ErrorForegroundColor,
}.Setup(t => t.Bind(
t,
dc => dc.AppState.SelectedTab.Value.SelectedsChildren.Value.Count == 0,
t => t.IsVisible,
fallbackValue: false)),
ElementPreviews()
.WithDataContextBinding<IRootViewModel, IElementPreviewViewModel>(
dc => (IElementPreviewViewModel)dc.ItemPreviewService.ItemPreview.Value
)
}
};
return view;
}
private IView<IElementPreviewViewModel> ElementPreviews()
{
var view = new Grid<IElementPreviewViewModel>
{
ChildInitializer =
{
new TextBlock<IElementPreviewViewModel>
{
TextAlignment = TextAlignment.Center,
Text = "Don't know how to preview this item.",
}.Setup(t => t.Bind(
t,
dc => dc.Mode == ItemPreviewMode.Unknown,
t => t.IsVisible,
v => v,
fallbackValue: false)),
new TextBlock<IElementPreviewViewModel>
{
TextAlignment = TextAlignment.Center,
Text = "Empty",
}.Setup(t => t.Bind(
t,
dc => dc.Mode == ItemPreviewMode.Empty,
t => t.IsVisible,
v => v,
fallbackValue: false)),
new Grid<IElementPreviewViewModel>
{
RowDefinitionsObject = "* Auto",
ChildInitializer =
{
new TextBlock<IElementPreviewViewModel>()
.Setup(t => t.Bind(
t,
dc => dc.TextContent,
t => t.Text)),
new TextBlock<IElementPreviewViewModel>
{
Extensions = {new GridPositionExtension(0, 1)}
}.Setup(t => t.Bind(
t,
dc => dc.TextEncoding,
t => t.Text,
v => $"Encoding: {v}"))
}
}.Setup(t => t.Bind(
t,
dc => dc.Mode == ItemPreviewMode.Text,
t => t.IsVisible,
v => v,
fallbackValue: false)),
}
};
view.Bind(
view,
dc => dc.Name == ElementPreviewViewModel.PreviewName,
v => v.IsVisible,
v => v
);
return view;
}
}

View File

@@ -34,6 +34,7 @@ public class MainWindow
private readonly FrequencyNavigation _frequencyNavigation; private readonly FrequencyNavigation _frequencyNavigation;
private readonly Dialogs _dialogs; private readonly Dialogs _dialogs;
private readonly Timeline _timeline; private readonly Timeline _timeline;
private readonly ItemPreviews _itemPreviews;
private readonly Lazy<IView> _root; private readonly Lazy<IView> _root;
@@ -44,7 +45,8 @@ public class MainWindow
CommandPalette commandPalette, CommandPalette commandPalette,
FrequencyNavigation frequencyNavigation, FrequencyNavigation frequencyNavigation,
Dialogs dialogs, Dialogs dialogs,
Timeline timeline) Timeline timeline,
ItemPreviews itemPreviews)
{ {
_rootViewModel = rootViewModel; _rootViewModel = rootViewModel;
_applicationContext = applicationContext; _applicationContext = applicationContext;
@@ -53,6 +55,7 @@ public class MainWindow
_frequencyNavigation = frequencyNavigation; _frequencyNavigation = frequencyNavigation;
_dialogs = dialogs; _dialogs = dialogs;
_timeline = timeline; _timeline = timeline;
_itemPreviews = itemPreviews;
_root = new Lazy<IView>(Initialize); _root = new Lazy<IView>(Initialize);
} }
@@ -141,7 +144,18 @@ public class MainWindow
{ {
ParentsItemsView().WithExtension(new GridPositionExtension(0, 0)), ParentsItemsView().WithExtension(new GridPositionExtension(0, 0)),
SelectedItemsView().WithExtension(new GridPositionExtension(1, 0)), SelectedItemsView().WithExtension(new GridPositionExtension(1, 0)),
SelectedsItemsView().WithExtension(new GridPositionExtension(2, 0)), new Grid<IRootViewModel>
{
Extensions =
{
new GridPositionExtension(2, 0)
},
ChildInitializer =
{
SelectedsItemsView(),
_itemPreviews.View()
}
}
} }
}, },
new ItemsControl<IRootViewModel, string> new ItemsControl<IRootViewModel, string>
@@ -347,15 +361,21 @@ public class MainWindow
{ {
var list = new ListView<IRootViewModel, IItemViewModel> var list = new ListView<IRootViewModel, IItemViewModel>
{ {
ListPadding = 8 ListPadding = 8,
ItemTemplate = item => ItemItemTemplate(item, new ItemViewRenderOptions())
}; };
list.ItemTemplate = item => ItemItemTemplate(item, new ItemViewRenderOptions());
list.Bind( list.Bind(
list, list,
root => root.AppState.SelectedTab.Value.SelectedsChildren.Value, dc => dc.AppState.SelectedTab.Value.SelectedsChildren.Value.Count > 0,
v => v.ItemsSource); l => l.IsVisible,
fallbackValue: false);
list.Bind(
list,
dc => dc.AppState.SelectedTab.Value.SelectedsChildren.Value,
v => v.ItemsSource,
fallbackValue: null);
return list; return list;
} }
@@ -364,14 +384,13 @@ public class MainWindow
{ {
var list = new ListView<IRootViewModel, IItemViewModel> var list = new ListView<IRootViewModel, IItemViewModel>
{ {
ListPadding = 8 ListPadding = 8,
ItemTemplate = item => ItemItemTemplate(item, new ItemViewRenderOptions())
}; };
list.ItemTemplate = item => ItemItemTemplate(item, new ItemViewRenderOptions());
list.Bind( list.Bind(
list, list,
root => root.AppState.SelectedTab.Value.ParentsChildren.Value, dc => dc.AppState.SelectedTab.Value.ParentsChildren.Value,
v => v.ItemsSource); v => v.ItemsSource);
return list; return list;

View File

@@ -1,5 +1,6 @@
using DeclarativeProperty; using DeclarativeProperty;
using FileTime.App.CommandPalette.ViewModels; using FileTime.App.CommandPalette.ViewModels;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using FileTime.App.Core.ViewModels.Timeline; using FileTime.App.Core.ViewModels.Timeline;
using FileTime.App.FrequencyNavigation.ViewModels; using FileTime.App.FrequencyNavigation.ViewModels;
@@ -17,6 +18,7 @@ public class RootViewModel : IRootViewModel
public IConsoleAppState AppState { get; } public IConsoleAppState AppState { get; }
public ICommandPaletteViewModel CommandPalette { get; } public ICommandPaletteViewModel CommandPalette { get; }
public IFrequencyNavigationViewModel FrequencyNavigation { get; } public IFrequencyNavigationViewModel FrequencyNavigation { get; }
public IItemPreviewService ItemPreviewService { get; }
public IDialogService DialogService { get; } public IDialogService DialogService { get; }
public ITimelineViewModel TimelineViewModel { get; } public ITimelineViewModel TimelineViewModel { get; }
public IDeclarativeProperty<VolumeSizeInfo?> VolumeSizeInfo { get;} public IDeclarativeProperty<VolumeSizeInfo?> VolumeSizeInfo { get;}
@@ -29,7 +31,8 @@ public class RootViewModel : IRootViewModel
ICommandPaletteViewModel commandPalette, ICommandPaletteViewModel commandPalette,
IDialogService dialogService, IDialogService dialogService,
ITimelineViewModel timelineViewModel, ITimelineViewModel timelineViewModel,
IFrequencyNavigationViewModel frequencyNavigation) IFrequencyNavigationViewModel frequencyNavigation,
IItemPreviewService itemPreviewService)
{ {
AppState = appState; AppState = appState;
PossibleCommands = possibleCommands; PossibleCommands = possibleCommands;
@@ -37,6 +40,7 @@ public class RootViewModel : IRootViewModel
DialogService = dialogService; DialogService = dialogService;
TimelineViewModel = timelineViewModel; TimelineViewModel = timelineViewModel;
FrequencyNavigation = frequencyNavigation; FrequencyNavigation = frequencyNavigation;
ItemPreviewService = itemPreviewService;
DialogService.ReadInput.PropertyChanged += (o, e) => DialogService.ReadInput.PropertyChanged += (o, e) =>
{ {

View File

@@ -45,6 +45,7 @@ public static class Startup
services.TryAddSingleton<Dialogs>(); services.TryAddSingleton<Dialogs>();
services.TryAddSingleton<Timeline>(); services.TryAddSingleton<Timeline>();
services.TryAddSingleton<FrequencyNavigation>(); services.TryAddSingleton<FrequencyNavigation>();
services.TryAddSingleton<ItemPreviews>();
return services; return services;
} }
} }

View File

@@ -1,5 +1,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Linq.Expressions;
using CircularBuffer; using CircularBuffer;
using DeclarativeProperty; using DeclarativeProperty;
using DynamicData; using DynamicData;
@@ -14,6 +15,8 @@ namespace FileTime.Core.Services;
public class Tab : ITab public class Tab : ITab
{ {
private record LastItemSelectingContext(IContainer? CurrentLocationValue);
private readonly ITimelessContentProvider _timelessContentProvider; private readonly ITimelessContentProvider _timelessContentProvider;
private readonly ITabEvents _tabEvents; private readonly ITabEvents _tabEvents;
private readonly DeclarativeProperty<IContainer?> _currentLocation = new(); private readonly DeclarativeProperty<IContainer?> _currentLocation = new();
@@ -22,9 +25,11 @@ public class Tab : ITab
private readonly ObservableCollection<ItemFilter> _itemFilters = new(); private readonly ObservableCollection<ItemFilter> _itemFilters = new();
private readonly CircularBuffer<FullName> _history = new(20); private readonly CircularBuffer<FullName> _history = new(20);
private readonly CircularBuffer<FullName> _future = new(20); private readonly CircularBuffer<FullName> _future = new(20);
private readonly List<AbsolutePath> _selectedItemCandidates = new();
private AbsolutePath? _currentSelectedItemCached; private AbsolutePath? _currentSelectedItemCached;
private PointInTime _currentPointInTime; private PointInTime _currentPointInTime;
private CancellationTokenSource? _setCurrentLocationCancellationTokenSource; private CancellationTokenSource? _setCurrentLocationCancellationTokenSource;
private LastItemSelectingContext? _lastItemSelectingContext;
public IDeclarativeProperty<IContainer?> CurrentLocation { get; } public IDeclarativeProperty<IContainer?> CurrentLocation { get; }
public IDeclarativeProperty<ObservableCollection<IItem>?> CurrentItems { get; } public IDeclarativeProperty<ObservableCollection<IItem>?> CurrentItems { get; }
@@ -81,47 +86,32 @@ public class Tab : ITab
return Task.FromResult(items); return Task.FromResult(items);
} }
).CombineLatest( ).CombineLatest(
Ordering, Ordering.Map(ordering =>
{
var (itemComparer, order) = ordering switch
{
ItemOrdering.Name => ((Expression<Func<IItem, IComparable>>) (i => i.DisplayName), ListSortDirection.Ascending),
ItemOrdering.NameDesc => (i => i.DisplayName, ListSortDirection.Descending),
ItemOrdering.CreationDate => (i => i.CreatedAt ?? DateTime.MinValue, ListSortDirection.Ascending),
ItemOrdering.CreationDateDesc => (i => i.CreatedAt ?? DateTime.MinValue, ListSortDirection.Descending),
ItemOrdering.LastModifyDate => (i => i.ModifiedAt ?? DateTime.MinValue, ListSortDirection.Ascending),
ItemOrdering.LastModifyDateDesc => (i => i.ModifiedAt ?? DateTime.MinValue, ListSortDirection.Descending),
ItemOrdering.Size => (i => GetSize(i), ListSortDirection.Ascending),
ItemOrdering.SizeDesc => (i => GetSize(i), ListSortDirection.Descending),
_ => throw new NotImplementedException()
};
return (itemComparer, order);
}),
(items, ordering) => (items, ordering) =>
{ {
if (items is null) return Task.FromResult<ObservableCollection<IItem>?>(null); if (items is null) return Task.FromResult<ObservableCollection<IItem>?>(null);
ObservableCollection<IItem>? orderedItems = ordering switch var (itemComparer, order) = ordering;
{
ItemOrdering.Name => ObservableCollection<IItem>? orderedItems = items
items .Ordering(i => i.Type)
.Ordering(i => i.Type) .ThenOrdering(itemComparer, order);
.ThenOrdering(i => i.DisplayName),
ItemOrdering.NameDesc =>
items
.Ordering(i => i.Type)
.ThenOrdering(i => i.DisplayName, ListSortDirection.Descending),
ItemOrdering.CreationDate =>
items
.Ordering(i => i.Type)
.ThenOrdering(i => i.CreatedAt),
ItemOrdering.CreationDateDesc =>
items
.Ordering(i => i.Type)
.ThenOrdering(i => i.CreatedAt, ListSortDirection.Descending),
ItemOrdering.LastModifyDate =>
items
.Ordering(i => i.Type)
.ThenOrdering(i => i.ModifiedAt),
ItemOrdering.LastModifyDateDesc =>
items
.Ordering(i => i.Type)
.ThenOrdering(i => i.ModifiedAt, ListSortDirection.Descending),
ItemOrdering.Size =>
items
.Ordering(i => i.Type)
.ThenOrdering(i => GetSize(i)),
ItemOrdering.SizeDesc =>
items
.Ordering(i => i.Type)
.ThenOrdering(i => GetSize(i), ListSortDirection.Descending),
_ => throw new NotImplementedException()
};
return Task.FromResult(orderedItems); return Task.FromResult(orderedItems);
} }
@@ -132,8 +122,25 @@ public class Tab : ITab
_currentRequestItem.DistinctUntilChanged(), _currentRequestItem.DistinctUntilChanged(),
(items, selected) => (items, selected) =>
{ {
if (selected != null && (items?.Any(i => i.FullName == selected.Path) ?? true)) return Task.FromResult<AbsolutePath?>(selected); var itemSelectingContext = new LastItemSelectingContext(CurrentLocation.Value);
var lastItemSelectingContext = _lastItemSelectingContext;
_lastItemSelectingContext = itemSelectingContext;
if (items == null || items.Count == 0) return Task.FromResult<AbsolutePath?>(null); if (items == null || items.Count == 0) return Task.FromResult<AbsolutePath?>(null);
if (selected != null)
{
if (items.Any(i => i.FullName == selected.Path))
return Task.FromResult<AbsolutePath?>(selected);
}
if (lastItemSelectingContext != null
&& itemSelectingContext == lastItemSelectingContext)
{
var candidate = _selectedItemCandidates.FirstOrDefault(c => items.Any(i => i.FullName?.Path == c.Path.Path));
if (candidate != null)
{
return Task.FromResult(candidate);
}
}
return Task.FromResult(GetSelectedItemByItems(items)); return Task.FromResult(GetSelectedItemByItems(items));
}).DistinctUntilChanged(); }).DistinctUntilChanged();
@@ -141,8 +148,31 @@ public class Tab : ITab
CurrentSelectedItem.Subscribe(async (s, _) => CurrentSelectedItem.Subscribe(async (s, _) =>
{ {
_currentSelectedItemCached = s; _currentSelectedItemCached = s;
await _currentRequestItem.SetValue(s); await _currentRequestItem.SetValue(s);
}); });
DeclarativePropertyHelpers.CombineLatest(
CurrentItems,
CurrentSelectedItem,
(items, selected) =>
{
if(items is null || selected is null) return Task.FromResult<IEnumerable<AbsolutePath>?>(null);
var primaryCandidates = items.SkipWhile(i => i.FullName is {Path: var p} && p != selected.Path.Path).Skip(1);
var secondaryCandidates = items.TakeWhile(i => i.FullName is {Path: var p} && p != selected.Path.Path).Reverse();
var candidates = primaryCandidates
.Concat(secondaryCandidates)
.Select(c => new AbsolutePath(_timelessContentProvider, c));
return Task.FromResult(candidates);
})
.Subscribe(candidates =>
{
if(candidates is null) return;
_selectedItemCandidates.Clear();
_selectedItemCandidates.AddRange(candidates);
});
} }
private static long GetSize(IItem item) private static long GetSize(IItem item)

View File

@@ -27,6 +27,7 @@
xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions" xmlns:ia="clr-namespace:Avalonia.Xaml.Interactions.Core;assembly=Avalonia.Xaml.Interactions"
xmlns:interactions="using:FileTime.Core.Interactions" xmlns:interactions="using:FileTime.Core.Interactions"
xmlns:itemPreview="clr-namespace:FileTime.App.Core.ViewModels.ItemPreview;assembly=FileTime.App.Core" xmlns:itemPreview="clr-namespace:FileTime.App.Core.ViewModels.ItemPreview;assembly=FileTime.App.Core"
xmlns:itemPreviewInterface="clr-namespace:FileTime.App.Core.ViewModels.ItemPreview;assembly=FileTime.App.Core.Abstraction"
xmlns:local="using:FileTime.GuiApp.App.Views" xmlns:local="using:FileTime.GuiApp.App.Views"
xmlns:local1="clr-namespace:FileTime.Providers.Local;assembly=FileTime.Providers.Local.Abstractions" xmlns:local1="clr-namespace:FileTime.Providers.Local;assembly=FileTime.Providers.Local.Abstractions"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -557,7 +558,7 @@
</TextBlock> </TextBlock>
</Grid> </Grid>
<Grid IsVisible="{Binding AppState.SelectedTab.Value.SelectedsChildren.Value, Converter={x:Static ObjectConverters.IsNull}, ConverterParameter=0, FallbackValue=False}" RowDefinitions="Auto, Auto"> <Grid IsVisible="{Binding AppState.SelectedTab.Value.CurrentSelectedItem.Value.BaseItem.Exceptions.Count, Converter={StaticResource GreaterThanConverter}, ConverterParameter=0, FallbackValue=False}" RowDefinitions="Auto, Auto">
<TextBlock <TextBlock
Foreground="{DynamicResource ErrorBrush}" Foreground="{DynamicResource ErrorBrush}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@@ -579,7 +580,7 @@
</Grid> </Grid>
<Grid DataContext="{Binding ItemPreviewService.ItemPreview.Value}" IsVisible="{Binding FallbackValue=false, Converter={x:Static ObjectConverters.IsNotNull}}"> <Grid DataContext="{Binding ItemPreviewService.ItemPreview.Value}" IsVisible="{Binding FallbackValue=false, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid IsVisible="{Binding Name, FallbackValue=false, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static itemPreview:ElementPreviewViewModel.PreviewName}}"> <Grid IsVisible="{Binding Name, FallbackValue=false, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static itemPreview:ElementPreviewViewModel.PreviewName}}">
<Grid x:DataType="itemPreview:ElementPreviewViewModel"> <Grid x:DataType="itemPreviewInterface:IElementPreviewViewModel">
<TextBlock <TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"
IsVisible="{Binding Mode, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static appCoreModels:ItemPreviewMode.Unknown}, FallbackValue={x:Static appCoreModels:ItemPreviewMode.Unknown}}" IsVisible="{Binding Mode, Converter={StaticResource EqualsConverter}, ConverterParameter={x:Static appCoreModels:ItemPreviewMode.Unknown}, FallbackValue={x:Static appCoreModels:ItemPreviewMode.Unknown}}"

View File

@@ -44,6 +44,12 @@ public abstract class ChildCollectionView<TConcrete, T>
}; };
} }
public override void AddChild(IView child)
{
base.AddChild(child);
_children.Add(child);
}
public override TChild AddChild<TChild>(TChild child) public override TChild AddChild<TChild>(TChild child)
{ {
child = base.AddChild(child); child = base.AddChild(child);

View File

@@ -1,8 +1,11 @@
using System.Collections; using System.Collections;
using System.Linq.Expressions;
using TerminalUI.Extensions;
namespace TerminalUI.Controls; namespace TerminalUI.Controls;
public record ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext>(IView<TTargetDataContext> Child, Func<TSourceDataContext?, TTargetDataContext?> DataContextMapper); public record ChildWithDataContextMapper<TSourceDataContext, TTargetDataContext>(IView<TTargetDataContext> Child, Func<TSourceDataContext?, TTargetDataContext?> DataContextMapper);
public record ChildWithDataContextBinding<TSourceDataContext, TTargetDataContext>(IView<TTargetDataContext> Child, Expression<Func<TSourceDataContext?, TTargetDataContext?>> DataContextExpression);
public class ChildInitializer<T> : IEnumerable<IView> public class ChildInitializer<T> : IEnumerable<IView>
{ {
@@ -18,6 +21,16 @@ public class ChildInitializer<T> : IEnumerable<IView>
public void Add<TDataContext>(ChildWithDataContextMapper<T, TDataContext> item) public void Add<TDataContext>(ChildWithDataContextMapper<T, TDataContext> item)
=> _childContainer.AddChild(item.Child, item.DataContextMapper); => _childContainer.AddChild(item.Child, item.DataContextMapper);
public void Add<TDataContext>(ChildWithDataContextBinding<T, TDataContext> item)
{
item.Child.Bind(
_childContainer,
item.DataContextExpression,
c => c.DataContext
);
_childContainer.AddChild(item.Child);
}
public IEnumerator<IView> GetEnumerator() => _childContainer.Children.GetEnumerator(); public IEnumerator<IView> GetEnumerator() => _childContainer.Children.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

View File

@@ -55,6 +55,7 @@ public interface IView<T> : IView
TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper) TChild CreateChild<TChild, TDataContext>(Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext>, new(); where TChild : IView<TDataContext>, new();
void AddChild(IView child);
TChild AddChild<TChild>(TChild child) where TChild : IView<T>; TChild AddChild<TChild>(TChild child) where TChild : IView<T>;
TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper) TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)

View File

@@ -26,12 +26,14 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
[Notify] private string? _text = string.Empty; [Notify] private string? _text = string.Empty;
[Notify] private TextAlignment _textAlignment = TextAlignment.Left; [Notify] private TextAlignment _textAlignment = TextAlignment.Left;
[Notify] private ITextFormat? _textFormat; [Notify] private ITextFormat? _textFormat;
[Notify] private int _textStartIndex;
public TextBlock() public TextBlock()
{ {
RerenderProperties.Add(nameof(Text)); RerenderProperties.Add(nameof(Text));
RerenderProperties.Add(nameof(TextAlignment)); RerenderProperties.Add(nameof(TextAlignment));
RerenderProperties.Add(nameof(TextFormat)); RerenderProperties.Add(nameof(TextFormat));
RerenderProperties.Add(nameof(TextStartIndex));
((INotifyPropertyChanged) this).PropertyChanged += (o, e) => ((INotifyPropertyChanged) this).PropertyChanged += (o, e) =>
{ {
@@ -78,7 +80,17 @@ public sealed partial class TextBlock<T> : View<TextBlock<T>, T>, IDisplayView
SetStyleColor(renderContext, foreground, background, _textFormat); SetStyleColor(renderContext, foreground, background, _textFormat);
RenderText(_textLines, renderContext, position, size, skipRender, TransformText); var textLines = _textLines;
if (_textStartIndex < _textLines.Length)
{
textLines = _textLines[_textStartIndex..];
}
else
{
_textStartIndex = _textLines.Length - size.Height;
}
RenderText(textLines, renderContext, position, size, skipRender, TransformText);
return !skipRender; return !skipRender;
} }

View File

@@ -376,6 +376,12 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
return child; return child;
} }
public virtual void AddChild(IView child)
{
Debug.Assert(child != null);
SetupNewChild(child, null);
}
public virtual TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper) public virtual TChild AddChild<TChild, TDataContext>(TChild child, Func<T?, TDataContext?> dataContextMapper)
where TChild : IView<TDataContext> where TChild : IView<TDataContext>
{ {
@@ -387,15 +393,18 @@ public abstract partial class View<TConcrete, T> : IView<T> where TConcrete : Vi
return child; return child;
} }
private void SetupNewChild(IView child, IDisposable dataContextmapper) private void SetupNewChild(IView child, IDisposable? dataContextMapper)
{ {
child.ApplicationContext = ApplicationContext; child.ApplicationContext = ApplicationContext;
child.Attached = Attached; child.Attached = Attached;
child.VisualParent = this; child.VisualParent = this;
VisualChildren.Add(child); VisualChildren.Add(child);
AddDisposable(dataContextmapper); if (dataContextMapper is not null)
child.AddDisposable(dataContextmapper); {
AddDisposable(dataContextMapper);
child.AddDisposable(dataContextMapper);
}
} }
public virtual void RemoveChild<TDataContext>(IView<TDataContext> child) public virtual void RemoveChild<TDataContext>(IView<TDataContext> child)

View File

@@ -1,4 +1,5 @@
using System.Linq.Expressions; using System.Collections;
using System.Linq.Expressions;
namespace TerminalUI.ExpressionTrackers; namespace TerminalUI.ExpressionTrackers;
@@ -26,6 +27,10 @@ public class BinaryTracker : ExpressionTrackerBase
{ {
ExpressionType.Equal => (v1, v2) => Equals(v1, v2), ExpressionType.Equal => (v1, v2) => Equals(v1, v2),
ExpressionType.NotEqual => (v1, v2) => !Equals(v1, v2), ExpressionType.NotEqual => (v1, v2) => !Equals(v1, v2),
ExpressionType.GreaterThan => (v1, v2) => Comparer.Default.Compare(v1, v2) > 0,
ExpressionType.GreaterThanOrEqual => (v1, v2) => Comparer.Default.Compare(v1, v2) >= 0,
ExpressionType.LessThan => (v1, v2) => Comparer.Default.Compare(v1, v2) < 0,
ExpressionType.LessThanOrEqual => (v1, v2) => Comparer.Default.Compare(v1, v2) <= 0,
_ => throw new NotImplementedException() _ => throw new NotImplementedException()
}; };

View File

@@ -19,6 +19,11 @@ public static class ViewExtensions
Func<TSourceDataContext?, TTargetDataContext?> dataContextMapper) Func<TSourceDataContext?, TTargetDataContext?> dataContextMapper)
=> new(view, dataContextMapper); => new(view, dataContextMapper);
public static ChildWithDataContextBinding<TSourceDataContext, TTargetDataContext> WithDataContextBinding<TSourceDataContext, TTargetDataContext>(
this IView<TTargetDataContext> view,
Expression<Func<TSourceDataContext?, TTargetDataContext?>> dataContextMapper)
=> new(view, dataContextMapper);
public static TView Setup<TView>(this TView view, Action<TView> action) public static TView Setup<TView>(this TView view, Action<TView> action)
{ {
action(view); action(view);