using System.Windows.Input; using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Styling; using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; /// /// Pagination is a control that displays a series of buttons that can be used to navigate to pages. /// CurrentPage starts from 1. /// Pagination only stores an approximate index internally. /// [TemplatePart(PART_PreviousButton, typeof(PaginationButton))] [TemplatePart(PART_NextButton, typeof(PaginationButton))] [TemplatePart(PART_ButtonPanel, typeof(StackPanel))] [TemplatePart(PART_QuickJumpInput, typeof(NumericIntUpDown))] public class Pagination : TemplatedControl { public const string PART_PreviousButton = "PART_PreviousButton"; public const string PART_NextButton = "PART_NextButton"; public const string PART_ButtonPanel = "PART_ButtonPanel"; public const string PART_QuickJumpInput = "PART_QuickJumpInput"; public static readonly StyledProperty CurrentPageProperty = AvaloniaProperty.Register( nameof(CurrentPage), coerce: CoerceCurrentPage); public static readonly RoutedEvent> CurrentPageChangedEvent = RoutedEvent.Register>(nameof(CurrentPageChanged), RoutingStrategies.Bubble); public static readonly StyledProperty CommandProperty = AvaloniaProperty.Register( nameof(Command)); public static readonly StyledProperty CommandParameterProperty = AvaloniaProperty.Register(nameof(CommandParameter)); public static readonly StyledProperty TotalCountProperty = AvaloniaProperty.Register( nameof(TotalCount)); public static readonly StyledProperty PageSizeProperty = AvaloniaProperty.Register( nameof(PageSize), 10); public static readonly DirectProperty PageCountProperty = AvaloniaProperty.RegisterDirect( nameof(PageCount), o => o.PageCount, (o, e) => o.PageCount = e); public static readonly StyledProperty> PageSizeOptionsProperty = AvaloniaProperty.Register>( nameof(PageSizeOptions)); public static readonly StyledProperty PageButtonThemeProperty = AvaloniaProperty.Register( nameof(PageButtonTheme)); public static readonly StyledProperty ShowPageSizeSelectorProperty = AvaloniaProperty.Register( nameof(ShowPageSizeSelector)); public static readonly StyledProperty ShowQuickJumpProperty = AvaloniaProperty.Register( nameof(ShowQuickJump)); private readonly PaginationButton[] _buttons = new PaginationButton[7]; private StackPanel? _buttonPanel; private PaginationButton? _nextButton; private int _pageCount; private PaginationButton? _previousButton; private NumericIntUpDown? _quickJumpInput; static Pagination() { PageSizeProperty.Changed.AddClassHandler((pagination, args) => pagination.OnPageSizeChanged(args)); CurrentPageProperty.Changed.AddClassHandler((pagination, args) => pagination.UpdateButtonsByCurrentPage(args.NewValue.Value)); CurrentPageProperty.Changed.AddClassHandler((pagination, args) => pagination.OnCurrentPageChanged(args)); TotalCountProperty.Changed.AddClassHandler((pagination, _) => pagination.UpdateButtonsByCurrentPage(pagination.CurrentPage)); } public int? CurrentPage { get => GetValue(CurrentPageProperty); set => SetValue(CurrentPageProperty, value); } public ICommand? Command { get => GetValue(CommandProperty); set => SetValue(CommandProperty, value); } public object? CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); } /// /// Total count of items. /// public int TotalCount { get => GetValue(TotalCountProperty); set => SetValue(TotalCountProperty, value); } /// /// Page size. /// public int PageSize { get => GetValue(PageSizeProperty); set => SetValue(PageSizeProperty, value); } /// /// Page count. /// public int PageCount { get => _pageCount; private set => SetAndRaise(PageCountProperty, ref _pageCount, value); } public AvaloniaList PageSizeOptions { get => GetValue(PageSizeOptionsProperty); set => SetValue(PageSizeOptionsProperty, value); } public ControlTheme PageButtonTheme { get => GetValue(PageButtonThemeProperty); set => SetValue(PageButtonThemeProperty, value); } public bool ShowPageSizeSelector { get => GetValue(ShowPageSizeSelectorProperty); set => SetValue(ShowPageSizeSelectorProperty, value); } public bool ShowQuickJump { get => GetValue(ShowQuickJumpProperty); set => SetValue(ShowQuickJumpProperty, value); } public static readonly StyledProperty DisplayCurrentPageInQuickJumperProperty = AvaloniaProperty.Register( nameof(DisplayCurrentPageInQuickJumper)); public bool DisplayCurrentPageInQuickJumper { get => GetValue(DisplayCurrentPageInQuickJumperProperty); set => SetValue(DisplayCurrentPageInQuickJumperProperty, value); } private static int? CoerceCurrentPage(AvaloniaObject arg1, int? arg2) { if (arg2 is null) return null; if (arg1 is Pagination p) arg2 = MathHelpers.SafeClamp(arg2.Value, 1, p.PageCount); return arg2; } private void OnCurrentPageChanged(AvaloniaPropertyChangedEventArgs args) { var oldValue = args.GetOldValue(); var newValue = args.GetNewValue(); var e = new ValueChangedEventArgs(CurrentPageChangedEvent, oldValue, newValue); if (DisplayCurrentPageInQuickJumper) { _quickJumpInput?.SetCurrentValue(NumericIntUpDown.ValueProperty, newValue); } RaiseEvent(e); } /// /// Raised when the changes. /// public event EventHandler>? CurrentPageChanged { add => AddHandler(CurrentPageChangedEvent, value); remove => RemoveHandler(CurrentPageChangedEvent, value); } private void OnPageSizeChanged(AvaloniaPropertyChangedEventArgs args) { var pageCount = TotalCount / args.NewValue.Value; var residue = TotalCount % args.NewValue.Value; if (residue > 0) pageCount++; PageCount = pageCount; if (CurrentPage > PageCount) CurrentPage = null; UpdateButtonsByCurrentPage(CurrentPage); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); Button.ClickEvent.RemoveHandler(OnButtonClick, _previousButton, _nextButton); _previousButton = e.NameScope.Find(PART_PreviousButton); _nextButton = e.NameScope.Find(PART_NextButton); _buttonPanel = e.NameScope.Find(PART_ButtonPanel); Button.ClickEvent.AddHandler(OnButtonClick, _previousButton, _nextButton); KeyDownEvent.RemoveHandler(OnQuickJumpInputKeyDown, _quickJumpInput); LostFocusEvent.RemoveHandler(OnQuickJumpInputLostFocus, _quickJumpInput); _quickJumpInput = e.NameScope.Find(PART_QuickJumpInput); KeyDownEvent.AddHandler(OnQuickJumpInputKeyDown, _quickJumpInput); LostFocusEvent.AddHandler(OnQuickJumpInputLostFocus, _quickJumpInput); InitializePanelButtons(); UpdateButtonsByCurrentPage(0); } private void OnQuickJumpInputKeyDown(object? sender, KeyEventArgs e) { if (e.Key is Key.Enter or Key.Return) SyncQuickJumperValue(); } private void OnQuickJumpInputLostFocus(object? sender, RoutedEventArgs e) { SyncQuickJumperValue(); } private void SyncQuickJumperValue() { if (_quickJumpInput is null) return; var value = _quickJumpInput?.Value; if (value is null) return; value = Clamp(value.Value, 1, PageCount); SetCurrentValue(CurrentPageProperty, value); if (!DisplayCurrentPageInQuickJumper) { _quickJumpInput?.SetCurrentValue(NumericIntUpDown.ValueProperty, null); } InvokeCommand(); } private void OnButtonClick(object? sender, RoutedEventArgs e) { var diff = Equals(sender, _previousButton) ? -1 : 1; AddCurrentPage(diff); InvokeCommand(); } private void InitializePanelButtons() { if (_buttonPanel is null) return; _buttonPanel.Children.Clear(); for (var i = 1; i <= 7; i++) { var button = new PaginationButton { Page = i, IsVisible = true }; _buttonPanel.Children.Add(button); _buttons[i - 1] = button; Button.ClickEvent.AddHandler(OnPageButtonClick, button); } } private void OnPageButtonClick(object? sender, RoutedEventArgs args) { if (sender is PaginationButton pageButton) { if (pageButton.IsFastForward) AddCurrentPage(-5); else if (pageButton.IsFastBackward) AddCurrentPage(5); else CurrentPage = pageButton.Page; } InvokeCommand(); } private void AddCurrentPage(int pageChange) { var newValue = (CurrentPage ?? 0) + pageChange; newValue = Clamp(newValue, 1, PageCount); SetCurrentValue(CurrentPageProperty, newValue); } private int Clamp(int value, int min, int max) { return value < min ? min : value > max ? max : value; } /// /// Update Button Content and Visibility by current page. /// /// private void UpdateButtonsByCurrentPage(int? page) { if (PageSize == 0) return; var pageCount = TotalCount / PageSize; if (_buttonPanel is null) { SetCurrentValue(PageCountProperty, pageCount); return; } var currentPage = CurrentPage; var residue = TotalCount % PageSize; if (residue > 0) pageCount++; if (pageCount <= 7) { for (var i = 0; i < 7; i++) if (i < pageCount) { _buttons[i].IsVisible = true; _buttons[i].SetStatus(i + 1, i + 1 == CurrentPage, false, false); } else { _buttons[i].IsVisible = false; } } else { for (var i = 0; i < 7; i++) _buttons[i].IsVisible = true; var mid = currentPage ?? 0; mid = Clamp(mid, 4, pageCount - 3); _buttons[3].Page = mid; _buttons[2].Page = mid - 1; _buttons[4].Page = mid + 1; _buttons[0].Page = 1; _buttons[6].Page = pageCount; if (mid > 4) _buttons[1].SetStatus(-1, false, true, false); else _buttons[1].SetStatus(mid - 2, false, false, false); if (mid < pageCount - 3) _buttons[5].SetStatus(-1, false, false, true); else _buttons[5].SetStatus(mid + 2, false, false, false); foreach (var button in _buttons) if (button.Page == currentPage) button.SetSelected(true); else button.SetSelected(false); } SetCurrentValue(PageCountProperty, pageCount); SetCurrentValue(CurrentPageProperty, currentPage); if (_previousButton != null) _previousButton.IsEnabled = (CurrentPage ?? int.MaxValue) > 1; if (_nextButton != null) _nextButton.IsEnabled = (CurrentPage ?? 0) < PageCount; } private void InvokeCommand() { if (Command != null && Command.CanExecute(CommandParameter)) Command.Execute(CommandParameter); } }