diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/GoBackCommand.cs b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/GoBackCommand.cs new file mode 100644 index 0000000..192cef5 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/GoBackCommand.cs @@ -0,0 +1,15 @@ +namespace FileTime.App.Core.UserCommand; + +public class GoBackCommand : IIdentifiableUserCommand +{ + public const string CommandName = "go_back"; + + public static GoBackCommand Instance { get; } = new(); + + private GoBackCommand() + { + } + + public string UserCommandID => CommandName; + public string Title => "Go back"; +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/GoForwardCommand.cs b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/GoForwardCommand.cs new file mode 100644 index 0000000..1f2c806 --- /dev/null +++ b/src/AppCommon/FileTime.App.Core.Abstraction/UserCommand/GoForwardCommand.cs @@ -0,0 +1,15 @@ +namespace FileTime.App.Core.UserCommand; + +public class GoForwardCommand : IIdentifiableUserCommand +{ + public const string CommandName = "go_forward"; + + public static GoForwardCommand Instance { get; } = new(); + + private GoForwardCommand() + { + } + + public string UserCommandID => CommandName; + public string Title => "Go forward"; +} \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs index 9af5822..b334c7e 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/ViewModels/IAppState.cs @@ -12,6 +12,7 @@ public interface IAppState IObservable SearchText { get; } IDeclarativeProperty ViewMode { get; } DeclarativeProperty RapidTravelText { get; } + IDeclarativeProperty RapidTravelTextDebounced { get; } ITimelineViewModel TimelineViewModel { get; } IDeclarativeProperty ContainerStatus { get; } diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs index d14458d..6626074 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/NavigationUserCommandHandlerService.cs @@ -68,7 +68,9 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase new TypeUserCommandHandler(CloseTab), new TypeUserCommandHandler(EnterRapidTravel), new TypeUserCommandHandler(ExitRapidTravel), + new TypeUserCommandHandler(GoBack), new TypeUserCommandHandler(GoByFrequency), + new TypeUserCommandHandler(GoForward), new TypeUserCommandHandler(GoToHome), new TypeUserCommandHandler(GoToPath), new TypeUserCommandHandler(GoToProvider), @@ -89,6 +91,20 @@ public class NavigationUserCommandHandlerService : UserCommandHandlerServiceBase }); } + private async Task GoBack() + { + if (_selectedTab?.Tab is null) return; + + await _selectedTab.Tab.GoBackAsync(); + } + + private async Task GoForward() + { + if (_selectedTab?.Tab is null) return; + + await _selectedTab.Tab.GoForwardAsync(); + } + private async Task RunOrOpen(RunOrOpenCommand command) { var item = command.Item ?? _currentSelectedItem?.Value; diff --git a/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs b/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs index bddf33e..9aa7464 100644 --- a/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs +++ b/src/AppCommon/FileTime.App.Core/StartupServices/DefaultIdentifiableCommandHandlerRegister.cs @@ -22,7 +22,9 @@ public class DefaultIdentifiableCommandHandlerRegister : IStartupHandler AddUserCommand(DeleteCommand.SoftDelete); AddUserCommand(EnterRapidTravelCommand.Instance); AddUserCommand(ExitRapidTravelCommand.Instance); + AddUserCommand(GoBackCommand.Instance); AddUserCommand(GoByFrequencyCommand.Instance); + AddUserCommand(GoForwardCommand.Instance); AddUserCommand(GoToHomeCommand.Instance); AddUserCommand(GoToPathCommand.Instance); AddUserCommand(GoToProviderCommand.Instance); diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs index 45c0b96..e8ab5c2 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/AppStateBase.cs @@ -26,12 +26,21 @@ public abstract partial class AppStateBase : IAppState public IDeclarativeProperty SelectedTab { get; private set; } public DeclarativeProperty RapidTravelText { get; private set; } + public IDeclarativeProperty RapidTravelTextDebounced { get; private set; } public IDeclarativeProperty ContainerStatus { get; private set; } partial void OnInitialize() { RapidTravelText = new(""); + RapidTravelTextDebounced = RapidTravelText + .Debounce(v => + string.IsNullOrEmpty(v) + ? TimeSpan.Zero + : TimeSpan.FromMilliseconds(200) + , resetTimer: true + ); + ViewMode = _viewMode; SearchText = _searchText.AsObservable(); diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs index b772f99..4e994c4 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/ItemViewModel.cs @@ -53,7 +53,7 @@ public abstract partial class ItemViewModel : IItemViewModel var displayName = itemViewModelType switch { - ItemViewModelType.Main => _appState.RapidTravelText.Map(async (s, _) => + ItemViewModelType.Main => _appState.RapidTravelTextDebounced.Map(async (s, _) => _appState.ViewMode.Value != Models.Enums.ViewMode.RapidTravel && _appState.SelectedTab.Value?.CurrentLocation.Value?.Provider is IItemNameConverterProvider nameConverterProvider ? (IReadOnlyList) await nameConverterProvider.GetItemNamePartsAsync(item) diff --git a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs index 21859df..82cbf7f 100644 --- a/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs +++ b/src/AppCommon/FileTime.App.Core/ViewModels/TabViewModel.cs @@ -138,7 +138,7 @@ public partial class TabViewModel : ITabViewModel CurrentSelectedItemAsContainer = CurrentSelectedItem.Map(i => i as IContainerViewModel); SelectedsChildren = CurrentSelectedItem - .Debounce(() => _refreshSmoothnessCalculator.RefreshDelay, resetTimer: true) + .Debounce(_ => _refreshSmoothnessCalculator.RefreshDelay, resetTimer: true) .DistinctUntilChanged() .Map(item => { diff --git a/src/Core/FileTime.Core.Abstraction/Collections/CircularBuffer.cs b/src/Core/FileTime.Core.Abstraction/Collections/CircularBuffer.cs new file mode 100644 index 0000000..db73b53 --- /dev/null +++ b/src/Core/FileTime.Core.Abstraction/Collections/CircularBuffer.cs @@ -0,0 +1,809 @@ +// https://github.com/ASolomatin/CircularBuffer-CSharp/blob/master/CircularBuffer/ + +using System.Collections; + +namespace CircularBuffer; +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) + +/// +/// Reference struct that contains two read only spans of type T +/// +public ref struct SpanTuple +{ + /// + /// First span + /// + public ReadOnlySpan A { get; } + + /// + /// Second span + /// + public ReadOnlySpan B { get; } + + /// + /// Constructor + /// + /// First span + /// Second span + public SpanTuple(ReadOnlySpan a, ReadOnlySpan b) + { + A = a; + B = b; + } + + /// + /// Deconstructs the current + /// + /// First span target + /// Second span target + public void Deconstruct(out ReadOnlySpan a, out ReadOnlySpan b) + { + a = A; + b = B; + } +} + +#endif + + +/// +/// Circular buffer. +/// +/// When writing to a full buffer: +/// PushBack -> removes this[0] / Front() +/// PushFront -> removes this[Size-1] / Back() +/// +/// this implementation is inspired by +/// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html +/// because I liked their interface. +/// +public interface ICircularBuffer : IReadOnlyCollection +{ + /// + /// Index access to elements in buffer. + /// Index does not loop around like when adding elements, + /// valid interval is [0;Size[ + /// + /// Index of element to access. + /// Thrown when index is outside of [; Size[ interval. + T this[int index] { get; set; } + + /// + /// Maximum capacity of the buffer. Elements pushed into the buffer after + /// maximum capacity is reached (IsFull = true), will remove an element. + /// + int Capacity { get; } + + /// + /// Boolean indicating if Circular is at full capacity. + /// Adding more elements when the buffer is full will + /// cause elements to be removed from the other end + /// of the buffer. + /// + bool IsFull { get; } + + /// + /// True if has no elements. + /// + bool IsEmpty { get; } + + /// + /// Current buffer size (the number of elements that the buffer has). + /// + [Obsolete("Use Count property instead")] + int Size { get; } + + /// + /// Element at the back of the buffer - this[Size - 1]. + /// + /// The value of the element of type T at the back of the buffer. + [Obsolete("Use Last() method instead")] + T Back(); + + /// + /// Element at the front of the buffer - this[0]. + /// + /// The value of the element of type T at the front of the buffer. + [Obsolete("Use First() method instead")] + T Front(); + + /// + /// Element at the back of the buffer - this[Size - 1]. + /// + /// The value of the element of type T at the back of the buffer. + T First(); + + /// + /// Element at the front of the buffer - this[0]. + /// + /// The value of the element of type T at the front of the buffer. + T Last(); + + /// + /// Clears the contents of the array. Size = 0, Capacity is unchanged. + /// + /// + void Clear(); + + /// + /// Removes the element at the back of the buffer. Decreasing the + /// Buffer size by 1. + /// + T PopBack(); + + /// + /// Removes the element at the front of the buffer. Decreasing the + /// Buffer size by 1. + /// + T PopFront(); + + /// + /// Pushes a new element to the back of the buffer. Back()/this[Size-1] + /// will now return this element. + /// + /// When the buffer is full, the element at Front()/this[0] will be + /// popped to allow for this new element to fit. + /// + /// Item to push to the back of the buffer + void PushBack(T item); + + /// + /// Pushes a new element to the front of the buffer. Front()/this[0] + /// will now return this element. + /// + /// When the buffer is full, the element at Back()/this[Size-1] will be + /// popped to allow for this new element to fit. + /// + /// Item to push to the front of the buffer + void PushFront(T item); + + /// + /// Copies the buffer contents to an array, according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) + /// + /// A new array with a copy of the buffer contents. + T[] ToArray(); + + /// + /// Copies the buffer contents to the array, according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) + /// + /// The array that is the destination of the elements copied from the current buffer. + void CopyTo(T[] array); + + /// + /// Copies the buffer contents to the array, according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) + /// + /// The array that is the destination of the elements copied from the current buffer. + /// A 32-bit integer that represents the index in array at which copying begins. + void CopyTo(T[] array, int index); + + /// + /// Copies the buffer contents to the array, according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) + /// + /// The array that is the destination of the elements copied from the current buffer. + /// A 64-bit integer that represents the index in array at which copying begins. + void CopyTo(T[] array, long index); + +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) + + /// + /// Copies the buffer contents to the , according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) + /// + /// The memory that is the destination of the elements copied from the current buffer. + void CopyTo(Memory memory); + + /// + /// Copies the buffer contents to the , according to the logical + /// contents of the buffer (i.e. independent of the internal + /// order/contents) + /// + /// The span that is the destination of the elements copied from the current buffer. + void CopyTo(Span span); + +#endif + + /// + /// Get the contents of the buffer as 2 ArraySegments. + /// Respects the logical contents of the buffer, where + /// each segment and items in each segment are ordered + /// according to insertion. + /// + /// Fast: does not copy the array elements. + /// Useful for methods like Send(IList<ArraySegment<Byte>>). + /// + /// Segments may be empty. + /// + /// An IList with 2 segments corresponding to the buffer content. + IList> ToArraySegments(); + +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) + + /// + /// Get the contents of the buffer as ref struct with 2 read only spans (). + /// Respects the logical contents of the buffer, where + /// each segment and items in each segment are ordered + /// according to insertion. + /// + /// Segments may be empty. + /// + /// A with 2 read only spans corresponding to the buffer content. + SpanTuple ToSpan(); + + /// + /// Get the contents of the buffer as tuple with 2 . + /// Respects the logical contents of the buffer, where + /// each segment and items in each segment are ordered + /// according to insertion. + /// + /// Segments may be empty. + /// + /// A tuple with 2 read only spans corresponding to the buffer content. + (ReadOnlyMemory A, ReadOnlyMemory B) ToMemory(); + +#endif +} + +/// +public class CircularBuffer : ICircularBuffer +{ + private readonly T[] _buffer; + + /// + /// The _start. Index of the first element in buffer. + /// + private int _start; + + /// + /// The _end. Index after the last element in the buffer. + /// + private int _end; + + /// + /// The _size. Buffer size. + /// + private int _size; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// Buffer capacity. Must be positive. + /// + public CircularBuffer(int capacity) + : this(capacity, new T[] { }) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// Buffer capacity. Must be positive. + /// + /// + /// Items to fill buffer with. Items length must be less than capacity. + /// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from + /// any enumerable. + /// + public CircularBuffer(int capacity, T[] items) + { + if (capacity < 1) + { + throw new ArgumentException( + "Circular buffer cannot have negative or zero capacity.", nameof(capacity)); + } + + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + if (items.Length > capacity) + { + throw new ArgumentException( + "Too many items to fit circular buffer", nameof(items)); + } + + _buffer = new T[capacity]; + + Array.Copy(items, _buffer, items.Length); + _size = items.Length; + + _start = 0; + _end = _size == capacity ? 0 : _size; + } + + /// + public int Capacity + { + get { return _buffer.Length; } + } + + /// + public bool IsFull + { + get { return Count == Capacity; } + } + + /// + public bool IsEmpty + { + get { return Count == 0; } + } + + /// + [Obsolete("Use Count property instead")] + public int Size => Count; + + /// + public int Count + { + get { return _size; } + } + + /// + [Obsolete("Use First() method instead")] + public T Front() => First(); + + /// + [Obsolete("Use Last() method instead")] + public T Back() => Last(); + + /// + public T First() + { + ThrowIfEmpty(); + return _buffer[_start]; + } + + /// + public T Last() + { + ThrowIfEmpty(); + return _buffer[(_end != 0 ? _end : Capacity) - 1]; + } + + /// + public T this[int index] + { + get + { + if (IsEmpty) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); + } + + if (index >= _size) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); + } + + int actualIndex = InternalIndex(index); + return _buffer[actualIndex]; + } + set + { + if (IsEmpty) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); + } + + if (index >= _size) + { + throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); + } + + int actualIndex = InternalIndex(index); + _buffer[actualIndex] = value; + } + } + + /// + public void PushBack(T item) + { + if (IsFull) + { + _buffer[_end] = item; + Increment(ref _end); + _start = _end; + } + else + { + _buffer[_end] = item; + Increment(ref _end); + ++_size; + } + } + + /// + public void PushFront(T item) + { + if (IsFull) + { + Decrement(ref _start); + _end = _start; + _buffer[_start] = item; + } + else + { + Decrement(ref _start); + _buffer[_start] = item; + ++_size; + } + } + + /// + public T PopBack() + { + ThrowIfEmpty("Cannot take elements from an empty buffer."); + Decrement(ref _end); + var value = _buffer[_start]; + _buffer[_end] = default(T); + --_size; + return value; + } + + /// + public T PopFront() + { + ThrowIfEmpty("Cannot take elements from an empty buffer."); + var value = _buffer[_start]; + _buffer[_start] = default(T); + Increment(ref _start); + --_size; + return value; + } + + /// + public void Clear() + { + // to clear we just reset everything. + _start = 0; + _end = 0; + _size = 0; + Array.Clear(_buffer, 0, _buffer.Length); + } + + /// + public T[] ToArray() + { + T[] newArray = new T[Count]; + CopyToInternal(newArray, 0); + return newArray; + } + + /// + public void CopyTo(T[] array) + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (array.Length < _size) + throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " + + "number of elements of the destination array.", nameof(array)); + + CopyToInternal(array, 0); + } + + /// + public void CopyTo(T[] array, int index) + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), $"{nameof(index)} is less than the lower bound of {nameof(array)}."); + + if (array.Length - index < _size) + throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " + + "number of elements from index to the end of the destination array.", nameof(array)); + + CopyToInternal(array, index); + } + + /// + public void CopyTo(T[] array, long index) + { + if (array is null) + throw new ArgumentNullException(nameof(array)); + + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index), $"{nameof(index)} is less than the lower bound of {nameof(array)}."); + + if (array.LongLength - index < _size) + throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " + + "number of elements from index to the end of the destination array.", nameof(array)); + + CopyToInternal(array, index); + } + +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) + + /// + public void CopyTo(Memory memory) + { + if (memory.Length < _size) + throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " + + "number of elements of the destination Memory.", nameof(memory)); + + CopyToInternal(memory); + } + + /// + public void CopyTo(Span span) + { + if (span.Length < _size) + throw new ArgumentException($"The number of elements in the source {nameof(CircularBuffer)} is greater than the available " + + "number of elements of the destination Span.", nameof(span)); + + CopyToInternal(span); + } + +#endif + + /// + public IList> ToArraySegments() + { + return new[] {ArrayOne(), ArrayTwo()}; + } + +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) + + /// + public SpanTuple ToSpan() + { + return new SpanTuple(SpanOne(), SpanTwo()); + } + + /// + public (ReadOnlyMemory A, ReadOnlyMemory B) ToMemory() + { + return (MemoryOne(), MemoryTwo()); + } + +#endif + + #region IEnumerable implementation + + /// + /// Returns an enumerator that iterates through this buffer. + /// + /// An enumerator that can be used to iterate this collection. + public IEnumerator GetEnumerator() + { + var segments = ToArraySegments(); + foreach (ArraySegment segment in segments) + { + for (int i = 0; i < segment.Count; i++) + { + yield return segment.Array[segment.Offset + i]; + } + } + } + + #endregion + + #region IEnumerable implementation + + IEnumerator IEnumerable.GetEnumerator() + { + return (IEnumerator) GetEnumerator(); + } + + #endregion + +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) + + private void CopyToInternal(T[] array, int index) + { + CopyToInternal(array.AsSpan(index)); + } + + private void CopyToInternal(Memory memory) + { + CopyToInternal(memory.Span); + } + + private void CopyToInternal(Span span) + { + var segments = ToSpan(); + segments.A.CopyTo(span); + segments.B.CopyTo(span.Slice(segments.A.Length)); + } + +#else + private void CopyToInternal(T[] array, int index) + { + var segments = ToArraySegments(); + var segment = segments[0]; + Array.Copy(segment.Array, segment.Offset, array, index, segment.Count); + index += segment.Count; + segment = segments[1]; + Array.Copy(segment.Array, segment.Offset, array, index, segment.Count); + } + +#endif + + private void CopyToInternal(T[] array, long index) + { + var segments = ToArraySegments(); + var segment = segments[0]; + Array.Copy(segment.Array, segment.Offset, array, index, segment.Count); + index += segment.Count; + segment = segments[1]; + Array.Copy(segment.Array, segment.Offset, array, index, segment.Count); + } + + private void ThrowIfEmpty(string message = "Cannot access an empty buffer.") + { + if (IsEmpty) + { + throw new InvalidOperationException(message); + } + } + + /// + /// Increments the provided index variable by one, wrapping + /// around if necessary. + /// + /// + private void Increment(ref int index) + { + if (++index == Capacity) + { + index = 0; + } + } + + /// + /// Decrements the provided index variable by one, wrapping + /// around if necessary. + /// + /// + private void Decrement(ref int index) + { + if (index == 0) + { + index = Capacity; + } + + index--; + } + + /// + /// Converts the index in the argument to an index in _buffer + /// + /// + /// The transformed index. + /// + /// + /// External index. + /// + private int InternalIndex(int index) + { + return _start + (index < (Capacity - _start) ? index : index - Capacity); + } + + // doing ArrayOne and ArrayTwo methods returning ArraySegment as seen here: + // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1957cccdcb0c4ef7d80a34a990065818d + // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b + // should help a lot with the code. + + #region Array items easy access. + + // The array is composed by at most two non-contiguous segments, + // the next two methods allow easy access to those. + + private ArraySegment ArrayOne() + { + if (IsEmpty) + { + return new ArraySegment(new T[0]); + } + else if (_start < _end) + { + return new ArraySegment(_buffer, _start, _end - _start); + } + else + { + return new ArraySegment(_buffer, _start, _buffer.Length - _start); + } + } + + private ArraySegment ArrayTwo() + { + if (IsEmpty) + { + return new ArraySegment(new T[0]); + } + else if (_start < _end) + { + return new ArraySegment(_buffer, _end, 0); + } + else + { + return new ArraySegment(_buffer, 0, _end); + } + } + +#if (NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER) + + private Span SpanOne() + { + if (IsEmpty) + { + return Span.Empty; + } + else if (_start < _end) + { + return _buffer.AsSpan(_start, _end - _start); + } + else + { + return _buffer.AsSpan(_start, _buffer.Length - _start); + } + } + + private Span SpanTwo() + { + if (IsEmpty) + { + return Span.Empty; + } + else if (_start < _end) + { + return _buffer.AsSpan(_end, 0); + } + else + { + return _buffer.AsSpan(0, _end); + } + } + + private Memory MemoryOne() + { + if (IsEmpty) + { + return Memory.Empty; + } + else if (_start < _end) + { + return _buffer.AsMemory(_start, _end - _start); + } + else + { + return _buffer.AsMemory(_start, _buffer.Length - _start); + } + } + + private Memory MemoryTwo() + { + if (IsEmpty) + { + return Memory.Empty; + } + else if (_start < _end) + { + return _buffer.AsMemory(_end, 0); + } + else + { + return _buffer.AsMemory(0, _end); + } + } + +#endif + + #endregion +} \ No newline at end of file diff --git a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs index 458fc9e..67a1fb2 100644 --- a/src/Core/FileTime.Core.Abstraction/Services/ITab.cs +++ b/src/Core/FileTime.Core.Abstraction/Services/ITab.cs @@ -18,4 +18,6 @@ public interface ITab : IAsyncInitable, IDisposable void RemoveItemFilter(string name); Task SetSelectedItem(AbsolutePath newSelectedItem); Task ForceSetCurrentLocation(IContainer newLocation); + Task GoBackAsync(); + Task GoForwardAsync(); } \ No newline at end of file diff --git a/src/Core/FileTime.Core.Services/Tab.cs b/src/Core/FileTime.Core.Services/Tab.cs index f6e4622..ea44171 100644 --- a/src/Core/FileTime.Core.Services/Tab.cs +++ b/src/Core/FileTime.Core.Services/Tab.cs @@ -1,8 +1,7 @@ using System.Collections.ObjectModel; -using System.Reactive.Subjects; +using CircularBuffer; using DeclarativeProperty; using DynamicData; -using DynamicData.Binding; using FileTime.App.Core.Services; using FileTime.Core.Helper; using FileTime.Core.Models; @@ -16,10 +15,11 @@ public class Tab : ITab private readonly ITimelessContentProvider _timelessContentProvider; private readonly ITabEvents _tabEvents; private readonly DeclarativeProperty _currentLocation = new(null); - private readonly BehaviorSubject _currentLocationForced = new(null); + private readonly DeclarativeProperty _currentLocationForced = new(null); private readonly DeclarativeProperty _currentRequestItem = new(null); private readonly ObservableCollection _itemFilters = new(); - private readonly DeclarativeProperty?> _itemFiltersProperty; + private readonly CircularBuffer _history = new(20); + private readonly CircularBuffer _future = new(20); private AbsolutePath? _currentSelectedItemCached; private PointInTime _currentPointInTime; private CancellationTokenSource? _setCurrentLocationCancellationTokenSource; @@ -38,11 +38,16 @@ public class Tab : ITab _timelessContentProvider = timelessContentProvider; _tabEvents = tabEvents; _currentPointInTime = null!; - _itemFiltersProperty = new(_itemFilters); + var itemFiltersProperty = new DeclarativeProperty>(_itemFilters) + .Watch, ItemFilter>(); _timelessContentProvider.CurrentPointInTime.Subscribe(p => _currentPointInTime = p); - CurrentLocation = _currentLocation; + CurrentLocation = DeclarativePropertyHelpers.Merge( + _currentLocation.DistinctUntilChanged(), + _currentLocationForced + ); + CurrentLocation.Subscribe((c, _) => { if (_currentSelectedItemCached is not null) @@ -55,7 +60,7 @@ public class Tab : ITab CurrentItems = DeclarativePropertyHelpers.CombineLatest( CurrentLocation, - _itemFiltersProperty.Watch, ItemFilter>(), + itemFiltersProperty, (container, filters) => { ObservableCollection? items = null; @@ -101,15 +106,6 @@ public class Tab : ITab }); } - static void UpdateConsumer(ObservableCollection? collection, ref OcConsumer? consumer) - { - if (collection is not IComputing computing) return; - - consumer?.Dispose(); - consumer = new OcConsumer(); - computing.For(consumer); - } - private static IItem MapItem(AbsolutePath item) { var t = Task.Run(async () => await item.ResolveAsync(true)); @@ -117,15 +113,9 @@ public class Tab : ITab return t.Result; } - private static SortExpressionComparer SortItems() - //TODO: Order - => SortExpressionComparer - .Ascending(i => i.Type) - .ThenByAscending(i => i.DisplayName.ToLower()); - public async Task InitAsync(IContainer currentLocation) - => await _currentLocation.SetValue(currentLocation); + => await SetCurrentLocation(currentLocation); private AbsolutePath? GetSelectedItemByItems(IReadOnlyCollection items) { @@ -158,9 +148,24 @@ public class Tab : ITab } public async Task SetCurrentLocation(IContainer newLocation) + { + _future.Clear(); + await SetCurrentLocation(newLocation, true); + } + + private async Task SetCurrentLocation(IContainer newLocation, bool addToHistory) { _setCurrentLocationCancellationTokenSource?.Cancel(); _setCurrentLocationCancellationTokenSource = new CancellationTokenSource(); + + if (addToHistory + && newLocation.FullName is { } fullName + && (_history.Count == 0 + || _history.Last() != fullName)) + { + _history.PushFront(fullName); + } + await _currentLocation.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token); if (newLocation.FullName != null) @@ -173,7 +178,7 @@ public class Tab : ITab { _setCurrentLocationCancellationTokenSource?.Cancel(); _setCurrentLocationCancellationTokenSource = new CancellationTokenSource(); - await _currentLocation.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token); + await _currentLocationForced.SetValue(newLocation, _setCurrentLocationCancellationTokenSource.Token); if (newLocation.FullName != null) { @@ -181,6 +186,32 @@ public class Tab : ITab } } + public async Task GoBackAsync() + { + if (_history.Count < 2) return; + + var currentLocationFullName = _history.PopFront(); + _future.PushFront(currentLocationFullName); + + var lastLocationFullName = _history.First(); + var container = (IContainer) await _timelessContentProvider.GetItemByFullNameAsync( + lastLocationFullName, + PointInTime.Present); + await SetCurrentLocation(container, false); + } + + public async Task GoForwardAsync() + { + if (_future.Count == 0) return; + + var fullName = _future.PopFront(); + _history.PushFront(fullName); + var container = (IContainer) await _timelessContentProvider.GetItemByFullNameAsync( + fullName, + PointInTime.Present); + await SetCurrentLocation(container, false); + } + public async Task SetSelectedItem(AbsolutePath newSelectedItem) { _setCurrentItemCancellationTokenSource?.Cancel(); diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App.Abstractions/Configuration/MainConfiguration.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App.Abstractions/Configuration/MainConfiguration.cs index c89447f..4bbff55 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App.Abstractions/Configuration/MainConfiguration.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App.Abstractions/Configuration/MainConfiguration.cs @@ -63,9 +63,9 @@ public static class MainConfiguration //new CommandBindingConfiguration(ConfigCommand.Edit, new KeyConfig(Key.F4)), new(EnterRapidTravelCommand.CommandName, new KeyConfig(Key.OemComma, shift: true)), new(EnterRapidTravelCommand.CommandName, new KeyConfig(Key.OemQuestion, shift: true)), - //new CommandBindingConfiguration(ConfigCommand.FindByName, new[] { Key.F, Key.N }), - //new CommandBindingConfiguration(ConfigCommand.FindByNameRegex, new[] { Key.F, Key.R }), + new(GoBackCommand.CommandName, new KeyConfig(Key.Left, alt: true)), new(GoByFrequencyCommand.CommandName, Key.Z), + new(GoForwardCommand.CommandName, new KeyConfig(Key.Right, alt: true)), new(GoToHomeCommand.CommandName, new[] {Key.G, Key.H}), new(GoToPathCommand.CommandName, new KeyConfig(Key.L, ctrl: true)), new(GoToPathCommand.CommandName, new[] {Key.G, Key.P}), diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Converters/NamePartShrinkerConverter.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Converters/NamePartShrinkerConverter.cs index 697abe7..d8ae287 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Converters/NamePartShrinkerConverter.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Converters/NamePartShrinkerConverter.cs @@ -1,6 +1,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Media; +using Avalonia.Threading; using FileTime.Core.Models; using FileTime.GuiApp.App.ViewModels; @@ -9,6 +10,7 @@ namespace FileTime.GuiApp.App.Converters; public class NamePartShrinkerConverter : IMultiValueConverter { private const int PixelPerChar = 8; + public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { if (values.Count > 0 && values[0] is IList nameParts) @@ -20,8 +22,13 @@ public class NamePartShrinkerConverter : IMultiValueConverter newNameParts = GetNamePartsForWidth(nameParts, width - attributeWidth); } - return newNameParts.Select(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)).ToList(); + var result = /* Dispatcher.UIThread.Invoke(() => */ + newNameParts.Select(p => new ItemNamePartViewModel(p.Text, p.IsSpecial ? TextDecorations.Underline : null)).ToList(); + /* ); */ + + return result; } + return null; } @@ -75,6 +82,7 @@ public class NamePartShrinkerConverter : IMultiValueConverter proposedText = proposedText[0..^1]; trimmed = true; } + newNameParts[trimmedIndex] = new ItemNamePart(proposedText + (trimmed ? "..." : "")); if (trimmed) break; } @@ -100,6 +108,7 @@ public class NamePartShrinkerConverter : IMultiValueConverter if (!string.IsNullOrWhiteSpace(proposedText)) newNameParts.Add(new ItemNamePart(proposedText, namePart.IsSpecial)); if (trimmed) break; } + if (newNameParts.Last().IsSpecial) { newNameParts.Add(new ItemNamePart("...")); @@ -109,6 +118,7 @@ public class NamePartShrinkerConverter : IMultiValueConverter var last = newNameParts.Last(); newNameParts[^1] = new ItemNamePart(last.Text + "..."); } + return newNameParts; } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Styles.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Styles.axaml index 2bc8b7b..2ac2dfa 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Styles.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Resources/Styles.axaml @@ -67,6 +67,11 @@ + + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/RapidTravelModeKeyInputHandler.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/RapidTravelModeKeyInputHandler.cs index 699843b..5012bd9 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/RapidTravelModeKeyInputHandler.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Services/RapidTravelModeKeyInputHandler.cs @@ -43,12 +43,22 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler _appState.SelectedTab.Subscribe(t => _selectedTab = t); _openModals = modalService.OpenModals.ToBindedCollection(); + + _appState.RapidTravelTextDebounced.Subscribe((v, _) => + { + if (_selectedTab?.Tab is not { } tab) return Task.CompletedTask; + tab.RemoveItemFilter(RapidTravelFilterName); + + if (v is null) return Task.CompletedTask; + + tab.AddItemFilter(new ItemFilter(RapidTravelFilterName, i => i.Name.ToLower().Contains(v))); + return Task.CompletedTask; + }); } public async Task HandleInputKey(Key key, SpecialKeysStatus specialKeysStatus, Action setHandled) { var keyString = key.ToString(); - var updateRapidTravelFilter = false; if (key == Key.Escape) { @@ -70,7 +80,6 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler await _appState.RapidTravelText.SetValue( _appState.RapidTravelText.Value![..^1] ); - updateRapidTravelFilter = true; } } else if (keyString.Length == 1) @@ -79,11 +88,10 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler await _appState.RapidTravelText.SetValue( _appState.RapidTravelText.Value + keyString.ToLower() ); - updateRapidTravelFilter = true; } else { - var currentKeyAsList = new List() { new KeyConfig(key) }; + var currentKeyAsList = new List() {new KeyConfig(key)}; var selectedCommandBinding = _keyboardConfigurationService.UniversalCommandBindings.FirstOrDefault(c => c.Keys.AreKeysEqual(currentKeyAsList)); if (selectedCommandBinding != null) { @@ -91,42 +99,6 @@ public class RapidTravelModeKeyInputHandler : IRapidTravelModeKeyInputHandler await CallCommandAsync(_identifiableUserCommandService.GetCommand(selectedCommandBinding.Command)); } } - - if (updateRapidTravelFilter) - { - if (_selectedTab?.Tab is not ITab tab) return; - - tab.RemoveItemFilter(RapidTravelFilterName); - tab.AddItemFilter(new ItemFilter(RapidTravelFilterName, i => i.Name.ToLower().Contains(_appState.RapidTravelText.Value!))); - /*var currentLocation = await _appState.SelectedTab.CurrentLocation.Container.WithoutVirtualContainer(MainPageViewModel.RAPIDTRAVEL); - var newLocation = new VirtualContainer( - currentLocation, - new List, IEnumerable>>() - { - container => container.Where(c => c.Name.ToLower().Contains(_appState.RapidTravelText)) - }, - new List, IEnumerable>>() - { - element => element.Where(e => e.Name.ToLower().Contains(_appState.RapidTravelText)) - }, - virtualContainerName: MainPageViewModel.RAPIDTRAVEL - ); - - await newLocation.Init(); - - await _appState.SelectedTab.OpenContainer(newLocation); - - var selectedItemName = _appState.SelectedTab.SelectedItem?.Item.Name; - var currentLocationItems = await _appState.SelectedTab.CurrentLocation.GetItems(); - if (currentLocationItems.FirstOrDefault(i => string.Equals(i.Item.Name, _appState.RapidTravelText, StringComparison.OrdinalIgnoreCase)) is IItemViewModel matchItem) - { - await _appState.SelectedTab.SetCurrentSelectedItem(matchItem.Item); - } - else if (!currentLocationItems.Select(i => i.Item.Name).Any(n => n == selectedItemName)) - { - await _appState.SelectedTab.MoveCursorToFirst(); - }*/ - } } private async Task CallCommandAsync(IUserCommand command) diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/ViewModels/IMainWindowViewModel.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/ViewModels/IMainWindowViewModel.cs index 0f786ea..b6a9e91 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/ViewModels/IMainWindowViewModel.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/ViewModels/IMainWindowViewModel.cs @@ -1,4 +1,5 @@ -using FileTime.App.CommandPalette.Services; +using DeclarativeProperty; +using FileTime.App.CommandPalette.Services; using FileTime.App.Core.Services; using FileTime.App.Core.ViewModels; using FileTime.App.FrequencyNavigation.Services; diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml index 2d1b0a3..c579a37 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml @@ -1,13 +1,13 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -17,10 +17,13 @@ - + diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml.cs index 75c9eb6..b96b900 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp.App/Views/PathPresenter.axaml.cs @@ -1,11 +1,53 @@ using Avalonia.Controls; +using Avalonia.Input; +using FileTime.App.Core.Services; +using FileTime.App.Core.UserCommand; +using FileTime.Core.Enums; +using FileTime.Core.Models; +using FileTime.Core.Timeline; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace FileTime.GuiApp.App.Views; public partial class PathPresenter : UserControl { + private readonly Lazy> _logger; + public PathPresenter() { InitializeComponent(); + _logger = new Lazy>( + () => DI.ServiceProvider.GetRequiredService>() + ); + } + + private async void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e) + { + try + { + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed + && DataContext is string fullPath + && sender is TextBlock textBlock) + { + var pathPart = textBlock.Text; + var path = fullPath[..(fullPath.IndexOf(pathPart) + pathPart.Length)]; + var timelessContentProvider = DI.ServiceProvider.GetRequiredService(); + var userCommandHandlerService = DI.ServiceProvider.GetRequiredService(); + await userCommandHandlerService.HandleCommandAsync( + new OpenContainerCommand( + new AbsolutePath( + timelessContentProvider, + PointInTime.Present, + new FullName(path), + AbsolutePathType.Container) + ) + ); + } + } + catch (Exception exception) + { + _logger.Value.LogError(exception, "Failed to open container"); + } } } \ No newline at end of file diff --git a/src/Library/DeclarativeProperty/DebounceProperty.cs b/src/Library/DeclarativeProperty/DebounceProperty.cs index 830df88..b26bd2d 100644 --- a/src/Library/DeclarativeProperty/DebounceProperty.cs +++ b/src/Library/DeclarativeProperty/DebounceProperty.cs @@ -3,7 +3,7 @@ public sealed class DebounceProperty : DeclarativePropertyBase { private readonly object _lock = new(); - private readonly Func _interval; + private readonly Func _interval; private DateTime _startTime = DateTime.MinValue; private T? _nextValue; private CancellationToken _nextCancellationToken; @@ -13,7 +13,7 @@ public sealed class DebounceProperty : DeclarativePropertyBase public DebounceProperty( IDeclarativeProperty from, - Func interval, + Func interval, Action? setValueHook = null) : base(from.Value, setValueHook) { _interval = interval; @@ -46,7 +46,7 @@ public sealed class DebounceProperty : DeclarativePropertyBase private async Task StartDebounceTask() { - while (DateTime.Now - _startTime < _interval()) + while (DateTime.Now - _startTime < _interval(_nextValue)) { await Task.Delay(WaitInterval); } diff --git a/src/Library/DeclarativeProperty/DeclarativeProperty.cs b/src/Library/DeclarativeProperty/DeclarativeProperty.cs index 732a0a8..84edf2a 100644 --- a/src/Library/DeclarativeProperty/DeclarativeProperty.cs +++ b/src/Library/DeclarativeProperty/DeclarativeProperty.cs @@ -16,6 +16,9 @@ public static class DeclarativePropertyHelpers Func> func, Action? setValueHook = null) => new(prop1, prop2, prop3, func, setValueHook); + + public static MergeProperty Merge(params IDeclarativeProperty[] props) + => new(props); } public sealed class DeclarativeProperty : DeclarativePropertyBase diff --git a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs index 01a871a..bf5aa8c 100644 --- a/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs +++ b/src/Library/DeclarativeProperty/DeclarativePropertyExtensions.cs @@ -7,9 +7,9 @@ namespace DeclarativeProperty; public static class DeclarativePropertyExtensions { public static IDeclarativeProperty Debounce(this IDeclarativeProperty from, TimeSpan interval, bool resetTimer = false) - => new DebounceProperty(from, () => interval) {ResetTimer = resetTimer}; + => new DebounceProperty(from, _ => interval) {ResetTimer = resetTimer}; - public static IDeclarativeProperty Debounce(this IDeclarativeProperty from, Func interval, bool resetTimer = false) + public static IDeclarativeProperty Debounce(this IDeclarativeProperty from, Func interval, bool resetTimer = false) => new DebounceProperty(from, interval) {ResetTimer = resetTimer}; public static IDeclarativeProperty Throttle(this IDeclarativeProperty from, TimeSpan interval) diff --git a/src/Library/DeclarativeProperty/MergeProperty.cs b/src/Library/DeclarativeProperty/MergeProperty.cs new file mode 100644 index 0000000..b317306 --- /dev/null +++ b/src/Library/DeclarativeProperty/MergeProperty.cs @@ -0,0 +1,17 @@ +namespace DeclarativeProperty; + +public class MergeProperty : DeclarativePropertyBase +{ + public MergeProperty(params IDeclarativeProperty[] props) + { + ArgumentNullException.ThrowIfNull(props); + + foreach (var prop in props) + { + prop.Subscribe(UpdateAsync); + } + } + + private async Task UpdateAsync(T? newValue, CancellationToken token) + => await SetNewValueAsync(newValue, token); +} \ No newline at end of file