From 40b5aacbc717f0c48769dc6c0c797b6deca1546e Mon Sep 17 00:00:00 2001 From: Tim Miller Date: Wed, 6 Aug 2025 17:03:56 +0900 Subject: [PATCH] Scroll Bar Context Menu V1 --- src/Avalonia.Controls/Primitives/ScrollBar.cs | 212 +++++++++++++++++- 1 file changed, 207 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 7df7012f8f..8141d5b409 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -1,14 +1,18 @@ using System; +using System.Linq; +using System.Windows.Input; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.Interactivity; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.Threading; -using Avalonia.Controls.Metadata; -using Avalonia.Automation.Peers; using Avalonia.VisualTree; -using Avalonia.Reactive; -using System.Linq; namespace Avalonia.Controls.Primitives { @@ -33,6 +37,7 @@ namespace Avalonia.Controls.Primitives [PseudoClasses(":vertical", ":horizontal")] public class ScrollBar : RangeBase { + /// /// Defines the property. /// @@ -85,6 +90,7 @@ namespace Avalonia.Controls.Primitives private bool _isExpanded; private CompositeDisposable? _ownerSubscriptions; private ScrollViewer? _owner; + private Point _lastRightClickPosition; /// /// Initializes static members of the class. @@ -103,6 +109,7 @@ namespace Avalonia.Controls.Primitives public ScrollBar() { UpdatePseudoClasses(Orientation); + ContextRequested += OnContextRequested; } /// @@ -506,5 +513,200 @@ 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.TryGetPosition(this, out var position)) + { + _lastRightClickPosition = position; + } + + if (Orientation == Orientation.Vertical) + { + SetCurrentValue(ContextMenuProperty, CreateVerticalContextMenu()); + } + else + { + var visualRoot = this.GetVisualRoot() as Visual; + var flowDirection = visualRoot?.FlowDirection ?? FlowDirection.LeftToRight; + SetCurrentValue(ContextMenuProperty, flowDirection == FlowDirection.LeftToRight + ? CreateHorizontalContextMenuLTR() + : CreateHorizontalContextMenuRTL()); + } + } + + private ContextMenu CreateVerticalContextMenu() + { + var contextMenu = new ContextMenu(); + contextMenu.Items.Add(CreateMenuItem("Scroll Here", "ScrollHere", () => ScrollHereAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Top", "Top", () => ScrollToTopAction(this))); + contextMenu.Items.Add(CreateMenuItem("Bottom", "Bottom", () => ScrollToBottomAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Page Up", "PageUp", () => PageUpAction(this))); + contextMenu.Items.Add(CreateMenuItem("Page Down", "PageDown", () => PageDownAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Scroll Up", "ScrollUp", () => LineUpAction(this))); + contextMenu.Items.Add(CreateMenuItem("Scroll Down", "ScrollDown", () => LineDownAction(this))); + return contextMenu; + } + + private ContextMenu CreateHorizontalContextMenuLTR() + { + var contextMenu = new ContextMenu(); + contextMenu.Items.Add(CreateMenuItem("Scroll Here", "ScrollHere", () => ScrollHereAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Left Edge", "LeftEdge", () => ScrollToLeftEndAction(this))); + contextMenu.Items.Add(CreateMenuItem("Right Edge", "RightEdge", () => ScrollToRightEndAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Page Left", "PageLeft", () => PageLeftAction(this))); + contextMenu.Items.Add(CreateMenuItem("Page Right", "PageRight", () => PageRightAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Scroll Left", "ScrollLeft", () => LineLeftAction(this))); + contextMenu.Items.Add(CreateMenuItem("Scroll Right", "ScrollRight", () => LineRightAction(this))); + return contextMenu; + } + + private ContextMenu CreateHorizontalContextMenuRTL() + { + var contextMenu = new ContextMenu(); + contextMenu.Items.Add(CreateMenuItem("Scroll Here", "ScrollHere", () => ScrollHereAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Right Edge", "RightEdge", () => ScrollToRightEndAction(this))); + contextMenu.Items.Add(CreateMenuItem("Left Edge", "LeftEdge", () => ScrollToLeftEndAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Page Right", "PageRight", () => PageRightAction(this))); + contextMenu.Items.Add(CreateMenuItem("Page Left", "PageLeft", () => PageLeftAction(this))); + contextMenu.Items.Add(new Separator()); + contextMenu.Items.Add(CreateMenuItem("Scroll Right", "ScrollRight", () => LineRightAction(this))); + contextMenu.Items.Add(CreateMenuItem("Scroll Left", "ScrollLeft", () => LineLeftAction(this))); + return contextMenu; + } + + private static MenuItem CreateMenuItem(string header, string automationId, Action action) + { + var menuItem = new MenuItem + { + Header = header, + Command = new SimpleCommand(action) + }; + AutomationProperties.SetAutomationId(menuItem, automationId); + return menuItem; + } + + private static void ScrollHereAction(ScrollBar scrollBar) + { + if (scrollBar.Track != null) + { + var track = scrollBar.Track; + var trackLength = scrollBar.Orientation == Orientation.Vertical ? track.Bounds.Height : track.Bounds.Width; + var thumbLength = scrollBar.Orientation == Orientation.Vertical ? track.Thumb?.Bounds.Height ?? 0 : track.Thumb?.Bounds.Width ?? 0; + var clickPosition = scrollBar.Orientation == Orientation.Vertical ? scrollBar._lastRightClickPosition.Y : scrollBar._lastRightClickPosition.X; + + if (trackLength > thumbLength) + { + var ratio = clickPosition / trackLength; + var range = scrollBar.Maximum - scrollBar.Minimum; + var value = scrollBar.Minimum + (ratio * range); + scrollBar.SetCurrentValue(ValueProperty, Math.Max(scrollBar.Minimum, Math.Min(scrollBar.Maximum, value))); + scrollBar.OnScroll(ScrollEventType.ThumbTrack); + } + } + } + + private static void ScrollToTopAction(ScrollBar scrollBar) + { + scrollBar.SetCurrentValue(ValueProperty, scrollBar.Minimum); + scrollBar.OnScroll(ScrollEventType.SmallDecrement); + } + + private static void ScrollToBottomAction(ScrollBar scrollBar) + { + scrollBar.SetCurrentValue(ValueProperty, scrollBar.Maximum); + scrollBar.OnScroll(ScrollEventType.SmallIncrement); + } + + private static void ScrollToLeftEndAction(ScrollBar scrollBar) + { + scrollBar.SetCurrentValue(ValueProperty, scrollBar.Minimum); + scrollBar.OnScroll(ScrollEventType.SmallDecrement); + } + + private static void ScrollToRightEndAction(ScrollBar scrollBar) + { + scrollBar.SetCurrentValue(ValueProperty, scrollBar.Maximum); + scrollBar.OnScroll(ScrollEventType.SmallIncrement); + } + + private static void PageUpAction(ScrollBar scrollBar) + { + scrollBar.LargeDecrement(); + } + + private static void PageDownAction(ScrollBar scrollBar) + { + scrollBar.LargeIncrement(); + } + + private static void PageLeftAction(ScrollBar scrollBar) + { + scrollBar.LargeDecrement(); + } + + private static void PageRightAction(ScrollBar scrollBar) + { + scrollBar.LargeIncrement(); + } + + private static void LineUpAction(ScrollBar scrollBar) + { + scrollBar.SmallDecrement(); + } + + private static void LineDownAction(ScrollBar scrollBar) + { + scrollBar.SmallIncrement(); + } + + private static void LineLeftAction(ScrollBar scrollBar) + { + scrollBar.SmallDecrement(); + } + + private static void LineRightAction(ScrollBar scrollBar) + { + scrollBar.SmallIncrement(); + } + + private Track? Track => this.GetTemplateChildren().OfType().FirstOrDefault(); + + private class SimpleCommand : ICommand + { + private readonly Action _action; + + public SimpleCommand(Action action) + { + _action = action; + } + + public event EventHandler? CanExecuteChanged; + + public bool CanExecute(object? parameter) => true; + + public void Execute(object? parameter) + { + _action(); + } + } } }