Skip to content

Commit b71e2ce

Browse files
committed
refactor: enhance auto-scrolling behavior and add scroll detection for tabs
1 parent 3a50975 commit b71e2ce

File tree

1 file changed

+180
-1
lines changed

1 file changed

+180
-1
lines changed

TabInfo.cs

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,9 @@ public TabInfo(
190190

191191
// Initialize tab header
192192
UpdateTabHeader();
193+
194+
// Set up scroll detection for smart auto-scrolling
195+
SetupScrollDetection();
193196
}
194197

195198
protected void OnPropertyChanged(string propertyName)
@@ -319,8 +322,26 @@ public void AppendContent(string text)
319322
if (_disposed)
320323
return;
321324

325+
// Check if user was at the bottom before adding content
326+
bool wasAtBottom = IsUserAtBottom();
327+
bool isTabFocused = IsTabCurrentlyFocused();
328+
322329
ContentTextBox.AppendText(text);
323-
ContentTextBox.ScrollToEnd();
330+
331+
// Only auto-scroll if user was already at the bottom or this tab is not currently focused
332+
if (wasAtBottom || !isTabFocused)
333+
{
334+
ContentTextBox.ScrollToEnd();
335+
}
336+
else
337+
{
338+
// User is scrolled up and tab is focused - indicate new content arrived
339+
// This will make the tab header show the "new content" indicator
340+
if (!HasNewContent)
341+
{
342+
SetHasNewContent(true);
343+
}
344+
}
324345

325346
// Check if we need to optimize memory after adding content
326347
OptimizeMemoryIfNeeded();
@@ -451,6 +472,150 @@ public string GetFullContent()
451472
}
452473
}
453474

475+
/// <summary>
476+
/// Checks if the user is currently scrolled to the bottom of the TextBox
477+
/// </summary>
478+
/// <returns>True if at bottom, false if scrolled up</returns>
479+
private bool IsUserAtBottom()
480+
{
481+
if (_disposed || ContentTextBox == null)
482+
return true; // Default to true to maintain existing behavior
483+
484+
try
485+
{
486+
// Find the ScrollViewer inside the TextBox
487+
var scrollViewer = FindScrollViewer(ContentTextBox);
488+
if (scrollViewer != null)
489+
{
490+
var verticalOffset = scrollViewer.VerticalOffset;
491+
var scrollableHeight = scrollViewer.ScrollableHeight;
492+
493+
// Consider "at bottom" if within a small tolerance (a few pixels)
494+
const double tolerance = 5.0;
495+
return Math.Abs(verticalOffset - scrollableHeight) <= tolerance;
496+
}
497+
498+
// Fallback: check if caret is at the end
499+
return ContentTextBox.CaretIndex == ContentTextBox.Text.Length;
500+
}
501+
catch
502+
{
503+
return true; // Default to true if we can't determine scroll position
504+
}
505+
}
506+
507+
/// <summary>
508+
/// Finds the ScrollViewer inside a control using visual tree walking
509+
/// </summary>
510+
private ScrollViewer? FindScrollViewer(DependencyObject parent)
511+
{
512+
if (parent == null)
513+
return null;
514+
515+
try
516+
{
517+
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
518+
{
519+
var child = VisualTreeHelper.GetChild(parent, i);
520+
521+
if (child is ScrollViewer scrollViewer)
522+
return scrollViewer;
523+
524+
var result = FindScrollViewer(child);
525+
if (result != null)
526+
return result;
527+
}
528+
}
529+
catch
530+
{
531+
// Ignore visual tree access errors
532+
}
533+
534+
return null;
535+
}
536+
537+
/// <summary>
538+
/// Checks if this tab is currently focused/selected
539+
/// </summary>
540+
/// <returns>True if this tab is currently active</returns>
541+
private bool IsTabCurrentlyFocused()
542+
{
543+
if (_disposed || TabItem?.Parent == null)
544+
return false;
545+
546+
try
547+
{
548+
// Check if this tab is the selected item in its parent TabControl
549+
if (TabItem.Parent is TabControl tabControl)
550+
{
551+
return tabControl.SelectedItem == TabItem;
552+
}
553+
554+
// Check if this tab has keyboard focus
555+
return TabItem.IsKeyboardFocused || ContentTextBox?.IsKeyboardFocused == true;
556+
}
557+
catch
558+
{
559+
return false;
560+
}
561+
}
562+
563+
/// <summary>
564+
/// Call this method when the user scrolls back to the bottom to clear the new content indicator
565+
/// </summary>
566+
public void OnUserScrolledToBottom()
567+
{
568+
if (HasNewContent)
569+
{
570+
SetHasNewContent(false);
571+
}
572+
}
573+
574+
/// <summary>
575+
/// Subscribe to TextBox scroll events to detect when user scrolls back to bottom
576+
/// This should be called from MainWindow to wire up the scroll detection
577+
/// </summary>
578+
public void SetupScrollDetection()
579+
{
580+
if (_disposed || ContentTextBox == null)
581+
return;
582+
583+
try
584+
{
585+
var scrollViewer = FindScrollViewer(ContentTextBox);
586+
if (scrollViewer != null)
587+
{
588+
scrollViewer.ScrollChanged += OnScrollChanged;
589+
}
590+
}
591+
catch (Exception ex)
592+
{
593+
System.Diagnostics.Debug.WriteLine($"Error setting up scroll detection for tab '{TabName}': {ex.Message}");
594+
}
595+
}
596+
597+
/// <summary>
598+
/// Handle scroll events to detect when user returns to bottom
599+
/// </summary>
600+
private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
601+
{
602+
if (_disposed)
603+
return;
604+
605+
try
606+
{
607+
// If user scrolled to bottom and we have new content indicator, clear it
608+
if (HasNewContent && IsUserAtBottom())
609+
{
610+
SetHasNewContent(false);
611+
}
612+
}
613+
catch (Exception ex)
614+
{
615+
System.Diagnostics.Debug.WriteLine($"Error in scroll change handler for tab '{TabName}': {ex.Message}");
616+
}
617+
}
618+
454619
#region IDisposable Implementation
455620

456621
/// <summary>
@@ -496,6 +661,20 @@ protected virtual void Dispose(bool disposing)
496661
// Clear property change event handlers
497662
PropertyChanged = null;
498663

664+
// Unsubscribe from scroll events
665+
try
666+
{
667+
var scrollViewer = FindScrollViewer(ContentTextBox);
668+
if (scrollViewer != null)
669+
{
670+
scrollViewer.ScrollChanged -= OnScrollChanged;
671+
}
672+
}
673+
catch
674+
{
675+
// Ignore errors during disposal
676+
}
677+
499678
// Force garbage collection of large content if present
500679
if (ContentTextBox != null && ContentTextBox.Text.Length > 10_000_000) // 10MB threshold
501680
{

0 commit comments

Comments
 (0)