From 9547beef87930d4219497f4c668a996f3023a933 Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Thu, 25 Jun 2026 17:12:31 +0900 Subject: [PATCH] Add Scrollbar Context Menu (#19413) * Scroll Bar Context Menu V1 * Add resource files * Use InvariantResources file * Add Unit Tests * Refactor to XAML * Add pointer type * Add tests --- .../Input/ContextRequestedEventArgs.cs | 2 + src/Avalonia.Controls/Primitives/ScrollBar.cs | 111 +++++++++ .../Controls/ScrollBar.xaml | 28 +++ .../Strings/InvariantResources.xaml | 13 + .../Controls/ScrollBar.xaml | 28 +++ .../Primitives/ScrollBarTests.cs | 222 ++++++++++++++++++ 6 files changed, 404 insertions(+) diff --git a/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs b/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs index e9e23d817c..6550b3fa62 100644 --- a/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs +++ b/src/Avalonia.Base/Input/ContextRequestedEventArgs.cs @@ -67,5 +67,7 @@ namespace Avalonia.Input } internal bool IsHolding => _holdRoutedEventArgs != null; + + internal PointerType? PointerType => _pointerEventArgs?.Pointer.Type; } } diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 89dcb9a8d4..6044777a40 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -5,6 +5,7 @@ using Avalonia.Input; using Avalonia.Layout; using Avalonia.Threading; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Templates; using Avalonia.Automation.Peers; using Avalonia.VisualTree; using Avalonia.Reactive; @@ -86,6 +87,7 @@ namespace Avalonia.Controls.Primitives private CompositeDisposable? _ownerSubscriptions; private ScrollViewer? _owner; private bool _isDragging; + private Point _lastRightClickPosition; /// /// Initializes static members of the class. @@ -105,6 +107,7 @@ namespace Avalonia.Controls.Primitives public ScrollBar() { UpdatePseudoClasses(Orientation); + ContextRequested += OnContextRequested; } /// @@ -521,5 +524,113 @@ namespace Avalonia.Controls.Primitives PseudoClasses.Set(":vertical", o == Orientation.Vertical); PseudoClasses.Set(":horizontal", o == Orientation.Horizontal); } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed) + { + _lastRightClickPosition = e.GetPosition(this); + } + } + + private void OnContextRequested(object? sender, ContextRequestedEventArgs e) + { + if (e.PointerType is PointerType.Touch or PointerType.Pen) + { + e.Handled = true; + return; + } + + if (e.TryGetPosition(this, out var position)) + { + _lastRightClickPosition = position; + } + } + + /// + /// Scrolls to the location at which the context menu was most recently requested. + /// + public void ScrollHere() + { + if (Track is not { } track) + { + return; + } + + var trackLength = Orientation == Orientation.Vertical ? track.Bounds.Height : track.Bounds.Width; + var thumbLength = Orientation == Orientation.Vertical ? track.Thumb?.Bounds.Height ?? 0 : track.Thumb?.Bounds.Width ?? 0; + var clickPosition = Orientation == Orientation.Vertical ? _lastRightClickPosition.Y : _lastRightClickPosition.X; + + if (trackLength > thumbLength) + { + var ratio = clickPosition / trackLength; + var range = Maximum - Minimum; + var value = Minimum + (ratio * range); + SetCurrentValue(ValueProperty, Math.Max(Minimum, Math.Min(Maximum, value))); + OnScroll(ScrollEventType.ThumbTrack); + } + } + + /// + /// Scrolls to the top (or left edge) of the scrollbar by setting to . + /// + public void ScrollToHome() + { + SetCurrentValue(ValueProperty, Minimum); + OnScroll(ScrollEventType.SmallDecrement); + } + + /// + /// Scrolls to the bottom (or right edge) of the scrollbar by setting to . + /// + public void ScrollToEnd() + { + SetCurrentValue(ValueProperty, Maximum); + OnScroll(ScrollEventType.SmallIncrement); + } + + /// + /// Scrolls up by one (a page) on a vertical scrollbar. + /// + public void PageUp() => LargeDecrement(); + + /// + /// Scrolls down by one (a page) on a vertical scrollbar. + /// + public void PageDown() => LargeIncrement(); + + /// + /// Scrolls left by one (a page) on a horizontal scrollbar. + /// + public void PageLeft() => LargeDecrement(); + + /// + /// Scrolls right by one (a page) on a horizontal scrollbar. + /// + public void PageRight() => LargeIncrement(); + + /// + /// Scrolls up by one on a vertical scrollbar. + /// + public void LineUp() => SmallDecrement(); + + /// + /// Scrolls down by one on a vertical scrollbar. + /// + public void LineDown() => SmallIncrement(); + + /// + /// Scrolls left by one on a horizontal scrollbar. + /// + public void LineLeft() => SmallDecrement(); + + /// + /// Scrolls right by one on a horizontal scrollbar. + /// + public void LineRight() => SmallIncrement(); + + private Track? Track => this.GetTemplateDescendants().OfType().FirstOrDefault(); } } diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml index 8af22dd0b9..69a632ddb5 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollBar.xaml @@ -18,6 +18,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -111,6 +137,7 @@