From 29a5c943cb0fc0ae048b2f1e44d16dc0d85dfeeb Mon Sep 17 00:00:00 2001 From: MBucari Date: Mon, 29 Dec 2025 19:36:15 -0700 Subject: [PATCH] Auto-scroll process queue --- .../Views/ProcessQueueControl.axaml | 2 +- .../Views/ProcessQueueControl.axaml.cs | 37 +++++++++++++++++++ .../ProcessQueue/ProcessQueueViewModel.cs | 5 ++- .../ProcessQueue/ProcessQueueControl.cs | 17 +++++++++ .../ProcessQueue/VirtualFlowControl.cs | 35 ++++++++++++++++++ 5 files changed, 94 insertions(+), 2 deletions(-) diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml index 163a6d3b..2b8331e1 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml @@ -34,7 +34,7 @@ HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" AllowAutoHide="False"> - + diff --git a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs index ef6cd82a..bb85da73 100644 --- a/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs +++ b/Source/LibationAvalonia/Views/ProcessQueueControl.axaml.cs @@ -1,6 +1,7 @@ using ApplicationServices; using Avalonia.Controls; using Avalonia.Data.Converters; +using Avalonia.Threading; using DataLayer; using LibationFileManager; using LibationUiBase; @@ -94,6 +95,42 @@ namespace LibationAvalonia.Views #endregion } + #region Auto-Scroll Current Item Into View + protected override void OnDataContextBeginUpdate() + { + if (DataContext is ProcessQueueViewModel vm) + { + vm.ProcessStart -= Book_ProcessStart; + } + base.OnDataContextBeginUpdate(); + } + + protected override void OnDataContextEndUpdate() + { + if (DataContext is ProcessQueueViewModel vm) + { + vm.ProcessStart += Book_ProcessStart; + } + base.OnDataContextEndUpdate(); + } + + private void Book_ProcessStart(object? sender, ProcessBookViewModel e) + { + Dispatcher.UIThread.Invoke(() => + { + if (Queue?.IndexOf(e) is int newtBookIndex && newtBookIndex > 0 && QueueListControl.Presenter?.Panel is VirtualizingStackPanel panel && itemIsVisible(newtBookIndex - 1, panel)) + { + // Only scroll the new item into view if the previous item is visible. + // This allows users to scroll through the queue without being interrupted. + QueueListControl.ScrollIntoView(newtBookIndex); + } + }); + + static bool itemIsVisible(int newtBookIndex, VirtualizingStackPanel panel) + => panel.FirstRealizedIndex <= newtBookIndex && panel.LastRealizedIndex >= newtBookIndex; + } + #endregion + public void NumericUpDown_KeyDown(object sender, Avalonia.Input.KeyEventArgs e) { if (e.Key == Avalonia.Input.Key.Enter && sender is Avalonia.Input.IInputElement input) input.Focus(); diff --git a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs index e6030ac4..bba5da0b 100644 --- a/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs +++ b/Source/LibationUiBase/ProcessQueue/ProcessQueueViewModel.cs @@ -264,7 +264,8 @@ public class ProcessQueueViewModel : ReactiveObject } #endregion - + public event EventHandler? ProcessStart; + public event EventHandler? ProcessEnd; private async Task QueueLoop() { try @@ -288,6 +289,7 @@ public class ProcessQueueViewModel : ReactiveObject Serilog.Log.Logger.Information("Begin processing queued item: '{item_LibraryBook}'", nextBook.LibraryBook); SpeedLimit = nextBook.Configuration.DownloadSpeedLimit / 1024m / 1024; + ProcessStart?.Invoke(this, nextBook); var result = await nextBook.ProcessOneAsync(); Serilog.Log.Logger.Information("Completed processing queued item: '{item_LibraryBook}' with result: {result}", nextBook.LibraryBook, result); @@ -310,6 +312,7 @@ public class ProcessQueueViewModel : ReactiveObject MessageBoxIcon.Asterisk); shownServiceOutageMessage = true; } + ProcessEnd?.Invoke(this, nextBook); } Serilog.Log.Logger.Information("Completed processing queue"); diff --git a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs index 5a14901e..de89f06c 100644 --- a/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs +++ b/Source/LibationWinForms/ProcessQueue/ProcessQueueControl.cs @@ -38,9 +38,26 @@ internal partial class ProcessQueueControl : UserControl logDGV.EnableHeadersVisualStyles = !Application.IsDarkModeEnabled; ViewModel.PropertyChanged += ProcessQueue_PropertyChanged; ViewModel.LogEntries.CollectionChanged += LogEntries_CollectionChanged; + ViewModel.ProcessStart += Book_ProcessStart; ProcessQueue_PropertyChanged(this, new PropertyChangedEventArgs(null)); } + private void Book_ProcessStart(object? sender, ProcessBookViewModel e) + { + Invoke(() => + { + if (ViewModel.Queue?.IndexOf(e) is int newtBookIndex && newtBookIndex > 0 && itemIsVisible(newtBookIndex - 1)) + { + // Only scroll the new item into view if the previous item is visible. + // This allows users to scroll through the queue without being interrupted. + virtualFlowControl2.ScrollIntoView(newtBookIndex); + } + }); + + bool itemIsVisible(int newtBookIndex) + => virtualFlowControl2.FirstRealizedIndex <= newtBookIndex && virtualFlowControl2.LastRealizedIndex >= newtBookIndex; + } + private void LogEntries_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { if (!IsDisposed && e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add) diff --git a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs index fbfa7b18..5e6e7e3e 100644 --- a/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs +++ b/Source/LibationWinForms/ProcessQueue/VirtualFlowControl.cs @@ -14,6 +14,17 @@ namespace LibationWinForms.ProcessQueue /// Triggered when one of the 's buttons has been clicked /// public event EventHandler? ButtonClicked; + + /// + /// Gets the index of the first realized element, or -1 if no elements are realized. + /// + public int FirstRealizedIndex { get; private set; } = -1; + + /// + /// Gets the index of the last realized element, or -1 if no elements are realized. + /// + public int LastRealizedIndex { get; private set; } = -1; + public IList? Items { get; private set; } private object? m_OldContext; @@ -199,6 +210,27 @@ namespace LibationWinForms.ProcessQueue } } + /// + /// Scrolls the specified item into view. + /// + /// The index of the item. + public void ScrollIntoView(int index) + { + if (index < 0 || index >= VirtualControlCount) + throw new ArgumentOutOfRangeException(nameof(index)); + int firstVisible = FirstVisibleVirtualIndex; + int lastVisible = firstVisible + (DisplayHeight / VirtualControlHeight) - 1; + if (index < firstVisible) + { + SetScrollPosition(index * VirtualControlHeight); + } + else if (index > lastVisible) + { + int newScrollPos = (index - (lastVisible - firstVisible)) * VirtualControlHeight; + SetScrollPosition(newScrollPos); + } + } + /// /// Calculated the virtual controls that are in view at the current scroll position and windows size, /// positions to simulate scroll activity, then fires updates the controls with @@ -229,6 +261,9 @@ namespace LibationWinForms.ProcessQueue { BookControls[i].Visible = i < numVisible; } + + FirstRealizedIndex = firstVisible; + LastRealizedIndex = firstVisible + numVisible - 1; } ///